diff --git a/components/esp_rainmaker/CMakeLists.txt b/components/esp_rainmaker/CMakeLists.txt index 4383700..6d137cd 100644 --- a/components/esp_rainmaker/CMakeLists.txt +++ b/components/esp_rainmaker/CMakeLists.txt @@ -6,6 +6,7 @@ set(core_srcs "src/core/esp_rmaker_core.c" "src/core/esp_rmaker_node_config.c" "src/core/esp_rmaker_client_data.c" "src/core/esp_rmaker_time_sync.c" + "src/core/esp_rmaker_timezone.c" "src/core/esp_rmaker_storage.c" "src/core/esp_rmaker_user_mapping.pb-c.c" "src/core/esp_rmaker_utils.c" diff --git a/components/esp_rainmaker/Kconfig.projbuild b/components/esp_rainmaker/Kconfig.projbuild index 2f369f3..653b514 100644 --- a/components/esp_rainmaker/Kconfig.projbuild +++ b/components/esp_rainmaker/Kconfig.projbuild @@ -85,6 +85,14 @@ menu "ESP RainMaker Config" default 1 if ESP_RMAKER_MQTT_PORT_443 default 2 if ESP_RMAKER_MQTT_PORT_8883 + config ESP_RMAKER_DEF_TIMEZONE + string "Default Timezone" + default "" + help + Default Timezone to use. Eg. "Asia/Shanghai", "America/Los_Angeles". + Check documentation for complete list of valid values. This value + will be used only if no timezone is set using the C APIs. + config ESP_RMAKER_SNTP_SERVER_NAME string "ESP RainMaker SNTP Server Name" default "pool.ntp.org" diff --git a/components/esp_rainmaker/include/esp_rmaker_standard_params.h b/components/esp_rainmaker/include/esp_rmaker_standard_params.h index fed25a5..a37b206 100644 --- a/components/esp_rainmaker/include/esp_rmaker_standard_params.h +++ b/components/esp_rainmaker/include/esp_rmaker_standard_params.h @@ -41,6 +41,8 @@ extern "C" #define ESP_RMAKER_DEF_OTA_STATUS_NAME "status" #define ESP_RMAKER_DEF_OTA_INFO_NAME "info" #define ESP_RMAKER_DEF_OTA_URL_NAME "url" +#define ESP_RMAKER_DEF_TIMEZONE_NAME "tz" +#define ESP_RMAKER_DEF_TIMEZONE_POSIX_NAME "tz_posix" /** * Create standard name param @@ -216,6 +218,31 @@ esp_rmaker_param_t *esp_rmaker_ota_info_param_create(const char *param_name); */ esp_rmaker_param_t *esp_rmaker_ota_url_param_create(const char *param_name); +/** + * Create standard Timezone param + * + * This will create the standard timezone parameter. + * + * @param[in] param_name Name of the parameter + * @param[in] val Default Value of the parameter (Eg. "Asia/Shanghai"). Can be kept NULL. + * + * @return Parameter handle on success. + * @return NULL in case of failures. + */ +esp_rmaker_param_t *esp_rmaker_timezone_param_create(const char *param_name, const char *val); + +/** + * Create standard POSIX Timezone param + * + * This will create the standard posix timezone parameter. + * + * @param[in] param_name Name of the parameter + * @param[in] val Default Value of the parameter (Eg. "CST-8"). Can be kept NULL. + * + * @return Parameter handle on success. + * @return NULL in case of failures. + */ +esp_rmaker_param_t *esp_rmaker_timezone_posix_param_create(const char *param_name, const char *val); #ifdef __cplusplus } #endif diff --git a/components/esp_rainmaker/include/esp_rmaker_standard_services.h b/components/esp_rainmaker/include/esp_rmaker_standard_services.h index e713a62..9964bd1 100644 --- a/components/esp_rainmaker/include/esp_rmaker_standard_services.h +++ b/components/esp_rainmaker/include/esp_rmaker_standard_services.h @@ -37,6 +37,23 @@ extern "C" */ esp_rmaker_device_t *esp_rmaker_ota_service_create(const char *serv_name, void *priv_data); +/** Create a standard OTA service + * + * This creates an OTA service with the mandatory parameters. The default parameter names will be used. + * Refer \ref esp_rmaker_standard_params.h for default names. + * + * @param[in] serv_name The unique service name + * @param[in] timezone Default value of timezone string (Eg. "Asia/Shanghai"). Can be kept NULL. + * @param[in] timezone_posix Default value of posix timezone string (Eg. "CST-8"). Can be kept NULL. + * @param[in] priv_data (Optional) Private data associated with the service. This should stay + * allocated throughout the lifetime of the service. + * + * @return service_handle on success. + * @return NULL in case of any error. + */ +esp_rmaker_device_t *esp_rmaker_time_service_create(const char *serv_name, const char *timezone, + const char *timezone_posix, void *priv_data); + #ifdef __cplusplus } #endif diff --git a/components/esp_rainmaker/include/esp_rmaker_standard_types.h b/components/esp_rainmaker/include/esp_rmaker_standard_types.h index 205bbff..96a04c5 100644 --- a/components/esp_rainmaker/include/esp_rmaker_standard_types.h +++ b/components/esp_rainmaker/include/esp_rmaker_standard_types.h @@ -41,7 +41,8 @@ extern "C" #define ESP_RMAKER_PARAM_OTA_STATUS "esp.param.ota_status" #define ESP_RMAKER_PARAM_OTA_INFO "esp.param.ota_info" #define ESP_RMAKER_PARAM_OTA_URL "esp.param.ota_url" - +#define ESP_RMAKER_PARAM_TIMEZONE "esp.param.tz" +#define ESP_RMAKER_PARAM_TIMEZONE_POSIX "esp.param.tz_posix" /********** STANDARD DEVICE TYPES **********/ @@ -52,6 +53,7 @@ extern "C" /********** STANDARD SERVICE TYPES **********/ #define ESP_RMAKER_SERVICE_OTA "esp.service.ota" +#define ESP_RMAKER_SERVICE_TIME "esp.service.time" #ifdef __cplusplus } diff --git a/components/esp_rainmaker/include/esp_rmaker_utils.h b/components/esp_rainmaker/include/esp_rmaker_utils.h index fa8464c..f506bf7 100644 --- a/components/esp_rainmaker/include/esp_rmaker_utils.h +++ b/components/esp_rainmaker/include/esp_rmaker_utils.h @@ -13,6 +13,7 @@ // limitations under the License. #pragma once #include +#include #include #ifdef __cplusplus @@ -23,6 +24,12 @@ extern "C" typedef struct esp_rmaker_time_config { /** If not specified, then 'CONFIG_ESP_RMAKER_SNTP_SERVER_NAME' is used as the SNTP server. */ char *sntp_server_name; + /** Optional callback to invoke, whenever time is synchronised. This will be called + * periodically as per the SNTP polling interval (which is 60min by default). + * If kept NULL, the default callback will be invoked, which will just print the + * current local time. + */ + sntp_sync_time_cb_t sync_time_cb; } esp_rmaker_time_config_t; /** Reboot the chip after a delay @@ -104,6 +111,64 @@ bool esp_rmaker_time_check(void); */ esp_err_t esp_rmaker_time_wait_for_sync(uint32_t ticks_to_wait); +/** Set POSIX timezone + * + * Set the timezone (TZ environment variable) as per the POSIX format + * specified in the [GNU libc documentation](https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html). + * Eg. For China: "CST-8" + * For US Pacific Time (including daylight saving information): "PST8PDT,M3.2.0,M11.1.0" + * + * @param[in] tz_posix NULL terminated TZ POSIX string + * + * @return ESP_OK on success + * @return error on failure + */ +esp_err_t esp_rmaker_time_set_timezone_posix(const char *tz_posix); + +/** Set timezone location string + * + * Set the timezone as a user friendly location string. + * Check [here](https://rainmaker.espressif.com/docs/time-service.html) for a list of valid values. + * + * Eg. For China: "Asia/Shanghai" + * For US Pacific Time: "America/Los_Angeles" + * + * @note Setting timezone using this API internally also sets the POSIX timezone string. + * + * @param[in] tz NULL terminated Timezone location string + * + * @return ESP_OK on success + * @return error on failure + */ +esp_err_t esp_rmaker_time_set_timezone(const char *tz); + +/** Enable Timezone Service + * + * This enables the ESP RainMaker standard timezone service which can be used to set + * timezone, either in POSIX or location string format. Please refer the specifications + * for additional details. + * + * @return ESP_OK on success + * @return error on failure + */ +esp_err_t esp_rmaker_timezone_service_enable(void); + +/** Get printable local time string + * + * Get a printable local time string, with information of timezone and Daylight Saving. + * Eg. "Tue Sep 1 09:04:38 2020 -0400[EDT], DST: Yes" + * "Tue Sep 1 21:04:04 2020 +0800[CST], DST: No" + * + * + * @param[out] buf Pointer to a pre-allocated buffer into which the time string will + * be populated. + * @param[in] buf_len Length of the above buffer. + * + * @return ESP_OK on success + * @return error on failure + */ +esp_err_t esp_rmaker_get_local_time_str(char *buf, size_t buf_len); + #ifdef __cplusplus } #endif diff --git a/components/esp_rainmaker/src/console/esp_rmaker_commands.c b/components/esp_rainmaker/src/console/esp_rmaker_commands.c index b5209ef..913d0f1 100644 --- a/components/esp_rainmaker/src/console/esp_rmaker_commands.c +++ b/components/esp_rainmaker/src/console/esp_rmaker_commands.c @@ -33,6 +33,7 @@ #include #include +#include #include @@ -352,6 +353,55 @@ static void register_wifi_prov() esp_console_cmd_register(&cmd); } +static int local_time_cli_handler(int argc, char *argv[]) +{ + char local_time[64]; + if (esp_rmaker_get_local_time_str(local_time, sizeof(local_time)) == ESP_OK) { + printf("%s: Current local time: %s\n", TAG, local_time); + } else { + printf("%s: Current local time (truncated): %s\n", TAG, local_time); + } + return ESP_OK; +} + +static int tz_set_cli_handler(int argc, char *argv[]) +{ + if (argc < 2) { + printf("%s: Invalid Usage.\n", TAG); + return ESP_ERR_INVALID_ARG; + } + if (strcmp(argv[1], "posix") == 0) { + if (argv[2]) { + esp_rmaker_time_set_timezone_posix(argv[2]); + } else { + printf("%s: Invalid Usage.\n", TAG); + return ESP_ERR_INVALID_ARG; + } + } else { + esp_rmaker_time_set_timezone(argv[1]); + } + return ESP_OK; +} + +static void register_time_commands() +{ + const esp_console_cmd_t local_time_cmd = { + .command = "local-time", + .help = "Get the local time of device.", + .func = &local_time_cli_handler, + }; + ESP_LOGI(TAG, "Registering command: %s", local_time_cmd.command); + esp_console_cmd_register(&local_time_cmd); + + const esp_console_cmd_t tz_set_cmd = { + .command = "tz-set", + .help = "Set Timezone. Usage: tz-set [posix] .", + .func = &tz_set_cli_handler, + }; + ESP_LOGI(TAG, "Registering command: %s", tz_set_cmd.command); + esp_console_cmd_register(&tz_set_cmd); +} + void register_commands() { register_generic_debug_commands(); @@ -359,4 +409,5 @@ void register_commands() register_user_node_mapping(); register_get_node_id(); register_wifi_prov(); + register_time_commands(); } diff --git a/components/esp_rainmaker/src/core/esp_rmaker_time_sync.c b/components/esp_rainmaker/src/core/esp_rmaker_time_sync.c index 0d64b38..11fea20 100644 --- a/components/esp_rainmaker/src/core/esp_rmaker_time_sync.c +++ b/components/esp_rainmaker/src/core/esp_rmaker_time_sync.c @@ -11,14 +11,154 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. +#include #include -#include "lwip/apps/sntp.h" +#include +#include + +#include +#include +#include #include static const char *TAG = "esp_rmaker_time_sync"; +#define ESP_RMAKER_NVS_PART_NAME "nvs" + +#define ESP_RMAKER_TIME_SERV_NAME "time" +#define ESP_RMAKER_NVS_TIME_NAMESPACE "rmaker_time" +#define ESP_RMAKER_TZ_POSIX_NVS_NAME "tz_posix" +#define ESP_RMAKER_TZ_NVS_NAME "tz" + #define REF_TIME 1546300800 /* 01-Jan-2019 00:00:00 */ static bool init_done = false; +extern const char *esp_rmaker_tz_db_get_posix_str(const char *name); + +#define ESP_RMAKER_DEF_TZ CONFIG_ESP_RMAKER_DEF_TIMEZONE + +esp_err_t esp_rmaker_get_local_time_str(char *buf, size_t buf_len) +{ + struct tm timeinfo; + char strftime_buf[64]; + time_t now; + time(&now); + localtime_r(&now, &timeinfo); + strftime(strftime_buf, sizeof(strftime_buf), "%c %z[%Z]", &timeinfo); + size_t print_size = snprintf(buf, buf_len, "%s, DST: %s", strftime_buf, timeinfo.tm_isdst ? "Yes" : "No"); + if (print_size >= buf_len) { + ESP_LOGE(TAG, "Buffer size %d insufficient for localtime string. REquired size: %d", buf_len, print_size); + return ESP_ERR_INVALID_ARG; + } + return ESP_OK; +} + +static esp_err_t esp_rmaker_print_current_time(void) +{ + char local_time[64]; + if (esp_rmaker_get_local_time_str(local_time, sizeof(local_time)) == ESP_OK) { + if (esp_rmaker_time_check() == false) { + ESP_LOGI(TAG, "Time not synchronised yet."); + } + ESP_LOGI(TAG, "The current time is: %s.", local_time); + return ESP_OK; + } + return ESP_FAIL; +} + +static char *__esp_rmaker_time_get_nvs(const char *key) +{ + char *val = NULL; + nvs_handle handle; + esp_err_t err = nvs_open_from_partition(ESP_RMAKER_NVS_PART_NAME, ESP_RMAKER_NVS_TIME_NAMESPACE, NVS_READONLY, &handle); + if (err != ESP_OK) { + return NULL; + } + size_t len = 0; + if ((err = nvs_get_blob(handle, key, NULL, &len)) == ESP_OK) { + val = calloc(1, len + 1); /* +1 for NULL termination */ + if (val) { + nvs_get_blob(handle, key, val, &len); + } + } + nvs_close(handle); + return val; + +} + +static esp_err_t __esp_rmaker_time_set_nvs(const char *key, const char *val) +{ + nvs_handle handle; + esp_err_t err = nvs_open_from_partition(ESP_RMAKER_NVS_PART_NAME, ESP_RMAKER_NVS_TIME_NAMESPACE, NVS_READWRITE, &handle); + if (err != ESP_OK) { + return err; + } + err = nvs_set_blob(handle, key, val, strlen(val)); + nvs_commit(handle); + nvs_close(handle); + return err; +} + +static char *esp_rmaker_time_get_timezone_posix(void) +{ + return __esp_rmaker_time_get_nvs(ESP_RMAKER_TZ_POSIX_NVS_NAME); +} + +static char *esp_rmaker_time_get_timezone(void) +{ + return __esp_rmaker_time_get_nvs(ESP_RMAKER_TZ_NVS_NAME); +} + +esp_err_t esp_rmaker_time_set_timezone_posix(const char *tz_posix) +{ + esp_err_t err = __esp_rmaker_time_set_nvs(ESP_RMAKER_TZ_POSIX_NVS_NAME, tz_posix); + if (err == ESP_OK) { + setenv("TZ", tz_posix, 1); + tzset(); + esp_rmaker_print_current_time(); + } + return err; +} + +esp_err_t esp_rmaker_time_set_timezone(const char *tz) +{ + const char *tz_posix = esp_rmaker_tz_db_get_posix_str(tz); + if (!tz_posix) { + return ESP_ERR_INVALID_ARG; + } + esp_err_t err = esp_rmaker_time_set_timezone_posix(tz_posix); + if (err == ESP_OK) { + err = __esp_rmaker_time_set_nvs(ESP_RMAKER_TZ_NVS_NAME, tz); + } + return err; +} + +esp_err_t esp_rmaker_timezone_enable(void) +{ + char *tz_posix = esp_rmaker_time_get_timezone_posix(); + if (tz_posix) { + setenv("TZ", tz_posix, 1); + tzset(); + free(tz_posix); + } else { + if (strlen(ESP_RMAKER_DEF_TZ) > 0) { + const char *tz_def = esp_rmaker_tz_db_get_posix_str(ESP_RMAKER_DEF_TZ); + if (tz_def) { + setenv("TZ", tz_def, 1); + tzset(); + return ESP_OK; + } else { + ESP_LOGE(TAG, "Invalid Timezone %s specified.", ESP_RMAKER_DEF_TZ); + return ESP_ERR_INVALID_ARG; + } + } + } + return ESP_OK; +} +static void esp_rmaker_time_sync_cb(struct timeval *tv) +{ + ESP_LOGI(TAG, "SNTP Synchronised."); + esp_rmaker_print_current_time(); +} esp_err_t esp_rmaker_time_sync_init(esp_rmaker_time_config_t *config) { @@ -37,6 +177,12 @@ esp_err_t esp_rmaker_time_sync_init(esp_rmaker_time_config_t *config) sntp_setoperatingmode(SNTP_OPMODE_POLL); sntp_setservername(0, sntp_server_name); sntp_init(); + if (config->sync_time_cb) { + sntp_set_time_sync_notification_cb(config->sync_time_cb); + } else { + sntp_set_time_sync_notification_cb(esp_rmaker_time_sync_cb); + } + esp_rmaker_timezone_enable(); init_done = true; return ESP_OK; } @@ -52,6 +198,7 @@ bool esp_rmaker_time_check(void) } #define DEFAULT_TICKS (2000 / portTICK_PERIOD_MS) /* 2 seconds in ticks */ + esp_err_t esp_rmaker_time_wait_for_sync(uint32_t ticks_to_wait) { if (!init_done) { @@ -77,12 +224,63 @@ esp_err_t esp_rmaker_time_wait_for_sync(uint32_t ticks_to_wait) } /* Get current time */ - struct tm timeinfo; - char strftime_buf[64]; - time_t now; - time(&now); - localtime_r(&now, &timeinfo); - strftime(strftime_buf, sizeof(strftime_buf), "%c", &timeinfo); - ESP_LOGI(TAG, "The current UTC time is: %s", strftime_buf); + esp_rmaker_print_current_time(); return ESP_OK; } + +static esp_err_t esp_rmaker_time_service_cb(const esp_rmaker_device_t *device, const esp_rmaker_param_t *param, + const esp_rmaker_param_val_t val, void *priv_data, esp_rmaker_write_ctx_t *ctx) +{ + esp_err_t err = ESP_FAIL; + if (strcmp(esp_rmaker_param_get_type(param), ESP_RMAKER_PARAM_TIMEZONE) == 0) { + ESP_LOGI(TAG, "Received value = %s for %s - %s", + val.val.s, esp_rmaker_device_get_name(device), esp_rmaker_param_get_name(param)); + err = esp_rmaker_time_set_timezone(val.val.s); + if (err == ESP_OK) { + char *tz_posix = esp_rmaker_time_get_timezone_posix(); + if (tz_posix) { + esp_rmaker_param_t *tz_posix_param = esp_rmaker_device_get_param_by_type( + device, ESP_RMAKER_PARAM_TIMEZONE_POSIX); + esp_rmaker_param_update_and_report(tz_posix_param, esp_rmaker_str(tz_posix)); + free(tz_posix); + } + } + } else if (strcmp(esp_rmaker_param_get_type(param), ESP_RMAKER_PARAM_TIMEZONE_POSIX) == 0) { + ESP_LOGI(TAG, "Received value = %s for %s - %s", + val.val.s, esp_rmaker_device_get_name(device), esp_rmaker_param_get_name(param)); + err = esp_rmaker_time_set_timezone_posix(val.val.s); + } + if (err == ESP_OK) { + esp_rmaker_param_update_and_report(param, val); + } + return err; +} + +static esp_err_t esp_rmaker_time_add_service(const char *tz, const char *tz_posix) +{ + esp_rmaker_device_t *service = esp_rmaker_time_service_create(ESP_RMAKER_TIME_SERV_NAME, tz, tz_posix, NULL); + if (!service) { + ESP_LOGE(TAG, "Failed to create Time Service"); + return ESP_FAIL; + } + esp_rmaker_device_add_cb(service, esp_rmaker_time_service_cb, NULL); + esp_err_t err = esp_rmaker_node_add_device(esp_rmaker_get_node(), service); + if (err == ESP_OK) { + ESP_LOGI(TAG, "Time service enabled"); + } + return err; +} + +esp_err_t esp_rmaker_timezone_service_enable(void) +{ + char *tz_posix = esp_rmaker_time_get_timezone_posix(); + char *tz = esp_rmaker_time_get_timezone(); + esp_err_t err = esp_rmaker_time_add_service(tz, tz_posix); + if (tz_posix) { + free(tz_posix); + } + if (tz) { + free(tz); + } + return err; +} diff --git a/components/esp_rainmaker/src/core/esp_rmaker_timezone.c b/components/esp_rainmaker/src/core/esp_rmaker_timezone.c new file mode 100644 index 0000000..059c55b --- /dev/null +++ b/components/esp_rainmaker/src/core/esp_rmaker_timezone.c @@ -0,0 +1,515 @@ +// MIT License +// +// Copyright (c) 2020 Nayar Systems +// Copyright (c) 2020 Jacob Lambert +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +// Original code taken from: https://github.com/jdlambert/micro_tz_db +// which was forked from https://github.com/nayarsystems/posix_tz_db + +#include + +typedef struct { + const char *name; + const char *posix_str; +} esp_rmaker_tz_db_pair_t; + +static const esp_rmaker_tz_db_pair_t esp_rmaker_tz_db_tzs[] = { + {"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-2"}, + {"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", "EAT-3"}, + {"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", "CST6CDT,M4.1.0,M10.5.0"}, + {"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", "MST7MDT,M4.1.0,M10.5.0"}, + {"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/Fortaleza", "<-03>3"}, + {"America/Fort_Nelson", "MST7"}, + {"America/Glace_Bay", "AST4ADT,M3.2.0,M11.1.0"}, + {"America/Godthab", "<-03>3<-02>,M3.5.0/-2,M10.5.0/-1"}, + {"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", "MST7MDT,M4.1.0,M10.5.0"}, + {"America/Menominee", "CST6CDT,M3.2.0,M11.1.0"}, + {"America/Merida", "CST6CDT,M4.1.0,M10.5.0"}, + {"America/Metlakatla", "AKST9AKDT,M3.2.0,M11.1.0"}, + {"America/Mexico_City", "CST6CDT,M4.1.0,M10.5.0"}, + {"America/Miquelon", "<-03>3<-02>,M3.2.0,M11.1.0"}, + {"America/Moncton", "AST4ADT,M3.2.0,M11.1.0"}, + {"America/Monterrey", "CST6CDT,M4.1.0,M10.5.0"}, + {"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/Ojinaga", "MST7MDT,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", "<-01>1<+00>,M3.5.0/0,M10.5.0/1"}, + {"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", "<+11>-11"}, + {"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", "<+06>-6"}, + {"Arctic/Longyearbyen", "CET-1CEST,M3.5.0,M10.5.0/3"}, + {"Asia/Aden", "<+03>-3"}, + {"Asia/Almaty", "<+06>-6"}, + {"Asia/Amman", "EET-2EEST,M3.5.4/24,M10.5.5/1"}, + {"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", "EET-2EEST,M3.5.5/0,M10.5.5/0"}, + {"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.5.5/0,M10.5.6/1"}, + {"Asia/Hebron", "EET-2EEST,M3.5.5/0,M10.5.6/1"}, + {"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<+0430>,J79/24,J263/24"}, + {"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/Stanley", "<-03>3"}, + {"Atlantic/St_Helena", "GMT0"}, + {"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", "<+03>-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", "<+04>-4"}, + {"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<+14>,M9.5.0/3,M4.1.0/4"}, + {"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<+13>,M11.2.0,M1.2.3/99"}, + {"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"} +}; + +static char lower(char start) { + if ('A' <= start && start <= 'Z') { + return start - 'A' + 'a'; + } + return start; +} + +/** + * Basically strcmp, but accounting for spaces that have become underscores + * @param[in] target - the 0-terminated string on the left hand side of the comparison + * @param[in] other - the 0-terminated string on the right hand side of the comparison + * @return > 0 if target comes before other alphabetically, + * ==0 if they're the same, + * < 0 if other comes before target alphabetically + * (we don't expect NULL arguments, but, -1 if either is NULL) + **/ +static int tz_name_cmp(const char * target, const char * other) +{ + if (!target || !other) { + return -1; + } + + while (*target) { + if (lower(*target) != lower(*other)) { + break; + } + do { + target++; + } while (*target == '_'); + do { + other++; + } while (*other == '_'); + } + + return lower(*target) - lower(*other); +} + +const char *esp_rmaker_tz_db_get_posix_str(const char *name) +{ + int lo = 0, hi = sizeof(esp_rmaker_tz_db_tzs) / sizeof(esp_rmaker_tz_db_pair_t); + while (lo < hi) { + int mid = (lo + hi) / 2; + esp_rmaker_tz_db_pair_t mid_pair = esp_rmaker_tz_db_tzs[mid]; + int comparison = tz_name_cmp(name, mid_pair.name); + if (comparison == 0) { + return mid_pair.posix_str; + } else if (comparison < 0) { + hi = mid; + } else { + lo = mid + 1; + } + } + return NULL; +} diff --git a/components/esp_rainmaker/src/standard_types/esp_rmaker_standard_params.c b/components/esp_rainmaker/src/standard_types/esp_rmaker_standard_params.c index 5bc3190..cb5075a 100644 --- a/components/esp_rainmaker/src/standard_types/esp_rmaker_standard_params.c +++ b/components/esp_rainmaker/src/standard_types/esp_rmaker_standard_params.c @@ -138,3 +138,17 @@ esp_rmaker_param_t *esp_rmaker_ota_url_param_create(const char *param_name) esp_rmaker_str(""), PROP_FLAG_WRITE); return param; } + +esp_rmaker_param_t *esp_rmaker_timezone_param_create(const char *param_name, const char *val) +{ + esp_rmaker_param_t *param = esp_rmaker_param_create(param_name, ESP_RMAKER_PARAM_TIMEZONE, + esp_rmaker_str(val), PROP_FLAG_READ | PROP_FLAG_WRITE); + return param; +} + +esp_rmaker_param_t *esp_rmaker_timezone_posix_param_create(const char *param_name, const char *val) +{ + esp_rmaker_param_t *param = esp_rmaker_param_create(param_name, ESP_RMAKER_PARAM_TIMEZONE_POSIX, + esp_rmaker_str(val), PROP_FLAG_READ | PROP_FLAG_WRITE); + return param; +} diff --git a/components/esp_rainmaker/src/standard_types/esp_rmaker_standard_services.c b/components/esp_rainmaker/src/standard_types/esp_rmaker_standard_services.c index 82f5174..3861c59 100644 --- a/components/esp_rainmaker/src/standard_types/esp_rmaker_standard_services.c +++ b/components/esp_rainmaker/src/standard_types/esp_rmaker_standard_services.c @@ -27,3 +27,15 @@ esp_rmaker_device_t *esp_rmaker_ota_service_create(const char *serv_name, void * return service; } +esp_rmaker_device_t *esp_rmaker_time_service_create(const char *serv_name, const char *timezone, + const char *timezone_posix, void *priv_data) +{ + esp_rmaker_device_t *service = esp_rmaker_service_create(serv_name, ESP_RMAKER_SERVICE_TIME, priv_data); + if (service) { + esp_rmaker_device_add_param(service, esp_rmaker_timezone_param_create( + ESP_RMAKER_DEF_TIMEZONE_NAME, timezone)); + esp_rmaker_device_add_param(service, esp_rmaker_timezone_posix_param_create( + ESP_RMAKER_DEF_TIMEZONE_POSIX_NAME, timezone_posix)); + } + return service; +}