5 Commits

Author SHA1 Message Date
Yurii
1903ee2bc7 chore: bump version to 1.5.3 2025-02-02 08:09:20 +03:00
Yurii
916a710064 refactor: improved connection to wifi 2025-01-30 21:46:06 +03:00
Yurii
38acae417d refactor: reworked BLE sensors
* add clean unused ble instances
* moved subscribe to notify to another method
* set date/time on BLE sensors
2025-01-30 01:46:55 +03:00
Yurii
3bc9fa81a8 feat: added ntp server and timezone settings 2025-01-30 01:25:05 +03:00
Yurii
cc2d6ef385 fix: increase keep alive timeout for mqtt #115 2025-01-24 21:07:08 +03:00
14 changed files with 794 additions and 76 deletions

View File

@@ -34,6 +34,10 @@ let paths = {
{
src: 'src_data/locales/*.json',
dest: 'data/static/locales/'
},
{
src: 'src_data/*.json',
dest: 'data/static/'
}
],
static: [

View File

@@ -148,10 +148,10 @@ namespace NetworkUtils {
// set policy manual for work 13 ch
{
#ifdef ARDUINO_ARCH_ESP8266
wifi_country_t country = {"CN", 1, 13, WIFI_COUNTRY_POLICY_AUTO};
wifi_country_t country = {"JP", 1, 14, WIFI_COUNTRY_POLICY_MANUAL};
wifi_set_country(&country);
#elif defined(ARDUINO_ARCH_ESP32)
const wifi_country_t country = {"CN", 1, 13, CONFIG_ESP32_PHY_MAX_WIFI_TX_POWER, WIFI_COUNTRY_POLICY_AUTO};
const wifi_country_t country = {"JP", 1, 14, CONFIG_ESP32_PHY_MAX_WIFI_TX_POWER, WIFI_COUNTRY_POLICY_MANUAL};
esp_wifi_set_country(&country);
#endif
}
@@ -384,7 +384,7 @@ namespace NetworkUtils {
protected:
const unsigned int reconnectInterval = 15000;
const unsigned int failedConnectTimeout = 185000;
const unsigned int connectionTimeout = 5000;
const unsigned int connectionTimeout = 10000;
const unsigned int resetConnectionTimeout = 90000;
YieldCallback yieldCallback = []() {

View File

@@ -14,7 +14,7 @@ extra_configs = secrets.default.ini
core_dir = .pio
[env]
version = 1.5.2
version = 1.5.3
framework = arduino
lib_deps =
bblanchon/ArduinoJson@^7.3.0

View File

@@ -38,6 +38,7 @@ protected:
unsigned long heatingDisabledTime = 0;
PumpStartReason extPumpStartReason = PumpStartReason::NONE;
unsigned long externalPumpStartTime = 0;
bool ntpStarted = false;
bool telnetStarted = false;
bool emergencyDetected = false;
unsigned long emergencyFlipTime = 0;
@@ -109,6 +110,16 @@ protected:
}
if (network->isConnected()) {
if (!this->ntpStarted) {
if (strlen(settings.system.ntp.server)) {
configTime(0, 0, settings.system.ntp.server);
setenv("TZ", settings.system.ntp.timezone, 1);
tzset();
this->ntpStarted = true;
}
}
if (!this->telnetStarted && telnetStream != nullptr) {
telnetStream->begin(23, false);
this->telnetStarted = true;
@@ -124,6 +135,10 @@ protected:
Sensors::setConnectionStatusByType(Sensors::Type::MANUAL, !settings.mqtt.enabled || vars.mqtt.connected, false);
} else {
if (this->ntpStarted) {
this->ntpStarted = false;
}
if (this->telnetStarted) {
telnetStream->stop();
this->telnetStarted = false;

View File

@@ -120,7 +120,6 @@ protected:
#endif
// client settings
this->client->setKeepAliveInterval(15000);
this->client->setTxPayloadSize(256);
#ifdef ARDUINO_ARCH_ESP8266
this->client->setConnectionTimeout(1000);
@@ -199,6 +198,7 @@ protected:
this->haHelper->updateCachedTopics();
this->client->stop();
this->client->setKeepAliveInterval(settings.mqtt.interval * 10000);
this->client->setId(networkSettings.hostname);
this->client->setUsernamePassword(settings.mqtt.user, settings.mqtt.password);
this->client->beginWill(this->haHelper->getDeviceTopic(F("status")).c_str(), 7, true, 1);

View File

@@ -31,12 +31,19 @@ protected:
const unsigned short dallasSearchInterval = 60000;
const unsigned short dallasPollingInterval = 10000;
const unsigned short globalPollingInterval = 15000;
#if USE_BLE
const unsigned int bleSetDtInterval = 7200000;
#endif
std::unordered_map<uint8_t, OneWire> owInstances;
std::unordered_map<uint8_t, DallasTemperature> dallasInstances;
std::unordered_map<uint8_t, unsigned long> dallasSearchTime;
std::unordered_map<uint8_t, bool> dallasPolling;
std::unordered_map<uint8_t, unsigned long> dallasLastPollingTime;
#if USE_BLE
std::unordered_map<uint8_t, bool> bleSubscribed;
std::unordered_map<uint8_t, unsigned long> bleLastSetDtTime;
#endif
unsigned long globalLastPollingTime = 0;
#if defined(ARDUINO_ARCH_ESP32)
@@ -83,8 +90,11 @@ protected:
pollingNtcSensors();
this->yield();
#if USE_BLE
cleanBleInstances();
pollingBleSensors();
this->yield();
#endif
this->globalLastPollingTime = millis();
}
@@ -277,7 +287,7 @@ protected:
unsigned long ts = millis();
if (this->dallasPolling[gpio]) {
auto minPollingTime = instance.millisToWaitForConversion(12) * 2;
unsigned long minPollingTime = instance.millisToWaitForConversion(12) * 2;
unsigned long estimatePollingTime = ts - this->dallasLastPollingTime[gpio];
// check conversion time
@@ -348,26 +358,6 @@ protected:
}
}
void pollingBleSensors() {
#if USE_BLE
if (!NimBLEDevice::isInitialized() && millis() > 5000) {
Log.sinfoln(FPSTR(L_SENSORS_BLE), F("Initialized"));
BLEDevice::init("");
NimBLEDevice::setPower(9);
}
for (uint8_t sensorId = 0; sensorId <= Sensors::getMaxSensorId(); sensorId++) {
auto& sSensor = Sensors::settings[sensorId];
if (!sSensor.enabled || sSensor.type != Sensors::Type::BLUETOOTH || sSensor.purpose == Sensors::Purpose::NOT_CONFIGURED) {
continue;
}
connectToBleDevice(sensorId);
}
#endif
}
void pollingNtcSensors() {
for (uint8_t sensorId = 0; sensorId <= Sensors::getMaxSensorId(); sensorId++) {
auto& sSensor = Sensors::settings[sensorId];
@@ -413,17 +403,139 @@ protected:
}
}
bool connectToBleDevice(const uint8_t sensorId) {
#if USE_BLE
void cleanBleInstances() {
#if USE_BLE
if (!NimBLEDevice::isInitialized()) {
return false;
return;
}
for (auto client : NimBLEDevice::getConnectedClients()) {
auto address = client->getPeerAddress();
bool used = false;
for (uint8_t sensorId = 0; sensorId <= Sensors::getMaxSensorId(); sensorId++) {
auto& sSensor = Sensors::settings[sensorId];
if (!sSensor.enabled || sSensor.type != Sensors::Type::BLUETOOTH || sSensor.purpose == Sensors::Purpose::NOT_CONFIGURED) {
continue;
}
auto pAddress = address.getVal();
uint8_t addr[] = {
pAddress[5], pAddress[4], pAddress[3],
pAddress[2], pAddress[1], pAddress[0]
};
if (isEqualAddress(addr, sSensor.address, sizeof(addr))) {
used = true;
break;
}
}
if (!used) {
Log.sinfoln(
FPSTR(L_SENSORS_BLE), F("Deleted unused client connected to %s"),
address.toString().c_str()
);
NimBLEDevice::deleteClient(client);
}
}
#endif
}
void pollingBleSensors() {
if (!NimBLEDevice::isInitialized() && millis() > 5000) {
Log.sinfoln(FPSTR(L_SENSORS_BLE), F("Initialized"));
BLEDevice::init("");
NimBLEDevice::setPower(9);
}
for (uint8_t sensorId = 0; sensorId <= Sensors::getMaxSensorId(); sensorId++) {
auto& sSensor = Sensors::settings[sensorId];
auto& rSensor = Sensors::results[sensorId];
if (!sSensor.enabled || sSensor.type != Sensors::Type::BLUETOOTH || sSensor.purpose == Sensors::Purpose::NOT_CONFIGURED) {
continue;
}
auto client = this->getBleClient(sensorId);
if (client == nullptr) {
continue;
}
if (!client->isConnected()) {
this->bleSubscribed[sensorId] = false;
this->bleLastSetDtTime[sensorId] = 0;
if (client->connect()) {
Log.sinfoln(
FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s': connected to %s"),
sensorId, sSensor.name, client->getPeerAddress().toString().c_str()
);
} else {
Log.swarningln(
FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s': failed connecting to %s"),
sensorId, sSensor.name, client->getPeerAddress().toString().c_str()
);
continue;
}
}
if (!this->bleSubscribed[sensorId]) {
if (this->subscribeToBleDevice(sensorId, client)) {
this->bleSubscribed[sensorId] = true;
} else {
this->bleSubscribed[sensorId] = false;
client->disconnect();
continue;
}
}
if (!rSensor.connected) {
rSensor.connected = true;
}
if (!this->bleLastSetDtTime[sensorId] || millis() - this->bleLastSetDtTime[sensorId] > this->bleSetDtInterval) {
struct tm ti;
if (getLocalTime(&ti)) {
if (this->setDateOnBleSensor(client, &ti)) {
Log.sinfoln(
FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s', successfully set date: %02d.%02d.%04d %02d:%02d:%02d"),
sensorId, sSensor.name,
ti.tm_mday, ti.tm_mon + 1, ti.tm_year + 1900, ti.tm_hour, ti.tm_min, ti.tm_sec
);
} else {
Log.swarningln(
FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s', failed set date: %02d.%02d.%04d %02d:%02d:%02d"),
sensorId, sSensor.name,
ti.tm_mday, ti.tm_mon + 1, ti.tm_year + 1900, ti.tm_hour, ti.tm_min, ti.tm_sec
);
}
this->bleLastSetDtTime[sensorId] = millis();
}
}
}
}
NimBLEClient* getBleClient(const uint8_t sensorId) {
if (!NimBLEDevice::isInitialized()) {
return nullptr;
}
auto& sSensor = Sensors::settings[sensorId];
auto& rSensor = Sensors::results[sensorId];
if (!sSensor.enabled || sSensor.type != Sensors::Type::BLUETOOTH || sSensor.purpose == Sensors::Purpose::NOT_CONFIGURED) {
return false;
return nullptr;
}
uint8_t addr[6] = {
@@ -432,19 +544,20 @@ protected:
};
const auto address = NimBLEAddress(addr, 0);
NimBLEClient* pClient = nullptr;
pClient = NimBLEDevice::getClientByPeerAddress(address);
NimBLEClient* pClient = NimBLEDevice::getClientByPeerAddress(address);
if (pClient == nullptr) {
pClient = NimBLEDevice::getDisconnectedClient();
}
if (pClient == nullptr) {
if (NimBLEDevice::getCreatedClientCount() >= NIMBLE_MAX_CONNECTIONS) {
return false;
return nullptr;
}
pClient = NimBLEDevice::createClient();
if (pClient == nullptr) {
return nullptr;
}
/**
* Set initial connection parameters:
@@ -457,28 +570,17 @@ protected:
pClient->setSelfDelete(false, true);
}
if(pClient->isConnected()) {
if (!rSensor.connected) {
rSensor.connected = true;
}
return true;
if (!pClient->isConnected()) {
pClient->setPeerAddress(address);
}
if (!pClient->connect(address)) {
Log.swarningln(
FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s': failed connecting to %s"),
sensorId, sSensor.name, address.toString().c_str()
);
return false;
}
Log.sinfoln(
FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s': connected to %s"),
sensorId, sSensor.name, address.toString().c_str()
);
return pClient;
}
bool subscribeToBleDevice(const uint8_t sensorId, NimBLEClient* pClient) {
auto& sSensor = Sensors::settings[sensorId];
auto pAddress = pClient->getPeerAddress().toString().c_str();
NimBLERemoteService* pService = nullptr;
NimBLERemoteCharacteristic* pChar = nullptr;
@@ -488,13 +590,13 @@ protected:
if (!pService) {
Log.straceln(
FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s': failed to find env service (%s) on device %s"),
sensorId, sSensor.name, serviceUuid.toString().c_str(), address.toString().c_str()
sensorId, sSensor.name, serviceUuid.toString().c_str(), pAddress
);
} else {
Log.straceln(
FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s': found env service (%s) on device %s"),
sensorId, sSensor.name, serviceUuid.toString().c_str(), address.toString().c_str()
sensorId, sSensor.name, serviceUuid.toString().c_str(), pAddress
);
// 0x2A6E - Notify temperature x0.01C (pvvx)
@@ -506,7 +608,7 @@ protected:
if (pChar && (pChar->canNotify() || pChar->canIndicate())) {
Log.straceln(
FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s': found temp char (%s) in env service on device %s"),
sensorId, sSensor.name, charUuid.toString().c_str(), address.toString().c_str()
sensorId, sSensor.name, charUuid.toString().c_str(), pAddress
);
pChar->unsubscribe();
@@ -561,14 +663,14 @@ protected:
Log.straceln(
FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s': subscribed to temp char (%s) in env service on device %s"),
sensorId, sSensor.name,
charUuid.toString().c_str(), address.toString().c_str()
charUuid.toString().c_str(), pAddress
);
} else {
Log.swarningln(
FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s': failed to subscribe to temp char (%s) in env service on device %s"),
sensorId, sSensor.name,
charUuid.toString().c_str(), address.toString().c_str()
charUuid.toString().c_str(), pAddress
);
}
}
@@ -583,7 +685,7 @@ protected:
if (pChar && (pChar->canNotify() || pChar->canIndicate())) {
Log.straceln(
FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s': found temp char (%s) in env service on device %s"),
sensorId, sSensor.name, charUuid.toString().c_str(), address.toString().c_str()
sensorId, sSensor.name, charUuid.toString().c_str(), pAddress
);
pChar->unsubscribe();
@@ -638,14 +740,14 @@ protected:
Log.straceln(
FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s': subscribed to temp char (%s) in env service on device %s"),
sensorId, sSensor.name,
charUuid.toString().c_str(), address.toString().c_str()
charUuid.toString().c_str(), pAddress
);
} else {
Log.swarningln(
FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s': failed to subscribe to temp char (%s) in env service on device %s"),
sensorId, sSensor.name,
charUuid.toString().c_str(), address.toString().c_str()
charUuid.toString().c_str(), pAddress
);
}
}
@@ -654,7 +756,7 @@ protected:
if (!tempNotifyCreated) {
Log.swarningln(
FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s': not found supported temp chars in env service on device %s"),
sensorId, sSensor.name, address.toString().c_str()
sensorId, sSensor.name, pAddress
);
pClient->disconnect();
@@ -672,7 +774,7 @@ protected:
if (pChar && (pChar->canNotify() || pChar->canIndicate())) {
Log.straceln(
FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s': found humidity char (%s) in env service on device %s"),
sensorId, sSensor.name, charUuid.toString().c_str(), address.toString().c_str()
sensorId, sSensor.name, charUuid.toString().c_str(), pAddress
);
pChar->unsubscribe();
@@ -727,14 +829,14 @@ protected:
Log.straceln(
FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s': subscribed to humidity char (%s) in env service on device %s"),
sensorId, sSensor.name,
charUuid.toString().c_str(), address.toString().c_str()
charUuid.toString().c_str(), pAddress
);
} else {
Log.swarningln(
FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s': failed to subscribe to humidity char (%s) in env service on device %s"),
sensorId, sSensor.name,
charUuid.toString().c_str(), address.toString().c_str()
charUuid.toString().c_str(), pAddress
);
}
}
@@ -743,7 +845,7 @@ protected:
if (!humidityNotifyCreated) {
Log.swarningln(
FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s': not found supported humidity chars in env service on device %s"),
sensorId, sSensor.name, address.toString().c_str()
sensorId, sSensor.name, pAddress
);
}
}
@@ -757,13 +859,13 @@ protected:
if (!pService) {
Log.straceln(
FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s': failed to find battery service (%s) on device %s"),
sensorId, sSensor.name, serviceUuid.toString().c_str(), address.toString().c_str()
sensorId, sSensor.name, serviceUuid.toString().c_str(), pAddress
);
} else {
Log.straceln(
FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s': found battery service (%s) on device %s"),
sensorId, sSensor.name, serviceUuid.toString().c_str(), address.toString().c_str()
sensorId, sSensor.name, serviceUuid.toString().c_str(), pAddress
);
// 0x2A19 - Notify the battery charge level 0..99% (pvvx)
@@ -775,7 +877,7 @@ protected:
if (pChar && (pChar->canNotify() || pChar->canIndicate())) {
Log.straceln(
FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s': found battery char (%s) in battery service on device %s"),
sensorId, sSensor.name, charUuid.toString().c_str(), address.toString().c_str()
sensorId, sSensor.name, charUuid.toString().c_str(), pAddress
);
pChar->unsubscribe();
@@ -830,14 +932,14 @@ protected:
Log.straceln(
FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s': subscribed to battery char (%s) in battery service on device %s"),
sensorId, sSensor.name,
charUuid.toString().c_str(), address.toString().c_str()
charUuid.toString().c_str(), pAddress
);
} else {
Log.swarningln(
FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s': failed to subscribe to battery char (%s) in battery service on device %s"),
sensorId, sSensor.name,
charUuid.toString().c_str(), address.toString().c_str()
charUuid.toString().c_str(), pAddress
);
}
}
@@ -846,18 +948,33 @@ protected:
if (!batteryNotifyCreated) {
Log.swarningln(
FPSTR(L_SENSORS_BLE), F("Sensor #%hhu '%s': not found supported battery chars in battery service on device %s"),
sensorId, sSensor.name, address.toString().c_str()
sensorId, sSensor.name, pAddress
);
}
}
}
return true;
#else
return false;
#endif
}
bool setDateOnBleSensor(NimBLEClient* pClient, const struct tm *ptm) {
auto ts = mkgmtime(ptm);
uint8_t data[5] = {};
data[0] = 0x23;
data[1] = ts & 0xff;
data[2] = (ts >> 8) & 0xff;
data[3] = (ts >> 16) & 0xff;
data[4] = (ts >> 24) & 0xff;
return pClient->setValue(
NimBLEUUID((uint16_t) 0x1f10),
NimBLEUUID((uint16_t) 0x1f1f),
NimBLEAttValue(data, sizeof(data))
);
}
#endif
void updateConnectionStatus() {
for (uint8_t sensorId = 0; sensorId <= Sensors::getMaxSensorId(); sensorId++) {
auto& sSensor = Sensors::settings[sensorId];
@@ -882,7 +999,8 @@ protected:
}
static bool isEqualAddress(const uint8_t *addr1, const uint8_t *addr2, const uint8_t length = 8) {
bool result = true;
return memcmp(addr1, addr2, length) == 0;
/*bool result = true;
for (uint8_t i = 0; i < length; i++) {
if (addr1[i] != addr2[i]) {
@@ -891,7 +1009,7 @@ protected:
}
}
return result;
return result;*/
}
static bool isEmptyAddress(const uint8_t *addr, const uint8_t length = 8) {

View File

@@ -36,6 +36,11 @@ struct Settings {
unsigned short port = DEFAULT_TELNET_PORT;
} telnet;
struct {
char server[49] = "pool.ntp.org";
char timezone[49] = "UTC0";
} ntp;
UnitSystem unitSystem = UnitSystem::METRIC;
byte statusLedGpio = DEFAULT_STATUS_LED_GPIO;
} system;

View File

@@ -135,6 +135,7 @@ const char S_MQTT[] PROGMEM = "mqtt";
const char S_NAME[] PROGMEM = "name";
const char S_NATIVE_HEATING_CONTROL[] PROGMEM = "nativeHeatingControl";
const char S_NETWORK[] PROGMEM = "network";
const char S_NTP[] PROGMEM = "ntp";
const char S_N_FACTOR[] PROGMEM = "n_factor";
const char S_OFFSET[] PROGMEM = "offset";
const char S_ON_ENABLED_HEATING[] PROGMEM = "onEnabledHeating";
@@ -190,6 +191,7 @@ const char S_TEMPERATURE[] PROGMEM = "temperature";
const char S_THRESHOLD_HIGH[] PROGMEM = "thresholdHigh";
const char S_THRESHOLD_LOW[] PROGMEM = "thresholdLow";
const char S_THRESHOLD_TIME[] PROGMEM = "thresholdTime";
const char S_TIMEZONE[] PROGMEM = "timezone";
const char S_TOTAL[] PROGMEM = "total";
const char S_TRESHOLD_TIME[] PROGMEM = "tresholdTime";
const char S_TURBO[] PROGMEM = "turbo";

View File

@@ -1,5 +1,43 @@
#include <Arduino.h>
bool isLeapYear(short year) {
if (year % 4 != 0) {
return false;
}
if (year % 100 != 0) {
return true;
}
return (year % 400) == 0;
}
// convert UTC tm time to time_t epoch time
// source: https://github.com/cyberman54/ESP32-Paxcounter/blob/master/src/timekeeper.cpp
time_t mkgmtime(const struct tm *ptm) {
const int SecondsPerMinute = 60;
const int SecondsPerHour = 3600;
const int SecondsPerDay = 86400;
const int DaysOfMonth[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
time_t secs = 0;
// tm_year is years since 1900
int year = ptm->tm_year + 1900;
for (int y = 1970; y < year; ++y) {
secs += (isLeapYear(y) ? 366 : 365) * SecondsPerDay;
}
// tm_mon is month from 0..11
for (int m = 0; m < ptm->tm_mon; ++m) {
secs += DaysOfMonth[m] * SecondsPerDay;
if (m == 1 && isLeapYear(year))
secs += SecondsPerDay;
}
secs += (ptm->tm_mday - 1) * SecondsPerDay;
secs += ptm->tm_hour * SecondsPerHour;
secs += ptm->tm_min * SecondsPerMinute;
secs += ptm->tm_sec;
return secs;
}
inline bool isDigit(const char* ptr) {
char* endPtr;
strtol(ptr, &endPtr, 10);
@@ -359,6 +397,10 @@ void settingsToJson(const Settings& src, JsonVariant dst, bool safe = false) {
telnet[FPSTR(S_ENABLED)] = src.system.telnet.enabled;
telnet[FPSTR(S_PORT)] = src.system.telnet.port;
auto ntp = system[FPSTR(S_NTP)].to<JsonObject>();
ntp[FPSTR(S_SERVER)] = src.system.ntp.server;
ntp[FPSTR(S_TIMEZONE)] = src.system.ntp.timezone;
system[FPSTR(S_UNIT_SYSTEM)] = static_cast<uint8_t>(src.system.unitSystem);
system[FPSTR(S_STATUS_LED_GPIO)] = src.system.statusLedGpio;
@@ -527,6 +569,24 @@ bool jsonToSettings(const JsonVariantConst src, Settings& dst, bool safe = false
}
}
if (!src[FPSTR(S_SYSTEM)][FPSTR(S_NTP)][FPSTR(S_SERVER)].isNull()) {
String value = src[FPSTR(S_SYSTEM)][FPSTR(S_NTP)][FPSTR(S_SERVER)].as<String>();
if (value.length() < sizeof(dst.system.ntp.server) && !value.equals(dst.system.ntp.server)) {
strcpy(dst.system.ntp.server, value.c_str());
changed = true;
}
}
if (!src[FPSTR(S_SYSTEM)][FPSTR(S_NTP)][FPSTR(S_TIMEZONE)].isNull()) {
String value = src[FPSTR(S_SYSTEM)][FPSTR(S_NTP)][FPSTR(S_TIMEZONE)].as<String>();
if (value.length() < sizeof(dst.system.ntp.timezone) && !value.equals(dst.system.ntp.timezone)) {
strcpy(dst.system.ntp.timezone, value.c_str());
changed = true;
}
}
if (!src[FPSTR(S_SYSTEM)][FPSTR(S_UNIT_SYSTEM)].isNull()) {
uint8_t value = src[FPSTR(S_SYSTEM)][FPSTR(S_UNIT_SYSTEM)].as<unsigned char>();
UnitSystem prevUnitSystem = dst.system.unitSystem;

View File

@@ -315,6 +315,11 @@
"title": "Telnet port",
"note": "Default: 23"
}
},
"ntp": {
"server": "NTP server",
"timezone": "Timezone",
"timezonePresets": "Select preset..."
}
},

View File

@@ -315,6 +315,11 @@
"title": "Porta Telnet",
"note": "Default: 23"
}
},
"ntp": {
"server": "NTP server",
"timezone": "Zona oraria",
"timezonePresets": "Seleziona preimpostato..."
}
},

View File

@@ -315,6 +315,11 @@
"title": "Telnet порт",
"note": "По умолчанию: 23"
}
},
"ntp": {
"server": "NTP сервер",
"timezone": "Часовой пояс",
"timezonePresets": "Выберите пресет..."
}
},

View File

@@ -85,6 +85,23 @@
</label>
</fieldset>
<fieldset>
<label>
<span data-i18n>settings.system.ntp.server</span>
<input type="text" name="system[ntp][server]" maxlength="48">
</label>
<label>
<span data-i18n>settings.system.ntp.timezone</span>
<div role="group">
<input type="text" name="system[ntp][timezone]" maxlength="48">
<select class="presetTimezones">
<option disabled selected data-i18n>settings.system.ntp.timezonePresets</option>
</select>
</div>
</label>
</fieldset>
<fieldset>
<label>
<span data-i18n>settings.system.statusLedGpio</span>
@@ -746,6 +763,8 @@
setSelectValue("[name='system[serial][baudrate]']", data.system.serial.baudrate);
setCheckboxValue("[name='system[telnet][enabled]']", data.system.telnet.enabled);
setInputValue("[name='system[telnet][port]']", data.system.telnet.port);
setInputValue("[name='system[ntp][server]']", data.system.ntp.server);
setInputValue("[name='system[ntp][timezone]']", data.system.ntp.timezone);
setRadioValue("[name='system[unitSystem]']", data.system.unitSystem);
setInputValue("[name='system[statusLedGpio]']", data.system.statusLedGpio < 255 ? data.system.statusLedGpio : '');
setBusy('#system-settings-busy', '#system-settings', false);
@@ -886,6 +905,28 @@
setBusy('#pid-settings-busy', '#pid-settings', false);
};
try {
const response = await fetch("/static/timezones.json");
if (!response.ok) {
throw new Error('Response not valid');
}
const result = await response.json();
const ptzSelector = document.querySelector(".presetTimezones");
for (const ptzName in result) {
ptzSelector.appendChild(
new Option(ptzName, result[ptzName])
);
}
ptzSelector.addEventListener("change", async (event) => {
document.querySelector("[name='system[ntp][timezone]']").value = event.target.value;
});
} catch (error) {
console.log(error);
}
try {
const response = await fetch("/api/settings", {
cache: "no-cache",

458
src_data/timezones.json Normal file
View File

@@ -0,0 +1,458 @@
{
"Africa/Abidjan":"GMT0",
"Africa/Accra":"GMT0",
"Africa/Addis_Ababa":"EAT-3",
"Africa/Algiers":"CET-1",
"Africa/Asmara":"EAT-3",
"Africa/Bamako":"GMT0",
"Africa/Bangui":"WAT-1",
"Africa/Banjul":"GMT0",
"Africa/Bissau":"GMT0",
"Africa/Blantyre":"CAT-2",
"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/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/Freetown":"GMT0",
"Africa/Gaborone":"CAT-2",
"Africa/Harare":"CAT-2",
"Africa/Johannesburg":"SAST-2",
"Africa/Juba":"CAT-2",
"Africa/Kampala":"EAT-3",
"Africa/Khartoum":"CAT-2",
"Africa/Kigali":"CAT-2",
"Africa/Kinshasa":"WAT-1",
"Africa/Lagos":"WAT-1",
"Africa/Libreville":"WAT-1",
"Africa/Lome":"GMT0",
"Africa/Luanda":"WAT-1",
"Africa/Lubumbashi":"CAT-2",
"Africa/Lusaka":"CAT-2",
"Africa/Malabo":"WAT-1",
"Africa/Maputo":"CAT-2",
"Africa/Maseru":"SAST-2",
"Africa/Mbabane":"SAST-2",
"Africa/Mogadishu":"EAT-3",
"Africa/Monrovia":"GMT0",
"Africa/Nairobi":"EAT-3",
"Africa/Ndjamena":"WAT-1",
"Africa/Niamey":"WAT-1",
"Africa/Nouakchott":"GMT0",
"Africa/Ouagadougou":"GMT0",
"Africa/Porto-Novo":"WAT-1",
"Africa/Sao_Tome":"GMT0",
"Africa/Tripoli":"EET-2",
"Africa/Tunis":"CET-1",
"Africa/Windhoek":"CAT-2",
"America/Adak":"HST10HDT,M3.2.0,M11.1.0",
"America/Anchorage":"AKST9AKDT,M3.2.0,M11.1.0",
"America/Anguilla":"AST4",
"America/Antigua":"AST4",
"America/Araguaina":"<-03>3",
"America/Argentina/Buenos_Aires":"<-03>3",
"America/Argentina/Catamarca":"<-03>3",
"America/Argentina/Cordoba":"<-03>3",
"America/Argentina/Jujuy":"<-03>3",
"America/Argentina/La_Rioja":"<-03>3",
"America/Argentina/Mendoza":"<-03>3",
"America/Argentina/Rio_Gallegos":"<-03>3",
"America/Argentina/Salta":"<-03>3",
"America/Argentina/San_Juan":"<-03>3",
"America/Argentina/San_Luis":"<-03>3",
"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/Atikokan":"EST5",
"America/Bahia":"<-03>3",
"America/Bahia_Banderas":"CST6",
"America/Barbados":"AST4",
"America/Belem":"<-03>3",
"America/Belize":"CST6",
"America/Blanc-Sablon":"AST4",
"America/Boa_Vista":"<-04>4",
"America/Bogota":"<-05>5",
"America/Boise":"MST7MDT,M3.2.0,M11.1.0",
"America/Cambridge_Bay":"MST7MDT,M3.2.0,M11.1.0",
"America/Campo_Grande":"<-04>4",
"America/Cancun":"EST5",
"America/Caracas":"<-04>4",
"America/Cayenne":"<-03>3",
"America/Cayman":"EST5",
"America/Chicago":"CST6CDT,M3.2.0,M11.1.0",
"America/Chihuahua":"CST6",
"America/Costa_Rica":"CST6",
"America/Creston":"MST7",
"America/Cuiaba":"<-04>4",
"America/Curacao":"AST4",
"America/Danmarkshavn":"GMT0",
"America/Dawson":"MST7",
"America/Dawson_Creek":"MST7",
"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/Eirunepe":"<-05>5",
"America/El_Salvador":"CST6",
"America/Fort_Nelson":"MST7",
"America/Fortaleza":"<-03>3",
"America/Glace_Bay":"AST4ADT,M3.2.0,M11.1.0",
"America/Godthab":"<-02>2<-01>,M3.5.0/-1,M10.5.0/0",
"America/Goose_Bay":"AST4ADT,M3.2.0,M11.1.0",
"America/Grand_Turk":"EST5EDT,M3.2.0,M11.1.0",
"America/Grenada":"AST4",
"America/Guadeloupe":"AST4",
"America/Guatemala":"CST6",
"America/Guayaquil":"<-05>5",
"America/Guyana":"<-04>4",
"America/Halifax":"AST4ADT,M3.2.0,M11.1.0",
"America/Havana":"CST5CDT,M3.2.0/0,M11.1.0/1",
"America/Hermosillo":"MST7",
"America/Indiana/Indianapolis":"EST5EDT,M3.2.0,M11.1.0",
"America/Indiana/Knox":"CST6CDT,M3.2.0,M11.1.0",
"America/Indiana/Marengo":"EST5EDT,M3.2.0,M11.1.0",
"America/Indiana/Petersburg":"EST5EDT,M3.2.0,M11.1.0",
"America/Indiana/Tell_City":"CST6CDT,M3.2.0,M11.1.0",
"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/Iqaluit":"EST5EDT,M3.2.0,M11.1.0",
"America/Jamaica":"EST5",
"America/Juneau":"AKST9AKDT,M3.2.0,M11.1.0",
"America/Kentucky/Louisville":"EST5EDT,M3.2.0,M11.1.0",
"America/Kentucky/Monticello":"EST5EDT,M3.2.0,M11.1.0",
"America/Kralendijk":"AST4",
"America/La_Paz":"<-04>4",
"America/Lima":"<-05>5",
"America/Los_Angeles":"PST8PDT,M3.2.0,M11.1.0",
"America/Lower_Princes":"AST4",
"America/Maceio":"<-03>3",
"America/Managua":"CST6",
"America/Manaus":"<-04>4",
"America/Marigot":"AST4",
"America/Martinique":"AST4",
"America/Matamoros":"CST6CDT,M3.2.0,M11.1.0",
"America/Mazatlan":"MST7",
"America/Menominee":"CST6CDT,M3.2.0,M11.1.0",
"America/Merida":"CST6",
"America/Metlakatla":"AKST9AKDT,M3.2.0,M11.1.0",
"America/Mexico_City":"CST6",
"America/Miquelon":"<-03>3<-02>,M3.2.0,M11.1.0",
"America/Moncton":"AST4ADT,M3.2.0,M11.1.0",
"America/Monterrey":"CST6",
"America/Montevideo":"<-03>3",
"America/Montreal":"EST5EDT,M3.2.0,M11.1.0",
"America/Montserrat":"AST4",
"America/Nassau":"EST5EDT,M3.2.0,M11.1.0",
"America/New_York":"EST5EDT,M3.2.0,M11.1.0",
"America/Nipigon":"EST5EDT,M3.2.0,M11.1.0",
"America/Nome":"AKST9AKDT,M3.2.0,M11.1.0",
"America/Noronha":"<-02>2",
"America/North_Dakota/Beulah":"CST6CDT,M3.2.0,M11.1.0",
"America/North_Dakota/Center":"CST6CDT,M3.2.0,M11.1.0",
"America/North_Dakota/New_Salem":"CST6CDT,M3.2.0,M11.1.0",
"America/Nuuk":"<-02>2<-01>,M3.5.0/-1,M10.5.0/0",
"America/Ojinaga":"CST6CDT,M3.2.0,M11.1.0",
"America/Panama":"EST5",
"America/Pangnirtung":"EST5EDT,M3.2.0,M11.1.0",
"America/Paramaribo":"<-03>3",
"America/Phoenix":"MST7",
"America/Port-au-Prince":"EST5EDT,M3.2.0,M11.1.0",
"America/Port_of_Spain":"AST4",
"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/Rankin_Inlet":"CST6CDT,M3.2.0,M11.1.0",
"America/Recife":"<-03>3",
"America/Regina":"CST6",
"America/Resolute":"CST6CDT,M3.2.0,M11.1.0",
"America/Rio_Branco":"<-05>5",
"America/Santarem":"<-03>3",
"America/Santiago":"<-04>4<-03>,M9.1.6/24,M4.1.6/24",
"America/Santo_Domingo":"AST4",
"America/Sao_Paulo":"<-03>3",
"America/Scoresbysund":"<-02>2<-01>,M3.5.0/-1,M10.5.0/0",
"America/Sitka":"AKST9AKDT,M3.2.0,M11.1.0",
"America/St_Barthelemy":"AST4",
"America/St_Johns":"NST3:30NDT,M3.2.0,M11.1.0",
"America/St_Kitts":"AST4",
"America/St_Lucia":"AST4",
"America/St_Thomas":"AST4",
"America/St_Vincent":"AST4",
"America/Swift_Current":"CST6",
"America/Tegucigalpa":"CST6",
"America/Thule":"AST4ADT,M3.2.0,M11.1.0",
"America/Thunder_Bay":"EST5EDT,M3.2.0,M11.1.0",
"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/Whitehorse":"MST7",
"America/Winnipeg":"CST6CDT,M3.2.0,M11.1.0",
"America/Yakutat":"AKST9AKDT,M3.2.0,M11.1.0",
"America/Yellowknife":"MST7MDT,M3.2.0,M11.1.0",
"Antarctica/Casey":"<+08>-8",
"Antarctica/Davis":"<+07>-7",
"Antarctica/DumontDUrville":"<+10>-10",
"Antarctica/Macquarie":"AEST-10AEDT,M10.1.0,M4.1.0/3",
"Antarctica/Mawson":"<+05>-5",
"Antarctica/McMurdo":"NZST-12NZDT,M9.5.0,M4.1.0/3",
"Antarctica/Palmer":"<-03>3",
"Antarctica/Rothera":"<-03>3",
"Antarctica/Syowa":"<+03>-3",
"Antarctica/Troll":"<+00>0<+02>-2,M3.5.0/1,M10.5.0/3",
"Antarctica/Vostok":"<+05>-5",
"Arctic/Longyearbyen":"CET-1CEST,M3.5.0,M10.5.0/3",
"Asia/Aden":"<+03>-3",
"Asia/Almaty":"<+05>-5",
"Asia/Amman":"<+03>-3",
"Asia/Anadyr":"<+12>-12",
"Asia/Aqtau":"<+05>-5",
"Asia/Aqtobe":"<+05>-5",
"Asia/Ashgabat":"<+05>-5",
"Asia/Atyrau":"<+05>-5",
"Asia/Baghdad":"<+03>-3",
"Asia/Bahrain":"<+03>-3",
"Asia/Baku":"<+04>-4",
"Asia/Bangkok":"<+07>-7",
"Asia/Barnaul":"<+07>-7",
"Asia/Beirut":"EET-2EEST,M3.5.0/0,M10.5.0/0",
"Asia/Bishkek":"<+06>-6",
"Asia/Brunei":"<+08>-8",
"Asia/Chita":"<+09>-9",
"Asia/Choibalsan":"<+08>-8",
"Asia/Colombo":"<+0530>-5:30",
"Asia/Damascus":"<+03>-3",
"Asia/Dhaka":"<+06>-6",
"Asia/Dili":"<+09>-9",
"Asia/Dubai":"<+04>-4",
"Asia/Dushanbe":"<+05>-5",
"Asia/Famagusta":"EET-2EEST,M3.5.0/3,M10.5.0/4",
"Asia/Gaza":"EET-2EEST,M3.4.4/50,M10.4.4/50",
"Asia/Hebron":"EET-2EEST,M3.4.4/50,M10.4.4/50",
"Asia/Ho_Chi_Minh":"<+07>-7",
"Asia/Hong_Kong":"HKT-8",
"Asia/Hovd":"<+07>-7",
"Asia/Irkutsk":"<+08>-8",
"Asia/Jakarta":"WIB-7",
"Asia/Jayapura":"WIT-9",
"Asia/Jerusalem":"IST-2IDT,M3.4.4/26,M10.5.0",
"Asia/Kabul":"<+0430>-4:30",
"Asia/Kamchatka":"<+12>-12",
"Asia/Karachi":"PKT-5",
"Asia/Kathmandu":"<+0545>-5:45",
"Asia/Khandyga":"<+09>-9",
"Asia/Kolkata":"IST-5:30",
"Asia/Krasnoyarsk":"<+07>-7",
"Asia/Kuala_Lumpur":"<+08>-8",
"Asia/Kuching":"<+08>-8",
"Asia/Kuwait":"<+03>-3",
"Asia/Macau":"CST-8",
"Asia/Magadan":"<+11>-11",
"Asia/Makassar":"WITA-8",
"Asia/Manila":"PST-8",
"Asia/Muscat":"<+04>-4",
"Asia/Nicosia":"EET-2EEST,M3.5.0/3,M10.5.0/4",
"Asia/Novokuznetsk":"<+07>-7",
"Asia/Novosibirsk":"<+07>-7",
"Asia/Omsk":"<+06>-6",
"Asia/Oral":"<+05>-5",
"Asia/Phnom_Penh":"<+07>-7",
"Asia/Pontianak":"WIB-7",
"Asia/Pyongyang":"KST-9",
"Asia/Qatar":"<+03>-3",
"Asia/Qyzylorda":"<+05>-5",
"Asia/Riyadh":"<+03>-3",
"Asia/Sakhalin":"<+11>-11",
"Asia/Samarkand":"<+05>-5",
"Asia/Seoul":"KST-9",
"Asia/Shanghai":"CST-8",
"Asia/Singapore":"<+08>-8",
"Asia/Srednekolymsk":"<+11>-11",
"Asia/Taipei":"CST-8",
"Asia/Tashkent":"<+05>-5",
"Asia/Tbilisi":"<+04>-4",
"Asia/Tehran":"<+0330>-3:30",
"Asia/Thimphu":"<+06>-6",
"Asia/Tokyo":"JST-9",
"Asia/Tomsk":"<+07>-7",
"Asia/Ulaanbaatar":"<+08>-8",
"Asia/Urumqi":"<+06>-6",
"Asia/Ust-Nera":"<+10>-10",
"Asia/Vientiane":"<+07>-7",
"Asia/Vladivostok":"<+10>-10",
"Asia/Yakutsk":"<+09>-9",
"Asia/Yangon":"<+0630>-6:30",
"Asia/Yekaterinburg":"<+05>-5",
"Asia/Yerevan":"<+04>-4",
"Atlantic/Azores":"<-01>1<+00>,M3.5.0/0,M10.5.0/1",
"Atlantic/Bermuda":"AST4ADT,M3.2.0,M11.1.0",
"Atlantic/Canary":"WET0WEST,M3.5.0/1,M10.5.0",
"Atlantic/Cape_Verde":"<-01>1",
"Atlantic/Faroe":"WET0WEST,M3.5.0/1,M10.5.0",
"Atlantic/Madeira":"WET0WEST,M3.5.0/1,M10.5.0",
"Atlantic/Reykjavik":"GMT0",
"Atlantic/South_Georgia":"<-02>2",
"Atlantic/St_Helena":"GMT0",
"Atlantic/Stanley":"<-03>3",
"Australia/Adelaide":"ACST-9:30ACDT,M10.1.0,M4.1.0/3",
"Australia/Brisbane":"AEST-10",
"Australia/Broken_Hill":"ACST-9:30ACDT,M10.1.0,M4.1.0/3",
"Australia/Currie":"AEST-10AEDT,M10.1.0,M4.1.0/3",
"Australia/Darwin":"ACST-9:30",
"Australia/Eucla":"<+0845>-8:45",
"Australia/Hobart":"AEST-10AEDT,M10.1.0,M4.1.0/3",
"Australia/Lindeman":"AEST-10",
"Australia/Lord_Howe":"<+1030>-10:30<+11>-11,M10.1.0,M4.1.0",
"Australia/Melbourne":"AEST-10AEDT,M10.1.0,M4.1.0/3",
"Australia/Perth":"AWST-8",
"Australia/Sydney":"AEST-10AEDT,M10.1.0,M4.1.0/3",
"Europe/Amsterdam":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Andorra":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Astrakhan":"<+04>-4",
"Europe/Athens":"EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Belgrade":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Berlin":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Bratislava":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Brussels":"CET-1CEST,M3.5.0,M10.5.0/3",
"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/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",
"Europe/Guernsey":"GMT0BST,M3.5.0/1,M10.5.0",
"Europe/Helsinki":"EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Isle_of_Man":"GMT0BST,M3.5.0/1,M10.5.0",
"Europe/Istanbul":"<+03>-3",
"Europe/Jersey":"GMT0BST,M3.5.0/1,M10.5.0",
"Europe/Kaliningrad":"EET-2",
"Europe/Kiev":"EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Kirov":"MSK-3",
"Europe/Lisbon":"WET0WEST,M3.5.0/1,M10.5.0",
"Europe/Ljubljana":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/London":"GMT0BST,M3.5.0/1,M10.5.0",
"Europe/Luxembourg":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Madrid":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Malta":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Mariehamn":"EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Minsk":"<+03>-3",
"Europe/Monaco":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Moscow":"MSK-3",
"Europe/Oslo":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Paris":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Podgorica":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Prague":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Riga":"EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Rome":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Samara":"<+04>-4",
"Europe/San_Marino":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Sarajevo":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Saratov":"<+04>-4",
"Europe/Simferopol":"MSK-3",
"Europe/Skopje":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Sofia":"EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Stockholm":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Tallinn":"EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Tirane":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Ulyanovsk":"<+04>-4",
"Europe/Uzhgorod":"EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Vaduz":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Vatican":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Vienna":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Vilnius":"EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Volgograd":"MSK-3",
"Europe/Warsaw":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Zagreb":"CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Zaporozhye":"EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Zurich":"CET-1CEST,M3.5.0,M10.5.0/3",
"Indian/Antananarivo":"EAT-3",
"Indian/Chagos":"<+06>-6",
"Indian/Christmas":"<+07>-7",
"Indian/Cocos":"<+0630>-6:30",
"Indian/Comoro":"EAT-3",
"Indian/Kerguelen":"<+05>-5",
"Indian/Mahe":"<+04>-4",
"Indian/Maldives":"<+05>-5",
"Indian/Mauritius":"<+04>-4",
"Indian/Mayotte":"EAT-3",
"Indian/Reunion":"<+04>-4",
"Pacific/Apia":"<+13>-13",
"Pacific/Auckland":"NZST-12NZDT,M9.5.0,M4.1.0/3",
"Pacific/Bougainville":"<+11>-11",
"Pacific/Chatham":"<+1245>-12:45<+1345>,M9.5.0/2:45,M4.1.0/3:45",
"Pacific/Chuuk":"<+10>-10",
"Pacific/Easter":"<-06>6<-05>,M9.1.6/22,M4.1.6/22",
"Pacific/Efate":"<+11>-11",
"Pacific/Enderbury":"<+13>-13",
"Pacific/Fakaofo":"<+13>-13",
"Pacific/Fiji":"<+12>-12",
"Pacific/Funafuti":"<+12>-12",
"Pacific/Galapagos":"<-06>6",
"Pacific/Gambier":"<-09>9",
"Pacific/Guadalcanal":"<+11>-11",
"Pacific/Guam":"ChST-10",
"Pacific/Honolulu":"HST10",
"Pacific/Kiritimati":"<+14>-14",
"Pacific/Kosrae":"<+11>-11",
"Pacific/Kwajalein":"<+12>-12",
"Pacific/Majuro":"<+12>-12",
"Pacific/Marquesas":"<-0930>9:30",
"Pacific/Midway":"SST11",
"Pacific/Nauru":"<+12>-12",
"Pacific/Niue":"<-11>11",
"Pacific/Norfolk":"<+11>-11<+12>,M10.1.0,M4.1.0/3",
"Pacific/Noumea":"<+11>-11",
"Pacific/Pago_Pago":"SST11",
"Pacific/Palau":"<+09>-9",
"Pacific/Pitcairn":"<-08>8",
"Pacific/Pohnpei":"<+11>-11",
"Pacific/Port_Moresby":"<+10>-10",
"Pacific/Rarotonga":"<-10>10",
"Pacific/Saipan":"ChST-10",
"Pacific/Tahiti":"<-10>10",
"Pacific/Tarawa":"<+12>-12",
"Pacific/Tongatapu":"<+13>-13",
"Pacific/Wake":"<+12>-12",
"Pacific/Wallis":"<+12>-12",
"Etc/UTC":"UTC0",
"Etc/GMT":"GMT0",
"Etc/GMT+1":"<-01>1",
"Etc/GMT+2":"<-02>2",
"Etc/GMT+3":"<-03>3",
"Etc/GMT+4":"<-04>4",
"Etc/GMT+5":"<-05>5",
"Etc/GMT+6":"<-06>6",
"Etc/GMT+7":"<-07>7",
"Etc/GMT+8":"<-08>8",
"Etc/GMT+9":"<-09>9",
"Etc/GMT+10":"<-10>10",
"Etc/GMT+11":"<-11>11",
"Etc/GMT+12":"<-12>12",
"Etc/GMT-1":"<+01>-1",
"Etc/GMT-2":"<+02>-2",
"Etc/GMT-3":"<+03>-3",
"Etc/GMT-4":"<+04>-4",
"Etc/GMT-5":"<+05>-5",
"Etc/GMT-6":"<+06>-6",
"Etc/GMT-7":"<+07>-7",
"Etc/GMT-8":"<+08>-8",
"Etc/GMT-9":"<+09>-9",
"Etc/GMT-10":"<+10>-10",
"Etc/GMT-11":"<+11>-11",
"Etc/GMT-12":"<+12>-12",
"Etc/GMT-13":"<+13>-13",
"Etc/GMT-14":"<+14>-14",
"Etc/Zulu":"UTC0"
}