diff --git a/CHANGELOG_LATEST.md b/CHANGELOG_LATEST.md index 915a07cf9..05949a47f 100644 --- a/CHANGELOG_LATEST.md +++ b/CHANGELOG_LATEST.md @@ -21,3 +21,4 @@ For more details go to [emsesp.org](https://emsesp.org/). - call compute value direct, enlarge TCP stack [#3127](https://github.com/emsesp/EMS-ESP32/discussions/3127) - 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 no sync. diff --git a/interface/src/app/settings/NTPSettings.tsx b/interface/src/app/settings/NTPSettings.tsx index ff517a266..51cd83157 100644 --- a/interface/src/app/settings/NTPSettings.tsx +++ b/interface/src/app/settings/NTPSettings.tsx @@ -8,6 +8,7 @@ import { Box, Button, Checkbox, + Grid, Dialog, DialogActions, DialogContent, @@ -39,7 +40,7 @@ import { formatLocalDateTime, updateValueDirty, useRest } from 'utils'; import { ValidationError, validate } from 'validators'; import { NTP_SETTINGS_VALIDATOR } from 'validators/ntp'; -import { TIME_ZONES, selectedTimeZone, useTimeZoneSelectItems } from './TZ'; +import { TIME_ZONES, selectedTimeZone, useTimeZoneSelectItems, timeZoneSelectItemsT } from './TZ'; const NTPSettings = () => { const { @@ -63,13 +64,17 @@ const NTPSettings = () => { // Memoized timezone select items for better performance const timeZoneItems = useTimeZoneSelectItems(); + const timeZoneItemsT = timeZoneSelectItemsT(); // Memoized selected timezone value const selectedTzValue = useMemo( () => (data ? selectedTimeZone(data.tz_label, data.tz_format) : undefined), [data?.tz_label, data?.tz_format] ); - +const selectedTzValueT = useMemo( + () => (data ? selectedTimeZone(data.tz_label_t, data.tz_format_t) : undefined), + [data?.tz_label_t, data?.tz_format_t] + ); const [localTime, setLocalTime] = useState(''); const [settingTime, setSettingTime] = useState(false); const [processing, setProcessing] = useState(false); @@ -149,6 +154,18 @@ const NTPSettings = () => { }, [updateFormValue] ); + +const changeTimeZoneT = useCallback( + (event: React.ChangeEvent) => { + void updateState(readNTPSettings(), (settings: NTPSettingsType) => ({ + ...settings, + tz_label_t: event.target.value, + tz_format_t: TIME_ZONES[event.target.value] + })); + updateFormValue(event); + }, + [updateFormValue] + ); // Memoize render content to prevent unnecessary re-renders const renderContent = useMemo(() => { @@ -174,6 +191,7 @@ const NTPSettings = () => { label={LL.NTP_SERVER()} fullWidth variant="outlined" + disabled={!data.enabled} value={data.server} onChange={updateFormValue} margin="normal" @@ -192,7 +210,45 @@ const NTPSettings = () => { {LL.TIME_ZONE()}... {timeZoneItems} - + {data.enabled && ( + + + + {LL.OFF()} + {LL.ON()} + {LL.USE()} {LL.TIME_ZONE()} + + + + {data.thermostat_option === 2 && ( + + {LL.TIME_ZONE()}... + {timeZoneItemsT} + + )} + + + )} {!data.enabled && !dirtyFlags.length && ( @@ -243,6 +299,7 @@ const NTPSettings = () => { updateFormValue, fieldErrors, selectedTzValue, + selectedTzValueT, changeTimeZone, timeZoneItems, dirtyFlags, diff --git a/interface/src/app/settings/TZ.tsx b/interface/src/app/settings/TZ.tsx index c734f1809..2e9e86190 100644 --- a/interface/src/app/settings/TZ.tsx +++ b/interface/src/app/settings/TZ.tsx @@ -495,3 +495,7 @@ const precomputedTimeZoneItems = TIME_ZONE_LABELS.map((label) => ( export function timeZoneSelectItems() { return precomputedTimeZoneItems; } + +export function timeZoneSelectItemsT() { + return precomputedTimeZoneItems; +} diff --git a/interface/src/types/ntp.ts b/interface/src/types/ntp.ts index 8f86303b6..075f41a73 100644 --- a/interface/src/types/ntp.ts +++ b/interface/src/types/ntp.ts @@ -16,6 +16,9 @@ export interface NTPSettingsType { server: string; tz_label: string; tz_format: string; + thermostat_option: number; + tz_label_t: string; + tz_format_t: string; } export interface Time { diff --git a/src/ESP32React/NTPSettingsService.cpp b/src/ESP32React/NTPSettingsService.cpp index 81e5cdf82..6ea3965c6 100644 --- a/src/ESP32React/NTPSettingsService.cpp +++ b/src/ESP32React/NTPSettingsService.cpp @@ -86,16 +86,22 @@ void NTPSettingsService::ntp_received(struct timeval * tv) { } void NTPSettings::read(NTPSettings & settings, JsonObject root) { - root["enabled"] = settings.enabled; - root["server"] = settings.server; - root["tz_label"] = settings.tzLabel; - root["tz_format"] = settings.tzFormat; + root["enabled"] = settings.enabled; + root["server"] = settings.server; + root["tz_label"] = settings.tzLabel; + root["tz_format"] = settings.tzFormat; + root["thermostat_option"] = settings.thermostat_option; + root["tz_label_t"] = settings.tzLabelT; + root["tz_format_t"] = settings.tzFormatT; } StateUpdateResult NTPSettings::update(JsonObject root, NTPSettings & settings) { - settings.enabled = root["enabled"] | FACTORY_NTP_ENABLED; - settings.server = root["server"] | FACTORY_NTP_SERVER; - settings.tzLabel = root["tz_label"] | FACTORY_NTP_TIME_ZONE_LABEL; - settings.tzFormat = root["tz_format"] | FACTORY_NTP_TIME_ZONE_FORMAT; + settings.enabled = root["enabled"] | FACTORY_NTP_ENABLED; + settings.server = root["server"] | FACTORY_NTP_SERVER; + settings.tzLabel = root["tz_label"] | FACTORY_NTP_TIME_ZONE_LABEL; + settings.tzFormat = root["tz_format"] | FACTORY_NTP_TIME_ZONE_FORMAT; + settings.thermostat_option = root["thermostat_option"] | 0; + settings.tzLabelT = root["tz_label_t"] | settings.tzLabel; + settings.tzFormatT = root["tz_format_t"] | settings.tzFormat; return StateUpdateResult::CHANGED; -} \ No newline at end of file +} diff --git a/src/ESP32React/NTPSettingsService.h b/src/ESP32React/NTPSettingsService.h index 635b68f1c..ede1997a2 100644 --- a/src/ESP32React/NTPSettingsService.h +++ b/src/ESP32React/NTPSettingsService.h @@ -30,10 +30,13 @@ class NTPSettings { public: - bool enabled = FACTORY_NTP_ENABLED; - String tzLabel = FACTORY_NTP_TIME_ZONE_LABEL; - String tzFormat = FACTORY_NTP_TIME_ZONE_FORMAT; - String server = FACTORY_NTP_SERVER; + bool enabled = FACTORY_NTP_ENABLED; + String tzLabel = FACTORY_NTP_TIME_ZONE_LABEL; + String tzFormat = FACTORY_NTP_TIME_ZONE_FORMAT; + String server = FACTORY_NTP_SERVER; + uint8_t thermostat_option = 0; + String tzLabelT = FACTORY_NTP_TIME_ZONE_LABEL; + String tzFormatT = FACTORY_NTP_TIME_ZONE_FORMAT; static void read(NTPSettings & settings, JsonObject root); static StateUpdateResult update(JsonObject root, NTPSettings & settings); diff --git a/src/devices/thermostat.cpp b/src/devices/thermostat.cpp index ad27fdc76..428d0eff6 100644 --- a/src/devices/thermostat.cpp +++ b/src/devices/thermostat.cpp @@ -1737,9 +1737,27 @@ void Thermostat::process_RCTime(std::shared_ptr telegram) { } // check clock - time_t now = time(nullptr); - tm * tm_ = localtime(&now); - bool tset_ = tm_->tm_year > 110; // year 2010 and up, time is valid + time_t now = time(nullptr); + tm * tm_; + bool sync = true; + EMSESP::esp32React.getNTPSettingsService()->read([&](const NTPSettings & settings) { + switch (settings.thermostat_option) { + default: + sync = false; + break; + case 1: + tm_ = localtime(&now); + break; + case 2: + setenv("TZ", settings.tzFormatT.c_str(), 1); + tzset(); + tm_ = localtime(&now); + setenv("TZ", settings.tzFormat.c_str(), 1); + tzset(); + break; + } + }); + 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_mon = telegram->message_data[1] - 1; tm_->tm_mday = telegram->message_data[3]; @@ -1756,11 +1774,12 @@ void Thermostat::process_RCTime(std::shared_ptr telegram) { has_update(dateTime_, newdatetime, sizeof(dateTime_)); bool ivtclock = (telegram->message_data[0] & 0x80) == 0x80; // dont sync ivt-clock, #439 - bool junkersclock = model() == EMSdevice::EMS_DEVICE_FLAG_JUNKERS; + // bool junkersclock = model() == EMSdevice::EMS_DEVICE_FLAG_JUNKERS; time_t ttime = mktime(tm_); // thermostat time // 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_)) { - double difference = difftime(now, ttime); + // if (sync && !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_)) { + int difference = difftime(now, ttime); if (difference > 15 || difference < -15) { if (setTimeRetry < 3) { if (!use_dst) { @@ -3064,7 +3083,21 @@ bool Thermostat::set_datetime(const char * value, const int8_t id) { uint8_t data[9]; if (dt == "ntp") { time_t now = time(nullptr); - tm * tm_ = localtime(&now); + tm * tm_; + EMSESP::esp32React.getNTPSettingsService()->read([&](const NTPSettings & settings) { + switch (settings.thermostat_option) { + default: + tm_ = localtime(&now); + break; + case 2: + setenv("TZ", settings.tzFormatT.c_str(), 1); + tzset(); + tm_ = localtime(&now); + setenv("TZ", settings.tzFormat.c_str(), 1); + tzset(); + break; + } + }); if (tm_->tm_year < 110) { // no valid time return false; } @@ -3080,10 +3113,7 @@ bool Thermostat::set_datetime(const char * value, const int8_t id) { data[5] = tm_->tm_sec; 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 - if (model() == EMSdevice::EMS_DEVICE_FLAG_JUNKERS) { - data[7] = 0; - } - } else if (dt.length() == 23) { + } else if (dt.length() == 23 || dt.length() == 21) { 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[2] = (dt[11] - '0') * 10 + (dt[12] - '0'); // hour @@ -3091,10 +3121,23 @@ bool Thermostat::set_datetime(const char * value, const int8_t id) { data[4] = (dt[14] - '0') * 10 + (dt[15] - '0'); // min data[5] = (dt[17] - '0') * 10 + (dt[18] - '0'); // sec data[6] = (dt[20] - '0'); // day of week, Mo:0 - data[7] = (dt[22] - '0') + 2; // DST and flag - if (model() == EMSdevice::EMS_DEVICE_FLAG_JUNKERS) { - data[7] = 0; - } + data[7] = dt.length() == 21 ? 2 : (dt[22] - '0') + 2; // DST and flag + } else if (dt.length() == 19 || dt.length() == 16) { + 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; + 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 { LOG_WARNING("Set date: invalid data, wrong length"); return false; @@ -3105,7 +3148,8 @@ bool Thermostat::set_datetime(const char * value, const int8_t id) { } 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]); diff --git a/src/emsesp_version.h b/src/emsesp_version.h index c31287b18..110b4c43a 100644 --- a/src/emsesp_version.h +++ b/src/emsesp_version.h @@ -1 +1 @@ -#define EMSESP_APP_VERSION "3.8.3-dev.9" +#define EMSESP_APP_VERSION "3.8.3-dev.10"