Adicionar primeiro

This commit is contained in:
2025-06-06 21:17:25 +01:00
parent c188084ba4
commit 282e7f517b
841 changed files with 199592 additions and 1 deletions

View File

@@ -0,0 +1,12 @@
set(srcs
"src/protocols.c"
"src/rest.c"
"src/mqtt.c"
"src/date_time.c"
)
idf_component_register(SRCS "${srcs}"
INCLUDE_DIRS "include"
PRIV_INCLUDE_DIRS "src"
PRIV_REQUIRES nvs_flash esp_http_server esp_netif esp_https_ota app_update json mqtt vfs spiffs # Use spiffs aqui
REQUIRES config api logger)

View File

@@ -0,0 +1,371 @@
#!/usr/bin/env python
import sys
ZONES_DIR = "/usr/share/zoneinfo/"
ZONES = [
"Africa/Abidjan",
"Africa/Algiers",
"Africa/Bissau",
"Africa/Cairo",
"Africa/Casablanca",
"Africa/Ceuta",
"Africa/El_Aaiun",
"Africa/Johannesburg",
"Africa/Juba",
"Africa/Khartoum",
"Africa/Lagos",
"Africa/Maputo",
"Africa/Monrovia",
"Africa/Nairobi",
"Africa/Ndjamena",
"Africa/Sao_Tome",
"Africa/Tripoli",
"Africa/Tunis",
"Africa/Windhoek",
"America/Adak",
"America/Anchorage",
"America/Araguaina",
"America/Argentina/Buenos_Aires",
"America/Argentina/Catamarca",
"America/Argentina/Cordoba",
"America/Argentina/Jujuy",
"America/Argentina/La_Rioja",
"America/Argentina/Mendoza",
"America/Argentina/Rio_Gallegos",
"America/Argentina/Salta",
"America/Argentina/San_Juan",
"America/Argentina/San_Luis",
"America/Argentina/Tucuman",
"America/Argentina/Ushuaia",
"America/Asuncion",
"America/Bahia",
"America/Bahia_Banderas",
"America/Barbados",
"America/Belem",
"America/Belize",
"America/Boa_Vista",
"America/Bogota",
"America/Boise",
"America/Cambridge_Bay",
"America/Campo_Grande",
"America/Cancun",
"America/Caracas",
"America/Cayenne",
"America/Cayman",
"America/Chicago",
"America/Chihuahua",
"America/Costa_Rica",
"America/Creston",
"America/Cuiaba",
"America/Curacao",
"America/Danmarkshavn",
"America/Dawson",
"America/Dawson_Creek",
"America/Denver",
"America/Detroit",
"America/Dominica",
"America/Edmonton",
"America/Eirunepe",
"America/El_Salvador",
"America/Fortaleza",
"America/Fort_Nelson",
"America/Glace_Bay",
"America/Godthab",
"America/Goose_Bay",
"America/Grand_Turk",
"America/Grenada",
"America/Guadeloupe",
"America/Guatemala",
"America/Guayaquil",
"America/Guyana",
"America/Halifax",
"America/Havana",
"America/Hermosillo",
"America/Indiana/Indianapolis",
"America/Indiana/Knox",
"America/Indiana/Marengo",
"America/Indiana/Petersburg",
"America/Indiana/Tell_City",
"America/Indiana/Vevay",
"America/Indiana/Vincennes",
"America/Indiana/Winamac",
"America/Inuvik",
"America/Iqaluit",
"America/Jamaica",
"America/Juneau",
"America/Kentucky/Louisville",
"America/Kentucky/Monticello",
"America/La_Paz",
"America/Lima",
"America/Los_Angeles",
"America/Maceio",
"America/Managua",
"America/Manaus",
"America/Marigot",
"America/Martinique",
"America/Matamoros",
"America/Mazatlan",
"America/Menominee",
"America/Merida",
"America/Metlakatla",
"America/Mexico_City",
"America/Miquelon",
"America/Moncton",
"America/Monterrey",
"America/Montevideo",
"America/New_York",
"America/Nome",
"America/Noronha",
"America/North_Dakota/Beulah",
"America/North_Dakota/Center",
"America/North_Dakota/New_Salem",
"America/Nuuk",
"America/Ojinaga",
"America/Panama",
"America/Paramaribo",
"America/Phoenix",
"America/Port-au-Prince",
"America/Porto_Velho",
"America/Puerto_Rico",
"America/Punta_Arenas",
"America/Rankin_Inlet",
"America/Recife",
"America/Regina",
"America/Resolute",
"America/Rio_Branco",
"America/Santarem",
"America/Santiago",
"America/Santo_Domingo",
"America/Sao_Paulo",
"America/Scoresbysund",
"America/Sitka",
"America/St_Johns",
"America/Swift_Current",
"America/Tegucigalpa",
"America/Thule",
"America/Tijuana",
"America/Toronto",
"America/Vancouver",
"America/Whitehorse",
"America/Winnipeg",
"America/Yakutat",
"Antarctica/Casey",
"Antarctica/Davis",
"Antarctica/Macquarie",
"Antarctica/Mawson",
"Antarctica/Palmer",
"Antarctica/Rothera",
"Antarctica/Troll",
"Asia/Almaty",
"Asia/Amman",
"Asia/Anadyr",
"Asia/Aqtau",
"Asia/Aqtobe",
"Asia/Ashgabat",
"Asia/Atyrau",
"Asia/Baghdad",
"Asia/Bahrain",
"Asia/Baku",
"Asia/Bangkok",
"Asia/Barnaul",
"Asia/Beirut",
"Asia/Bishkek",
"Asia/Brunei",
"Asia/Chita",
"Asia/Choibalsan",
"Asia/Colombo",
"Asia/Damascus",
"Asia/Dhaka",
"Asia/Dili",
"Asia/Dubai",
"Asia/Dushanbe",
"Asia/Famagusta",
"Asia/Gaza",
"Asia/Hebron",
"Asia/Ho_Chi_Minh",
"Asia/Hong_Kong",
"Asia/Hovd",
"Asia/Irkutsk",
"Asia/Jakarta",
"Asia/Jayapura",
"Asia/Jerusalem",
"Asia/Kabul",
"Asia/Kamchatka",
"Asia/Karachi",
"Asia/Kathmandu",
"Asia/Khandyga",
"Asia/Kolkata",
"Asia/Krasnoyarsk",
"Asia/Kuala_Lumpur",
"Asia/Kuching",
"Asia/Kuwait",
"Asia/Macau",
"Asia/Magadan",
"Asia/Makassar",
"Asia/Manila",
"Asia/Nicosia",
"Asia/Novokuznetsk",
"Asia/Novosibirsk",
"Asia/Omsk",
"Asia/Oral",
"Asia/Pontianak",
"Asia/Pyongyang",
"Asia/Qatar",
"Asia/Qyzylorda",
"Asia/Riyadh",
"Asia/Sakhalin",
"Asia/Samarkand",
"Asia/Seoul",
"Asia/Shanghai",
"Asia/Singapore",
"Asia/Srednekolymsk",
"Asia/Taipei",
"Asia/Tashkent",
"Asia/Tbilisi",
"Asia/Tehran",
"Asia/Thimphu",
"Asia/Tokyo",
"Asia/Tomsk",
"Asia/Ulaanbaatar",
"Asia/Urumqi",
"Asia/Ust-Nera",
"Asia/Vientiane",
"Asia/Vladivostok",
"Asia/Yakutsk",
"Asia/Yangon",
"Asia/Yekaterinburg",
"Asia/Yerevan",
"Atlantic/Azores",
"Atlantic/Bermuda",
"Atlantic/Canary",
"Atlantic/Cape_Verde",
"Atlantic/Faroe",
"Atlantic/Madeira",
"Atlantic/South_Georgia",
"Atlantic/Stanley",
"Australia/Adelaide",
"Australia/Brisbane",
"Australia/Broken_Hill",
"Australia/Darwin",
"Australia/Eucla",
"Australia/Hobart",
"Australia/Lindeman",
"Australia/Lord_Howe",
"Australia/Melbourne",
"Australia/Perth",
"Australia/Sydney",
"Europe/Andorra",
"Europe/Astrakhan",
"Europe/Athens",
"Europe/Belgrade",
"Europe/Berlin",
"Europe/Brussels",
"Europe/Bucharest",
"Europe/Budapest",
"Europe/Chisinau",
"Europe/Dublin",
"Europe/Gibraltar",
"Europe/Helsinki",
"Europe/Istanbul",
"Europe/Kaliningrad",
"Europe/Kyiv",
"Europe/Kirov",
"Europe/Lisbon",
"Europe/London",
"Europe/Madrid",
"Europe/Malta",
"Europe/Minsk",
"Europe/Moscow",
"Europe/Paris",
"Europe/Prague",
"Europe/Riga",
"Europe/Rome",
"Europe/Samara",
"Europe/Saratov",
"Europe/Simferopol",
"Europe/Sofia",
"Europe/Tallinn",
"Europe/Tirane",
"Europe/Ulyanovsk",
"Europe/Vienna",
"Europe/Vilnius",
"Europe/Volgograd",
"Europe/Warsaw",
"Europe/Zurich",
"Indian/Chagos",
"Indian/Maldives",
"Indian/Mauritius",
"Pacific/Apia",
"Pacific/Auckland",
"Pacific/Bougainville",
"Pacific/Chatham",
"Pacific/Chuuk",
"Pacific/Easter",
"Pacific/Efate",
"Pacific/Enderbury",
"Pacific/Fakaofo",
"Pacific/Fiji",
"Pacific/Funafuti",
"Pacific/Galapagos",
"Pacific/Gambier",
"Pacific/Guadalcanal",
"Pacific/Guam",
"Pacific/Honolulu",
"Pacific/Kiritimati",
"Pacific/Kosrae",
"Pacific/Kwajalein",
"Pacific/Majuro",
"Pacific/Marquesas",
"Pacific/Midway",
"Pacific/Nauru",
"Pacific/Niue",
"Pacific/Norfolk",
"Pacific/Noumea",
"Pacific/Pago_Pago",
"Pacific/Palau",
"Pacific/Pitcairn",
"Pacific/Port_Moresby",
"Pacific/Rarotonga",
"Pacific/Tahiti",
"Pacific/Tarawa",
"Pacific/Tongatapu",
"Etc/GMT",
"Etc/GMT-1",
"Etc/GMT-2",
"Etc/GMT-3",
"Etc/GMT-4",
"Etc/GMT-5",
"Etc/GMT-6",
"Etc/GMT-7",
"Etc/GMT-8",
"Etc/GMT-9",
"Etc/GMT-10",
"Etc/GMT-11",
"Etc/GMT-12",
"Etc/GMT-13",
"Etc/GMT-14",
"Etc/GMT+1",
"Etc/GMT+2",
"Etc/GMT+3",
"Etc/GMT+4",
"Etc/GMT+5",
"Etc/GMT+6",
"Etc/GMT+7",
"Etc/GMT+8",
"Etc/GMT+9",
"Etc/GMT+10",
"Etc/GMT+11",
"Etc/GMT+12",
"Etc/UTC"
]
def get_tz_string(timezone):
data = open(ZONES_DIR + timezone, "rb").read().split(b"\n")[-2]
return data.decode("utf-8")
if __name__ == "__main__":
f = open("./src/tz_data.h", "w")
for timezone in ZONES:
f.write('\t{{"{}", "{}"}},\n'.format(timezone, get_tz_string(timezone)))
f.close()

View File

@@ -0,0 +1,61 @@
#ifndef DATE_TIME_H
#define DATE_TIME_H
#include <stdbool.h>
#include "esp_err.h"
/**
* @brief Initialize timezone and NTP
*
*/
void date_time_init(void);
/**
* @brief Return true if NTP is enabled, stored in NVS
*
* @return true
* @return false
*/
bool date_time_is_ntp_enabled(void);
/**
* @brief Get NTP server, string length 64, stored in NVS
*
* @param value
*/
void date_time_get_ntp_server(char* value);
/**
* @brief Return true if NTP server from DHCP fetch is enabled, stored in NVS
*
* @return true
* @return false
*/
bool date_time_is_ntp_from_dhcp(void);
/**
* @brief Set timezone and NTP config
*
* @param enabled
* @param server
* @param from_dhcp
* @return esp_err_t
*/
esp_err_t date_time_set_ntp_config(bool enabled, const char* server, bool from_dhcp);
/**
* @brief Get timezone, string length 64, stored in NVS
*
* @param value
*/
void date_time_get_timezone(char* value);
/**
* @brief Set timezone, string length 64, stored in NVS
*
* @param value
*/
esp_err_t date_time_set_timezone(const char* value);
#endif /* DATE_TIME_H */

View File

@@ -0,0 +1,27 @@
#ifndef MODBUS_TCP_H
#define MODBUS_TCP_H
#include <stdbool.h>
/**
* @brief Init modbus tcp
*
*/
void modbus_tcp_init(void);
/**
* @brief Set enabled, stored in NVS
*
* @param enabled
*/
void modbus_tcp_set_enabled(bool enabled);
/**
* @brief Get enabled, stored in NVS
*
* @return true
* @return false
*/
bool modbus_tcp_is_enabled(void);
#endif /* MODBUS_TCP_H */

View File

@@ -0,0 +1,70 @@
#ifndef MQTT_H_
#define MQTT_H_
#include <stdint.h>
#include <stdbool.h>
#include "esp_err.h"
/**
* @brief Initialize MQTT
*
*/
void mqtt_init(void);
/**
* @brief Set MQTT config
*
* @param enabled
* @param server NULL value will be skiped
* @param base_topic NULL value will be skiped
* @param user NULL value will be skiped
* @param password NULL value will be skiped
* @param periodicity 0 value will be skiped
* @return esp_err_t
*/
esp_err_t mqtt_set_config(bool enabled, const char* server, const char* base_topic, const char* user, const char* password, uint16_t periodicity);
/**
* @brief Get MQTT enabled, stored in NVS
*
* @return true
* @return false
*/
bool mqtt_get_enabled(void);
/**
* @brief Get MQTT server, string length 64, stored in NVS
*
* @param value
*/
void mqtt_get_server(char* value);
/**
* @brief Get MQTT password, string length 64, stored in NVS
*
* @param value
*/
void mqtt_get_password(char* value);
/**
* @brief Get MQTT base topic, string length 32, stored in NVS
*
* @param value
*/
void mqtt_get_base_topic(char* value);
/**
* @brief Get MQTT user, string length 32, stored in NVS
*
* @param value
*/
void mqtt_get_user(char* value);
/**
* @brief Get MQTT periodicity in second, stored in NVS
*
* @return uint16_t
*/
uint16_t mqtt_get_periodicity(void);
#endif /* MQTT_H_ */

View File

@@ -0,0 +1,6 @@
#ifndef PROTOCOLS_H_
#define PROTOCOLS_H_
void protocols_init(void);
#endif /* PROTOCOLS_H_ */

View File

@@ -0,0 +1,10 @@
#ifndef REST_H_
#define REST_H_
/**
* @brief Init rest server
*
*/
void rest_init(void);
#endif /* REST_H_ */

View File

@@ -0,0 +1,22 @@
-----BEGIN CERTIFICATE-----
MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD
QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT
MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j
b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG
9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB
CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97
nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt
43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P
T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4
gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO
BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR
TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw
DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr
hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg
06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF
PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls
YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk
CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4=
-----END CERTIFICATE-----

View File

@@ -0,0 +1,136 @@
#include <time.h>
#include "esp_log.h"
#include "esp_sntp.h"
#include "esp_netif_sntp.h"
#include "nvs.h"
#include "date_time.h"
#define NVS_NAMESPACE "date_time"
#define NVS_NTP_ENABLED "ntp_en"
#define NVS_NTP_SERVER "ntp_server"
#define NVS_NTP_FROM_DHCP "ntp_from_dhcp"
#define NVS_TIMEZONE "timezone"
static const char* TAG = "date_time";
static nvs_handle nvs;
static char ntp_server[64]; // if renew_servers_after_new_IP = false, will be used static string reference
static const char* tz_data[][2] = {
#include "tz_data.h"
{NULL, NULL}
};
static const char* find_tz(const char* name)
{
if (name != NULL) {
int index = 0;
while (true) {
if (tz_data[index][0] == NULL) {
return NULL;
}
if (strcmp(tz_data[index][0], name) == 0) {
return tz_data[index][1];
}
index++;
}
}
return NULL;
}
void date_time_init(void)
{
ESP_ERROR_CHECK(nvs_open(NVS_NAMESPACE, NVS_READWRITE, &nvs));
if (date_time_is_ntp_enabled()) {
date_time_get_ntp_server(ntp_server);
esp_sntp_config_t config = ESP_NETIF_SNTP_DEFAULT_CONFIG(ntp_server);
esp_err_t ret = esp_netif_sntp_init(&config);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "SNTP init return %s", esp_err_to_name(ret));
}
}
char str[64];
date_time_get_timezone(str);
const char *tz = find_tz(str);
if (tz) {
setenv("TZ", tz, 1);
tzset();
} else {
ESP_LOGW(TAG, "Unknown timezone %s", str);
}
}
bool date_time_is_ntp_enabled(void)
{
uint8_t value = false;
nvs_get_u8(nvs, NVS_NTP_ENABLED, &value);
return value;
}
void date_time_get_ntp_server(char* value)
{
size_t len = 64;
value[0] = '\0';
nvs_get_str(nvs, NVS_NTP_SERVER, value, &len);
}
bool date_time_is_ntp_from_dhcp(void)
{
uint8_t value = false;
nvs_get_u8(nvs, NVS_NTP_FROM_DHCP, &value);
return value;
}
esp_err_t date_time_set_ntp_config(bool enabled, const char* server, bool from_dhcp)
{
esp_err_t ret = ESP_OK;
esp_netif_sntp_deinit();
if (enabled) {
strcpy(ntp_server, server);
esp_sntp_config_t config = ESP_NETIF_SNTP_DEFAULT_CONFIG(ntp_server);
config.renew_servers_after_new_IP = from_dhcp;
ret = esp_netif_sntp_init(&config);
}
if (ret == ESP_OK) {
nvs_set_u8(nvs, NVS_NTP_ENABLED, enabled);
nvs_set_str(nvs, NVS_NTP_SERVER, server);
nvs_set_u8(nvs, NVS_NTP_FROM_DHCP, from_dhcp);
}
return ret;
}
void date_time_get_timezone(char* value)
{
size_t len = 64;
value[0] = '\0';
strcpy(value, "Etc/UTC");
nvs_get_str(nvs, NVS_TIMEZONE, value, &len);
}
esp_err_t date_time_set_timezone(const char* value)
{
const char* tz = find_tz(value);
if (tz) {
setenv("TZ", tz, 1);
tzset();
nvs_set_str(nvs, NVS_TIMEZONE, value);
return ESP_OK;
} else {
ESP_LOGW(TAG, "Unknown timezone %s", value);
return ESP_ERR_INVALID_ARG;
}
}

340
components/protocols/src/mqtt.c Executable file
View File

@@ -0,0 +1,340 @@
#include <string.h>
#include <sys/param.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "esp_log.h"
#include "esp_event.h"
#include "mqtt_client.h"
#include "nvs.h"
#include "mqtt.h"
#include "json.h"
#include "board_config.h"
#include "timeout_utils.h"
#define NVS_NAMESPACE "mqtt"
#define NVS_ENABLED "enabled"
#define NVS_SERVER "server"
#define NVS_BASE_TOPIC "base_topic"
#define NVS_USER "user"
#define NVS_PASSWORD "password"
#define NVS_PERIODICITY "periodicity"
static const char* TAG = "mqtt";
static nvs_handle nvs;
static TaskHandle_t client_task = NULL;
static esp_mqtt_client_handle_t client = NULL;
static uint16_t periodicity = 30;
static void subcribe_topics(void)
{
ESP_LOGI(TAG, "subcribe_topics");
char topic[48];
mqtt_get_base_topic(topic);
strcat(topic, "/request/#");
esp_mqtt_client_subscribe(client, topic, 0);
ESP_LOGI(TAG, "data: %s", topic);
mqtt_get_base_topic(topic);
strcat(topic, "/set/config/#");
esp_mqtt_client_subscribe(client, topic, 0);
ESP_LOGI(TAG, "data: %s", topic);
mqtt_get_base_topic(topic);
strcat(topic, "/enable");
esp_mqtt_client_subscribe(client, topic, 0);
ESP_LOGI(TAG, "data: %s", topic);
}
static void publish_message(const char* topic, cJSON* root)
{
ESP_LOGI(TAG, "publish_message");
char target_topic[48];
mqtt_get_base_topic(target_topic);
strcat(target_topic, topic);
const char* json = cJSON_PrintUnformatted(root);
esp_mqtt_client_publish(client, target_topic, json, 0, 1, 0);
free((void*)json);
}
static void handle_message(const char* topic, const char* data)
{
char base_topic[32];
mqtt_get_base_topic(base_topic);
ESP_LOGI(TAG, "Topic: %s", topic);
ESP_LOGI(TAG, "data: %s", data);
ESP_LOGI(TAG, "base_topic: %s", base_topic);
if (strncmp(topic, base_topic, strlen(base_topic)) == 0) {
const char* sub_topic = &topic[strlen(base_topic)];
ESP_LOGI(TAG, "Sub_topic: %s", sub_topic);
if (strcmp(sub_topic, "/request/config/evse") == 0) {
cJSON* root = json_get_evse_config();
publish_message("/response/config/evse", root);
cJSON_Delete(root);
} else if (strcmp(sub_topic, "/request/config/wifi") == 0) {
cJSON* root = json_get_wifi_config();
publish_message("/response/config/wifi", root);
cJSON_Delete(root);
} else if (strcmp(sub_topic, "/request/config/mqtt") == 0) {
cJSON* root = json_get_mqtt_config();
publish_message("/response/config/mqtt", root);
cJSON_Delete(root);
} else if (strcmp(sub_topic, "/request/boardConfig") == 0) {
cJSON* root = json_get_board_config();
publish_message("/response/boardConfig", root);
cJSON_Delete(root);
} else if (strcmp(sub_topic, "/request/info") == 0) {
cJSON* root = json_get_info();
publish_message("/response/info", root);
cJSON_Delete(root);
} else if (strcmp(sub_topic, "/request/restart") == 0) {
timeout_restart();
} else if (strcmp(sub_topic, "/set/config/evse") == 0) {
cJSON* root = cJSON_Parse(data);
json_set_evse_config(root);
cJSON_Delete(root);
} else if (strcmp(sub_topic, "/set/config/wifi") == 0) {
cJSON* root = cJSON_Parse(data);
json_set_wifi_config(root, true);
cJSON_Delete(root);
} else if (strcmp(sub_topic, "/set/config/mqtt") == 0) {
cJSON* root = cJSON_Parse(data);
json_set_mqtt_config(root);
cJSON_Delete(root);
}
}
}
static void event_handler(void* handler_args, esp_event_base_t base, int32_t event_id, void* event_data)
{
esp_mqtt_event_handle_t event = event_data;
char topic[48];
char data[256];
ESP_LOGI(TAG, "Handle Data 1");
switch (event_id) {
case MQTT_EVENT_CONNECTED:
ESP_LOGI(TAG, "Connected");
vTaskResume(client_task);
subcribe_topics();
break;
case MQTT_EVENT_DISCONNECTED:
ESP_LOGI(TAG, "Disconnected");
break;
case MQTT_EVENT_DATA:
ESP_LOGI(TAG, "Handle Data 2");
memset(topic, 0, sizeof(topic));
strncpy(topic, event->topic, MIN(event->topic_len, sizeof(topic) - 1));
memset(data, 0, sizeof(data));
strncpy(data, event->data, MIN(event->data_len, sizeof(data) - 1));
handle_message(topic, data);
break;
default:
break;
}
}
static void client_task_func(void* param)
{
while (true) {
if (!client) {
vTaskSuspend(NULL);
}
cJSON* root = json_get_state();
publish_message("/state", root);
cJSON_Delete(root);
vTaskDelay(pdMS_TO_TICKS(periodicity * 1000));
}
}
static void client_start(void)
{
char server[64];
char user[32];
char password[64];
mqtt_get_server(server);
mqtt_get_user(user);
mqtt_get_password(password);
ESP_LOGI(TAG, "Client Start");
esp_mqtt_client_config_t cfg = {
.broker.address.uri = server,
.credentials.username = user,
.credentials.authentication.password = password
};
if (client) {
if (esp_mqtt_set_config(client, &cfg) != ESP_OK) {
ESP_LOGW(TAG, "Cant set config");
}
} else {
client = esp_mqtt_client_init(&cfg);
if (esp_mqtt_client_register_event(client, ESP_EVENT_ANY_ID, event_handler, client) != ESP_OK) {
ESP_LOGW(TAG, "Cant register handler");
}
if (client == NULL) {
ESP_LOGW(TAG, "Cant set config");
} else {
if (esp_mqtt_client_start(client) != ESP_OK) {
ESP_LOGW(TAG, "Cant start");
}
}
}
}
static void client_stop(void)
{
if (client != NULL) {
esp_mqtt_client_destroy(client);
client = NULL;
}
}
void mqtt_init(void)
{
ESP_ERROR_CHECK(nvs_open(NVS_NAMESPACE, NVS_READWRITE, &nvs));
nvs_get_u16(nvs, NVS_PERIODICITY, &periodicity);
esp_register_shutdown_handler(&client_stop);
xTaskCreate(client_task_func, "mqtt_client_task", 3 * 1024, NULL, 5, &client_task);
if (mqtt_get_enabled()) {
client_start();
}
}
esp_err_t mqtt_set_config(bool enabled, const char* server, const char* base_topic, const char* user, const char* password, uint16_t _periodicity)
{
if (enabled) {
if (server == NULL || strlen(server) == 0) {
size_t len = 0;
nvs_get_str(nvs, NVS_SERVER, NULL, &len);
if (len <= 1) {
ESP_LOGE(TAG, "Required server");
return ESP_ERR_INVALID_ARG;
}
}
if (base_topic == NULL || strlen(base_topic) == 0) {
size_t len = 0;
nvs_get_str(nvs, NVS_BASE_TOPIC, NULL, &len);
if (len <= 1) {
ESP_LOGE(TAG, "Required base topic");
return ESP_ERR_INVALID_ARG;
}
}
if (_periodicity == 0) {
ESP_LOGE(TAG, "Periodicity muse be larger than zero");
return ESP_ERR_INVALID_ARG;
}
}
if (server != NULL && strlen(server) > 63) {
ESP_LOGE(TAG, "Server out of range");
return ESP_ERR_INVALID_ARG;
}
if (base_topic != NULL && strlen(base_topic) > 31) {
ESP_LOGE(TAG, "Base topic out of range");
return ESP_ERR_INVALID_ARG;
}
if (user != NULL && strlen(user) > 31) {
ESP_LOGE(TAG, "User out of range");
return ESP_ERR_INVALID_ARG;
}
if (password != NULL && strlen(password) > 63) {
ESP_LOGE(TAG, "Password out of range");
return ESP_ERR_INVALID_ARG;
}
nvs_set_u8(nvs, NVS_ENABLED, enabled);
nvs_set_str(nvs, NVS_SERVER, server);
nvs_set_str(nvs, NVS_BASE_TOPIC, base_topic);
nvs_set_str(nvs, NVS_USER, user);
nvs_set_str(nvs, NVS_PASSWORD, password);
nvs_set_u16(nvs, NVS_PERIODICITY, _periodicity);
periodicity = _periodicity;
nvs_commit(nvs);
if (enabled) {
client_start();
} else {
client_stop();
}
return ESP_OK;
}
bool mqtt_get_enabled(void)
{
uint8_t value = false;
nvs_get_u8(nvs, NVS_ENABLED, &value);
return value;
}
void mqtt_get_server(char* value)
{
size_t len = 64;
value[0] = '\0';
nvs_get_str(nvs, NVS_SERVER, value, &len);
}
void mqtt_get_base_topic(char* value)
{
size_t len = 32;
value[0] = '\0';
nvs_get_str(nvs, NVS_BASE_TOPIC, value, &len);
}
void mqtt_get_user(char* value)
{
size_t len = 32;
value[0] = '\0';
nvs_get_str(nvs, NVS_USER, value, &len);
}
void mqtt_get_password(char* value)
{
size_t len = 64;
value[0] = '\0';
nvs_get_str(nvs, NVS_PASSWORD, value, &len);
}
uint16_t mqtt_get_periodicity(void)
{
return periodicity;
}

View File

@@ -0,0 +1,13 @@
#include "protocols.h"
#include "date_time.h"
#include "rest.h"
#include "mqtt.h"
//#include "modbus_tcp.h"
void protocols_init(void)
{
date_time_init();
rest_init();
//mqtt_init();
//modbus_tcp_init();
}

226
components/protocols/src/rest.c Executable file
View File

@@ -0,0 +1,226 @@
/* HTTP Restful API Server
This example code is in the Public Domain (or CC0 licensed, at your option.)
Unless required by applicable law or agreed to in writing, this
software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
CONDITIONS OF ANY KIND, either express or implied.
*/
#include <string.h>
#include <fcntl.h>
#include "esp_http_server.h"
#include "esp_chip_info.h"
#include "esp_random.h"
#include "esp_log.h"
#include "esp_vfs.h"
#include "cJSON.h"
static const char *REST_TAG = "esp-rest";
#define REST_CHECK(a, str, goto_tag, ...) \
do \
{ \
if (!(a)) \
{ \
ESP_LOGE(REST_TAG, "%s(%d): " str, __FUNCTION__, __LINE__, ##__VA_ARGS__); \
goto goto_tag; \
} \
} while (0)
#define FILE_PATH_MAX (ESP_VFS_PATH_MAX + 128)
#define SCRATCH_BUFSIZE (10240)
typedef struct rest_server_context {
char base_path[ESP_VFS_PATH_MAX + 1];
char scratch[SCRATCH_BUFSIZE];
} rest_server_context_t;
#define CHECK_FILE_EXTENSION(filename, ext) (strcasecmp(&filename[strlen(filename) - strlen(ext)], ext) == 0)
/* Set HTTP response content type according to file extension */
static esp_err_t set_content_type_from_file(httpd_req_t *req, const char *filepath)
{
const char *type = "text/plain";
if (CHECK_FILE_EXTENSION(filepath, ".html")) {
type = "text/html";
} else if (CHECK_FILE_EXTENSION(filepath, ".js")) {
type = "application/javascript";
} else if (CHECK_FILE_EXTENSION(filepath, ".css")) {
type = "text/css";
} else if (CHECK_FILE_EXTENSION(filepath, ".png")) {
type = "image/png";
} else if (CHECK_FILE_EXTENSION(filepath, ".ico")) {
type = "image/x-icon";
} else if (CHECK_FILE_EXTENSION(filepath, ".svg")) {
type = "text/xml";
}
return httpd_resp_set_type(req, type);
}
/* Send HTTP response with the contents of the requested file */
static esp_err_t rest_common_get_handler(httpd_req_t *req)
{
char filepath[FILE_PATH_MAX];
rest_server_context_t *rest_context = (rest_server_context_t *)req->user_ctx;
strlcpy(filepath, rest_context->base_path, sizeof(filepath));
if (req->uri[strlen(req->uri) - 1] == '/') {
strlcat(filepath, "/index.html", sizeof(filepath));
} else {
strlcat(filepath, req->uri, sizeof(filepath));
}
int fd = open(filepath, O_RDONLY, 0);
if (fd == -1) {
ESP_LOGE(REST_TAG, "Failed to open file : %s", filepath);
/* Respond with 500 Internal Server Error */
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to read existing file");
return ESP_FAIL;
}
set_content_type_from_file(req, filepath);
char *chunk = rest_context->scratch;
ssize_t read_bytes;
do {
/* Read file in chunks into the scratch buffer */
read_bytes = read(fd, chunk, SCRATCH_BUFSIZE);
if (read_bytes == -1) {
ESP_LOGE(REST_TAG, "Failed to read file : %s", filepath);
} else if (read_bytes > 0) {
/* Send the buffer contents as HTTP response chunk */
if (httpd_resp_send_chunk(req, chunk, read_bytes) != ESP_OK) {
close(fd);
ESP_LOGE(REST_TAG, "File sending failed!");
/* Abort sending file */
httpd_resp_sendstr_chunk(req, NULL);
/* Respond with 500 Internal Server Error */
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to send file");
return ESP_FAIL;
}
}
} while (read_bytes > 0);
/* Close file after sending complete */
close(fd);
ESP_LOGI(REST_TAG, "File sending complete");
/* Respond with an empty chunk to signal HTTP response completion */
httpd_resp_send_chunk(req, NULL, 0);
return ESP_OK;
}
/* Simple handler for light brightness control */
static esp_err_t light_brightness_post_handler(httpd_req_t *req)
{
int total_len = req->content_len;
int cur_len = 0;
char *buf = ((rest_server_context_t *)(req->user_ctx))->scratch;
int received = 0;
if (total_len >= SCRATCH_BUFSIZE) {
/* Respond with 500 Internal Server Error */
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "content too long");
return ESP_FAIL;
}
while (cur_len < total_len) {
received = httpd_req_recv(req, buf + cur_len, total_len);
if (received <= 0) {
/* Respond with 500 Internal Server Error */
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to post control value");
return ESP_FAIL;
}
cur_len += received;
}
buf[total_len] = '\0';
cJSON *root = cJSON_Parse(buf);
int red = cJSON_GetObjectItem(root, "red")->valueint;
int green = cJSON_GetObjectItem(root, "green")->valueint;
int blue = cJSON_GetObjectItem(root, "blue")->valueint;
ESP_LOGI(REST_TAG, "Light control: red = %d, green = %d, blue = %d", red, green, blue);
cJSON_Delete(root);
httpd_resp_sendstr(req, "Post control value successfully");
return ESP_OK;
}
/* Simple handler for getting system handler */
static esp_err_t system_info_get_handler(httpd_req_t *req)
{
httpd_resp_set_type(req, "application/json");
cJSON *root = cJSON_CreateObject();
esp_chip_info_t chip_info;
esp_chip_info(&chip_info);
cJSON_AddStringToObject(root, "version", IDF_VER);
cJSON_AddNumberToObject(root, "cores", chip_info.cores);
const char *sys_info = cJSON_Print(root);
httpd_resp_sendstr(req, sys_info);
free((void *)sys_info);
cJSON_Delete(root);
return ESP_OK;
}
/* Simple handler for getting temperature data */
static esp_err_t temperature_data_get_handler(httpd_req_t *req)
{
httpd_resp_set_type(req, "application/json");
cJSON *root = cJSON_CreateObject();
cJSON_AddNumberToObject(root, "raw", esp_random() % 20);
const char *sys_info = cJSON_Print(root);
httpd_resp_sendstr(req, sys_info);
free((void *)sys_info);
cJSON_Delete(root);
return ESP_OK;
}
esp_err_t rest_init(const char *base_path)
{
REST_CHECK(base_path, "wrong base path", err);
rest_server_context_t *rest_context = calloc(1, sizeof(rest_server_context_t));
REST_CHECK(rest_context, "No memory for rest context", err);
strlcpy(rest_context->base_path, base_path, sizeof(rest_context->base_path));
httpd_handle_t server = NULL;
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.uri_match_fn = httpd_uri_match_wildcard;
ESP_LOGI(REST_TAG, "Starting HTTP Server");
REST_CHECK(httpd_start(&server, &config) == ESP_OK, "Start server failed", err_start);
/* URI handler for fetching system info */
httpd_uri_t system_info_get_uri = {
.uri = "/api/v1/system/info",
.method = HTTP_GET,
.handler = system_info_get_handler,
.user_ctx = rest_context
};
httpd_register_uri_handler(server, &system_info_get_uri);
/* URI handler for fetching temperature data */
httpd_uri_t temperature_data_get_uri = {
.uri = "/api/v1/temp/raw",
.method = HTTP_GET,
.handler = temperature_data_get_handler,
.user_ctx = rest_context
};
httpd_register_uri_handler(server, &temperature_data_get_uri);
/* URI handler for light brightness control */
httpd_uri_t light_brightness_post_uri = {
.uri = "/api/v1/light/brightness",
.method = HTTP_POST,
.handler = light_brightness_post_handler,
.user_ctx = rest_context
};
httpd_register_uri_handler(server, &light_brightness_post_uri);
/* URI handler for getting web server files */
httpd_uri_t common_get_uri = {
.uri = "/*",
.method = HTTP_GET,
.handler = rest_common_get_handler,
.user_ctx = rest_context
};
httpd_register_uri_handler(server, &common_get_uri);
return ESP_OK;
err_start:
free(rest_context);
err:
return ESP_FAIL;
}

View File

@@ -0,0 +1,354 @@
{"Africa/Abidjan", "GMT0"},
{"Africa/Algiers", "CET-1"},
{"Africa/Bissau", "GMT0"},
{"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/El_Aaiun", "<+01>-1"},
{"Africa/Johannesburg", "SAST-2"},
{"Africa/Juba", "CAT-2"},
{"Africa/Khartoum", "CAT-2"},
{"Africa/Lagos", "WAT-1"},
{"Africa/Maputo", "CAT-2"},
{"Africa/Monrovia", "GMT0"},
{"Africa/Nairobi", "EAT-3"},
{"Africa/Ndjamena", "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/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/Asuncion", "<-04>4<-03>,M10.1.0/0,M3.4.0/0"},
{"America/Bahia", "<-03>3"},
{"America/Bahia_Banderas", "CST6"},
{"America/Barbados", "AST4"},
{"America/Belem", "<-03>3"},
{"America/Belize", "CST6"},
{"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/Fortaleza", "<-03>3"},
{"America/Fort_Nelson", "MST7"},
{"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/La_Paz", "<-04>4"},
{"America/Lima", "<-05>5"},
{"America/Los_Angeles", "PST8PDT,M3.2.0,M11.1.0"},
{"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/New_York", "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/Paramaribo", "<-03>3"},
{"America/Phoenix", "MST7"},
{"America/Port-au-Prince", "EST5EDT,M3.2.0,M11.1.0"},
{"America/Porto_Velho", "<-04>4"},
{"America/Puerto_Rico", "AST4"},
{"America/Punta_Arenas", "<-03>3"},
{"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_Johns", "NST3:30NDT,M3.2.0,M11.1.0"},
{"America/Swift_Current", "CST6"},
{"America/Tegucigalpa", "CST6"},
{"America/Thule", "AST4ADT,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/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"},
{"Antarctica/Casey", "<+11>-11"},
{"Antarctica/Davis", "<+07>-7"},
{"Antarctica/Macquarie", "AEST-10AEDT,M10.1.0,M4.1.0/3"},
{"Antarctica/Mawson", "<+05>-5"},
{"Antarctica/Palmer", "<-03>3"},
{"Antarctica/Rothera", "<-03>3"},
{"Antarctica/Troll", "<+00>0<+02>-2,M3.5.0/1,M10.5.0/3"},
{"Asia/Almaty", "<+06>-6"},
{"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/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/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/South_Georgia", "<-02>2"},
{"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/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/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/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/Chisinau", "EET-2EEST,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/Helsinki", "EET-2EEST,M3.5.0/3,M10.5.0/4"},
{"Europe/Istanbul", "<+03>-3"},
{"Europe/Kaliningrad", "EET-2"},
{"Europe/Kyiv", "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/London", "GMT0BST,M3.5.0/1,M10.5.0"},
{"Europe/Madrid", "CET-1CEST,M3.5.0,M10.5.0/3"},
{"Europe/Malta", "CET-1CEST,M3.5.0,M10.5.0/3"},
{"Europe/Minsk", "<+03>-3"},
{"Europe/Moscow", "MSK-3"},
{"Europe/Paris", "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/Saratov", "<+04>-4"},
{"Europe/Simferopol", "MSK-3"},
{"Europe/Sofia", "EET-2EEST,M3.5.0/3,M10.5.0/4"},
{"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/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/Zurich", "CET-1CEST,M3.5.0,M10.5.0/3"},
{"Indian/Chagos", "<+06>-6"},
{"Indian/Maldives", "<+05>-5"},
{"Indian/Mauritius", "<+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/Port_Moresby", "<+10>-10"},
{"Pacific/Rarotonga", "<-10>10"},
{"Pacific/Tahiti", "<-10>10"},
{"Pacific/Tarawa", "<+12>-12"},
{"Pacific/Tongatapu", "<+13>-13"},
{"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-13", "<+13>-13"},
{"Etc/GMT-14", "<+14>-14"},
{"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/UTC", "UTC0"},

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,15 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap" rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script>
<title>Vite + React</title>
<script type="module" crossorigin src="/assets/index-FEzsI7yf.js"></script>
</head>
<body>
<div id="root"></div>
</body>
</html>

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB