Merge pull request #3158 from MichaelDvP/core3

updates from dev
This commit is contained in:
Proddy
2026-07-15 18:02:36 +02:00
committed by GitHub
32 changed files with 6391 additions and 6173 deletions
+9 -1
View File
@@ -17,6 +17,12 @@ For more details go to [emsesp.org](https://emsesp.org/).
- prevent system crash during WiFi network discovery on ESP32-S3 hardware [#3128](https://github.com/emsesp/EMS-ESP32/pull/3128) - prevent system crash during WiFi network discovery on ESP32-S3 hardware [#3128](https://github.com/emsesp/EMS-ESP32/pull/3128)
- check arithmetric operations on strings [#3127](https://github.com/emsesp/EMS-ESP32/discussions/3127) - check arithmetric operations on strings [#3127](https://github.com/emsesp/EMS-ESP32/discussions/3127)
- fix setting date/time on Junkers thermostats - fix setting date/time on Junkers thermostats
- offset of hpminflowtemp [#3144](https://github.com/emsesp/EMS-ESP32/issues/3144)
- water circulation command [#3149](https://github.com/emsesp/EMS-ESP32/pull/3149)
- start scheduler after customEntities, delay scheduler loop after startup, handling of `system/restart` command [#3146](https://github.com/emsesp/EMS-ESP32/issues/3146)
- possible memory leaks fixed.
- repeated startup schedules compute value
- shunting yard show json
## Changed ## Changed
@@ -24,5 +30,7 @@ For more details go to [emsesp.org](https://emsesp.org/).
- Scheduler name is now mandatory - Scheduler name is now mandatory
- network fallback to AP only after start [#3090](https://github.com/emsesp/EMS-ESP32/issues/3090) - network fallback to AP only after start [#3090](https://github.com/emsesp/EMS-ESP32/issues/3090)
- replaced Web async-validator with custom validator and toast with native snackbar to reduce bundle size - replaced Web async-validator with custom validator and toast with native snackbar to reduce bundle size
- Dewtemperature for Easycontrol calculated by ems-esp [3135](https://github.com/emsesp/EMS-ESP32/issues/3135) - Dewtemperature for Easycontrol calculated by ems-esp [#3135](https://github.com/emsesp/EMS-ESP32/issues/3135)
- add option for sync thermostat to ntp, allow different time zones [#3148](https://github.com/emsesp/EMS-ESP32/discussions/3148), **defaults to sync**.
- block too many GET requests [mentioned in #3104](https://github.com/emsesp/EMS-ESP32/issues/3104)
- Gateway and Connect devices are not shown in the Devices page [3126](https://github.com/emsesp/EMS-ESP32/discussions/3126) - Gateway and Connect devices are not shown in the Devices page [3126](https://github.com/emsesp/EMS-ESP32/discussions/3126)
+1 -1
View File
@@ -8146,7 +8146,7 @@ uint8
| datetime | date/time | string | | false | DEVICE_DATA | 0 | 13 | 1 | | datetime | date/time | string | | false | DEVICE_DATA | 0 | 13 | 1 |
| outdoortemp | outside temperature | int16 | C | false | DEVICE_DATA | 13 | 1 | 1/10 | | outdoortemp | outside temperature | int16 | C | false | DEVICE_DATA | 13 | 1 | 1/10 |
| src1.currtemp | current room temperature | int16 | C | false | SRC | 0 | 1 | 1/10 | | src1.currtemp | current room temperature | int16 | C | false | SRC | 0 | 1 | 1/10 |
| src1.airhumidity | relative air humidity | int8 | % | false | SRC | 1 | 1 | 1 | | src1.airhumidity | relative air humidity | uint8 | % | false | SRC | 1 | 1 | 1 |
| src1.dewtemperature | dew point temperature | int16 | C | false | SRC | 2 | 1 | 1/10 | | src1.dewtemperature | dew point temperature | int16 | C | false | SRC | 2 | 1 | 1/10 |
uint8 uint8
| src1.seltemp | selected room temperature | uint8 (>=5<=30) | C | true | SRC | 3 | 1 | 1/2 | | src1.seltemp | selected room temperature | uint8 (>=5<=30) | C | true | SRC | 3 | 1 | 1/2 |
+6019 -6019
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -24,6 +24,7 @@ build_flags =
-D FACTORY_NTP_TIME_ZONE_LABEL=\"Europe/Amsterdam\" -D FACTORY_NTP_TIME_ZONE_LABEL=\"Europe/Amsterdam\"
-D FACTORY_NTP_TIME_ZONE_FORMAT=\"CET-1CEST,M3.5.0,M10.5.0/3\" -D FACTORY_NTP_TIME_ZONE_FORMAT=\"CET-1CEST,M3.5.0,M10.5.0/3\"
-D FACTORY_NTP_SERVER=\"time.google.com\" -D FACTORY_NTP_SERVER=\"time.google.com\"
-D FACTORY_NTP_THERMOSTAT_SYNC=1
; MQTT settings ; MQTT settings
-D FACTORY_MQTT_ENABLED=false -D FACTORY_MQTT_ENABLED=false
+4 -4
View File
@@ -2161,8 +2161,8 @@ packages:
node-html-parser@6.1.13: node-html-parser@6.1.13:
resolution: {integrity: sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==} resolution: {integrity: sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==}
node-releases@2.0.50: node-releases@2.0.51:
resolution: {integrity: sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==} resolution: {integrity: sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
normalize-package-data@2.5.0: normalize-package-data@2.5.0:
@@ -3847,7 +3847,7 @@ snapshots:
baseline-browser-mapping: 2.10.42 baseline-browser-mapping: 2.10.42
caniuse-lite: 1.0.30001800 caniuse-lite: 1.0.30001800
electron-to-chromium: 1.5.389 electron-to-chromium: 1.5.389
node-releases: 2.0.50 node-releases: 2.0.51
update-browserslist-db: 1.2.3(browserslist@4.28.5) update-browserslist-db: 1.2.3(browserslist@4.28.5)
buffer-alloc-unsafe@1.1.0: {} buffer-alloc-unsafe@1.1.0: {}
@@ -5137,7 +5137,7 @@ snapshots:
css-select: 5.2.2 css-select: 5.2.2
he: 1.2.0 he: 1.2.0
node-releases@2.0.50: {} node-releases@2.0.51: {}
normalize-package-data@2.5.0: normalize-package-data@2.5.0:
dependencies: dependencies:
+55 -2
View File
@@ -7,6 +7,7 @@ import {
Box, Box,
Button, Button,
Checkbox, Checkbox,
Grid,
Dialog, Dialog,
DialogActions, DialogActions,
DialogContent, DialogContent,
@@ -39,7 +40,7 @@ import { ValidationError, validate } from 'validators';
import { NTP_SETTINGS_VALIDATOR } from 'validators/ntp'; import { NTP_SETTINGS_VALIDATOR } from 'validators/ntp';
import type { ValidateFieldsError } from 'validators/schema'; import type { ValidateFieldsError } from 'validators/schema';
import { TIME_ZONES, selectedTimeZone, useTimeZoneSelectItems } from './TZ'; import { TIME_ZONES, selectedTimeZone, useTimeZoneSelectItems, timeZoneSelectItemsT } from './TZ';
const NTPSettings = () => { const NTPSettings = () => {
const { const {
@@ -62,11 +63,15 @@ const NTPSettings = () => {
useLayoutTitle('NTP'); useLayoutTitle('NTP');
const timeZoneItems = useTimeZoneSelectItems(); const timeZoneItems = useTimeZoneSelectItems();
const timeZoneItemsT = timeZoneSelectItemsT();
const selectedTzValue = data const selectedTzValue = data
? selectedTimeZone(data.tz_label, data.tz_format) ? selectedTimeZone(data.tz_label, data.tz_format)
: undefined; : undefined;
const selectedTzValueT = data
? selectedTimeZone(data.tz_label_t, data.tz_format_t)
: undefined;
const [localTime, setLocalTime] = useState<string>(''); const [localTime, setLocalTime] = useState<string>('');
const [settingTime, setSettingTime] = useState<boolean>(false); const [settingTime, setSettingTime] = useState<boolean>(false);
const [processing, setProcessing] = useState<boolean>(false); const [processing, setProcessing] = useState<boolean>(false);
@@ -131,6 +136,15 @@ const NTPSettings = () => {
updateFormValue(event); updateFormValue(event);
}; };
const changeTimeZoneT = (event: React.ChangeEvent<HTMLInputElement>) => {
void updateState(readNTPSettings(), (settings: NTPSettingsType) => ({
...settings,
tz_label_t: event.target.value,
tz_format_t: TIME_ZONES[event.target.value]
}));
updateFormValue(event);
};
const renderContent = () => { const renderContent = () => {
if (!data) { if (!data) {
return <FormLoader onRetry={loadData} errorMessage={errorMessage || ''} />; return <FormLoader onRetry={loadData} errorMessage={errorMessage || ''} />;
@@ -154,6 +168,7 @@ const NTPSettings = () => {
label={LL.NTP_SERVER()} label={LL.NTP_SERVER()}
fullWidth fullWidth
variant="outlined" variant="outlined"
disabled={!data.enabled}
value={data.server} value={data.server}
onChange={updateFormValue} onChange={updateFormValue}
margin="normal" margin="normal"
@@ -172,7 +187,45 @@ const NTPSettings = () => {
<MenuItem disabled>{LL.TIME_ZONE()}...</MenuItem> <MenuItem disabled>{LL.TIME_ZONE()}...</MenuItem>
{timeZoneItems} {timeZoneItems}
</ValidatedTextField> </ValidatedTextField>
{data.enabled && (
<Grid container spacing={2} rowSpacing={0}>
<Grid>
<ValidatedTextField
fieldErrors={fieldErrors || {}}
name="thermostat_sync"
label="Sync EMS-Thermostat"
variant="outlined"
sx={{ width: '30ch' }}
value={data.thermostat_sync}
onChange={updateFormValue}
margin="normal"
select
>
<MenuItem value={0}>{LL.OFF()}</MenuItem>
<MenuItem value={1}>{LL.ON()}</MenuItem >
<MenuItem value={2} >{LL.USE()} {LL.TIME_ZONE()}</MenuItem >
</ValidatedTextField >
</Grid>
<Grid>
{data.thermostat_sync === 2 && (
<ValidatedTextField
fieldErrors={fieldErrors || {}}
name="tz_label_t"
label={LL.TIME_ZONE() + ' Thermostat'}
sx={{ width: '30ch' }}
variant="outlined"
value={selectedTzValueT}
onChange={changeTimeZoneT}
margin="normal"
select
>
<MenuItem disabled>{LL.TIME_ZONE()}...</MenuItem>
{timeZoneItemsT}
</ValidatedTextField>
)}
</Grid>
</Grid>
)}
<Box sx={{ display: 'flex', flexWrap: 'wrap' }}> <Box sx={{ display: 'flex', flexWrap: 'wrap' }}>
{!data.enabled && !dirtyFlags.length && ( {!data.enabled && !dirtyFlags.length && (
<Box sx={{ flexWrap: 'nowrap', whiteSpace: 'nowrap' }}> <Box sx={{ flexWrap: 'nowrap', whiteSpace: 'nowrap' }}>
+4
View File
@@ -483,3 +483,7 @@ export function useTimeZoneSelectItems() {
export function timeZoneSelectItems() { export function timeZoneSelectItems() {
return precomputedTimeZoneItems; return precomputedTimeZoneItems;
} }
export function timeZoneSelectItemsT() {
return precomputedTimeZoneItems;
}
+3
View File
@@ -293,11 +293,13 @@ const en: Translation = {
TIMER: 'Timer', TIMER: 'Timer',
ONCHANGE: 'On Change', ONCHANGE: 'On Change',
CONDITION: 'Condition', CONDITION: 'Condition',
SCHEDULE_UPDATED: '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', CUSTOM_ENTITIES: 'Custom Entities',
ENTITIES_HELP_1: 'Define custom EMS entities or dynamic user variables', ENTITIES_HELP_1: 'Define custom EMS entities or dynamic user variables',
ENTITIES_UPDATED: 'Entities updated',
WRITEABLE: 'Writeable', WRITEABLE: 'Writeable',
SHOWING: 'Showing', SHOWING: 'Showing',
SEARCH: 'Search', SEARCH: 'Search',
@@ -318,6 +320,7 @@ const en: Translation = {
DOWNLOAD_UPLOAD_1: 'Download and Upload Settings and Firmware', DOWNLOAD_UPLOAD_1: 'Download and Upload Settings and Firmware',
MODULES: 'Modules', MODULES: 'Modules',
MODULES_1: 'Activate or deactivate external modules', MODULES_1: 'Activate or deactivate external modules',
MODULES_UPDATED: 'Modules updated',
MODULES_DESCRIPTION: 'Click on the Module to activate or de-activate EMS-ESP library modules', MODULES_DESCRIPTION: 'Click on the Module to activate or de-activate EMS-ESP library modules',
MODULES_NONE: 'No external modules detected', MODULES_NONE: 'No external modules detected',
RENAME: 'Rename', RENAME: 'Rename',
+3
View File
@@ -16,6 +16,9 @@ export interface NTPSettingsType {
server: string; server: string;
tz_label: string; tz_label: string;
tz_format: string; tz_format: string;
thermostat_sync: number;
tz_label_t: string;
tz_format_t: string;
} }
export interface Time { export interface Time {
+6 -2
View File
@@ -84,8 +84,12 @@ class DummySettings {
uint8_t provisionMode = 0; uint8_t provisionMode = 0;
// NTP // NTP
String server = "pool.ntp.org"; String server = "pool.ntp.org";
String tzLabel = "Europe/London"; String tzLabel = "Europe/London";
String tzFormat = "GMT0BST,M3.5.0/1,M10.5.0";
String tzLabelT = "Europe/Berlin";
String tzFormatT = "CET-1CEST,M3.5.0,M10.5.0/3";
uint8_t thermostat_sync = 0;
static void read(DummySettings & settings, JsonObject root) {}; static void read(DummySettings & settings, JsonObject root) {};
static void read(DummySettings & settings) {}; static void read(DummySettings & settings) {};
+14 -8
View File
@@ -74,16 +74,22 @@ void NTPSettingsService::ntp_received(struct timeval * tv) {
} }
void NTPSettings::read(NTPSettings & settings, JsonObject root) { void NTPSettings::read(NTPSettings & settings, JsonObject root) {
root["enabled"] = settings.enabled; root["enabled"] = settings.enabled;
root["server"] = settings.server; root["server"] = settings.server;
root["tz_label"] = settings.tzLabel; root["tz_label"] = settings.tzLabel;
root["tz_format"] = settings.tzFormat; root["tz_format"] = settings.tzFormat;
root["thermostat_sync"] = settings.thermostat_sync;
root["tz_label_t"] = settings.tzLabelT;
root["tz_format_t"] = settings.tzFormatT;
} }
StateUpdateResult NTPSettings::update(JsonObject root, NTPSettings & settings) { StateUpdateResult NTPSettings::update(JsonObject root, NTPSettings & settings) {
settings.enabled = root["enabled"] | FACTORY_NTP_ENABLED; settings.enabled = root["enabled"] | FACTORY_NTP_ENABLED;
settings.server = root["server"] | FACTORY_NTP_SERVER; settings.server = root["server"] | FACTORY_NTP_SERVER;
settings.tzLabel = root["tz_label"] | FACTORY_NTP_TIME_ZONE_LABEL; settings.tzLabel = root["tz_label"] | FACTORY_NTP_TIME_ZONE_LABEL;
settings.tzFormat = root["tz_format"] | FACTORY_NTP_TIME_ZONE_FORMAT; settings.tzFormat = root["tz_format"] | FACTORY_NTP_TIME_ZONE_FORMAT;
settings.thermostat_sync = root["thermostat_sync"] | FACTORY_NTP_THERMOSTAT_SYNC;
settings.tzLabelT = root["tz_label_t"] | FACTORY_NTP_TIME_ZONE_LABEL;
settings.tzFormatT = root["tz_format_t"] | FACTORY_NTP_TIME_ZONE_FORMAT;
return StateUpdateResult::CHANGED; return StateUpdateResult::CHANGED;
} }
+11 -4
View File
@@ -19,6 +19,10 @@
#define FACTORY_NTP_TIME_ZONE_FORMAT "GMT0BST,M3.5.0/1,M10.5.0" #define FACTORY_NTP_TIME_ZONE_FORMAT "GMT0BST,M3.5.0/1,M10.5.0"
#endif #endif
#ifndef FACTORY_NTP_THERMOSTAT_SYNC
#define FACTORY_NTP_THERMOSTAT_SYNC 1
#endif
#ifndef FACTORY_NTP_SERVER #ifndef FACTORY_NTP_SERVER
#define FACTORY_NTP_SERVER "time.google.com" #define FACTORY_NTP_SERVER "time.google.com"
#endif #endif
@@ -30,10 +34,13 @@
class NTPSettings { class NTPSettings {
public: public:
bool enabled = FACTORY_NTP_ENABLED; bool enabled = FACTORY_NTP_ENABLED;
String tzLabel = FACTORY_NTP_TIME_ZONE_LABEL; String tzLabel = FACTORY_NTP_TIME_ZONE_LABEL;
String tzFormat = FACTORY_NTP_TIME_ZONE_FORMAT; String tzFormat = FACTORY_NTP_TIME_ZONE_FORMAT;
String server = FACTORY_NTP_SERVER; String server = FACTORY_NTP_SERVER;
uint8_t thermostat_sync = FACTORY_NTP_THERMOSTAT_SYNC;
String tzLabelT = FACTORY_NTP_TIME_ZONE_LABEL;
String tzFormatT = FACTORY_NTP_TIME_ZONE_FORMAT;
static void read(NTPSettings & settings, JsonObject root); static void read(NTPSettings & settings, JsonObject root);
static StateUpdateResult update(JsonObject root, NTPSettings & settings); static StateUpdateResult update(JsonObject root, NTPSettings & settings);
+6 -11
View File
@@ -1,4 +1,5 @@
#include "WiFiScanner.h" #include "WiFiScanner.h"
#include <emsesp.h>
WiFiScanner::WiFiScanner(AsyncWebServer * server, SecurityManager * securityManager) { WiFiScanner::WiFiScanner(AsyncWebServer * server, SecurityManager * securityManager) {
securityManager->addEndpoint(server, SCAN_NETWORKS_SERVICE_PATH, AuthenticationPredicates::IS_ADMIN, [this](AsyncWebServerRequest * request) { securityManager->addEndpoint(server, SCAN_NETWORKS_SERVICE_PATH, AuthenticationPredicates::IS_ADMIN, [this](AsyncWebServerRequest * request) {
@@ -20,12 +21,12 @@ void WiFiScanner::scanNetworks(AsyncWebServerRequest * request) {
} }
void WiFiScanner::listNetworks(AsyncWebServerRequest * request) { void WiFiScanner::listNetworks(AsyncWebServerRequest * request) {
const int numNetworks = WiFi.scanComplete(); const int16_t numNetworks = WiFi.scanComplete();
if (numNetworks > -1) { if (numNetworks != -1) {
auto * response = new AsyncJsonResponse(false); auto * response = new emsesp::PsramAsyncJsonResponse(false);
JsonObject root = response->getRoot(); JsonObject root = response->getRoot();
JsonArray networks = root["networks"].to<JsonArray>(); JsonArray networks = root["networks"].to<JsonArray>();
for (uint8_t i = 0; i < numNetworks; i++) { for (int16_t i = 0; i < numNetworks; i++) {
JsonObject network = networks.add<JsonObject>(); JsonObject network = networks.add<JsonObject>();
network["rssi"] = WiFi.RSSI(i); network["rssi"] = WiFi.RSSI(i);
network["ssid"] = WiFi.SSID(i); network["ssid"] = WiFi.SSID(i);
@@ -35,13 +36,7 @@ void WiFiScanner::listNetworks(AsyncWebServerRequest * request) {
} }
response->setLength(); response->setLength();
request->send(response); request->send(response);
} else if (numNetworks == -1) {
request->send(202); // special code to indicate scan in progress
} else { } else {
auto * response = new AsyncJsonResponse(false); request->send(202); // special code to indicate scan in progress
JsonObject root = response->getRoot();
root["networks"].to<JsonArray>();
response->setLength();
request->send(response);
} }
} }
+4 -2
View File
@@ -537,11 +537,13 @@ bool AnalogSensor::update(uint8_t gpio, const char * org_name, double offset, do
if (deleted) { if (deleted) {
LOG_DEBUG("Removing analog sensor GPIO %02d", gpio); LOG_DEBUG("Removing analog sensor GPIO %02d", gpio);
EMSESP::system_.remove_gpio(gpio); // remove from used list only EMSESP::system_.remove_gpio(gpio); // remove from used list only
EMSESP::nvs_.remove(AnalogCustomization.name); if (EMSESP::nvs_.isKey(AnalogCustomization.name)) {
EMSESP::nvs_.remove(AnalogCustomization.name);
}
settings.analogCustomizations.remove(AnalogCustomization); settings.analogCustomizations.remove(AnalogCustomization);
} else { } else {
// update existing record // update existing record
if (!strcmp(name, AnalogCustomization.name)) { if (!strcmp(name, AnalogCustomization.name) && EMSESP::nvs_.isKey(AnalogCustomization.name)) {
EMSESP::nvs_.remove(AnalogCustomization.name); EMSESP::nvs_.remove(AnalogCustomization.name);
} }
strlcpy(AnalogCustomization.name, name, sizeof(AnalogCustomization.name)); strlcpy(AnalogCustomization.name, name, sizeof(AnalogCustomization.name));
+1 -1
View File
@@ -382,7 +382,7 @@ uint8_t Command::call(const uint8_t device_type, const char * command, const cha
// rather than returning its definition, so skip the value-info lookup and fall through // rather than returning its definition, so skip the value-info lookup and fall through
// to the registered command function. The list keywords above are still handled. // to the registered command function. The list keywords above are still handled.
bool is_named_command = (device_type == EMSdevice::DeviceType::COMMAND) && strcmp(cmd, F_(info)) && strcmp(cmd, F_(values)) && strcmp(cmd, F_(entities)) bool is_named_command = (device_type == EMSdevice::DeviceType::COMMAND) && strcmp(cmd, F_(info)) && strcmp(cmd, F_(values)) && strcmp(cmd, F_(entities))
&& strcmp(cmd, F_(metrics)); && strcmp(cmd, F_(metrics)) && !strchr(cmd, '/') && is_admin;
if (!is_named_command && EMSESP::get_device_value_info(output, cmd, id, device_type)) { // entity = cmd if (!is_named_command && EMSESP::get_device_value_info(output, cmd, id, device_type)) { // entity = cmd
LOG_DEBUG("Fetched device entity/attributes for %s/%s (id=%d)", dname, cmd, id); LOG_DEBUG("Fetched device entity/attributes for %s/%s (id=%d)", dname, cmd, id);
return CommandRet::OK; return CommandRet::OK;
+1 -1
View File
@@ -1799,8 +1799,8 @@ void EMSESP::start() {
// this will also handle any MQTT subscriptions // this will also handle any MQTT subscriptions
webCustomizationService.begin(); // load the customizations webCustomizationService.begin(); // load the customizations
webCommandService.begin(); // load the user commands webCommandService.begin(); // load the user commands
webSchedulerService.begin(); // load the scheduler events
webCustomEntityService.begin(); // load the custom telegram reads webCustomEntityService.begin(); // load the custom telegram reads
webSchedulerService.begin(); // load the scheduler events
// perform any system upgrades // perform any system upgrades
if (!factory_settings) { if (!factory_settings) {
+13
View File
@@ -1030,4 +1030,17 @@ float Helpers::numericoperator2scalefactor(int8_t numeric_operator) {
return -numeric_operator; return -numeric_operator;
} }
int16_t Helpers::calc_dew(int16_t temp, uint8_t humi) {
if (humi == EMS_VALUE_UINT8_NOTSET || temp == EMS_VALUE_INT16_NOTSET) {
return EMS_VALUE_INT16_NOTSET;
}
const float k2 = 17.62;
const float k3 = 243.12;
const float t = (float)temp / 10;
const float h = (float)humi / 100;
int16_t dt = (10 * k3 * (((k2 * t) / (k3 + t)) + log(h)) / (((k2 * k3) / (k3 + t)) - log(h)));
return dt;
}
} // namespace emsesp } // namespace emsesp
+1
View File
@@ -84,6 +84,7 @@ class Helpers {
static uint8_t count_items(const char * const * list); static uint8_t count_items(const char * const * list);
static const char * translated_word(const char * const * strings, const bool force_en = false); static const char * translated_word(const char * const * strings, const bool force_en = false);
static int16_t calc_dew(int16_t temp, uint8_t hum);
#ifdef EMSESP_STANDALONE #ifdef EMSESP_STANDALONE
static char * ultostr(char * ptr, uint32_t value, const uint8_t base); static char * ultostr(char * ptr, uint32_t value, const uint8_t base);
+23 -36
View File
@@ -299,8 +299,8 @@ void Roomctrl::temperature(uint8_t addr, uint8_t dst, uint8_t hc) {
if (type_[hc] == RC20) { // RC20, telegram 0xAF if (type_[hc] == RC20) { // RC20, telegram 0xAF
data[2] = 0xAF; data[2] = 0xAF;
data[3] = 0; data[3] = 0;
data[4] = (uint8_t)(remotetemp_[hc] >> 8); data[4] = static_cast<uint8_t>(remotetemp_[hc] >> 8);
data[5] = (uint8_t)(remotetemp_[hc] & 0xFF); data[5] = static_cast<uint8_t>(remotetemp_[hc] & 0xFF);
data[6] = 0; data[6] = 0;
data[7] = EMSbus::calculate_crc(data, 7); // append CRC data[7] = EMSbus::calculate_crc(data, 7); // append CRC
EMSuart::transmit(data, 8); EMSuart::transmit(data, 8);
@@ -309,8 +309,8 @@ void Roomctrl::temperature(uint8_t addr, uint8_t dst, uint8_t hc) {
data[3] = 0; data[3] = 0;
data[4] = 0; data[4] = 0;
data[5] = 0x23; // fixed for all hc data[5] = 0x23; // fixed for all hc
data[6] = (uint8_t)(remotetemp_[hc] >> 8); data[6] = static_cast<uint8_t>(remotetemp_[hc] >> 8);
data[7] = (uint8_t)(remotetemp_[hc] & 0xFF); data[7] = static_cast<uint8_t>(remotetemp_[hc] & 0xFF);
data[8] = EMSbus::calculate_crc(data, 8); // append CRC data[8] = EMSbus::calculate_crc(data, 8); // append CRC
EMSuart::transmit(data, 9); EMSuart::transmit(data, 9);
} else if (type_[hc] == RC200) { // RC200, telegram 42B, ff } else if (type_[hc] == RC200) { // RC200, telegram 42B, ff
@@ -318,11 +318,11 @@ void Roomctrl::temperature(uint8_t addr, uint8_t dst, uint8_t hc) {
data[3] = 0; data[3] = 0;
data[4] = 3; data[4] = 3;
data[5] = 0x2B + hc; data[5] = 0x2B + hc;
data[6] = (uint8_t)(remotetemp_[hc] >> 8); data[6] = static_cast<uint8_t>(remotetemp_[hc] >> 8);
data[7] = (uint8_t)(remotetemp_[hc] & 0xFF); data[7] = static_cast<uint8_t>(remotetemp_[hc] & 0xFF);
uint16_t t1 = remotetemp_[hc] * 10; uint16_t t1 = remotetemp_[hc] * 10;
data[8] = (uint8_t)(t1 >> 8); data[8] = static_cast<uint8_t>(t1 >> 8);
data[9] = (uint8_t)(t1 & 0xFF); data[9] = static_cast<uint8_t>(t1 & 0xFF);
data[10] = 1; // not sure what this is and if we need it, maybe mode? data[10] = 1; // not sure what this is and if we need it, maybe mode?
data[11] = EMSbus::calculate_crc(data, 11); // append CRC data[11] = EMSbus::calculate_crc(data, 11); // append CRC
EMSuart::transmit(data, 12); EMSuart::transmit(data, 12);
@@ -331,8 +331,8 @@ void Roomctrl::temperature(uint8_t addr, uint8_t dst, uint8_t hc) {
data[3] = 0; data[3] = 0;
data[4] = 3; data[4] = 3;
data[5] = 0x2B + hc; data[5] = 0x2B + hc;
data[6] = (uint8_t)(remotetemp_[hc] >> 8); data[6] = static_cast<uint8_t>(remotetemp_[hc] >> 8);
data[7] = (uint8_t)(remotetemp_[hc] & 0xFF); data[7] = static_cast<uint8_t>(remotetemp_[hc] & 0xFF);
data[8] = EMSbus::calculate_crc(data, 8); // append CRC data[8] = EMSbus::calculate_crc(data, 8); // append CRC
EMSuart::transmit(data, 9); EMSuart::transmit(data, 9);
} else if (type_[hc] == SENSOR) { // wireless sensor, broadcast id 435 } else if (type_[hc] == SENSOR) { // wireless sensor, broadcast id 435
@@ -340,8 +340,8 @@ void Roomctrl::temperature(uint8_t addr, uint8_t dst, uint8_t hc) {
data[3] = 0; data[3] = 0;
data[4] = 3; data[4] = 3;
data[5] = 0x35 + hc; data[5] = 0x35 + hc;
data[6] = (uint8_t)(remotetemp_[hc] >> 8); data[6] = static_cast<uint8_t>(remotetemp_[hc] >> 8);
data[7] = (uint8_t)(remotetemp_[hc] & 0xFF); data[7] = static_cast<uint8_t>(remotetemp_[hc] & 0xFF);
data[8] = EMSbus::calculate_crc(data, 8); // append CRC data[8] = EMSbus::calculate_crc(data, 8); // append CRC
EMSuart::transmit(data, 9); EMSuart::transmit(data, 9);
} else if (type_[hc] == RT800) { // RC200, telegram 42B, ff } else if (type_[hc] == RT800) { // RC200, telegram 42B, ff
@@ -349,11 +349,11 @@ void Roomctrl::temperature(uint8_t addr, uint8_t dst, uint8_t hc) {
data[3] = 0; data[3] = 0;
data[4] = 3; data[4] = 3;
data[5] = 0x2B + hc; data[5] = 0x2B + hc;
data[6] = (uint8_t)(remotetemp_[hc] >> 8); data[6] = static_cast<uint8_t>(remotetemp_[hc] >> 8);
data[7] = (uint8_t)(remotetemp_[hc] & 0xFF); data[7] = static_cast<uint8_t>(remotetemp_[hc] & 0xFF);
uint16_t t1 = remotetemp_[hc] * 10; uint16_t t1 = remotetemp_[hc] * 10;
data[8] = (uint8_t)(t1 >> 8); data[8] = static_cast<uint8_t>(t1 >> 8);
data[9] = (uint8_t)(t1 & 0xFF); data[9] = static_cast<uint8_t>(t1 & 0xFF);
data[10] = 1; // not sure what this is and if we need it, maybe mode? data[10] = 1; // not sure what this is and if we need it, maybe mode?
data[11] = 9; // not sure what this is and if we need it, maybe mode? data[11] = 9; // not sure what this is and if we need it, maybe mode?
data[12] = EMSbus::calculate_crc(data, 12); // append CRC data[12] = EMSbus::calculate_crc(data, 12); // append CRC
@@ -363,11 +363,11 @@ void Roomctrl::temperature(uint8_t addr, uint8_t dst, uint8_t hc) {
data[3] = 0; data[3] = 0;
data[4] = 3; data[4] = 3;
data[5] = 0x2B + hc; data[5] = 0x2B + hc;
data[6] = (uint8_t)(remotetemp_[hc] >> 8); data[6] = static_cast<uint8_t>(remotetemp_[hc] >> 8);
data[7] = (uint8_t)(remotetemp_[hc] & 0xFF); data[7] = static_cast<uint8_t>(remotetemp_[hc] & 0xFF);
uint16_t t1 = remotetemp_[hc] * 10; uint16_t t1 = remotetemp_[hc] * 10;
data[8] = (uint8_t)(t1 >> 8); data[8] = static_cast<uint8_t>(t1 >> 8);
data[9] = (uint8_t)(t1 & 0xFF); data[9] = static_cast<uint8_t>(t1 & 0xFF);
data[10] = 0; // not sure what this is and if we need it data[10] = 0; // not sure what this is and if we need it
data[11] = 0; // not sure what this is and if we need it data[11] = 0; // not sure what this is and if we need it
data[12] = EMSbus::calculate_crc(data, 12); // append CRC data[12] = EMSbus::calculate_crc(data, 12); // append CRC
@@ -377,7 +377,7 @@ void Roomctrl::temperature(uint8_t addr, uint8_t dst, uint8_t hc) {
// send telegram 0x047B only for RC100H // send telegram 0x047B only for RC100H
void Roomctrl::humidity(uint8_t addr, uint8_t dst, uint8_t hc) { void Roomctrl::humidity(uint8_t addr, uint8_t dst, uint8_t hc) {
int16_t dew = calc_dew(remotetemp_[hc], remotehum_[hc]); int16_t dew = Helpers::calc_dew(remotetemp_[hc], remotehum_[hc]);
int8_t dew8 = EMS_VALUE_INT8_NOTSET; int8_t dew8 = EMS_VALUE_INT8_NOTSET;
if (dew != EMS_VALUE_INT16_NOTSET) { if (dew != EMS_VALUE_INT16_NOTSET) {
dew8 = static_cast<int8_t>((dew >= 0 ? dew + 5 : dew - 5) / 10); dew8 = static_cast<int8_t>((dew >= 0 ? dew + 5 : dew - 5) / 10);
@@ -391,8 +391,8 @@ void Roomctrl::humidity(uint8_t addr, uint8_t dst, uint8_t hc) {
data[5] = 0x7B + hc; data[5] = 0x7B + hc;
data[6] = static_cast<uint8_t>(dew8); data[6] = static_cast<uint8_t>(dew8);
data[7] = remotehum_[hc]; data[7] = remotehum_[hc];
data[8] = static_cast<uint8_t>(static_cast<uint16_t>(dew) >> 8); data[8] = static_cast<uint8_t>(dew >> 8);
data[9] = static_cast<uint8_t>(static_cast<uint16_t>(dew) & 0xFF); data[9] = static_cast<uint8_t>(dew & 0x00FF);
data[10] = EMSbus::calculate_crc(data, 10); // append CRC data[10] = EMSbus::calculate_crc(data, 10); // append CRC
EMSuart::transmit(data, 11); EMSuart::transmit(data, 11);
} }
@@ -440,17 +440,4 @@ void Roomctrl::replyF7(uint8_t addr, uint8_t dst, uint8_t offset, uint8_t typehh
EMSuart::transmit(data, 10); EMSuart::transmit(data, 10);
} }
int16_t Roomctrl::calc_dew(int16_t temp, uint8_t humi) {
if (humi == EMS_VALUE_UINT8_NOTSET || temp == EMS_VALUE_INT16_NOTSET) {
return EMS_VALUE_INT16_NOTSET;
}
const float k2 = 17.62;
const float k3 = 243.12;
const float t = (float)temp / 10;
const float h = (float)humi / 100;
int16_t dt = (10 * k3 * (((k2 * t) / (k3 + t)) + log(h)) / (((k2 * k3) / (k3 + t)) - log(h)));
return dt;
}
} // namespace emsesp } // namespace emsesp
+1 -2
View File
@@ -34,8 +34,7 @@ class Roomctrl {
static bool is_remote(const uint8_t hc) { static bool is_remote(const uint8_t hc) {
return (hc < 4 && remotetemp_[hc] != EMS_VALUE_INT16_NOTSET); return (hc < 4 && remotetemp_[hc] != EMS_VALUE_INT16_NOTSET);
} }
static void set_timeout(uint8_t t); static void set_timeout(uint8_t t);
static int16_t calc_dew(int16_t temp, uint8_t hum);
private: private:
static constexpr uint32_t SEND_INTERVAL = 15000; // 15 sec static constexpr uint32_t SEND_INTERVAL = 15000; // 15 sec
+3 -11
View File
@@ -42,17 +42,8 @@ std::deque<Token> exprToTokens(const std::string & expr) {
i += (*p == '{') ? 1 : (*p == '}') ? -1 : 0; i += (*p == '{') ? 1 : (*p == '}') ? -1 : 0;
++p; ++p;
} }
if (*p) { const auto s = std::string(b, p);
++p; tokens.emplace_back(Token::Type::String, s, -3);
}
// Use string_view to avoid unnecessary string copies
std::string_view s(b, p - b);
auto n = s.find("\"\"");
while (n != std::string_view::npos) {
s.remove_prefix(n + 2);
n = s.find("\"\"");
}
tokens.emplace_back(Token::Type::String, std::string(s), -3);
if (*p == '\0') { if (*p == '\0') {
--p; --p;
} }
@@ -862,6 +853,7 @@ std::string compute(const std::string & expr) {
} }
result = json.as<std::string>(); result = json.as<std::string>();
} }
expr_new.replace(f, e - f, result.c_str());
} }
} }
expr_new.replace(f, e - f, result); expr_new.replace(f, e - f, result);
+3 -1
View File
@@ -1958,7 +1958,9 @@ bool System::get_value_info(JsonObject output, const char * cmd) {
LOG_ERROR("empty system command"); LOG_ERROR("empty system command");
return false; return false;
} }
if (!strcmp(cmd, "restart")) { // restart is a command, not an entity
return false;
}
// check for hardcoded "info"/"value" // check for hardcoded "info"/"value"
if (!strcmp(cmd, F_(info)) || !strcmp(cmd, F_(values))) { if (!strcmp(cmd, F_(info)) || !strcmp(cmd, F_(values))) {
return command_info("", 0, output); return command_info("", 0, output);
+81 -16
View File
@@ -45,7 +45,8 @@ Connect::Connect(uint8_t device_type, uint8_t device_id, uint8_t product_id, con
register_telegram_type(0x1230 + i, "Roomparams", false, MAKE_PF_CB(process_roomThermostatParam)); // fetch for active circuits register_telegram_type(0x1230 + i, "Roomparams", false, MAKE_PF_CB(process_roomThermostatParam)); // fetch for active circuits
register_telegram_type(0x1244 + i, "Roomdata", false, MAKE_PF_CB(process_roomThermostatData)); // broadcasted register_telegram_type(0x1244 + i, "Roomdata", false, MAKE_PF_CB(process_roomThermostatData)); // broadcasted
} }
register_telegram_type(0xDB65, "Roomschedule", true, MAKE_PF_CB(process_roomSchedule)); register_telegram_type(0x0B65, "Roomschedule", true, MAKE_PF_CB(process_roomSchedule));
register_telegram_type(0xF7, "MenuConfig", false, MAKE_PF_CB(process_roomConfig));
// 0x2040, broadcast 36 bytes: // 0x2040, broadcast 36 bytes:
// data: 0E 60 00 DF 0D AF 0A 46 0A 46 02 9A 1C 53 1C 53 12 AD 12 AD 00 00 13 C2 // data: 0E 60 00 DF 0D AF 0A 46 0A 46 02 9A 1C 53 1C 53 12 AD 12 AD 00 00 13 C2
// data: 1F 37 1F 37 00 00 00 00 18 97 11 27 (offset 24) // data: 1F 37 1F 37 00 00 00 00 18 97 11 27 (offset 24)
@@ -88,7 +89,7 @@ void Connect::process_RCTime(const std::shared_ptr<const Telegram> & telegram) {
void Connect::register_device_values_room(std::shared_ptr<Connect::RoomCircuit> room) { void Connect::register_device_values_room(std::shared_ptr<Connect::RoomCircuit> room) {
auto tag = DeviceValueTAG::TAG_SRC1 + room->room(); auto tag = DeviceValueTAG::TAG_SRC1 + room->room();
register_device_value(tag, &room->temp_, DeviceValueType::INT16, DeviceValueNumOp::DV_NUMOP_DIV10, FL_(roomTemp), DeviceValueUOM::DEGREES); register_device_value(tag, &room->temp_, DeviceValueType::INT16, DeviceValueNumOp::DV_NUMOP_DIV10, FL_(roomTemp), DeviceValueUOM::DEGREES);
register_device_value(tag, &room->humidity_, DeviceValueType::INT8, FL_(airHumidity), DeviceValueUOM::PERCENT); register_device_value(tag, &room->humidity_, DeviceValueType::UINT8, FL_(airHumidity), DeviceValueUOM::PERCENT);
register_device_value(tag, &room->dewtemp_, DeviceValueType::INT16, DeviceValueNumOp::DV_NUMOP_DIV10, FL_(dewTemperature), DeviceValueUOM::DEGREES); register_device_value(tag, &room->dewtemp_, DeviceValueType::INT16, DeviceValueNumOp::DV_NUMOP_DIV10, FL_(dewTemperature), DeviceValueUOM::DEGREES);
register_device_value( register_device_value(
tag, &room->seltemp_, DeviceValueType::UINT8, DeviceValueNumOp::DV_NUMOP_DIV2, FL_(selRoomTemp), DeviceValueUOM::DEGREES, MAKE_CF_CB(set_seltemp), 5, 30); tag, &room->seltemp_, DeviceValueType::UINT8, DeviceValueNumOp::DV_NUMOP_DIV2, FL_(selRoomTemp), DeviceValueUOM::DEGREES, MAKE_CF_CB(set_seltemp), 5, 30);
@@ -133,16 +134,8 @@ void Connect::process_roomThermostat(const std::shared_ptr<const Telegram> & tel
if (!Mqtt::ha_enabled() || (Helpers::hasValue(rc->mode_) && Helpers::hasValue(rc->icon_))) { if (!Mqtt::ha_enabled() || (Helpers::hasValue(rc->mode_) && Helpers::hasValue(rc->icon_))) {
has_update(telegram, rc->seltemp_, 3); has_update(telegram, rc->seltemp_, 3);
} }
// calculate dew temperature // calculate dew temperature
if (rc->humidity_ >= 0 && rc->humidity_ <= 100) { has_update(rc->dewtemp_, Helpers::calc_dew(rc->temp_, rc->humidity_));
const float k2 = 17.62;
const float k3 = 243.12;
const float t = (float)rc->temp_ / 10;
const float h = (float)rc->humidity_ / 100;
int16_t dt = (10 * k3 * (((k2 * t) / (k3 + t)) + log(h)) / (((k2 * k3) / (k3 + t)) - log(h)));
has_update(rc->dewtemp_, dt);
}
} }
// gateway(0x48) W gateway(0x50), ?(0x0B42), data: 01 // icon in offset 0 // gateway(0x48) W gateway(0x50), ?(0x0B42), data: 01 // icon in offset 0
@@ -168,11 +161,33 @@ void Connect::process_roomThermostatSettings(const std::shared_ptr<const Telegra
if (rc == nullptr) { if (rc == nullptr) {
return; return;
} }
has_enumupdate(telegram, rc->mode_, 0, {3, 1, 0}); // modes off, manual auto // has_enumupdate(telegram, rc->mode_, 0, {3, 1, 0}); // modes off, manual auto
// has_update(telegram, rc->mode_, 0); // modes: auto, heat, cool, off // has_update(telegram, rc->mode_, 0); // modes: auto, heat, cool, off
// has_update(telegram, rc->tempautotemp_, 1); // FF means off // has_update(telegram, rc->tempautotemp_, 1); // FF means off
// has_update(telegram, rc->manualtemp_, 3); // has_update(telegram, rc->manualtemp_, 3);
// has_update(telegram, rc->coolmode_, 4); // 01-cooling, 00-heating
// has_update(telegram, rc->coolautotemp_, 5);
// has_update(telegram, rc->cooltemp_, 6);
has_update(telegram, rc->childlock_, 7); has_update(telegram, rc->childlock_, 7);
uint8_t mh = 0xFF;
uint8_t mc = 0xFF;
telegram->read_value(mh, 0);
telegram->read_value(mc, 4);
if (mc == 3) {
rc->coolmode_ = 1;
rc->mode_ = 0;
} else if (mh == 3) {
rc->coolmode_ = 0;
rc->mode_ = 0;
} else if (mc == 1) {
rc->coolmode_ = 1;
rc->mode_ = 1;
} else if (mh == 1) {
rc->coolmode_ = 0;
rc->mode_ = 1;
} else {
rc->mode_ = 2;
}
} }
// unknown telegrams, needs fetch // unknown telegrams, needs fetch
@@ -203,6 +218,53 @@ void Connect::process_roomSchedule(const std::shared_ptr<const Telegram> & teleg
toggle_fetch(telegram->type_id, false); // fetch only once if all is initialized toggle_fetch(telegram->type_id, false); // fetch only once if all is initialized
} }
void Connect::process_roomConfig(const std::shared_ptr<const Telegram> & telegram) {
if (telegram->offset == 0 && telegram->message_length > 3) {
uint16_t t = 0;
telegram->read_value(t, 1);
if (t >= 0x0AB5 && t <= 0x0AB5 + 15) {
auto rc = room_circuit(t - 0x0AB5);
if (rc == nullptr) {
return;
}
uint8_t bits = telegram->message_data[3];
if (rc->mode_ == 2) {
if (bits & 0x02) {
rc->coolmode_ = 0;
} else if (bits & 0x20) {
rc->coolmode_ = 1;
}
}
/*
switch (bits & 0x6A) {
case 0: // cooling off
rc->mode_ = 0;
rc->coolmode_ = 1;
break;
case 0x40: // manual cooling or heating off
rc->mode_ = 1;
rc->coolmode_ = 1;
break;
case 0x20: // auto cooling
rc->mode_ = 2;
rc->coolmode_ = 1;
break;
case 0x48: // manual heating
case 0x08: // manual heating
rc->mode_ = 1;
rc->coolmode_ = 0;
break;
case 0x42: // auto heating
case 0x02: // auto heating
rc->mode_ = 2;
rc->coolmode_ = 0;
break;
}
*/
}
}
}
// Settings: // Settings:
bool Connect::set_mode(const char * value, const int8_t id) { bool Connect::set_mode(const char * value, const int8_t id) {
@@ -216,7 +278,7 @@ bool Connect::set_mode(const char * value, const int8_t id) {
return false; return false;
} }
} }
write_command(0xBB5 + rc->room(), 0, v); // no validate, mode change is broadcasted write_command(0xBB5 + rc->room(), rc->coolmode_ == 1 ? 4 : 0, v); // no validate, mode change is broadcasted
return true; return true;
} }
@@ -226,10 +288,13 @@ bool Connect::set_seltemp(const char * value, const int8_t id) {
return false; return false;
} }
float v; float v;
if (Helpers::value2float(value, v)) { if (Helpers::value2temperature(value, v)) {
// depends on mode, auto (2 for enum_mode2, 0 for enum_mode8) set in offset 1 // depends on mode, auto (2 for enum_mode2, 0 for enum_mode8) set in offset 1
write_command(0xBB5 + rc->room(), rc->mode_ == 2 ? 1 : 3, v == -1 ? 0xFF : uint8_t(v * 2)); if (rc->coolmode_ == 1) {
// write_command(0xBB5 + rc->room(), rc->mode_ == 0 ? 1 : 3, v == -1 ? 0xFF : uint8_t(v * 2)); write_command(0xBB5 + rc->room(), rc->mode_ == 2 ? 5 : 6, v == -1 ? 0xFF : uint8_t(v * 2));
} else {
write_command(0xBB5 + rc->room(), rc->mode_ == 2 ? 1 : 3, v == -1 ? 0xFF : uint8_t(v * 2));
}
return true; return true;
} }
return false; return false;
+5 -1
View File
@@ -34,15 +34,18 @@ class Connect : public EMSdevice {
} }
~RoomCircuit() = default; ~RoomCircuit() = default;
int16_t temp_; int16_t temp_;
int8_t humidity_; uint8_t humidity_;
uint8_t seltemp_; uint8_t seltemp_;
uint8_t mode_; uint8_t mode_;
char name_[51]; char name_[51];
int16_t dewtemp_; int16_t dewtemp_;
uint8_t childlock_; uint8_t childlock_;
uint8_t icon_; uint8_t icon_;
uint8_t coolmode_;
// uint8_t tempautotemp_; // uint8_t tempautotemp_;
// uint8_t manualtemp_; // uint8_t manualtemp_;
// uint8_t coolautotemp_;
// uint8_t cooltemp_;
uint8_t room() { uint8_t room() {
return room_; return room_;
@@ -62,6 +65,7 @@ class Connect : public EMSdevice {
void process_roomThermostatParam(const std::shared_ptr<const Telegram> & telegram); void process_roomThermostatParam(const std::shared_ptr<const Telegram> & telegram);
void process_roomThermostatData(const std::shared_ptr<const Telegram> & telegram); void process_roomThermostatData(const std::shared_ptr<const Telegram> & telegram);
void process_roomSchedule(const std::shared_ptr<const Telegram> & telegram); void process_roomSchedule(const std::shared_ptr<const Telegram> & telegram);
void process_roomConfig(const std::shared_ptr<const Telegram> & telegram);
bool set_mode(const char * value, const int8_t id); bool set_mode(const char * value, const int8_t id);
bool set_seltemp(const char * value, const int8_t id); bool set_seltemp(const char * value, const int8_t id);
bool set_name(const char * value, const int8_t id); bool set_name(const char * value, const int8_t id);
+82 -29
View File
@@ -816,7 +816,6 @@ void Thermostat::process_RemoteTemp(const std::shared_ptr<const Telegram> & tele
// e.g. "38 10 FF 00 03 7B 08 24 00 4B" // e.g. "38 10 FF 00 03 7B 08 24 00 4B"
void Thermostat::process_RemoteHumidity(const std::shared_ptr<const Telegram> & telegram) { void Thermostat::process_RemoteHumidity(const std::shared_ptr<const Telegram> & telegram) {
// has_update(telegram, dewtemperature_, 0); // this is int8 // has_update(telegram, dewtemperature_, 0); // this is int8
// some thermostats use short telegram with int8 dewpoint, https://github.com/emsesp/EMS-ESP32/issues/1491 // some thermostats use short telegram with int8 dewpoint, https://github.com/emsesp/EMS-ESP32/issues/1491
// but it's not a dewpoint in offset 0, removed, see https://github.com/emsesp/EMS-ESP32/issues/3135 // but it's not a dewpoint in offset 0, removed, see https://github.com/emsesp/EMS-ESP32/issues/3135
has_update(telegram, humidity_, 1); has_update(telegram, humidity_, 1);
@@ -825,7 +824,8 @@ void Thermostat::process_RemoteHumidity(const std::shared_ptr<const Telegram> &
if (telegram->read_value(dew, 2)) { if (telegram->read_value(dew, 2)) {
has_update(dewtemperature_, dew); has_update(dewtemperature_, dew);
} else if (telegram->offset == 0 && telegram->message_length < 4) { } else if (telegram->offset == 0 && telegram->message_length < 4) {
has_update(dewtemperature_, Roomctrl::calc_dew(tempsensor1_, humidity_)); dew = Helpers::calc_dew(tempsensor1_, humidity_);
has_update(dewtemperature_, dew);
} }
} }
@@ -1477,9 +1477,14 @@ void Thermostat::process_HPSet(const std::shared_ptr<const Telegram> & telegram)
if (hc == nullptr) { if (hc == nullptr) {
return; return;
} }
has_update(telegram, hc->dewoffset, 4); // 7-35°C if (hc->control == 3 || hc->control == 6) { // control with humidity
has_update(telegram, hc->roomtempdiff, 3); // 1-10K has_update(telegram, hc->hpminflowtemp, 0); // 7-35°C
has_update(telegram, hc->hpminflowtemp, 0); // 2-10K } else {
has_update(telegram, hc->hpminflowtemp, 1); // 7-35°C
}
// has_update(telegram, hc->roomtempoffset, 2); // 1-10K // not implemented, mentioned in #3144
has_update(telegram, hc->roomtempdiff, 3); // 1-10K
has_update(telegram, hc->dewoffset, 4); // 2-10K
} }
// type 0x41 - data from the RC30 thermostat(0x10) - 14 bytes long // type 0x41 - data from the RC30 thermostat(0x10) - 14 bytes long
@@ -1737,9 +1742,18 @@ void Thermostat::process_RCTime(const std::shared_ptr<const Telegram> & telegram
} }
// check clock // check clock
time_t now = time(nullptr); time_t now = time(nullptr);
tm * tm_ = localtime(&now); bool sync = false;
bool tset_ = tm_->tm_year > 110; // year 2010 and up, time is valid // change to thermostat timezone if different
EMSESP::esp32React.getNTPSettingsService()->read([&](const NTPSettings & settings) {
sync = settings.thermostat_sync != 0;
if (settings.thermostat_sync == 2) {
setenv("TZ", settings.tzFormatT.c_str(), 1);
tzset();
}
});
tm * tm_ = localtime(&now);
bool tset_ = tm_->tm_year > 110; // year 2010 and up, time is valid
tm_->tm_year = (telegram->message_data[0] & 0x7F) + 100; // IVT tm_->tm_year = (telegram->message_data[0] & 0x7F) + 100; // IVT
tm_->tm_mon = telegram->message_data[1] - 1; tm_->tm_mon = telegram->message_data[1] - 1;
tm_->tm_mday = telegram->message_data[3]; tm_->tm_mday = telegram->message_data[3];
@@ -1755,20 +1769,27 @@ void Thermostat::process_RCTime(const std::shared_ptr<const Telegram> & telegram
strftime(newdatetime, sizeof(dateTime_), "%d.%m.%Y %H:%M", tm_); strftime(newdatetime, sizeof(dateTime_), "%d.%m.%Y %H:%M", tm_);
has_update(dateTime_, newdatetime, sizeof(dateTime_)); has_update(dateTime_, newdatetime, sizeof(dateTime_));
bool ivtclock = (telegram->message_data[0] & 0x80) == 0x80; // dont sync ivt-clock, #439 bool ivtclock = (telegram->message_data[0] & 0x80) == 0x80; // dont sync ivt-clock, #439
bool junkersclock = model() == EMSdevice::EMS_DEVICE_FLAG_JUNKERS; tm_->tm_isdst = -1; // determine dst
time_t ttime = mktime(tm_); // thermostat time time_t ttime = mktime(tm_); // make thermostat time
// change back emsesp timezone
EMSESP::esp32React.getNTPSettingsService()->read([&](const NTPSettings & settings) {
if (settings.thermostat_sync == 2) {
setenv("TZ", settings.tzFormat.c_str(), 1);
tzset();
}
});
// correct thermostat clock if we have valid ntp time, and could write the command // correct thermostat clock if we have valid ntp time, and could write the command
if (!ivtclock && !junkersclock && tset_ && EMSESP::system_.ntp_connected() && !EMSESP::system_.readonly_mode() && has_command(&dateTime_)) { if (sync && !ivtclock && tset_ && EMSESP::system_.ntp_connected() && !EMSESP::system_.readonly_mode() && has_command(&dateTime_)) {
double difference = difftime(now, ttime); int difference = difftime(now, ttime);
if (difference > 15 || difference < -15) { if (difference > 15 || difference < -15) {
if (setTimeRetry < 3) { if (setTimeRetry < 3) {
if (!use_dst) { if (!use_dst) {
set_datetime("ntp", 0); // set from NTP without dst set_datetime("ntp", 0); // set from NTP without dst
LOG_INFO("thermostat time correction from ntp, ignoring dst"); LOG_INFO("thermostat time correction %d seconds from ntp, ignoring dst", difference);
} else { } else {
set_datetime("ntp", -1); // set from NTP set_datetime("ntp", -1); // set from NTP
LOG_INFO("thermostat time correction from ntp"); LOG_INFO("thermostat time correction %d seconds from ntp", difference);
} }
setTimeRetry++; setTimeRetry++;
} }
@@ -1778,10 +1799,6 @@ void Thermostat::process_RCTime(const std::shared_ptr<const Telegram> & telegram
} }
#ifndef EMSESP_STANDALONE #ifndef EMSESP_STANDALONE
if (!tset_ && tm_->tm_year > 110) { // emsesp clock not set, but thermostat clock if (!tset_ && tm_->tm_year > 110) { // emsesp clock not set, but thermostat clock
if (ivtclock) {
tm_->tm_isdst = -1; // determine dst
ttime = mktime(tm_); // thermostat time
}
struct timeval newnow = {.tv_sec = ttime, .tv_usec = 0}; struct timeval newnow = {.tv_sec = ttime, .tv_usec = 0};
#if CONFIG_IDF_TARGET_ESP32C3 #if CONFIG_IDF_TARGET_ESP32C3
// unknown how to set time on C3 // unknown how to set time on C3
@@ -1932,7 +1949,11 @@ bool Thermostat::set_hpminflowtemp(const char * value, const int8_t id) {
} }
int v; int v;
if (Helpers::value2temperature(value, v)) { if (Helpers::value2temperature(value, v)) {
write_command(hp_typeids[hc->hc()], 0, v, hp_typeids[hc->hc()]); if (hc->control == 3 || hc->control == 6) { // remotes with humidity
write_command(hp_typeids[hc->hc()], 0, v, hp_typeids[hc->hc()]);
} else {
write_command(hp_typeids[hc->hc()], 1, v, hp_typeids[hc->hc()]);
}
return true; return true;
} }
return false; return false;
@@ -3061,6 +3082,15 @@ bool Thermostat::set_datetime(const char * value, const int8_t id) {
if (dt == "ntp") { if (dt == "ntp") {
time_t now = time(nullptr); time_t now = time(nullptr);
tm * tm_ = localtime(&now); tm * tm_ = localtime(&now);
EMSESP::esp32React.getNTPSettingsService()->read([&](const NTPSettings & settings) {
if (settings.thermostat_sync == 2) {
setenv("TZ", settings.tzFormatT.c_str(), 1);
tzset();
tm_ = localtime(&now);
setenv("TZ", settings.tzFormat.c_str(), 1);
tzset();
}
});
if (tm_->tm_year < 110) { // no valid time if (tm_->tm_year < 110) { // no valid time
return false; return false;
} }
@@ -3076,10 +3106,7 @@ bool Thermostat::set_datetime(const char * value, const int8_t id) {
data[5] = tm_->tm_sec; data[5] = tm_->tm_sec;
data[6] = (tm_->tm_wday + 6) % 7; // Bosch counts from Mo, time from Su data[6] = (tm_->tm_wday + 6) % 7; // Bosch counts from Mo, time from Su
data[7] = (id == 0) ? 2 : tm_->tm_isdst + 2; // set DST and flag for ext. clock data[7] = (id == 0) ? 2 : tm_->tm_isdst + 2; // set DST and flag for ext. clock
if (model() == EMSdevice::EMS_DEVICE_FLAG_JUNKERS) { } else if (dt.length() == 23 || dt.length() == 21) {
data[7] = 0;
}
} else if (dt.length() == 23) {
data[0] = (dt[7] - '0') * 100 + (dt[8] - '0') * 10 + (dt[9] - '0'); // year data[0] = (dt[7] - '0') * 100 + (dt[8] - '0') * 10 + (dt[9] - '0'); // year
data[1] = (dt[3] - '0') * 10 + (dt[4] - '0'); // month data[1] = (dt[3] - '0') * 10 + (dt[4] - '0'); // month
data[2] = (dt[11] - '0') * 10 + (dt[12] - '0'); // hour data[2] = (dt[11] - '0') * 10 + (dt[12] - '0'); // hour
@@ -3087,10 +3114,35 @@ bool Thermostat::set_datetime(const char * value, const int8_t id) {
data[4] = (dt[14] - '0') * 10 + (dt[15] - '0'); // min data[4] = (dt[14] - '0') * 10 + (dt[15] - '0'); // min
data[5] = (dt[17] - '0') * 10 + (dt[18] - '0'); // sec data[5] = (dt[17] - '0') * 10 + (dt[18] - '0'); // sec
data[6] = (dt[20] - '0'); // day of week, Mo:0 data[6] = (dt[20] - '0'); // day of week, Mo:0
data[7] = (dt[22] - '0') + 2; // DST and flag data[7] = dt.length() == 21 ? 2 : (dt[22] - '0') + 2; // DST and flag
if (model() == EMSdevice::EMS_DEVICE_FLAG_JUNKERS) { } else if (dt.length() == 19 || dt.length() == 16) {
data[7] = 0; time_t now = time(nullptr);
} tm * tm_ = localtime(&now);
data[0] = (dt[7] - '0') * 100 + (dt[8] - '0') * 10 + (dt[9] - '0'); // year
tm_->tm_year = 100 + data[0];
data[1] = (dt[3] - '0') * 10 + (dt[4] - '0'); // month
tm_->tm_mon = data[1] - 1;
tm_->tm_hour = data[2] = (dt[11] - '0') * 10 + (dt[12] - '0'); // hour
tm_->tm_mday = data[3] = (dt[0] - '0') * 10 + (dt[1] - '0'); // day
tm_->tm_min = data[4] = (dt[14] - '0') * 10 + (dt[15] - '0'); // min
tm_->tm_sec = data[5] = dt.length() == 16 ? 0 : (dt[17] - '0') * 10 + (dt[18] - '0'); // sec
tm_->tm_isdst = -1;
// use thermostat time zone to determine day of week and daylight saving
EMSESP::esp32React.getNTPSettingsService()->read([&](const NTPSettings & settings) {
if (settings.thermostat_sync == 2) {
setenv("TZ", settings.tzFormatT.c_str(), 1);
tzset();
auto t = mktime(tm_);
tm_ = localtime(&t);
setenv("TZ", settings.tzFormat.c_str(), 1);
tzset();
} else {
auto t = mktime(tm_);
tm_ = localtime(&t);
}
});
data[6] = (tm_->tm_wday + 6) % 7; // Bosch counts from Mo, time from Su
data[7] = tm_->tm_isdst + 2; // set DST and flag for ext. clock
} else { } else {
LOG_WARNING("Set date: invalid data, wrong length"); LOG_WARNING("Set date: invalid data, wrong length");
return false; return false;
@@ -3101,7 +3153,8 @@ bool Thermostat::set_datetime(const char * value, const int8_t id) {
} }
if (model() == EMSdevice::EMS_DEVICE_FLAG_JUNKERS) { if (model() == EMSdevice::EMS_DEVICE_FLAG_JUNKERS) {
data[6]++; // Junkers use 1-7 for day of the week data[6]++; // Junkers use 1-7 for day of the week
data[7] = 0; // no dst setting
} }
// LOG_INFO("Setting date and time: %02d.%02d.2%03d-%02d:%02d:%02d-%d-%d", data[3], data[1], data[0], data[2], data[4], data[5], data[6], data[7]); // LOG_INFO("Setting date and time: %02d.%02d.2%03d-%02d:%02d:%02d-%d-%d", data[3], data[1], data[0], data[2], data[4], data[5], data[6], data[7]);
+5 -1
View File
@@ -361,7 +361,11 @@ bool Water::set_wwCircTc(const char * value, const int8_t id) {
if (!Helpers::value2bool(value, b)) { if (!Helpers::value2bool(value, b)) {
return false; return false;
} }
write_command(0x7A5, 4, b ? 0xFF : 0x00, 0x7A5); if (flags() == EMSdevice::EMS_DEVICE_FLAG_MMPLUS) {
write_command(0x33B + dhw_, 4, b ? 0x01 : 0x00, 0x33B + dhw_);
} else { // SM100
write_command(0x7A5, 4, b ? 0xFF : 0x00, 0x7A5);
}
return true; return true;
} }
+1 -1
View File
@@ -1 +1 @@
#define EMSESP_APP_VERSION "3.9.0-dev.16" #define EMSESP_APP_VERSION "3.9.0-dev.17"
+7
View File
@@ -39,6 +39,12 @@ WebAPIService::WebAPIService(AsyncWebServer * server, SecurityManager * security
// POST|GET api/{device} // POST|GET api/{device}
// POST|GET api/{device}/{entity} // POST|GET api/{device}/{entity}
void WebAPIService::webAPIService(AsyncWebServerRequest * request, JsonVariant json) { void WebAPIService::webAPIService(AsyncWebServerRequest * request, JsonVariant json) {
static bool busy = false;
if (busy && request->method() == HTTP_GET) {
request->send(429); // too many requests
return;
}
busy = true;
JsonDocument input_doc; // has no body JSON so create dummy as empty input object JsonDocument input_doc; // has no body JSON so create dummy as empty input object
JsonObject input; JsonObject input;
// if no body then treat it as a secure GET // if no body then treat it as a secure GET
@@ -53,6 +59,7 @@ void WebAPIService::webAPIService(AsyncWebServerRequest * request, JsonVariant j
input["data"] = json.as<std::string>(); input["data"] = json.as<std::string>();
} }
parse(request, input); parse(request, input);
busy = false;
} }
// for POSTS accepting plain text data // for POSTS accepting plain text data
+8 -9
View File
@@ -372,15 +372,14 @@ std::string WebCommandService::get_metrics_prometheus() {
} }
void WebCommandService::get_value_json(JsonObject output, const CommandItem & ci) { void WebCommandService::get_value_json(JsonObject output, const CommandItem & ci) {
output["name"] = (const char *)ci.name; output["name"] = (const char *)ci.name;
// output["fullname"] = (const char *)ci.name; output["fullname"] = (const char *)ci.name;
// output["type"] = "command"; output["type"] = "string";
output["command"] = ci.cmd; output["command"] = ci.cmd;
output["value"] = ci.value; output["value"] = ci.value;
// bool hasName = ci.name[0] != '\0'; output["readable"] = true;
// output["readable"] = hasName; output["writeable"] = true;
// output["writeable"] = hasName; output["visible"] = true;
// output["visible"] = hasName;
} }
void WebCommandService::publish(const bool force) { void WebCommandService::publish(const bool force) {
+5 -3
View File
@@ -77,7 +77,9 @@ StateUpdateResult WebCustomEntity::update(JsonObject root, WebCustomEntity & web
if (entityItem.ram == 2) { // NVS if (entityItem.ram == 2) { // NVS
char key[sizeof(entityItem.name) + 2]; char key[sizeof(entityItem.name) + 2];
snprintf(key, sizeof(key), "c:%s", entityItem.name); snprintf(key, sizeof(key), "c:%s", entityItem.name);
EMSESP::nvs_.remove(key); if (EMSESP::nvs_.isKey(key)) {
EMSESP::nvs_.remove(key);
}
} }
if (entityItem.ram) { // save name/value pairs for change checking if (entityItem.ram) { // save name/value pairs for change checking
doc[entityItem.name] = entityItem.value; doc[entityItem.name] = entityItem.value;
@@ -419,9 +421,9 @@ void WebCustomEntityService::get_value_json(JsonObject output, CustomEntityItem
output["fullname"] = (const char *)entity.name; output["fullname"] = (const char *)entity.name;
output["storage"] = entity.ram == 1 ? "ram" : entity.ram == 2 ? "nvs" : "ems"; output["storage"] = entity.ram == 1 ? "ram" : entity.ram == 2 ? "nvs" : "ems";
output["type"] = entity.value_type == DeviceValueType::BOOL ? "boolean" : entity.value_type == DeviceValueType::STRING ? "string" : F_(number); output["type"] = entity.value_type == DeviceValueType::BOOL ? "boolean" : entity.value_type == DeviceValueType::STRING ? "string" : F_(number);
// output["readable"] = true; output["readable"] = true;
output["writeable"] = entity.writeable; output["writeable"] = entity.writeable;
// output["visible"] = true; output["visible"] = true;
if (entity.ram == 0) { if (entity.ram == 0) {
output["device_id"] = Helpers::hextoa(entity.device_id); output["device_id"] = Helpers::hextoa(entity.device_id);
+7 -4
View File
@@ -214,10 +214,9 @@ void WebSchedulerService::get_value_json(JsonObject output, const ScheduleItem &
output["time"] = scheduleItem.time; output["time"] = scheduleItem.time;
} }
output["cmd_name"] = scheduleItem.cmd_name; output["cmd_name"] = scheduleItem.cmd_name;
// bool hasName = scheduleItem.name[0] != '\0'; output["readable"] = true;
// output["readable"] = hasName; output["writeable"] = true;
// output["writeable"] = hasName; output["visible"] = true;
// output["visible"] = hasName;
} }
// publish single value // publish single value
@@ -370,6 +369,10 @@ void WebSchedulerService::loop() {
if (scheduleItems_->empty()) { if (scheduleItems_->empty()) {
return; return;
} }
// do not execute any command in the first 60 secondes
if (uuid::get_uptime_sec() < 60) {
return;
}
// check if we have onChange events // check if we have onChange events
while (!cmd_changed_.empty()) { while (!cmd_changed_.empty()) {
+1
View File
@@ -247,6 +247,7 @@ void WebStatusService::action(AsyncWebServerRequest * request, JsonVariant json)
if (!ok) { if (!ok) {
EMSESP::logger().err("Action '%s' failed", action.c_str()); EMSESP::logger().err("Action '%s' failed", action.c_str());
request->send(400); // bad request request->send(400); // bad request
delete response;
return; return;
} }