Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions Data/system/timezones.csv
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@
"Africa/Brazzaville","WAT-1"
"Africa/Bujumbura","CAT-2"
"Africa/Cairo","EET-2EEST,M4.5.5/0,M10.5.4/24"
"Africa/Casablanca","<+01>-1"
"Africa/Casablanca","<+00>0"
"Africa/Ceuta","CET-1CEST,M3.5.0,M10.5.0/3"
"Africa/Conakry","GMT0"
"Africa/Dakar","GMT0"
"Africa/Dar_es_Salaam","EAT-3"
"Africa/Djibouti","EAT-3"
"Africa/Douala","WAT-1"
"Africa/El_Aaiun","<+01>-1"
"Africa/El_Aaiun","<+00>0"
"Africa/Freetown","GMT0"
"Africa/Gaborone","CAT-2"
"Africa/Harare","CAT-2"
Expand Down Expand Up @@ -68,7 +68,7 @@
"America/Argentina/Tucuman","<-03>3"
"America/Argentina/Ushuaia","<-03>3"
"America/Aruba","AST4"
"America/Asuncion","<-04>4<-03>,M10.1.0/0,M3.4.0/0"
"America/Asuncion","<-03>3"
"America/Atikokan","EST5"
"America/Bahia","<-03>3"
"America/Bahia_Banderas","CST6"
Expand Down Expand Up @@ -97,7 +97,7 @@
"America/Denver","MST7MDT,M3.2.0,M11.1.0"
"America/Detroit","EST5EDT,M3.2.0,M11.1.0"
"America/Dominica","AST4"
"America/Edmonton","MST7MDT,M3.2.0,M11.1.0"
"America/Edmonton","CST6"
"America/Eirunepe","<-05>5"
"America/El_Salvador","CST6"
"America/Fortaleza","<-03>3"
Expand All @@ -122,7 +122,7 @@
"America/Indiana/Vevay","EST5EDT,M3.2.0,M11.1.0"
"America/Indiana/Vincennes","EST5EDT,M3.2.0,M11.1.0"
"America/Indiana/Winamac","EST5EDT,M3.2.0,M11.1.0"
"America/Inuvik","MST7MDT,M3.2.0,M11.1.0"
"America/Inuvik","CST6"
"America/Iqaluit","EST5EDT,M3.2.0,M11.1.0"
"America/Jamaica","EST5"
"America/Juneau","AKST9AKDT,M3.2.0,M11.1.0"
Expand Down Expand Up @@ -169,7 +169,7 @@
"America/Porto_Velho","<-04>4"
"America/Puerto_Rico","AST4"
"America/Punta_Arenas","<-03>3"
"America/Rainy_River","CST6CDT,M3.2.0,M11.1.0"
"America/Rainy_River","EST5"
"America/Rankin_Inlet","CST6CDT,M3.2.0,M11.1.0"
"America/Recife","<-03>3"
"America/Regina","CST6"
Expand All @@ -194,11 +194,11 @@
"America/Tijuana","PST8PDT,M3.2.0,M11.1.0"
"America/Toronto","EST5EDT,M3.2.0,M11.1.0"
"America/Tortola","AST4"
"America/Vancouver","PST8PDT,M3.2.0,M11.1.0"
"America/Vancouver","MST7"
"America/Whitehorse","MST7"
"America/Winnipeg","CST6CDT,M3.2.0,M11.1.0"
"America/Winnipeg","EST5"
"America/Yakutat","AKST9AKDT,M3.2.0,M11.1.0"
"America/Yellowknife","MST7MDT,M3.2.0,M11.1.0"
"America/Yellowknife","CST6"
"Antarctica/Casey","<+08>-8"
"Antarctica/Davis","<+07>-7"
"Antarctica/DumontDUrville","<+10>-10"
Expand Down Expand Up @@ -326,7 +326,7 @@
"Europe/Bucharest","EET-2EEST,M3.5.0/3,M10.5.0/4"
"Europe/Budapest","CET-1CEST,M3.5.0,M10.5.0/3"
"Europe/Busingen","CET-1CEST,M3.5.0,M10.5.0/3"
"Europe/Chisinau","EET-2EEST,M3.5.0,M10.5.0/3"
"Europe/Chisinau","EET-2EEST,M3.5.0/3,M10.5.0/4"
"Europe/Copenhagen","CET-1CEST,M3.5.0,M10.5.0/3"
"Europe/Dublin","IST-1GMT0,M10.5.0,M3.5.0/1"
"Europe/Gibraltar","CET-1CEST,M3.5.0,M10.5.0/3"
Expand Down
23 changes: 22 additions & 1 deletion Tactility/Private/Tactility/settings/TimePrivate.h
Original file line number Diff line number Diff line change
@@ -1,7 +1,28 @@
#pragma once

#include <string>
#include <string_view>

namespace tt::settings {

/** Applies the saved time zone, first updating its code to the bundled timezones.csv row for its name */
void initTimeZone();

}
/**
* Find the code of a time zone in the bundled timezones.csv
* @param[in] name the time zone name, e.g. "Europe/Amsterdam"
* @param[out] outCode the code, set only when found
* @return true when found
*/
bool findTimeZoneCode(const std::string& name, std::string& outCode);

/**
* Find the code of a time zone in CSV text with the format of timezones.csv (LF or CRLF line endings)
* @param[in] csv the CSV text
* @param[in] name the time zone name, e.g. "Europe/Amsterdam"
* @param[out] outCode the code, set only when found
* @return true when found
*/
bool findTimeZoneCode(std::string_view csv, const std::string& name, std::string& outCode);

} // namespace tt::settings
52 changes: 51 additions & 1 deletion Tactility/Source/settings/time.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@

#include <lvgl/widgets/statusbar.h>

#include <generated_assets.h>

#include <string_view>

#ifdef ESP_PLATFORM
#include <ctime>
#endif
Expand All @@ -37,11 +41,57 @@ bool getPreferencesPath(std::string& outPath) {
return true;
}

void saveTimeZoneCode(const std::string& code) {
std::string path;
if (getPreferencesPath(path)) {
Preferences* preferences = preferences_open(path.c_str());
if (preferences != nullptr) {
preferences_put_string(preferences, TIMEZONE_PREFERENCES_KEY_CODE, code.c_str());
preferences_close(preferences);
}
}
}

} // namespace

bool findTimeZoneCode(std::string_view csv, const std::string& name, std::string& outCode) {
// Lines look like "Europe/Amsterdam","CET-1CEST,M3.5.0,M10.5.0/3"
const std::string prefix = "\"" + name + "\",\"";
size_t start = 0;
while (start < csv.size()) {
size_t end = csv.find('\n', start);
if (end == std::string_view::npos) {
end = csv.size();
}
auto line = csv.substr(start, end - start);
if (line.ends_with('\r')) {
line.remove_suffix(1); // CRLF line ending
}
if (line.starts_with(prefix) && line.size() > prefix.size() && line.ends_with('"')) {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
outCode = line.substr(prefix.size(), line.size() - prefix.size() - 1);
return true;
}
start = end + 1;
}
return false;
}

bool findTimeZoneCode(const std::string& name, std::string& outCode) {
const std::string_view csv(reinterpret_cast<const char*>(asset_timezones_csv), asset_timezones_csv_size);
return findTimeZoneCode(csv, name, outCode);
}

void initTimeZone() {
auto code = getTimeZoneCode();
// The saved code is a copy of the timezones.csv row from when the time zone was picked. When a
// country changes its rules, a firmware update ships a new row, so use that for the same name.
std::string bundled_code;
if (hasTimeZone() && findTimeZoneCode(getTimeZoneName(), bundled_code) && bundled_code != code) {
LOG_I(TAG, "Updating the time zone code from %s to %s", code.c_str(), bundled_code.c_str());
saveTimeZoneCode(bundled_code);
code = bundled_code;
}
#ifdef ESP_PLATFORM
auto code= getTimeZoneCode();
if (!code.empty()) {
setenv("TZ", code.c_str(), 1);
tzset();
Expand Down
81 changes: 81 additions & 0 deletions Tactility/Tests/Source/TimeZoneTest.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
#include "doctest.h"

#include <Tactility/settings/Time.h>
#include <Tactility/settings/TimePrivate.h>

#include <tactility/paths.h>
#include <tactility/preferences.h>

#include <string>
#include <string_view>

using namespace tt;

namespace {

/** Writes the time zone like an older firmware did, without the radio region and event of setTimeZone() */
void saveTimeZone(const char* name, const char* code) {
char root[128];
REQUIRE(paths_get_data_path(root, sizeof(root)) == ERROR_NONE);
const std::string path = std::string(root) + "/settings/time.properties";
Preferences* preferences = preferences_open(path.c_str());
REQUIRE(preferences != nullptr);
preferences_put_string(preferences, "tz_name", name);
preferences_put_string(preferences, "tz_code", code);
preferences_close(preferences);
}

} // namespace

TEST_CASE("findTimeZoneCode finds the bundled code of a time zone") {
std::string code;
CHECK(settings::findTimeZoneCode("Europe/Amsterdam", code));
CHECK_EQ(code, "CET-1CEST,M3.5.0,M10.5.0/3");
CHECK(settings::findTimeZoneCode("Africa/Abidjan", code)); // first row
CHECK_EQ(code, "GMT0");
CHECK(settings::findTimeZoneCode("Etc/Zulu", code)); // last row
CHECK_EQ(code, "UTC0");
}

TEST_CASE("findTimeZoneCode only matches whole names") {
std::string code = "unchanged";
CHECK_FALSE(settings::findTimeZoneCode("Europe/Amster", code));
CHECK_FALSE(settings::findTimeZoneCode("Amsterdam", code));
CHECK_FALSE(settings::findTimeZoneCode("", code));
CHECK_FALSE(settings::findTimeZoneCode("Mars/Olympus_Mons", code));
CHECK_EQ(code, "unchanged");
CHECK(settings::findTimeZoneCode("Etc/GMT+1", code));
CHECK_EQ(code, "<-01>1");
}

TEST_CASE("findTimeZoneCode accepts CRLF line endings") {
const std::string_view csv = "\"Africa/Abidjan\",\"GMT0\"\r\n\"America/Winnipeg\",\"EST5\"\r\n\"Etc/Zulu\",\"UTC0\"\r";
std::string code;
CHECK(settings::findTimeZoneCode(csv, "Africa/Abidjan", code));
CHECK_EQ(code, "GMT0");
CHECK(settings::findTimeZoneCode(csv, "America/Winnipeg", code));
CHECK_EQ(code, "EST5");
CHECK(settings::findTimeZoneCode(csv, "Etc/Zulu", code)); // last row, no line feed
CHECK_EQ(code, "UTC0");
}

TEST_CASE("initTimeZone replaces an outdated saved code with the bundled one") {
saveTimeZone("America/Winnipeg", "CST6CDT,M3.2.0,M11.1.0");
settings::initTimeZone();
std::string bundled;
REQUIRE(settings::findTimeZoneCode("America/Winnipeg", bundled));
CHECK_EQ(bundled, "EST5"); // CST all year from 1 November 2026 (IANA tz 2026e)
CHECK_EQ(settings::getTimeZoneCode(), bundled);
CHECK_EQ(settings::getTimeZoneName(), "America/Winnipeg");
}

TEST_CASE("initTimeZone keeps a saved code that is current or not in the list") {
saveTimeZone("Europe/Amsterdam", "CET-1CEST,M3.5.0,M10.5.0/3");
settings::initTimeZone();
CHECK_EQ(settings::getTimeZoneCode(), "CET-1CEST,M3.5.0,M10.5.0/3");

saveTimeZone("Custom/Zone", "<+0530>-5:30");
settings::initTimeZone();
CHECK_EQ(settings::getTimeZoneCode(), "<+0530>-5:30");
CHECK_EQ(settings::getTimeZoneName(), "Custom/Zone");
}
Loading