This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
ESPHome external component for monitoring Tigo solar optimizers via RS485/UART. Two C++ components (tigo_monitor, tigo_server) run on ESP32 using the ESP-IDF framework (not Arduino). FreeRTOS tasks/semaphores handle all async operations.
# Compile (check for errors without flashing)
esphome compile boards/esp32s3-atoms3r.yaml
# Compile and flash
esphome run boards/esp32s3-atoms3r.yaml
# View logs
esphome logs boards/esp32s3-atoms3r.yamlThere is no test suite. Testing is manual (sensor updates in HA, web UI, CCA sync, memory monitoring).
components/tigo_monitor/— Core component: UART frame parsing, device tracking, sensor publishing, CCA sync__init__.py,sensor.py,button.py,binary_sensor.py— Python config schemas + ESPHome codegen (no runtime logic)tigo_monitor.h/tigo_monitor.cpp— C++ implementation (~3,200 lines)
components/tigo_server/— HTTP web server with 5 HTML pages + RESTful JSON API__init__.py— Config schematigo_web_server.h/tigo_web_server.cpp— C++ implementation (~4,200 lines)
All features follow this pattern — Python schema, C++ header setter, C++ implementation:
- Python (
__init__.pyor.py): Schema validation viacv.Schema, codegen callscg.add(var.set_xxx(...)) - C++ header: Setter method
set_xxx()stores value in member variable - C++ impl: Uses the stored value in processing logic
UART reads 12-byte telemetry frames from RS485 bus. Three frame types:
- Power frames (0x0D/0x0F): Voltage, current, power, temperature
- Status frames (0x09): Device status flags
- Node table frames (0x27): 16-char device addresses/barcodes
process_frame() dispatches to process_power_frame(), process_09_frame(), process_27_frame().
sensor.py uses keyword-based detection to route sensor configs to the correct schema. Keywords like "energy"/"kwh" → energy sensor, "frame"/"missed" → missed frame counter, "power"/"watt" → power sensor. When adding new sensor types, update both keyword detection and schema mapping.
PSRAM is required for 15+ devices. Custom STL-compatible allocators (PSRAMAllocator) back large data structures:
psram_vector<DeviceData>for device listpsram_map<std::string, NodeInfo>for node tablePSRAMStringfor JSON/HTML response building
Always use PSRAM containers for large data. Internal RAM is <200KB; PSRAM is 8MB. Use #ifdef USE_ESP_IDF guards for PSRAM types.
Boards without PSRAM (e.g. boards/esp32-lilygo-t-can485.yaml) are supported for sensors-only builds — tigo_monitor without tigo_server. The psram_* aliases still compile there; psram_malloc() keys off the IDF's CONFIG_SPIRAM and resolves straight to the internal heap when it is unset, so no call site changes. Keep it that way: gate on CONFIG_SPIRAM inside the allocator rather than sprinkling new #ifdefs at usage sites, and never assume USE_ESP_IDF implies PSRAM exists. The web server genuinely does require PSRAM and must stay out of those configs.
5 HTML pages (/, /nodes, /status, /yaml, /cca) + JSON API endpoints (/api/devices, /api/overview, /api/strings, /api/status, /api/health, /api/inverters, /api/energy-history). Auth: api_token for API, HTTP Basic for HTML pages. All responses built in PSRAM via PSRAMString.
Fuzzy barcode matching (match_barcode()): compares last 6 chars of UART-discovered addresses against CCA barcodes with 1-char tolerance. On match, populates inverter_name, mppt_label, panel_name in NodeInfo.
- Always apply
power_calibration_multiplier to ALL power calculations (individual sensors, string aggregation, web API, power sums) - Avoid string allocations in loops — reuse static buffers to prevent heap fragmentation (see CHANGELOG 1.2.0 for the pattern)
- JSON field naming:
snake_casein JSON,kebab-casein HTML IDs,camelCasein JavaScript - When renaming methods in
tigo_monitor.h, update all call sites: header → member variable → web server → Python config → JavaScript - When changing any
/api/*JSON shape (rename a field, restructure a response), updatesite/screenshots/fixtures.mjsin the same commit. The docs screenshots are rendered from the realapp.htmlagainst those fixtures at build time — a stale fixture doesn't fail the build, it publishes screenshots of a UI renderingundefined. Check withcd site && npm run screenshots— which also refreshes the committed README images indocs/images/, so commit those too - Sensor/text_sensor/binary_sensor are pulled in by
AUTO_LOADintigo_monitor/__init__.py, becausetigo_monitor.hincludes all three unconditionally. YAML no longer needs to declare empty stanzas for them. If you add an unconditional#includeof another ESPHome component to that header, add it toAUTO_LOADtoo — otherwise configs that don't happen to use it fail with "No such file or directory". Prefer aUSE_*guard (astime/buttonalready do) when the dependency is genuinely optional CONFIG_UART_ISR_IN_IRAM: "y"in sdkconfig is required for reduced frame loss
main: Trunk. Stable releases are taggedvX.Y.Zhere, and current development also lands here via short-lived branches. Base for new work and the target for PRs.feature/*,fix/*: Short-lived branches created offmain, merged back tomain.- Legacy branches
dev(abandoned ~2026-04) andnext(lagging, no unique work) are not the current line — start new work frommain.
- Update
CHANGELOG.mdunder## [Unreleased](group by Added/Fixed/Changed/Removed). - Move the
## [Unreleased]entries under a new## [X.Y.Z]heading, create an annotated tagvX.Y.Z, push, and create the GitHub release.
(The web UI shows ESPHome's own version via esphome/core/version.h — there is no project-specific version constant to bump.)