WiFi.begin() without params connects to any SDK-cached network, including open public hotspots, and then overwrites config.ssid with the wrong SSID. Now Try 1 (SDK credentials) is only attempted on first boot when no SSID is configured. Once the user has saved credentials, we go straight to Try 2 (EEPROM config), so the correct network is always used. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reverse Engineering a €5 AliExpress Weather Clock: A Security Story
TL;DR
I bought a cute weather clock kit from AliExpress (TJ-56-654) and discovered it was leaking my WiFi password in plaintext to anyone within radio range. So I ripped out the firmware, wrote my own, and ended up with a fully async, OTA-updatable, Home Assistant-ready smart clock that's actually secure.
Table of Contents
- The Discovery: When "Smart" Means "Insecure"
- The Device
- The Investigation
- The Solution: Custom Firmware
- Technical Deep Dive
- The Journey: v1.7 → v1.9.2
- What's Next: Home Assistant Integration
- How to Flash This Firmware
- Web Interface
- API Documentation
- Security Improvements
- Lessons Learned
- Credits
The Discovery: When "Smart" Means "Insecure"
It started innocently enough. I ordered what looked like a fun DIY electronics project: an ESP8266-based weather clock with a transparent acrylic case and an OLED display. The listing promised:
- ✅ WiFi weather updates
- ✅ 3-day forecast
- ✅ Temperature, humidity, date/time
- ✅ "Intelligent connected to WIFI"
What they didn't mention:
🚨 CRITICAL SECURITY FLAW 🚨
When you first set up the device, it creates an access point with a default password. Fair enough - that's how WiFiManager works. But here's where it gets bad:
- You connect to the AP (192.168.4.1)
- You configure your home WiFi credentials
- Device connects to your network
- The open AP stays active in parallel
- Your WiFi password is displayed in plaintext on the config page
Anyone within WiFi range could:
- Connect to the device's AP (weak default password)
- Browse to 192.168.4.1
- Read your WiFi password in plaintext
- Access your network
This is a textbook example of poor IoT security design. No thanks.
The Device
Product: ESP8266 Mini Weather Clock Kit Model: TJ-56-654 Price: ~€5 EUR Source: AliExpress Link
Original Hardware Specifications
| Component | Details |
|---|---|
| MCU | ESP-01S (ESP8266EX, 1MB flash, 80KB RAM) |
| Display | GM009605v4.3 OLED (128x64, I2C) |
| Power | 5V USB (Micro-USB) |
| Case | Transparent acrylic (40x40x43mm) |
| PCB | TJ-56-654 main board |
What It Came With
- Acrylic case parts (6 pieces)
- ESP-01S WiFi module
- OLED display module
- Main PCB with headers
- USB power cable
- Brass standoffs and screws
- Pin headers (soldering required)
Original Firmware Issues
Beyond the password leak:
- Dependency on QWeather API: Requires account registration, project setup, API key management
- Chinese cloud service: All weather data routes through proprietary servers
- No OTA updates: Firmware updates require disassembly and FTDI connection
- Limited features: Fixed display modes, no customization
- Unknown code: Closed-source firmware, no way to audit what it's doing
The Investigation
Opening It Up
The transparent case made inspection easy - just unscrew the brass standoffs. Inside:
- ESP-01S module clearly labeled with pinout
- I2C OLED display connected via 4 pins (VCC, GND, SDA, SCL)
- No additional sensors (temperature/humidity were from weather API, not local)
The ESP-01S pinout is printed right on the PCB:
3V3 | GND
TX | GPIO0 (I2C SDA)
RX | GPIO2 (I2C SCL)
EN | GND
Connecting FTDI
To flash custom firmware, you need:
- FTDI USB-to-Serial adapter (3.3V! Not 5V - you'll fry the ESP8266)
- Jumper wires
- Steady hands
Wiring:
FTDI ESP-01S
────────────────────
3V3 → 3V3
GND → GND
TX → RX
RX → TX
GND → GPIO0 (for programming mode)
Boot into flash mode:
- Connect GPIO0 to GND
- Power on the device
- Remove GPIO0 to GND connection after boot
- Device is now in programming mode
Programming:
- Use Arduino IDE with ESP8266 board support
- Select board: "Generic ESP8266 Module"
- Flash size: 1MB (FS:64KB OTA:~470KB)
- Upload speed: 115200 baud
After the first flash with OTA support, you never need wires again - all updates happen over WiFi.
The Solution: Custom Firmware
I decided to write a complete replacement firmware with:
Core Principles
- Security First: No hardcoded credentials, no open networks, WiFiManager with proper AP timeout
- Privacy: Use free, open APIs (Open-Meteo instead of QWeather)
- Maintainability: OTA updates for painless improvements
- Performance: Fully async architecture, no blocking operations
- Reliability: Proper error handling, exponential backoff, memory safety
Features Implemented
🌐 Network & Time
- WiFiManager captive portal for secure first-time setup
- Hybrid WiFi: Synchronous on boot (ensures proper init), async reconnect during operation
- NTP time sync with configurable server and interval
- Timezone support with automatic European DST calculation
- mDNS: Access via
http://tj56654-clock.local/
🌦️ Weather Data
- Open-Meteo API: Free, no registration, no API key
- Configurable location: Latitude/longitude + city name
- Data: Temperature, sunrise, sunset, daylight duration
- Smart updates: Async fetch every 30 minutes (configurable)
🔄 OTA Updates
- Web-based OTA: Upload .bin files via browser at
/update - ArduinoOTA: Update directly from Arduino IDE
- Non-blocking: System stays responsive during updates
- Secure: Password-protected upload (admin/admin - change it!)
📺 Display Modes
Three rotating display screens (configurable interval):
-
Time Mode
- Large HH:MM display
- Blinking colon animation
- Day of week and date
- 12/24 hour format support
-
Weather Mode
- Temperature with superscript °c
- City name
- Clean, minimalist layout
-
Sunrise/Sunset Mode
- Sunrise time with ↑ arrow
- Sunset time with ↓ arrow
- Daylight duration (e.g., "Day 9h 41m")
All modes are center-aligned, rotation-aware, and gracefully handle missing data.
🌐 Web Interface
/- Home page with live time/config- Full configuration form/debug- System diagnostics/update- OTA firmware upload
🔌 REST API
All endpoints return JSON:
GET /api/time- Current timeGET /api/status- System status (WiFi, uptime, heap)GET /api/debug- Detailed diagnosticsGET /api/weather- Weather + sunrise/sunsetGET /api/config- Export configurationPOST /api/config- Import configurationPOST /api/eeprom-clear- Factory resetPOST /api/reboot- Remote reboot
Technical Deep Dive
Architecture: Fully Async State Machines
The firmware uses zero blocking operations in the main loop. Everything is state-machine-based:
Weather State Machine
enum WeatherState { IDLE, REQUESTING, SUCCESS, FAILED };
Uses AsyncHTTPRequest library:
- Non-blocking HTTP requests
- Callback-based response handling
- Exponential backoff on failures (1s → 2s → 4s)
- Maximum 3 retries before giving up
NTP State Machine
enum NTPState { IDLE, REQUEST_SENT, WAITING, SUCCESS, FAILED };
Custom manual NTP implementation:
- Builds raw UDP packets (48 bytes)
- Non-blocking
parsePacket()checks - 5-second timeout
- Independent epoch tracking for accuracy between syncs
WiFi State Machine
enum WiFiConnectionState { IDLE, CONNECTING, CONNECTED, FAILED };
Hybrid model (this was critical!):
- Setup phase: Synchronous connection (waits up to 10 seconds)
- Why? OTA, web server, NTP all need WiFi ready
- Without this, device shows blank display for 10+ seconds
- Loop phase: Async reconnection (checks every 5 seconds)
- Why? Don't freeze the entire system if WiFi drops
Memory Optimization
ESP8266 has strict memory limits:
| Memory Type | Total | Used | Usage | Status |
|---|---|---|---|---|
| Flash | 1,048,576 | 408,844 | 38% | ✅ Plenty |
| RAM | 80,192 | 37,644 | 46% | ✅ Safe |
| IRAM | 65,536 | 61,987 | 94% | ⚠️ Critical |
IRAM Crisis Solution:
IRAM (Instruction RAM) is limited and fills fast. The solution: ICACHE_FLASH_ATTR macro.
void ICACHE_FLASH_ATTR handleConfig() {
// This function's code lives in Flash, not IRAM
// Saves precious IRAM at cost of slightly slower execution
}
Applied to 26 functions (web handlers, display, config utilities), reducing IRAM pressure from overflow risk to sustainable 94%.
String Safety:
Avoid String concatenation in loops (causes heap fragmentation):
// ❌ BAD - 140+ concatenations
String html = "";
html += F("<!DOCTYPE html>");
html += F("<head>..."); // x138 more times
// ✅ GOOD - Chunked responses
server.setContentLength(CONTENT_LENGTH_UNKNOWN);
server.send(200, "text/html", "");
server.sendContent_P(HTML_HEADER);
server.sendContent_P(HTML_FOOTER);
server.sendContent(""); // End
Configuration Storage
26-field struct stored in EEPROM (512 bytes):
struct Config {
char ssid[32];
char password[64];
int timezone_offset;
bool dst_enabled;
uint8_t brightness;
char ntp_server[64];
unsigned long ntp_interval;
bool hour_format_24;
char hostname[32];
float latitude;
float longitude;
char city_name[32];
bool weather_enabled;
unsigned long weather_interval;
unsigned long display_rotation_sec;
bool show_weather;
bool show_sunrise_sunset;
uint8_t display_orientation;
uint32_t magic; // 0xC10CC10C - validation
};
EEPROM validation: Magic number check prevents loading corrupted data. On validation failure, gracefully defaults to safe config.
Display Hardware Discovery
This took 3 firmware iterations to get right:
v1.5: Assumed TM1637 (7-segment LED driver)
- ❌ Wrong - device has OLED, not 7-segment LEDs
v1.6: Tried TM1650 (another LED driver)
- ❌ Wrong - I2C addresses didn't match
v1.7: Identified GM009605v4.3 (SSD1306-compatible OLED)
- ✅ Correct! Used Adafruit_SSD1306 library
- ✅ Discovered swapped pins: SDA on GPIO0, SCL on GPIO2
Pin mapping quirk:
Standard ESP8266 I2C uses GPIO4 (SDA) and GPIO5 (SCL), but ESP-01S only exposes GPIO0 and GPIO2. The board designer mapped:
- GPIO0 → SDA (unusual)
- GPIO2 → SCL (unusual)
This is backwards from typical breakout boards, but works perfectly once configured:
Wire.begin(0, 2); // SDA=GPIO0, SCL=GPIO2
The Journey: v1.7 → v1.9.2
v1.7: Display Discovery ✅
- Identified correct display hardware
- Basic time display working
- WiFiManager integration
- First OTA deployment
v1.8: Stability & Security 🔒
Goals: Fix memory issues, eliminate security holes
Changes:
- IRAM optimization (added
ICACHE_FLASH_ATTRto 26 functions) - Removed hardcoded WiFi credentials
- Fixed NTP interval bug (config value was ignored)
- Fixed boolean parsing in JSON import
- Added input validation (buffer overflow protection)
- Chunked HTTP responses (eliminated 140+ String concatenations)
Result: IRAM usage 94% → 70%, no memory leaks, secure config storage
v1.9.0: Full Async Refactoring ⚡
Goals: Eliminate all blocking operations
Changes:
- Async HTTP weather fetch (AsyncHTTPRequest library)
- Custom async NTP implementation (manual UDP packets)
- Async WiFi connection (state machine)
- Removed all
delay()calls fromloop() - Exponential backoff retry logic
Performance:
| Operation | Before (v1.8) | After (v1.9.0) | Improvement |
|---|---|---|---|
| Weather fetch | 1-10s blocking | 0ms | ✅ Async callback |
| NTP sync | 5-20s blocking | 0ms | ✅ Non-blocking UDP |
| WiFi reconnect | 15s blocking | 0ms | ✅ State machine |
| Loop time | 10ms minimum | <1ms | ✅ 10x faster |
Result: Device stays responsive during OTA updates while weather is fetching!
v1.9.1: Hybrid Fix 🎯
Problem Discovered:
After deploying v1.9.0, the display showed blank screen for 10 seconds after boot, with DNS resolution failed errors in logs.
Root Cause:
Making WiFi fully async broke the initialization order:
void setup() {
setupWiFi(); // Returns immediately (async)
setupOTA(); // WiFi NOT ready! ❌
setupWebServer(); // WiFi NOT ready! ❌
testInternetConnectivity(); // WiFi NOT ready! → DNS error
}
Solution: Hybrid Model
| Phase | WiFi Mode | Blocking? | Why? |
|---|---|---|---|
setup() |
Synchronous | 10s max | OTA/web/NTP need WiFi ready |
loop() |
Asynchronous | 0s | Don't freeze on reconnect |
Results:
- ✅ Display shows time immediately after WiFi connects (~15 sec boot)
- ✅ No "DNS resolution failed" errors
- ✅ Proper initialization order guaranteed
- ✅ Device never freezes on WiFi loss during operation
v1.9.2: WiFi Resilience (Current) 🛡️
Problem Discovered:
After WiFi outages, the device would clear stored credentials and enter AP mode, requiring manual reconfiguration every time the router restarted.
Root Cause:
Aggressive credential clearing on connection failure:
if (wifiRetry.currentRetry >= wifiRetry.maxRetries) {
memset(config.ssid, 0, sizeof(config.ssid)); // ❌ Clears credentials!
memset(config.password, 0, sizeof(config.password));
saveConfig();
// Enter AP mode...
}
Solution: Resilient WiFi
| Feature | Before (v1.9.1) | After (v1.9.2) |
|---|---|---|
| Credential clearing | After 5 failed attempts | Never |
| Retry strategy | Give up after 5 tries | Infinite with backoff |
| Max retry interval | N/A | 5 minutes |
| Fallback AP | After clearing credentials | After ~5 min (dual STA+AP mode) |
| Clock during outage | Blank display | Shows last synced time |
Key Changes:
- Never clear credentials on connection failure
- Exponential backoff: 5s → 10s → 20s → ... → 5min max
- Fallback AP ("TJ56654-Setup") enabled after ~5 min, while still retrying
- Dual STA+AP mode: Device continues reconnect attempts while AP is active
- SDK credentials support: Tries WiFiManager-stored credentials first, then EEPROM
- "No WiFi" display: Shows retry countdown instead of cryptic numbers
- "!" indicator: Shown in date line when WiFi disconnected
Network Activity Summary:
| Service | Interval | Endpoint | Protocol |
|---|---|---|---|
| NTP | 1 hour | pool.ntp.org:123 | UDP |
| Weather | 30 min | api.open-meteo.com | HTTP |
| mDNS | continuous | 224.0.0.251 | UDP multicast |
~50 requests/day total.
Results:
- ✅ Credentials persist through WiFi outages
- ✅ Device automatically reconnects when WiFi returns
- ✅ Clock continues running with last synced time
- ✅ User can reconfigure via fallback AP if needed
Startup Timeline:
[0-5s] Display init, startup animation
[5-15s] WiFi connection (SYNCHRONOUS in setup())
✅ WiFi connected! IP assigned
[15-20s] OTA init, web server start, NTP client ready
✅ Internet test: PASSED
[20-30s] First async NTP sync
✅ Time synced and displayed
Memory Evolution
| Version | RAM Usage | IRAM Usage | Flash Usage | Notes |
|---|---|---|---|---|
| v1.7 | 34,980 (43%) | 61,987 (94%) | 407,500 (38%) | IRAM crisis |
| v1.8 | 36,980 (46%) | 45,120 (68%) | 407,800 (38%) | ICACHE_FLASH_ATTR fix |
| v1.9.0 | 37,516 (46%) | 61,987 (94%) | 408,540 (38%) | Async libs added |
| v1.9.1 | 37,644 (46%) | 61,987 (94%) | 408,844 (38%) | Hybrid WiFi fix |
| v1.9.2 | 37,800 (47%) | 61,987 (94%) | 409,100 (39%) | WiFi resilience |
Verdict: Stable memory usage, no leaks detected after 24h+ uptime tests.
What's Next: Home Assistant Integration
The firmware is designed to be extensible. Next planned features:
Custom Display Screens
Pull data from Home Assistant via REST API:
- Smart home stats: Energy usage, room temperatures
- Sensor data: Air quality, CO2 levels
- Automation states: Alarm status, door locks
MQTT Integration
- Publish time/weather data to MQTT broker
- Subscribe to topics for display content
- Enable automation triggers (e.g., display alert when door opens)
WebSocket Live Updates
Replace polling with WebSocket for:
- Real-time config changes without page refresh
- Live display preview in web UI
- Push notifications for firmware updates
How to Flash This Firmware
Requirements
- Hardware: TJ-56-654 weather clock or compatible ESP-01S + OLED setup
- FTDI Adapter: 3.3V USB-to-Serial (CP2102, FT232RL, CH340)
- Software: Arduino IDE 1.8.x or 2.x
Arduino IDE Setup
-
Install ESP8266 Board Support
- File → Preferences
- Additional Board Manager URLs:
http://arduino.esp8266.com/stable/package_esp8266com_index.json - Tools → Board → Boards Manager → Search "ESP8266" → Install
-
Install Required Libraries
- Sketch → Include Library → Manage Libraries
- Install:
Adafruit GFX LibraryAdafruit SSD1306NTPClientWiFiManager(by tzapu)AsyncHTTPRequest_GenericESPAsyncTCPArduinoJson(by Benoit Blanchon)
-
Board Configuration
- Board: "Generic ESP8266 Module"
- Flash Size: "1MB (FS:64KB OTA:~470KB)"
- Flash Mode: "DIO"
- Flash Frequency: "40MHz"
- CPU Frequency: "80MHz"
- Upload Speed: "115200"
First Flash (via FTDI)
-
Wire the ESP-01S:
FTDI 3.3V → ESP-01S 3V3 FTDI GND → ESP-01S GND FTDI TX → ESP-01S RX FTDI RX → ESP-01S TX FTDI GND → ESP-01S GPIO0 (boot mode) -
Compile and Upload:
- Open
clock_ntp_ota_v1.9.ino - Sketch → Upload
- Wait for "Done uploading"
- Remove GPIO0-to-GND jumper
- Press reset or power cycle
- Open
-
Initial Setup:
- Device creates AP: "TJ56654-Setup"
- Connect to it (password:
12345678) - Captive portal opens automatically
- Select your WiFi network and enter password
- Device reboots and connects
Subsequent Updates (OTA)
-
Via Web Interface (easiest):
- Browse to
http://192.168.x.x/update(find IP from router) - Or use mDNS:
http://tj56654-clock.local/update - Login:
admin/admin - Choose .bin file from
build/folder - Click "Update"
- Device reboots automatically (~15 seconds)
- Browse to
-
Via Arduino IDE:
- Tools → Port → Select "tj56654-clock at 192.168.x.x"
- Sketch → Upload
- No wires needed!
Web Interface
Home Page (/)
Current time display with live updates via JavaScript (fetches /api/time every second).
Configuration Page (/config)
Comprehensive settings form:
WiFi Settings
- SSID
- Password
- Hostname (for mDNS)
Time Settings
- Timezone offset (seconds from UTC)
- DST enabled (European rules)
- NTP server address
- NTP sync interval (seconds)
- Hour format (12h/24h)
Weather Settings
- Enabled/disabled toggle
- Latitude
- Longitude
- City name (for display)
- Update interval (seconds)
Display Settings
- Brightness (0-7)
- Rotation (0°, 90°, 180°, 270°)
- Display rotation interval (seconds)
- Show weather screen (toggle)
- Show sunrise/sunset screen (toggle)
All settings persist to EEPROM and survive reboots.
Debug Page (/debug)
Real-time diagnostics:
- System: Uptime, free heap, chip ID, flash size
- WiFi: SSID, IP, signal strength, MAC address, gateway, DNS
- Time: Current time, timezone, DST status, NTP sync status
- NTP Stats: Last sync, attempts, successes, failures
- Weather: Temperature, sunrise/sunset, last update, API status
- Network Tests: Internet connectivity, DNS resolution
- Display: Current mode, rotation, brightness
Perfect for troubleshooting connectivity or API issues.
API Documentation
All endpoints return JSON (except /update which is for file upload).
GET /api/time
Current time information.
Response:
{
"current": "14:23:45",
"date": "2026-01-03",
"day": "Friday",
"timezone_offset": 0,
"dst_active": false
}
GET /api/status
System status overview.
Response:
{
"wifi": {
"ssid": "MyNetwork",
"ip": "192.168.1.47",
"rssi": -38,
"hostname": "tj56654-clock"
},
"time": {
"current": "14:23:45",
"timezone_offset": 0,
"ntp_synced": true
},
"system": {
"uptime": 3627,
"free_heap": 35104,
"chip_id": "f77134"
}
}
GET /api/weather
Current weather data.
Response:
{
"temperature": 15.4,
"city": "Portimao",
"sunrise": "07:48",
"sunset": "17:29",
"daylight_hours": 9,
"daylight_minutes": 41,
"last_update": "14:20:00",
"valid": true
}
GET /api/config
Export full configuration as JSON.
Response:
{
"ssid": "MyNetwork",
"timezone_offset": 0,
"dst_enabled": true,
"brightness": 5,
"ntp_server": "pool.ntp.org",
"ntp_interval": 3600,
"hour_format_24": true,
"hostname": "tj56654-clock",
"latitude": 37.19,
"longitude": -8.54,
"city_name": "Portimao",
"weather_enabled": true,
"weather_interval": 1800,
"display_rotation_sec": 5,
"show_weather": true,
"show_sunrise_sunset": true,
"display_orientation": 0
}
POST /api/config
Import configuration from JSON.
Request Body: Same structure as export response (password field optional for security).
Response:
{
"status": "ok"
}
Device automatically reboots after import.
POST /api/eeprom-clear
Factory reset (clears EEPROM).
Response:
{
"status": "cleared"
}
Device reboots to WiFiManager captive portal.
POST /api/reboot
Remote reboot.
Response:
{
"status": "rebooting"
}
Device reboots immediately.
Security Improvements
What Changed from Original Firmware
| Issue | Original | Custom Firmware |
|---|---|---|
| WiFi Password Leak | Plaintext in open AP | No open AP after setup |
| Persistent AP | Always active | Only on first boot or failure |
| API Keys | QWeather requires registration | Open-Meteo (no key needed) |
| Cloud Dependency | Chinese servers | Direct API calls, no intermediary |
| Firmware Updates | Manual FTDI only | OTA via WiFi (password-protected) |
| Config Access | No authentication | Admin password required |
| Code Transparency | Closed source | Open source (you're reading it!) |
Best Practices Implemented
- WiFiManager Timeout: AP automatically closes after 180 seconds if no configuration
- Fallback AP Mode: If credentials fail, device creates secure AP ("TJ56654-Clock" with password)
- EEPROM Validation: Magic number check prevents loading corrupted data
- Input Sanitization: Buffer overflow protection on all user inputs
- Memory Safety: No dynamic String allocations in loops, fixed-size buffers
- Error Handling: Graceful degradation (e.g., display shows time even if weather fails)
Recommended Post-Flash Steps
-
Change OTA password: Edit line ~60 in
.inofile:ArduinoOTA.setPassword("admin"); // Change this! -
Change web admin password: Edit line ~430:
if (!server.authenticate("admin", "admin")) { // Change this! -
Set strong WiFi AP fallback password: Edit line ~780:
WiFi.softAP("TJ56654-Clock", "12345678"); // Change this! -
Disable unnecessary features: If you don't need weather, disable it in
/configto save bandwidth
Lessons Learned
Hardware
- Always check pinouts: Don't assume standard pin mappings - this device swaps SDA/SCL
- FTDI is your friend: A $2 adapter unlocks any ESP8266 device
- Transparent cases are great: Made debugging and identification trivial
- Read the PCB silk screen: Model numbers and pin labels save hours of guessing
Software
- Async is hard but worth it: Fully non-blocking architecture eliminates user-facing freezes
- IRAM is precious: On ESP8266, use
ICACHE_FLASH_ATTRliberally - Hybrid approaches work: Don't be dogmatic - synchronous WiFi in setup() solved a critical UX issue
- State machines scale: Better than callback hell for complex async operations
- Test on real hardware: Emulators can't catch pin mapping errors or memory constraints
Security
- IoT security is often terrible: Always audit devices before trusting them on your network
- Open source is safer: Closed firmware is a black box - you have no idea what it's doing
- Defaults matter: Insecure defaults (open AP, plaintext passwords) lead to real vulnerabilities
- Defense in depth: Multiple layers (WiFiManager timeout, password protection, validation) catch mistakes
Development
- OTA from day 1: Flashing via FTDI gets old fast - build OTA support early
- Version your work: Backup files (.bak, .bak2) saved me multiple times
- Document as you go: Release notes and architecture docs prevent "what was I thinking?" moments
- Incremental improvements: v1.7 → v1.8 → v1.9.x made debugging manageable
Credits
Hardware: TJ-56-654 Weather Clock Kit (AliExpress)
Firmware: Written from scratch with love and frustration
Libraries Used:
APIs:
- Open-Meteo - Free weather API, no registration required
Tools:
- Arduino IDE 2.x
- FTDI FT232RL USB-to-Serial adapter
- Lots of coffee ☕
Project Structure
esp8266-weather-clock/
├── firmware/
│ └── clock_ntp_ota_v1.9/
│ └── clock_ntp_ota_v1.9.ino # Main firmware (~2,100 lines)
├── docs/
│ ├── HARDWARE.md # Hardware specifications
│ ├── INSTALLATION.md # Flashing guide
│ ├── v1.9_RELEASE_NOTES.md # v1.9.0 async refactoring
│ ├── v1.9.1_HYBRID_FIX.md # WiFi startup fix
│ └── v1.9.2_WIFI_RESILIENCE.md # WiFi resilience documentation
├── CHANGELOG.md # Version history
└── README.md # This file
License
This project is released into the public domain. Do whatever you want with it. If you improve it, consider sharing your changes - that's how we make IoT better.
Final Thoughts
This project started as "I don't trust this device" and ended as "I built something better."
The original firmware had security holes you could drive a truck through. The custom replacement:
- ✅ Doesn't leak WiFi passwords
- ✅ Uses free, open APIs
- ✅ Updates over WiFi
- ✅ Runs fully async (no freezing)
- ✅ Integrates with Home Assistant (coming soon)
- ✅ Is completely auditable (you're reading the source)
Total cost: €5 hardware + a weekend of tinkering.
If you have one of these devices, flash this firmware. If you're buying IoT gadgets, always audit them first. And if something seems insecure, fix it yourself - that's the hacker spirit.
Now go make something cool. 🚀
P.S.: If you found this useful, consider starring the repo. If you found a bug, open an issue. If you want to add Home Assistant screens, let's collaborate - I'm planning that next!
Author: apetrochenko Date: 2026-01-06 Firmware Version: v1.9.2 (Production Ready)