Custom Home Assistant integration for controlling Solamagic 2000BT Bluetooth infrared patio heaters. This project represents reverse-engineering of the proprietary xHeatlink mobile app's Bluetooth Low Energy (BLE) protocol through packet sniffing and protocol analysis.
Author: Jocke (Pocke01)
Repository: https://github.com/Pocke01/homeassistant-solamagic
Status: Production-ready, submitted to HACS
Primary Language: Python (first Python project for author)
Documentation Language: English (developer is Swedish)
The integration was built by analyzing Bluetooth packet captures from the official xHeatlink iOS app using Apple PacketLogger. Through systematic analysis, we discovered:
-
Critical BLE Handles:
0x001F(or0x001Eon some models): Initialization/unlock characteristic0x0028: Command characteristic (power level control)0x002F&0x0032: Status notification characteristics
-
Initialization Sequence:
- Read current value from init handle
- Echo that exact value back to the same handle
- Enable CCCDs (Client Characteristic Configuration Descriptors) in specific order
- Only then can commands be sent
-
Command Protocol:
- 2-byte commands sent without response
- Format:
[power/mode, level] - OFF:
0x00 0x21 - 33%:
0x01 0x21 - 66%:
0x01 0x42 - 100%:
0x01 0x64
-
Device-Specific Init Tokens:
- Different heater units require different initialization payloads
- Must be read from device on first connection and saved
- Pattern:
FF FF FF FD [XX XX] 00 00 00(9 bytes)
Through community testing, we discovered two hardware variants:
- Model A: Init handle
0x001F(31) - most common - Model B: Init handle
0x001E(30) - entire GATT table shifted by -1
This led to implementing auto-detection that tries multiple handle candidates.
custom_components/solamagic/
├── __init__.py # Integration setup, service handlers
├── bluetooth.py # Low-level BLE client (SolamagicBleClient)
├── client.py # High-level control client (SolamagicClient)
├── climate.py # Climate entity (HVAC control)
├── sensor.py # Sensors (power level, RSSI, connection status)
├── config_flow.py # Configuration UI and Bluetooth discovery
├── const.py # Constants, handles, commands
├── manifest.json # Integration metadata
├── services.yaml # Service definitions
├── strings.json # UI strings (English base)
└── translations/
├── en.json # English
├── sv.json # Swedish
└── da.json # Danish
SolamagicBleClient (bluetooth.py)
- Manages BLE connection lifecycle
- Handles GATT operations (read, write, notifications)
- Implements auto-disconnect timer (180s) to allow mobile app access
- Parses status notifications from heater
- Returns BleakClientWithServiceCache for operations
SolamagicClient (client.py)
- Provides high-level control methods (
set_level,turn_on,turn_off) - Manages initialization sequence
- Coordinates between BLE layer and Home Assistant entities
- Handles status callbacks to entities
SolamagicClimate (climate.py)
- Home Assistant climate entity
- Preset modes: Low (33%), Medium (66%), High (100%)
- HVAC modes: OFF, HEAT
- Syncs with power sensor for state consistency
User Action (HA UI/Automation)
↓
Climate Entity / Service Call
↓
SolamagicClient (high-level)
↓
SolamagicBleClient (BLE operations)
↓
Heater Hardware
↓
BLE Notifications
↓
Status Callback
↓
Sensor & Climate Entity Update
The exact order is critical and was discovered through packet analysis:
1. Write init token to handle 0x001F (or 0x001E)
2. Enable CCCD 0x0030 (for handle 0x002F) - FIRST!
3. Enable CCCD 0x0033 (for handle 0x0032) - SECOND!
4. Enable CCCD 0x0029 (for handle 0x0028) - LAST!Any other order will fail. The init value must be the exact value read from the heater.
Status notifications are 20 bytes with this structure:
Byte 15: Power status (0x00 = OFF, 0x01 = ON)
Byte 16: Level code (0x21 = 33%, 0x42 = 66%, 0x64 = 100%)
The heater sends multiple notifications on state change (first OFF, then new state).
To allow the xHeatlink mobile app to connect, the integration:
- Implements 180-second disconnect timer
- Resets timer on any activity
- Disconnects automatically when idle
- Reconnects on-demand when commands are sent
This was crucial for user acceptance - users can still use the mobile app.
Added to support multiple heater models:
HANDLE_INIT_CANDIDATES = [0x001F, 0x001E, 0x001D]
# Try each until one works
for handle in HANDLE_INIT_CANDIDATES:
try:
init_value = read_gatt_char(handle)
if init_value:
# Save working handle for future use
save_to_config(handle)
break
except:
continue- Apple PacketLogger - Primary tool for iOS app analysis
- Wireshark - Alternative for deeper packet analysis
- ESPHome Bluetooth Proxy - For testing in Home Assistant
- Home Assistant development container
- Real hardware: Solamagic 2000BT heaters
- Multiple test units with different firmware versions
- Home Assistant BLE integration documentation
- Bleak library (Python BLE library)
- HACS submission guidelines
- Home Assistant integration quality scale
Cause: Heater uses handle 30 instead of 31
Solution: Auto-detection tries multiple handles (31, 30, 29)
Cause: Init sequence not completed or wrong order
Solution: Ensure CCCDs enabled in exact order after init write
Cause: Notifications not properly enabled
Solution: Verify CCCD writes successful, check callback registration
Cause: Home Assistant holding connection
Solution: Auto-disconnect timer (180s), or manual disconnect service
Cause: Bluetooth connection stale or heater needs power cycle
Solution: Disconnect/reconnect, power cycle heater
Following Home Assistant integration standards:
- Error Handling: Use
HomeAssistantErrorfor user-facing errors - Async Patterns: Proper async/await, no blocking operations
- Timing: Named constants instead of hardcoded values
- Logging: Appropriate log levels (DEBUG for operations, INFO for state changes, WARNING for issues)
- Type Hints: Full type annotations throughout
- Documentation: Docstrings for all public methods
- Testing: Manual testing with real hardware (automated tests pending)
Advanced services for power users and debugging:
solamagic.set_level- Set power level (0/33/66/100)solamagic.write_handle- Raw write to command handlesolamagic.write_handle_any- Write to any handle (debugging)solamagic.write_uuid- Write to characteristic by UUIDsolamagic.disconnect- Force disconnectsolamagic.scan_init_handles- Diagnostic tool to find init handle
A diagnostic service was created to help identify the correct init handle on different heater models:
# Scans handles 15-40 by default
# Identifies readable handles
# Flags potential init tokens (9 bytes starting with 0xFF)
# Logs complete results for analysisUsage:
- Enable DEBUG logging for
custom_components.solamagic - Call
solamagic.scan_init_handlesservice - Check logs for
🎯 POTENTIAL INIT TOKEN FOUND - Review
📊 SCAN COMPLETEsummary
Currently supports:
- English (en.json) - Base language
- Swedish (sv.json) - Developer's native language
- Danish (da.json) - Added for community user
All UI strings, service descriptions, and entity names are translatable.
Potential improvements identified:
- Automated Testing: Unit tests for BLE protocol, integration tests
- Additional Models: Support for Solamagic dimmer variants
- Energy Monitoring: Calculate power consumption based on level
- Schedules: Built-in scheduling without automations
- Groups: Control multiple heaters as one entity
- OTA Updates: If heaters support firmware updates via BLE
- GitHub Issues: Bug reports and feature requests
- Home Assistant Community Forum: User discussions
- HACS: Primary distribution method
- Documentation: README.md in repository
- Protocol Analysis: Sniff BLE packets from mobile app
- Hypothesis: Formulate theory about protocol behavior
- Implementation: Code the feature in Python
- Testing: Verify with real hardware
- Iteration: Refine based on results
- Documentation: Update code comments and docs
- Community Testing: Beta users test changes
- Release: Tag version, submit to HACS
- Read-echo-save pattern for initialization
- CCCD order matters
- Multiple notifications per state change
- Handle numbering can vary between firmware versions
- Device registry critical for multi-device support
- Event system better than callback chains for state sync
- Auto-disconnect improves user experience
- Diagnostic services invaluable for community support
- Type hints catch bugs early
- Async patterns need careful deadlock prevention
- Logging levels are communication with users
- Error messages should guide users to solutions
This integration would not exist without:
- Solamagic/KOCH - Hardware manufacturer
- Dension - xHeatlink app developer (protocol source)
- Home Assistant Community - Testing and feedback
- Bleak Library - Python BLE foundation
- Claude (Anthropic) - Development assistance and code review
- 0.5.2 - Handle auto-detection for multi-model support
- 0.5.1 - Added diagnostic scanner service
- 0.5.0 - HACS submission, Danish translation
- 0.4.x - Init token discovery and save
- 0.3.x - Auto-disconnect timer
- 0.2.x - Climate entity with presets
- 0.1.x - Initial BLE control implementation
See repository LICENSE file.
Last Updated: 2026-02-01
Document Version: 1.0
For Claude Code & Future Development Reference