Files
MeshCore-mqtt-observer/src/helpers/bridges/MQTTBridge.cpp
T
agessaman b47c565b0d Add timezone support to MQTT Bridge and CLI configuration
- Introduce timezone settings in NodePrefs for persistent storage
- Update CLI commands to get/set timezone and offset
- Modify MQTT message builder to utilize timezone for accurate timestamps
- Implement timezone handling in MQTTBridge for local time conversion
- Include timezone library dependency in platformio.ini
2025-12-21 11:26:11 -08:00

665 lines
22 KiB
C++

#include "MQTTBridge.h"
#include "../MQTTMessageBuilder.h"
#include <NTPClient.h>
#include <WiFiUdp.h>
#include <Timezone.h>
#ifdef WITH_MQTT_BRIDGE
MQTTBridge::MQTTBridge(NodePrefs *prefs, mesh::PacketManager *mgr, mesh::RTCClock *rtc)
: BridgeBase(prefs, mgr, rtc), _mqtt_client(nullptr), _wifi_client(nullptr),
_active_brokers(0), _queue_head(0), _queue_tail(0), _queue_count(0),
_last_status_publish(0), _status_interval(300000), // 5 minutes default
_ntp_client(_ntp_udp, "pool.ntp.org", 0, 60000), _last_ntp_sync(0), _ntp_synced(false),
_timezone(nullptr) {
// Initialize default values
strncpy(_origin, "MeshCore-Repeater", sizeof(_origin) - 1);
strncpy(_iata, "XXX", sizeof(_iata) - 1);
strncpy(_device_id, "DEVICE_ID_PLACEHOLDER", sizeof(_device_id) - 1);
strncpy(_firmware_version, "unknown", sizeof(_firmware_version) - 1);
strncpy(_board_model, "unknown", sizeof(_board_model) - 1);
_status_enabled = true;
_packets_enabled = true;
_raw_enabled = false;
// Initialize packet queue
memset(_packet_queue, 0, sizeof(_packet_queue));
// Set default broker configuration
setBrokerDefaults();
}
void MQTTBridge::begin() {
MQTT_DEBUG_PRINTLN("Initializing MQTT Bridge...");
// Update origin and IATA from preferences
strncpy(_origin, _prefs->mqtt_origin, sizeof(_origin) - 1);
_origin[sizeof(_origin) - 1] = '\0';
strncpy(_iata, _prefs->mqtt_iata, sizeof(_iata) - 1);
_iata[sizeof(_iata) - 1] = '\0';
// Strip quotes from origin if present
MQTT_DEBUG_PRINTLN("Origin before stripping: '%s'", _origin);
size_t origin_len = strlen(_origin);
if (origin_len > 0) {
if (_origin[0] == '"') {
memmove(_origin, _origin + 1, origin_len);
origin_len--;
}
if (origin_len > 0 && _origin[origin_len-1] == '"') {
_origin[origin_len-1] = '\0';
}
}
MQTT_DEBUG_PRINTLN("Origin after stripping: '%s'", _origin);
// Strip quotes from IATA if present
MQTT_DEBUG_PRINTLN("IATA before stripping: '%s'", _iata);
size_t iata_len = strlen(_iata);
if (iata_len > 0) {
if (_iata[0] == '"') {
memmove(_iata, _iata + 1, iata_len);
iata_len--;
}
if (iata_len > 0 && _iata[iata_len-1] == '"') {
_iata[iata_len-1] = '\0';
}
}
MQTT_DEBUG_PRINTLN("IATA after stripping: '%s'", _iata);
// Update enabled flags from preferences
_status_enabled = _prefs->mqtt_status_enabled;
_packets_enabled = _prefs->mqtt_packets_enabled;
_raw_enabled = _prefs->mqtt_raw_enabled;
_status_interval = _prefs->mqtt_status_interval;
MQTT_DEBUG_PRINTLN("Origin: %s, IATA: %s", _origin, _iata);
MQTT_DEBUG_PRINTLN("Device ID: %s", _device_id);
MQTT_DEBUG_PRINTLN("WiFi SSID: %s", _prefs->wifi_ssid);
// Initialize WiFi
MQTT_DEBUG_PRINTLN("Starting WiFi...");
WiFi.mode(WIFI_STA);
WiFi.begin(_prefs->wifi_ssid, _prefs->wifi_password);
// Wait for WiFi connection
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 20) {
delay(500);
MQTT_DEBUG_PRINT(".");
attempts++;
}
if (WiFi.status() == WL_CONNECTED) {
MQTT_DEBUG_PRINTLN("WiFi connected! IP: %s", WiFi.localIP().toString().c_str());
// Sync time with NTP
syncTimeWithNTP();
} else {
MQTT_DEBUG_PRINTLN("WiFi connection failed!");
return;
}
// Initialize WiFi client
_wifi_client = new WiFiClient();
_mqtt_client = new PubSubClient(*_wifi_client);
// Set default broker (meshtastic.pugetmesh.org)
setBroker(0, "meshtastic.pugetmesh.org", 1883, "meshdev", "large4cats", true);
// Connect to brokers
connectToBrokers();
_initialized = true;
MQTT_DEBUG_PRINTLN("MQTT Bridge initialized");
}
void MQTTBridge::end() {
MQTT_DEBUG_PRINTLN("Stopping MQTT Bridge...");
// Disconnect from all brokers
for (int i = 0; i < MAX_MQTT_BROKERS_COUNT; i++) {
if (_brokers[i].enabled && _brokers[i].connected) {
_mqtt_client->disconnect();
_brokers[i].connected = false;
}
}
// Clear packet queue
_queue_count = 0;
_queue_head = 0;
_queue_tail = 0;
// Clean up resources
if (_mqtt_client) {
delete _mqtt_client;
_mqtt_client = nullptr;
}
if (_wifi_client) {
delete _wifi_client;
_wifi_client = nullptr;
}
_initialized = false;
MQTT_DEBUG_PRINTLN("MQTT Bridge stopped");
}
void MQTTBridge::loop() {
if (!_initialized) return;
// Maintain broker connections
connectToBrokers();
// Process packet queue
processPacketQueue();
// Periodic NTP sync (every hour)
if (WiFi.status() == WL_CONNECTED && millis() - _last_ntp_sync > 3600000) {
syncTimeWithNTP();
}
// Publish status updates
if (_status_enabled && millis() - _last_status_publish > _status_interval) {
publishStatus();
_last_status_publish = millis();
}
}
void MQTTBridge::onPacketReceived(mesh::Packet *packet) {
if (!_initialized || !_packets_enabled) return;
// Queue packet for transmission
queuePacket(packet, false);
}
void MQTTBridge::sendPacket(mesh::Packet *packet) {
if (!_initialized || !_packets_enabled) return;
// Queue packet for transmission
queuePacket(packet, true);
}
void MQTTBridge::connectToBrokers() {
for (int i = 0; i < MAX_MQTT_BROKERS_COUNT; i++) {
if (!_brokers[i].enabled) continue;
// Check if we need to attempt connection
if (!_brokers[i].connected &&
millis() - _brokers[i].last_attempt > _brokers[i].reconnect_interval) {
MQTT_DEBUG_PRINTLN("Connecting to broker %d: %s:%d", i, _brokers[i].host, _brokers[i].port);
// Set broker for this connection
_mqtt_client->setServer(_brokers[i].host, _brokers[i].port);
// Generate unique client ID
char client_id[32];
snprintf(client_id, sizeof(client_id), "%s_%d_%lu", _origin, i, millis());
// Attempt connection
bool connected = _mqtt_client->connect(
client_id,
_brokers[i].username,
_brokers[i].password
);
if (connected) {
_brokers[i].connected = true;
_brokers[i].reconnect_interval = 5000; // Reset to 5 seconds
_active_brokers++;
MQTT_DEBUG_PRINTLN("Connected to broker %d", i);
// Publish initial status
if (_status_enabled) {
publishStatus();
}
} else {
_brokers[i].connected = false;
_brokers[i].last_attempt = millis();
// Exponential backoff: 5s, 10s, 20s, 30s max
_brokers[i].reconnect_interval = min(30000UL, _brokers[i].reconnect_interval * 2);
MQTT_DEBUG_PRINTLN("Failed to connect to broker %d", i);
}
}
// Maintain connection
if (_brokers[i].connected) {
_mqtt_client->loop();
if (!_mqtt_client->connected()) {
_brokers[i].connected = false;
_active_brokers--;
MQTT_DEBUG_PRINTLN("Lost connection to broker %d", i);
}
}
}
}
void MQTTBridge::processPacketQueue() {
if (_queue_count == 0 || !isAnyBrokerConnected()) return;
// Process up to 5 packets per loop to avoid blocking
int processed = 0;
while (_queue_count > 0 && processed < 5) {
QueuedPacket& queued = _packet_queue[_queue_head];
// Publish packet
publishPacket(queued.packet, queued.is_tx);
// Publish raw if enabled
if (_raw_enabled) {
publishRaw(queued.packet);
}
// Remove from queue
dequeuePacket();
processed++;
}
}
void MQTTBridge::publishStatus() {
if (!isAnyBrokerConnected()) return;
char json_buffer[512];
char origin_id[65];
char timestamp[32];
char radio_info[64];
// Get current timestamp in ISO 8601 format
struct tm timeinfo;
if (getLocalTime(&timeinfo)) {
strftime(timestamp, sizeof(timestamp), "%Y-%m-%dT%H:%M:%S.000000", &timeinfo);
} else {
strcpy(timestamp, "2024-01-01T12:00:00.000000");
}
// Build radio info string (freq,bw,sf,cr)
snprintf(radio_info, sizeof(radio_info), "%.6f,%.1f,%d,%d",
_prefs->freq, _prefs->bw, _prefs->sf, _prefs->cr);
// Use actual device ID
strncpy(origin_id, _device_id, sizeof(origin_id) - 1);
origin_id[sizeof(origin_id) - 1] = '\0';
// Build status message
int len = MQTTMessageBuilder::buildStatusMessage(
_origin,
origin_id,
_board_model, // model - now dynamic!
_firmware_version, // firmware version
radio_info,
"custom-mqtt-bridge",
"online",
timestamp,
json_buffer,
sizeof(json_buffer)
);
if (len > 0) {
// Publish to all connected brokers
for (int i = 0; i < MAX_MQTT_BROKERS_COUNT; i++) {
if (_brokers[i].enabled && _brokers[i].connected) {
char topic[128];
snprintf(topic, sizeof(topic), "meshcore/%s/%s/status", _iata, _device_id);
MQTT_DEBUG_PRINTLN("Publishing status to topic: %s", topic);
_mqtt_client->setServer(_brokers[i].host, _brokers[i].port);
_mqtt_client->publish(topic, json_buffer, true); // retained
}
}
}
}
void MQTTBridge::publishPacket(mesh::Packet* packet, bool is_tx) {
if (!packet) return;
char json_buffer[1024];
char origin_id[65];
// Use actual device ID
strncpy(origin_id, _device_id, sizeof(origin_id) - 1);
origin_id[sizeof(origin_id) - 1] = '\0';
// Build packet message
int len = MQTTMessageBuilder::buildPacketJSON(
packet, is_tx, _origin, origin_id, _timezone, json_buffer, sizeof(json_buffer)
);
if (len > 0) {
// Publish to all connected brokers
for (int i = 0; i < MAX_MQTT_BROKERS_COUNT; i++) {
if (_brokers[i].enabled && _brokers[i].connected) {
char topic[128];
snprintf(topic, sizeof(topic), "meshcore/%s/%s/packets", _iata, _device_id);
MQTT_DEBUG_PRINTLN("Publishing packet to topic: %s", topic);
_mqtt_client->setServer(_brokers[i].host, _brokers[i].port);
_mqtt_client->publish(topic, json_buffer);
}
}
}
}
void MQTTBridge::publishRaw(mesh::Packet* packet) {
if (!packet) return;
char json_buffer[512];
char origin_id[65];
// Use actual device ID
strncpy(origin_id, _device_id, sizeof(origin_id) - 1);
origin_id[sizeof(origin_id) - 1] = '\0';
// Build raw message
int len = MQTTMessageBuilder::buildRawJSON(
packet, _origin, origin_id, _timezone, json_buffer, sizeof(json_buffer)
);
if (len > 0) {
// Publish to all connected brokers
for (int i = 0; i < MAX_MQTT_BROKERS_COUNT; i++) {
if (_brokers[i].enabled && _brokers[i].connected) {
char topic[128];
snprintf(topic, sizeof(topic), "meshcore/%s/%s/raw", _iata, _device_id);
_mqtt_client->setServer(_brokers[i].host, _brokers[i].port);
_mqtt_client->publish(topic, json_buffer);
}
}
}
}
void MQTTBridge::queuePacket(mesh::Packet* packet, bool is_tx) {
if (_queue_count >= MAX_QUEUE_SIZE) {
// Queue full, remove oldest
dequeuePacket();
}
QueuedPacket& queued = _packet_queue[_queue_tail];
queued.packet = packet;
queued.timestamp = millis();
queued.is_tx = is_tx;
_queue_tail = (_queue_tail + 1) % MAX_QUEUE_SIZE;
_queue_count++;
}
void MQTTBridge::dequeuePacket() {
if (_queue_count == 0) return;
_queue_head = (_queue_head + 1) % MAX_QUEUE_SIZE;
_queue_count--;
}
bool MQTTBridge::isAnyBrokerConnected() {
for (int i = 0; i < MAX_MQTT_BROKERS_COUNT; i++) {
if (_brokers[i].enabled && _brokers[i].connected) {
return true;
}
}
return false;
}
void MQTTBridge::setBrokerDefaults() {
for (int i = 0; i < MAX_MQTT_BROKERS_COUNT; i++) {
memset(&_brokers[i], 0, sizeof(MQTTBroker));
_brokers[i].port = 1883;
_brokers[i].qos = 0;
_brokers[i].enabled = false;
_brokers[i].connected = false;
_brokers[i].reconnect_interval = 5000; // 5 seconds
}
}
void MQTTBridge::setBroker(int broker_index, const char* host, uint16_t port,
const char* username, const char* password, bool enabled) {
if (broker_index < 0 || broker_index >= MAX_MQTT_BROKERS_COUNT) return;
MQTTBroker& broker = _brokers[broker_index];
strncpy(broker.host, host, sizeof(broker.host) - 1);
broker.port = port;
strncpy(broker.username, username, sizeof(broker.username) - 1);
strncpy(broker.password, password, sizeof(broker.password) - 1);
broker.enabled = enabled;
broker.connected = false;
broker.reconnect_interval = 5000;
}
void MQTTBridge::setOrigin(const char* origin) {
strncpy(_origin, origin, sizeof(_origin) - 1);
_origin[sizeof(_origin) - 1] = '\0';
}
void MQTTBridge::setIATA(const char* iata) {
strncpy(_iata, iata, sizeof(_iata) - 1);
_iata[sizeof(_iata) - 1] = '\0';
}
void MQTTBridge::setDeviceID(const char* device_id) {
strncpy(_device_id, device_id, sizeof(_device_id) - 1);
_device_id[sizeof(_device_id) - 1] = '\0';
MQTT_DEBUG_PRINTLN("Device ID set to: %s", _device_id);
}
void MQTTBridge::setFirmwareVersion(const char* firmware_version) {
strncpy(_firmware_version, firmware_version, sizeof(_firmware_version) - 1);
_firmware_version[sizeof(_firmware_version) - 1] = '\0';
}
void MQTTBridge::setBoardModel(const char* board_model) {
strncpy(_board_model, board_model, sizeof(_board_model) - 1);
_board_model[sizeof(_board_model) - 1] = '\0';
}
void MQTTBridge::setMessageTypes(bool status, bool packets, bool raw) {
_status_enabled = status;
_packets_enabled = packets;
_raw_enabled = raw;
}
int MQTTBridge::getConnectedBrokers() const {
int count = 0;
for (int i = 0; i < MAX_MQTT_BROKERS_COUNT; i++) {
if (_brokers[i].enabled && _brokers[i].connected) {
count++;
}
}
return count;
}
int MQTTBridge::getQueueSize() const {
return _queue_count;
}
void MQTTBridge::syncTimeWithNTP() {
if (!WiFi.isConnected()) {
MQTT_DEBUG_PRINTLN("Cannot sync time - WiFi not connected");
return;
}
MQTT_DEBUG_PRINTLN("Syncing time with NTP...");
// Begin NTP client
_ntp_client.begin();
// Force update (blocking call with timeout)
if (_ntp_client.forceUpdate()) {
unsigned long epochTime = _ntp_client.getEpochTime();
// Set system timezone to UTC first
// This ensures time() returns UTC time
configTime(0, 0, "pool.ntp.org");
// Update the device's RTC clock with UTC time
if (_rtc) {
_rtc->setCurrentTime(epochTime);
_ntp_synced = true;
_last_ntp_sync = millis();
MQTT_DEBUG_PRINTLN("Time synced: %lu", epochTime);
// Set timezone from string (with DST support)
MQTT_DEBUG_PRINTLN("Setting timezone: %s", _prefs->timezone_string);
// Create timezone object based on timezone string
Timezone* tz = createTimezoneFromString(_prefs->timezone_string);
if (tz) {
MQTT_DEBUG_PRINTLN("Timezone created successfully");
// Store timezone for later use in message building
_timezone = tz;
} else {
MQTT_DEBUG_PRINTLN("Failed to create timezone, using UTC");
// Create UTC timezone as fallback
TimeChangeRule utc = {"UTC", Last, Sun, Mar, 0, 0};
_timezone = new Timezone(utc, utc);
}
// Show current time in both UTC and local
struct tm* utc_timeinfo = gmtime((time_t*)&epochTime);
struct tm* local_timeinfo = localtime((time_t*)&epochTime);
if (utc_timeinfo) {
MQTT_DEBUG_PRINTLN("UTC time: %04d-%02d-%02d %02d:%02d:%02d",
utc_timeinfo->tm_year + 1900, utc_timeinfo->tm_mon + 1, utc_timeinfo->tm_mday,
utc_timeinfo->tm_hour, utc_timeinfo->tm_min, utc_timeinfo->tm_sec);
}
if (local_timeinfo) {
MQTT_DEBUG_PRINTLN("Local time: %04d-%02d-%02d %02d:%02d:%02d",
local_timeinfo->tm_year + 1900, local_timeinfo->tm_mon + 1, local_timeinfo->tm_mday,
local_timeinfo->tm_hour, local_timeinfo->tm_min, local_timeinfo->tm_sec);
}
} else {
MQTT_DEBUG_PRINTLN("No RTC clock available for time sync");
}
} else {
MQTT_DEBUG_PRINTLN("NTP sync failed");
}
_ntp_client.end();
}
Timezone* MQTTBridge::createTimezoneFromString(const char* tz_string) {
// Create Timezone objects for common IANA timezone strings
// Using TimeChangeRule definitions for proper DST handling
// North America
if (strcmp(tz_string, "America/Los_Angeles") == 0 || strcmp(tz_string, "America/Vancouver") == 0) {
TimeChangeRule pst = {"PST", First, Sun, Nov, 2, -480}; // UTC-8
TimeChangeRule pdt = {"PDT", Second, Sun, Mar, 2, -420}; // UTC-7
return new Timezone(pdt, pst);
} else if (strcmp(tz_string, "America/Denver") == 0) {
TimeChangeRule mst = {"MST", First, Sun, Nov, 2, -420}; // UTC-7
TimeChangeRule mdt = {"MDT", Second, Sun, Mar, 2, -360}; // UTC-6
return new Timezone(mdt, mst);
} else if (strcmp(tz_string, "America/Chicago") == 0) {
TimeChangeRule cst = {"CST", First, Sun, Nov, 2, -360}; // UTC-6
TimeChangeRule cdt = {"CDT", Second, Sun, Mar, 2, -300}; // UTC-5
return new Timezone(cdt, cst);
} else if (strcmp(tz_string, "America/New_York") == 0 || strcmp(tz_string, "America/Toronto") == 0) {
TimeChangeRule est = {"EST", First, Sun, Nov, 2, -300}; // UTC-5
TimeChangeRule edt = {"EDT", Second, Sun, Mar, 2, -240}; // UTC-4
return new Timezone(edt, est);
} else if (strcmp(tz_string, "America/Anchorage") == 0) {
TimeChangeRule akst = {"AKST", First, Sun, Nov, 2, -540}; // UTC-9
TimeChangeRule akdt = {"AKDT", Second, Sun, Mar, 2, -480}; // UTC-8
return new Timezone(akdt, akst);
} else if (strcmp(tz_string, "Pacific/Honolulu") == 0) {
TimeChangeRule hst = {"HST", Last, Sun, Oct, 2, -600}; // UTC-10 (no DST)
return new Timezone(hst, hst);
// Europe
} else if (strcmp(tz_string, "Europe/London") == 0) {
TimeChangeRule gmt = {"GMT", Last, Sun, Oct, 2, 0}; // UTC+0
TimeChangeRule bst = {"BST", Last, Sun, Mar, 1, 60}; // UTC+1
return new Timezone(bst, gmt);
} else if (strcmp(tz_string, "Europe/Paris") == 0 || strcmp(tz_string, "Europe/Berlin") == 0) {
TimeChangeRule cet = {"CET", Last, Sun, Oct, 3, 60}; // UTC+1
TimeChangeRule cest = {"CEST", Last, Sun, Mar, 2, 120}; // UTC+2
return new Timezone(cest, cet);
} else if (strcmp(tz_string, "Europe/Moscow") == 0) {
TimeChangeRule msk = {"MSK", Last, Sun, Oct, 3, 180}; // UTC+3 (no DST since 2014)
return new Timezone(msk, msk);
// Asia
} else if (strcmp(tz_string, "Asia/Tokyo") == 0) {
TimeChangeRule jst = {"JST", Last, Sun, Oct, 2, 540}; // UTC+9 (no DST)
return new Timezone(jst, jst);
} else if (strcmp(tz_string, "Asia/Shanghai") == 0 || strcmp(tz_string, "Asia/Hong_Kong") == 0) {
TimeChangeRule cst = {"CST", Last, Sun, Oct, 2, 480}; // UTC+8 (no DST)
return new Timezone(cst, cst);
} else if (strcmp(tz_string, "Asia/Kolkata") == 0) {
TimeChangeRule ist = {"IST", Last, Sun, Oct, 2, 330}; // UTC+5:30 (no DST)
return new Timezone(ist, ist);
} else if (strcmp(tz_string, "Asia/Dubai") == 0) {
TimeChangeRule gst = {"GST", Last, Sun, Oct, 2, 240}; // UTC+4 (no DST)
return new Timezone(gst, gst);
// Australia
} else if (strcmp(tz_string, "Australia/Sydney") == 0 || strcmp(tz_string, "Australia/Melbourne") == 0) {
TimeChangeRule aest = {"AEST", First, Sun, Apr, 3, 600}; // UTC+10
TimeChangeRule aedt = {"AEDT", First, Sun, Oct, 2, 660}; // UTC+11
return new Timezone(aedt, aest);
} else if (strcmp(tz_string, "Australia/Perth") == 0) {
TimeChangeRule awst = {"AWST", Last, Sun, Oct, 2, 480}; // UTC+8 (no DST)
return new Timezone(awst, awst);
// Timezone abbreviations (with DST handling)
} else if (strcmp(tz_string, "PDT") == 0 || strcmp(tz_string, "PST") == 0) {
// Pacific Time (PST/PDT)
TimeChangeRule pst = {"PST", First, Sun, Nov, 2, -480}; // UTC-8
TimeChangeRule pdt = {"PDT", Second, Sun, Mar, 2, -420}; // UTC-7
return new Timezone(pdt, pst);
} else if (strcmp(tz_string, "MDT") == 0 || strcmp(tz_string, "MST") == 0) {
// Mountain Time (MST/MDT)
TimeChangeRule mst = {"MST", First, Sun, Nov, 2, -420}; // UTC-7
TimeChangeRule mdt = {"MDT", Second, Sun, Mar, 2, -360}; // UTC-6
return new Timezone(mdt, mst);
} else if (strcmp(tz_string, "CDT") == 0 || strcmp(tz_string, "CST") == 0) {
// Central Time (CST/CDT)
TimeChangeRule cst = {"CST", First, Sun, Nov, 2, -360}; // UTC-6
TimeChangeRule cdt = {"CDT", Second, Sun, Mar, 2, -300}; // UTC-5
return new Timezone(cdt, cst);
} else if (strcmp(tz_string, "EDT") == 0 || strcmp(tz_string, "EST") == 0) {
// Eastern Time (EST/EDT)
TimeChangeRule est = {"EST", First, Sun, Nov, 2, -300}; // UTC-5
TimeChangeRule edt = {"EDT", Second, Sun, Mar, 2, -240}; // UTC-4
return new Timezone(edt, est);
} else if (strcmp(tz_string, "BST") == 0 || strcmp(tz_string, "GMT") == 0) {
// British Time (GMT/BST)
TimeChangeRule gmt = {"GMT", Last, Sun, Oct, 2, 0}; // UTC+0
TimeChangeRule bst = {"BST", Last, Sun, Mar, 1, 60}; // UTC+1
return new Timezone(bst, gmt);
} else if (strcmp(tz_string, "CEST") == 0 || strcmp(tz_string, "CET") == 0) {
// Central European Time (CET/CEST)
TimeChangeRule cet = {"CET", Last, Sun, Oct, 3, 60}; // UTC+1
TimeChangeRule cest = {"CEST", Last, Sun, Mar, 2, 120}; // UTC+2
return new Timezone(cest, cet);
// UTC and simple offsets
} else if (strcmp(tz_string, "UTC") == 0) {
TimeChangeRule utc = {"UTC", Last, Sun, Mar, 0, 0};
return new Timezone(utc, utc);
} else if (strncmp(tz_string, "UTC", 3) == 0) {
// Handle UTC+/-X format (UTC-8, UTC+5, etc.)
int offset = atoi(tz_string + 3);
TimeChangeRule utc_offset = {"UTC", Last, Sun, Mar, 0, offset * 60};
return new Timezone(utc_offset, utc_offset);
} else if (strncmp(tz_string, "GMT", 3) == 0) {
// Handle GMT+/-X format (GMT-8, GMT+5, etc.)
int offset = atoi(tz_string + 3);
TimeChangeRule gmt_offset = {"GMT", Last, Sun, Mar, 0, offset * 60};
return new Timezone(gmt_offset, gmt_offset);
} else if (strncmp(tz_string, "+", 1) == 0 || strncmp(tz_string, "-", 1) == 0) {
// Handle simple +/-X format (+5, -8, etc.)
int offset = atoi(tz_string);
TimeChangeRule offset_tz = {"TZ", Last, Sun, Mar, 0, offset * 60};
return new Timezone(offset_tz, offset_tz);
} else {
// Unknown timezone, return null
MQTT_DEBUG_PRINTLN("Unknown timezone: %s", tz_string);
return nullptr;
}
}
#endif