273 lines
7.9 KiB
C
273 lines
7.9 KiB
C
#include "tool_set_timezone.h"
|
|
#include "mimi_config.h"
|
|
#include "proxy/http_proxy.h"
|
|
|
|
#include <string.h>
|
|
#include <strings.h>
|
|
#include <stdlib.h>
|
|
#include <stdbool.h>
|
|
#include <time.h>
|
|
#include <sys/time.h>
|
|
#include "esp_log.h"
|
|
#include "esp_http_client.h"
|
|
#include "esp_crt_bundle.h"
|
|
#include "nvs.h"
|
|
#include "cJSON.h"
|
|
|
|
static const char *TAG = "tool_timezone";
|
|
|
|
static const char *MONTHS[] = {
|
|
"Jan","Feb","Mar","Apr","May","Jun",
|
|
"Jul","Aug","Sep","Oct","Nov","Dec"
|
|
};
|
|
|
|
typedef struct {
|
|
char date_val[64];
|
|
} tz_time_ctx_t;
|
|
|
|
static esp_err_t tz_http_event_handler(esp_http_client_event_t *evt)
|
|
{
|
|
tz_time_ctx_t *ctx = evt->user_data;
|
|
if (evt->event_id == HTTP_EVENT_ON_HEADER) {
|
|
if (strcasecmp(evt->header_key, "Date") == 0 && ctx) {
|
|
strncpy(ctx->date_val, evt->header_value, sizeof(ctx->date_val) - 1);
|
|
ctx->date_val[sizeof(ctx->date_val) - 1] = '\0';
|
|
}
|
|
}
|
|
return ESP_OK;
|
|
}
|
|
|
|
static bool parse_and_set_time_from_date(const char *date_str)
|
|
{
|
|
int day, year, hour, min, sec;
|
|
char mon_str[4] = {0};
|
|
if (sscanf(date_str, "%*[^,], %d %3s %d %d:%d:%d",
|
|
&day, mon_str, &year, &hour, &min, &sec) != 6) {
|
|
return false;
|
|
}
|
|
|
|
int mon = -1;
|
|
for (int i = 0; i < 12; i++) {
|
|
if (strcmp(mon_str, MONTHS[i]) == 0) { mon = i; break; }
|
|
}
|
|
if (mon < 0) return false;
|
|
|
|
struct tm tm = {
|
|
.tm_sec = sec, .tm_min = min, .tm_hour = hour,
|
|
.tm_mday = day, .tm_mon = mon, .tm_year = year - 1900,
|
|
};
|
|
|
|
setenv("TZ", "UTC0", 1);
|
|
tzset();
|
|
time_t t = mktime(&tm);
|
|
|
|
if (t < 0) return false;
|
|
|
|
struct timeval tv = { .tv_sec = t };
|
|
settimeofday(&tv, NULL);
|
|
return true;
|
|
}
|
|
|
|
static bool fetch_and_set_time(void)
|
|
{
|
|
if (http_proxy_is_enabled()) {
|
|
proxy_conn_t *conn = proxy_conn_open("api.telegram.org", 443, 10000);
|
|
if (!conn) return false;
|
|
|
|
const char *req = "HEAD / HTTP/1.1\r\nHost: api.telegram.org\r\nConnection: close\r\n\r\n";
|
|
if (proxy_conn_write(conn, req, strlen(req)) < 0) {
|
|
proxy_conn_close(conn);
|
|
return false;
|
|
}
|
|
|
|
char buf[1024];
|
|
int total = 0;
|
|
while (total < (int)sizeof(buf) - 1) {
|
|
int n = proxy_conn_read(conn, buf + total, sizeof(buf) - 1 - total, 10000);
|
|
if (n <= 0) break;
|
|
total += n;
|
|
buf[total] = '\0';
|
|
if (strstr(buf, "\r\n\r\n")) break;
|
|
}
|
|
proxy_conn_close(conn);
|
|
|
|
char *date_hdr = strcasestr(buf, "\r\nDate: ");
|
|
if (!date_hdr) return false;
|
|
date_hdr += 8;
|
|
char *eol = strstr(date_hdr, "\r\n");
|
|
if (!eol) return false;
|
|
|
|
char date_val[64];
|
|
size_t dlen = eol - date_hdr;
|
|
if (dlen >= sizeof(date_val)) return false;
|
|
memcpy(date_val, date_hdr, dlen);
|
|
date_val[dlen] = '\0';
|
|
|
|
return parse_and_set_time_from_date(date_val);
|
|
} else {
|
|
tz_time_ctx_t ctx = {0};
|
|
esp_http_client_config_t config = {
|
|
.url = "https://api.telegram.org/",
|
|
.method = HTTP_METHOD_HEAD,
|
|
.timeout_ms = 10000,
|
|
.crt_bundle_attach = esp_crt_bundle_attach,
|
|
.event_handler = tz_http_event_handler,
|
|
.user_data = &ctx,
|
|
};
|
|
|
|
esp_http_client_handle_t client = esp_http_client_init(&config);
|
|
if (!client) return false;
|
|
|
|
esp_err_t err = esp_http_client_perform(client);
|
|
esp_http_client_cleanup(client);
|
|
|
|
if (err != ESP_OK || ctx.date_val[0] == '\0') return false;
|
|
return parse_and_set_time_from_date(ctx.date_val);
|
|
}
|
|
}
|
|
|
|
/* Common timezone mappings for user-friendly names */
|
|
typedef struct {
|
|
const char *name;
|
|
const char *posix_tz;
|
|
} tz_mapping_t;
|
|
|
|
static const tz_mapping_t tz_mappings[] = {
|
|
{ "Asia/Shanghai", "CST-8" },
|
|
{ "Asia/Beijing", "CST-8" },
|
|
{ "Asia/Hong_Kong", "HKT-8" },
|
|
{ "Asia/Tokyo", "JST-9" },
|
|
{ "Asia/Seoul", "KST-9" },
|
|
{ "Asia/Singapore", "SGT-8" },
|
|
{ "Asia/Kolkata", "IST-5:30" },
|
|
{ "Asia/Dubai", "GST-4" },
|
|
{ "Europe/London", "GMT0BST,M3.5.0/1,M10.5.0" },
|
|
{ "Europe/Paris", "CET-1CEST,M3.5.0,M10.5.0/3" },
|
|
{ "Europe/Berlin", "CET-1CEST,M3.5.0,M10.5.0/3" },
|
|
{ "America/New_York", "EST5EDT,M3.2.0,M11.1.0" },
|
|
{ "America/Chicago", "CST6CDT,M3.2.0,M11.1.0" },
|
|
{ "America/Denver", "MST7MDT,M3.2.0,M11.1.0" },
|
|
{ "America/Los_Angeles", "PST8PDT,M3.2.0,M11.1.0" },
|
|
{ "Australia/Sydney", "AEST-10AEDT,M10.1.0,M4.1.0/3" },
|
|
{ "UTC", "UTC0" },
|
|
{ "GMT", "GMT0" },
|
|
};
|
|
|
|
static const char *resolve_timezone(const char *tz_str)
|
|
{
|
|
if (!tz_str || !tz_str[0]) return NULL;
|
|
|
|
for (size_t i = 0; i < sizeof(tz_mappings) / sizeof(tz_mappings[0]); i++) {
|
|
if (strcmp(tz_str, tz_mappings[i].name) == 0) {
|
|
return tz_mappings[i].posix_tz;
|
|
}
|
|
}
|
|
|
|
return tz_str;
|
|
}
|
|
|
|
static bool validate_timezone(const char *tz_str)
|
|
{
|
|
if (!tz_str || !tz_str[0]) return false;
|
|
if (strlen(tz_str) > 64) return false;
|
|
|
|
for (size_t i = 0; i < sizeof(tz_mappings) / sizeof(tz_mappings[0]); i++) {
|
|
if (strcmp(tz_str, tz_mappings[i].name) == 0) return true;
|
|
}
|
|
|
|
if (strchr(tz_str, '+') || strchr(tz_str, '-') ||
|
|
strcmp(tz_str, "UTC0") == 0 || strcmp(tz_str, "GMT0") == 0) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static esp_err_t save_timezone_nvs(const char *tz_str)
|
|
{
|
|
nvs_handle_t nvs;
|
|
esp_err_t err = nvs_open("system_config", NVS_READWRITE, &nvs);
|
|
if (err != ESP_OK) {
|
|
ESP_LOGE(TAG, "Failed to open NVS: %s", esp_err_to_name(err));
|
|
return err;
|
|
}
|
|
|
|
err = nvs_set_str(nvs, MIMI_NVS_KEY_TIMEZONE, tz_str);
|
|
if (err != ESP_OK) {
|
|
ESP_LOGE(TAG, "Failed to save timezone: %s", esp_err_to_name(err));
|
|
nvs_close(nvs);
|
|
return err;
|
|
}
|
|
|
|
err = nvs_commit(nvs);
|
|
nvs_close(nvs);
|
|
|
|
if (err != ESP_OK) {
|
|
ESP_LOGE(TAG, "Failed to commit NVS: %s", esp_err_to_name(err));
|
|
return err;
|
|
}
|
|
|
|
return ESP_OK;
|
|
}
|
|
|
|
esp_err_t tool_set_timezone_execute(const char *input_json, char *output, size_t output_size)
|
|
{
|
|
ESP_LOGI(TAG, "Setting timezone...");
|
|
|
|
cJSON *root = cJSON_Parse(input_json);
|
|
if (!root) {
|
|
snprintf(output, output_size, "Error: invalid JSON input");
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
|
|
cJSON *tz_item = cJSON_GetObjectItem(root, "timezone");
|
|
if (!tz_item || !cJSON_IsString(tz_item)) {
|
|
cJSON_Delete(root);
|
|
snprintf(output, output_size, "Error: 'timezone' field required (string)");
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
|
|
const char *input_tz = tz_item->valuestring;
|
|
const char *resolved_tz = resolve_timezone(input_tz);
|
|
|
|
if (!resolved_tz || !validate_timezone(resolved_tz)) {
|
|
cJSON_Delete(root);
|
|
snprintf(output, output_size, "Error: invalid timezone format '%s'. Use POSIX format (e.g. CST-8) or city name (e.g. Asia/Shanghai)", input_tz);
|
|
return ESP_ERR_INVALID_ARG;
|
|
}
|
|
|
|
esp_err_t err = save_timezone_nvs(resolved_tz);
|
|
if (err != ESP_OK) {
|
|
cJSON_Delete(root);
|
|
snprintf(output, output_size, "Error: failed to save timezone (%s)", esp_err_to_name(err));
|
|
return err;
|
|
}
|
|
|
|
setenv("TZ", resolved_tz, 1);
|
|
tzset();
|
|
|
|
time_t now = time(NULL);
|
|
bool time_valid = (now > 1700000000); /* 2023-11-15 as sanity check */
|
|
|
|
if (!time_valid) {
|
|
ESP_LOGI(TAG, "System time appears invalid, fetching from NTP server...");
|
|
if (fetch_and_set_time()) {
|
|
now = time(NULL);
|
|
ESP_LOGI(TAG, "Time fetched via HTTP after timezone set");
|
|
} else {
|
|
ESP_LOGW(TAG, "Failed to fetch time via HTTP, waiting for SNTP sync");
|
|
}
|
|
}
|
|
|
|
struct tm tm_now;
|
|
localtime_r(&now, &tm_now);
|
|
char time_str[64];
|
|
strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S %Z", &tm_now);
|
|
|
|
cJSON_Delete(root);
|
|
snprintf(output, output_size, "Timezone set to '%s'. Current time: %s", resolved_tz, time_str);
|
|
ESP_LOGI(TAG, "Timezone set to: %s, current time: %s", resolved_tz, time_str);
|
|
|
|
return ESP_OK;
|
|
}
|