# BTHome v2 Parser/Serializer for Elixir
A comprehensive, type-safe implementation of the BTHome v2 protocol for Elixir. This library provides serialization and deserialization of sensor data according to the [BTHome v2 specification](https://bthome.io/).
## Features
- 🔒 **Type Safety** - Uses structs for measurements and decoded data
- ✅ **Validation** - Comprehensive validation of measurement types and values
- 🚨 **Error Handling** - Structured errors with context information
- ⚡ **Performance** - Compile-time optimizations for fast lookups
- 🔄 **Compatibility** - Supports both struct and map-based APIs
- 📊 **Complete Coverage** - Supports all BTHome v2 sensor types
- 🛡️ **Error Recovery** - Graceful handling of unknown object IDs
## Installation
Add `bthome` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:bthome, "~> 0.1.0"}
]
end
```
## Quick Start
```elixir
# Create measurements using the builder pattern (recommended)
binary = BTHome.new_packet()
|> BTHome.add_measurement(:temperature, 23.45)
|> BTHome.add_measurement(:motion, true)
|> BTHome.serialize!()
# => <<64, 2, 41, 9, 33, 1>>
# Serialize to binary format
{:ok, binary} = BTHome.serialize([temp, motion])
# => {:ok, <<64, 2, 41, 9, 33, 1>>}
# Deserialize back to structs
{:ok, decoded} = BTHome.deserialize(binary)
# => {:ok, %BTHome.DecodedData{
# version: 2,
# encrypted: false,
# trigger_based: false,
# measurements: [
# %BTHome.Measurement{type: :temperature, value: 23.45, unit: "°C"},
# %BTHome.Measurement{type: :motion, value: true, unit: nil}
# ]
# }}
# Deserialize back (recommended)
measurements = BTHome.deserialize_measurements!(binary)
# => %{temperature: 23.45, motion: true}
# Direct access to values
temp = measurements.temperature # 23.45
motion = measurements.motion # true
```
## Supported Sensor Types
### Environmental Sensors
| Type | Unit | Description | Range |
|------|------|-------------|-------|
| `:temperature` | °C | Temperature | -327.68 to 327.67 °C |
| `:humidity` | % | Relative humidity | 0 to 655.35% |
| `:pressure` | hPa | Atmospheric pressure | 0 to 167772.15 hPa |
| `:illuminance` | lux | Light level | 0 to 167772.15 lux |
| `:battery` | % | Battery level | 0 to 255% |
| `:energy` | kWh | Energy consumption | 0 to 16777.215 kWh |
| `:power` | W | Power consumption | 0 to 167772.15 W |
| `:voltage` | V | Voltage | 0 to 65.535 V |
| `:pm2_5` | µg/m³ | PM2.5 particles | 0 to 65535 µg/m³ |
| `:pm10` | µg/m³ | PM10 particles | 0 to 65535 µg/m³ |
| `:co2` | ppm | CO2 concentration | 0 to 65535 ppm |
| `:tvoc` | µg/m³ | Total VOC | 0 to 65535 µg/m³ |
### Binary Sensors
| Type | Description |
|------|-------------|
| `:motion` | Motion detection |
| `:door` | Door state (open/closed) |
| `:window` | Window state (open/closed) |
| `:occupancy` | Room occupancy |
| `:presence` | Presence detection |
| `:smoke` | Smoke detection |
| `:gas_detected` | Gas detection |
| `:carbon_monoxide` | CO detection |
| `:battery_low` | Low battery warning |
| `:battery_charging` | Charging status |
| `:connectivity` | Connection status |
| `:problem` | Problem/fault status |
| `:safety` | Safety status |
| `:tamper` | Tamper detection |
| `:vibration` | Vibration detection |
## API Documentation
### Creating Measurements
```elixir
# Recommended: Using the measurement function with validation
{:ok, temp} = BTHome.measurement(:temperature, 23.45)
{:ok, motion} = BTHome.measurement(:motion, true)
# With custom unit override
{:ok, temp_f} = BTHome.measurement(:temperature, 74.21, unit: "°F")
# Direct struct creation (advanced)
%BTHome.Measurement{
type: :temperature,
value: 23.45,
unit: "°C"
}
```
### Serialization
```elixir
# Basic serialization
measurements = [temp, motion]
{:ok, binary} = BTHome.serialize(measurements)
# With encryption flag
{:ok, encrypted_binary} = BTHome.serialize(measurements, true)
# Legacy map format (still supported)
legacy_measurements = [
%{type: :temperature, value: 23.45},
%{type: :humidity, value: 67.8}
]
{:ok, binary} = BTHome.serialize(legacy_measurements)
```
### Builder Pattern API
For a more fluent, pipeable approach to creating BTHome packets:
```elixir
# Create and serialize in a single pipeline
{:ok, binary} = BTHome.new_packet()
|> BTHome.add_measurement(:temperature, 23.45)
|> BTHome.add_measurement(:motion, true)
|> BTHome.add_measurement(:humidity, 67.8)
|> BTHome.serialize()
# With encryption
{:ok, encrypted_binary} = BTHome.new_packet()
|> BTHome.add_measurement(:temperature, 23.45)
|> BTHome.add_measurement(:battery, 85)
|> BTHome.serialize(true)
# Error handling with builder pattern
result = BTHome.new_packet()
|> BTHome.add_measurement(:temperature, 23.45)
|> BTHome.add_measurement(:invalid_type, 42) # This will cause an error
|> BTHome.serialize()
case result do
{:ok, binary} -> process_binary(binary)
{:error, error} -> handle_error(error)
end
# Custom measurement options
{:ok, binary} = BTHome.new_packet()
|> BTHome.add_measurement(:temperature, 74.21, unit: "°F")
|> BTHome.add_measurement(:voltage, 3.3, object_id: 0x0C)
|> BTHome.serialize()
```
The builder pattern provides several advantages:
- **Fluent API**: Chain operations naturally with the pipe operator
- **Error Accumulation**: Invalid measurements are stored but don't break the chain
- **Validation**: Each measurement is validated when added
- **Flexibility**: Mix with existing APIs or use standalone
### Deserialization
**Recommended: Use `deserialize_measurements!/1` for the most ergonomic API:**
```elixir
# Simple, direct access to measurements
binary = <<64, 2, 202, 9, 3, 191, 19>>
measurements = BTHome.deserialize_measurements!(binary)
# Direct access to values
temp = measurements.temperature # 25.06
humidity = measurements.humidity # 50.23
# Handle multiple measurements of the same type
measurements.voltage # [3.1, 2.8] - list for multiple values
# Binary sensors
measurements.motion # true/false
```
**Alternative: Use the tuple-returning version for explicit error handling:**
```elixir
case BTHome.deserialize_measurements(binary) do
{:ok, measurements} ->
IO.puts("Temperature: #{measurements.temperature}°C")
{:error, reason} ->
IO.puts("Failed to decode: #{reason}")
end
```
**Low-level: Access the full decoded structure:**
```elixir
{:ok, decoded} = BTHome.deserialize(binary)
# Access metadata
decoded.version # => 2
decoded.encrypted # => false
decoded.trigger_based # => false
# Access measurements as structs
[temp_measurement, humidity_measurement] = decoded.measurements
temp_measurement.value # 25.06
temp_measurement.unit # "°C"
```
### Convenience Functions
#### `deserialize_measurements!/1` (Recommended)
The most ergonomic way to access measurement data. Returns measurements as a map and raises on error:
```elixir
# Simple, direct access - no pattern matching needed
binary = <<64, 2, 202, 9, 3, 191, 19>>
measurements = BTHome.deserialize_measurements!(binary)
# Direct access to values
temp = measurements.temperature # 25.06
humidity = measurements.humidity # 50.23
# Handle multiple measurements of the same type
binary_with_multiple_temps = <<64, 2, 202, 9, 18, 2, 100, 5>>
measurements = BTHome.deserialize_measurements!(binary_with_multiple_temps)
measurements.temperature # [25.06, 13.0] - returns list for multiple values
# Binary sensors
binary_sensor = <<64, 15, 1>>
measurements = BTHome.deserialize_measurements!(binary_sensor)
measurements.generic_boolean # true
```
#### `deserialize_measurements/1`
For explicit error handling, use the tuple-returning version:
```elixir
case BTHome.deserialize_measurements(binary) do
{:ok, measurements} ->
# Process measurements
IO.puts("Temperature: #{measurements.temperature}°C")
{:error, %BTHome.Error{message: message}} ->
IO.puts("Decoding failed: #{message}")
end
```
### Validation
```elixir
# Validate individual measurement
case BTHome.validate_measurement(measurement) do
:ok -> IO.puts("Valid measurement")
{:error, error} -> IO.puts("Invalid: #{error.message}")
end
# Validate list of measurements
case BTHome.validate_measurements(measurements) do
:ok -> BTHome.serialize(measurements)
{:error, error} -> handle_validation_error(error)
end
```
### Error Handling
All functions return structured errors with context:
```elixir
{:error, %BTHome.Error{
type: :validation, # :validation, :encoding, or :decoding
message: "Unsupported measurement type: :invalid",
context: %{type: :invalid} # Additional debugging info
}}
```
## Advanced Usage
### Custom Object IDs
```elixir
# Override default object ID (advanced use case)
{:ok, measurement} = BTHome.measurement(:temperature, 23.45, object_id: 0x02)
```
### Backwards Compatibility
The library maintains backwards compatibility with the legacy API:
```elixir
BTHome.serialize(measurements)
BTHome.deserialize(binary)
BTHome.measurement(:temperature, 23.45)
```
### Performance Considerations
The library uses compile-time optimizations for maximum performance:
- O(1) type lookups using compile-time maps
- Set-based binary sensor detection
- Minimal runtime overhead for validation
### Error Recovery
The decoder includes error recovery for unknown object IDs:
```elixir
# If binary contains unknown object IDs, they are skipped
# and parsing continues with known measurements
{:ok, decoded} = BTHome.deserialize(binary_with_unknown_data)
# Successfully returns known measurements, skips unknown ones
```
## Examples
### IoT Sensor Data
```elixir
# Traditional approach
measurements = [
%{type: :temperature, value: 22.5},
%{type: :humidity, value: 45.0},
%{type: :battery, value: 85}
]
{:ok, binary} = BTHome.serialize(measurements)
data = BTHome.deserialize_measurements!(binary)
# => %{temperature: 22.5, humidity: 45.0, battery: 85}
# IoT Sensor Data - Builder pattern approach
binary = BTHome.Packet.new()
|> BTHome.Packet.add_measurement(:temperature, 22.5)
|> BTHome.Packet.add_measurement(:humidity, 45.0)
|> BTHome.Packet.add_measurement(:battery, 85)
|> BTHome.Packet.serialize!()
data = BTHome.deserialize_measurements!(binary)
# => %{temperature: 22.5, humidity: 45.0, battery: 85}
```
### Home Automation
```elixir
# Traditional approach
measurements = [
%{type: :motion, value: true},
%{type: :door, value: false},
%{type: :temperature, value: 21.0}
]
{:ok, binary} = BTHome.serialize(measurements)
data = BTHome.deserialize_measurements!(binary)
# => %{motion: true, door: false, temperature: 21.0}
# Home Automation - Builder pattern approach
binary = BTHome.Packet.new()
|> BTHome.Packet.add_measurement(:motion, true)
|> BTHome.Packet.add_measurement(:door, false)
|> BTHome.Packet.add_measurement(:temperature, 21.0)
|> BTHome.Packet.serialize!()
data = BTHome.deserialize_measurements!(binary)
# => %{motion: true, door: false, temperature: 21.0}
```
### Environmental Monitoring
```elixir
# Environmental Monitoring - Traditional approach
measurements = [
%{type: :temperature, value: 23.1},
%{type: :humidity, value: 58.3},
%{type: :pressure, value: 1013.25},
%{type: :pm2_5, value: 12},
%{type: :pm10, value: 18},
%{type: :co2, value: 420}
]
{:ok, binary} = BTHome.serialize(measurements)
data = BTHome.deserialize_measurements!(binary)
# => %{temperature: 23.1, humidity: 58.3, pressure: 1013.25, pm2_5: 12, pm10: 18, co2: 420}
# Environmental Monitoring - Builder pattern approach
binary = BTHome.Packet.new()
|> BTHome.Packet.add_measurement(:temperature, 23.1)
|> BTHome.Packet.add_measurement(:humidity, 58.3)
|> BTHome.Packet.add_measurement(:pressure, 1013.25)
|> BTHome.Packet.add_measurement(:pm2_5, 12)
|> BTHome.Packet.add_measurement(:pm10, 18)
|> BTHome.Packet.add_measurement(:co2, 420)
|> BTHome.Packet.serialize!()
data = BTHome.deserialize_measurements!(binary)
# => %{temperature: 23.1, humidity: 58.3, pressure: 1013.25, pm2_5: 12, pm10: 18, co2: 420}
```
## Testing
Run the test suite:
```bash
mix test
```
The library includes comprehensive tests covering:
- All sensor types and their ranges
- Serialization/deserialization round trips
- Validation edge cases
- Error recovery scenarios
- Backwards compatibility
## Contributing
1. Fork the repository
2. Create a feature branch
3. Add tests for new functionality
4. Ensure all tests pass
5. Submit a pull request
## License
This project is licensed under the MIT License - see the LICENSE file for details.
## References
- [BTHome v2 Specification](https://bthome.io/)
- [Bluetooth Low Energy](https://www.bluetooth.com/specifications/bluetooth-core-specification/)
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
be found at <https://hexdocs.pm/bthome>.