Technical overview of the Home Assistant Floorplan integration architecture.
Home Assistant Core
│
├─ Floorplan Integration
│ ├─ Config Flow (UI Configuration)
│ ├─ Service Handler (20+ services)
│ ├─ Floorplan Manager (Data persistence)
│ │ ├─ Floors Registry
│ │ ├─ Rooms Registry
│ │ ├─ Static Entities Registry
│ │ └─ Beacon Nodes Registry
│ │
│ ├─ Location Provider System
│ │ ├─ Provider Base Class
│ │ ├─ Bermuda Provider (Trilateration)
│ │ └─ Provider Registry
│ │
│ └─ Services API
│
└─ Lovelace Card
├─ Calls floorplan.* services
├─ Receives entity coordinates
└─ Renders 2D floorplan visualization
| Component | Responsibility | Language |
|---|---|---|
__init__.py |
Integration setup, service registration, provider lifecycle | Python |
config_flow.py |
UI configuration wizard, provider selection | Python |
floorplan_manager.py |
YAML file I/O, data persistence, registry management | Python |
location_provider.py |
Abstract base class for providers | Python |
providers/bermuda.py |
BLE trilateration calculations | Python |
const.py |
Constants, configuration keys | Python |
| Lovelace Card | Visualization, service calls | TypeScript/Lit |
Floorplan (root)
├─ Floors
│ ├─ floor_id
│ │ ├─ height: float (meters)
│ │ └─ ...
│ └─ ...
│
├─ Rooms
│ ├─ room_id
│ │ ├─ name: string
│ │ ├─ floor: floor_id reference
│ │ ├─ area: area_id reference (optional)
│ │ └─ boundaries: [[x, y], ...]
│ └─ ...
│
├─ Static Entities
│ ├─ entity_id
│ │ └─ coordinates: [x, y, z]
│ └─ ...
│
└─ Moving Entities
├─ Beacon Nodes
│ ├─ node_id
│ │ └─ coordinates: [x, y, z]
│ └─ ...
└─ ...3D Cartesian Coordinates:
- X: Horizontal left/right (meters, positive right)
- Y: Horizontal forward/back (meters, positive forward)
- Z: Vertical up/down (meters, positive up)
Origin Convention:
- Typically placed at corner of home
- All coordinates relative to this origin
- Floor height defines ceiling height of that floor (used for beacon/entity filtering by floor)
Examples:
light.living_room: [5, 4, 1.8]
# 5m right, 4m forward, 1.8m up (typical ceiling light height)
camera.front_door: [0, 0, 2.2]
# At origin corner, 2.2m high (porch overhang)
beacon_node_1: [12, 2.5, 2.0]
# 12m right, 2.5m forward, 2.0m up (wall-mounted)~/.homeassistant/
└─ floorplan/
└─ floorplan.yaml # User-edited configuration
Format: YAML with sections for floors, rooms, static entities, moving entities
Lifecycle:
- User creates/edits
floorplan.yaml - Integration loads on startup or reload
- Floorplan Manager parses YAML → in-memory registries
- Services query registries
- User updates → restart to reload (or hot-reload planned)
# Floorplan Manager
floors: dict[str, FloorData] # floor_id → floor data
rooms: dict[str, RoomData] # room_id → room data
static_entities: dict[str, list[float]] # entity_id → [x, y, z]
beacon_nodes: dict[str, list[float]] # node_id → [x, y, z]All registries loaded from YAML on initialization.
Services registered during integration setup (async_setup):
async def async_setup(hass, config):
# Register all services
hass.services.async_register(
DOMAIN,
"get_rooms_by_floor",
handle_get_rooms_by_floor,
...
)
# ... more servicesasync def handle_service_call(hass, service_call):
"""Handle service call."""
# 1. Extract data
data = service_call.data
# 2. Validate
if not data.get("entity_id"):
raise ServiceValidationError("entity_id required")
# 3. Execute business logic
result = manager.get_static_entity(data["entity_id"])
# 4. Return response
return {
"entity_id": data["entity_id"],
"coordinates": result
}get_rooms_by_floorget_roomget_static_entitiesget_entity_coordinatesget_beacon_nodes
Characteristics:
- No state changes
- Fast execution
- Queryable any time
add_static_entityupdate_static_entitydelete_static_entityadd_beacon_nodeupdate_beacon_nodedelete_beacon_node
Characteristics:
- Modify registries
- May save to YAML (planned)
- Require validation
get_moving_entity_coordinatesget_all_moving_entity_coordinates
Characteristics:
- Delegate to active provider
- Dynamic calculation
- Real-time data
class LocationProvider:
"""Base class for location providers."""
async def init(self, hass, config) -> bool:
"""Initialize provider. Return True if ready."""
async def get_moving_entity_coordinates(
self, entity_id: str
) -> Optional[tuple[float, float, float]]:
"""Get [X, Y, Z] for entity or None."""
async def get_all_moving_entities(
self
) -> dict[str, tuple[float, float, float]]:
"""Get all tracked entities."""
async def shutdown(self) -> None:
"""Cleanup on disable."""class BermudaProvider(LocationProvider):
"""BLE trilateration via Bermuda integration."""
def __init__(self, hass, manager, config):
self.hass = hass
self.manager = manager # Access beacon nodes
self.config = config
async def init(self):
# Check if Bermuda available
# Setup listeners
# Verify beacon nodes >= 3
return True
async def get_moving_entity_coordinates(self, entity_id):
# Get distance_to_* sensors from Bermuda
# Extract distances from state
# Perform trilateration
# Return [x, y, z]Integration Startup
↓
Read provider config (bermuda.enabled: true)
↓
Instantiate active providers
↓
Call provider.init() for each
↓
Providers ready → services available
↓
Service calls delegate to providers
↓
Integration Shutdown
↓
Call provider.shutdown() for cleanup
# Load enabled providers from configuration
ACTIVE_PROVIDERS = {}
providers_config = config.get("providers", {})
bermuda_config = providers_config.get("bermuda", {})
if bermuda_config.get("enabled", True):
provider = BermudaProvider(hass, manager, config)
if await provider.init(hass):
ACTIVE_PROVIDERS["bermuda"] = providerGiven:
- N beacon nodes at positions:
$P_i = (x_i, y_i, z_i)$ - Measured distances:
$d_i$
Find: Position
def trilaterate(beacon_positions, distances):
"""
Calculate position from beacon positions and distances.
Args:
beacon_positions: list of [x, y, z] coordinates
distances: list of distances to each beacon
Returns:
[x, y, z] position or None if failed
"""
# Setup least-squares problem
# Use Levenberg-Marquardt algorithm
# Iterate until convergence
# Return best fit position- Maximum iterations: 100
- Tolerance: 1e-6 meters
- Typical convergence: 5-10 iterations
| Factor | Impact | Improvement |
|---|---|---|
| Number of beacons | >3 minimum, 4+ better | Add more beacons |
| Beacon geometry | Linear (bad) → triangular (good) | Optimize placement |
| Distance accuracy | RSSI variance | Stable environment |
| Z-coordinate | Limited by geometry | Use with caution |
User installs integration
↓
Settings → Devices & Services → Create Integration
↓
Config Flow Step 1: Provider Selection
Checkbox: "Enable Bermuda Location Provider"
[Submit]
↓
Config saved to Home Assistant
User creates floorplan/floorplan.yaml
↓
Integration loads on startup
↓
Floorplan Manager parses YAML
↓
Registries populated
↓
Services available
- YAML
floorplan/floorplan.yaml(primary) - Integration config (provider enable/disable)
- Default values (if missing)
async def async_setup(hass, config):
# 1. Create coordinator/manager
manager = FloorplanManager(hass)
# 2. Load configuration
loaded = await manager.async_load_config()
# 3. Initialize providers
providers = await init_providers(hass, manager, config)
# 4. Register services
register_services(hass, manager, providers)
# 5. Store for later access
hass.data[DOMAIN] = {
"manager": manager,
"providers": providers
}
return TrueUser edits configuration.yaml
↓
Settings → Developer Tools → YAML → Reload Floorplan
↓
Integration reloads
↓
New configuration applied
async def async_unload_platform(hass, platform):
# 1. Shutdown providers
for provider in providers.values():
await provider.shutdown()
# 2. Unregister services
# (automatic by Home Assistant)
# 3. Cleanup
del hass.data[DOMAIN]
return True# YAML validation
- Floors section: required, at least 1
- Rooms: optional, required fields: name, floor, boundaries
- Static entities: optional, required fields: coordinates
- Beacon nodes: optional, required fields: coordinates# Service call validation
- entity_id: must exist in Home Assistant (optional check)
- coordinates: must be [float, float, float]
- node_id: must be registered in Bluetooth registry{
"success": false,
"error": "Device ID not registered",
"error_code": "INVALID_DEVICE_ID"
}| Operation | Complexity | Typical Time |
|---|---|---|
| Get room by ID | O(1) | <1ms |
| Get all rooms | O(n) | 1-5ms |
| Get static entity | O(1) | <1ms |
| Get all entities | O(m) | 1-10ms |
| Trilaterate position | O(k²) | 5-50ms |
| Get all moving entities | O(m×k²) | 50-500ms |
- n = number of rooms
- m = number of moving entities
- k = number of beacons (iterations)
Static data:
- Floors: ~100 bytes each
- Rooms: ~500 bytes each (including boundaries)
- Static entities: ~50 bytes each
- Beacon nodes: ~50 bytes each
Total for typical home:
- 4 floors, 15 rooms, 30 entities, 4 beacons
- ~25 KB total
- Tested with: 50 rooms, 100 entities, 20 beacons, 10 moving devices
- Performance: Linear with entity count
- Bottleneck: Trilateration calculation (scales with devices × beacons)
-
Create Provider Class
class ESPresenseProvider(LocationProvider): async def init(self, hass, config): # Initialize async def get_moving_entity_coordinates(self, entity_id): # Calculate position
-
Register Provider
if config.get("enable_espresense"): provider = ESPresenseProvider(...) await provider.init(hass)
-
Add UI Option
- Update
config_flow.pywith new toggle - Add translation
- Update
-
Implement Handler
async def handle_custom_service(hass, call): # Logic here return result
-
Register Service
hass.services.async_register( DOMAIN, "custom_service", handle_custom_service, schema=vol.Schema({...}) )
- Config parsing
- Trilateration calculations
- Registry operations
- Service calls
- Provider initialization
- Data persistence
- Full workflow: setup → config → service calls
- Lovelace card interaction
- No sensitive data stored (positions are relative)
- Local-only by default
- Can be exposed via public integrations
- Services callable by any user with permission
- No built-in access control
- Relies on Home Assistant service call permissions
- Validate all service inputs
- Prevent injection attacks
- Sanitize device IDs
- Hot Reload: Update floorplan without restart
- Multiple Providers: Run Bermuda + ESPresense simultaneously
- Advanced Confidence: Return confidence scores
- Geofencing: Determine which room entity is in
- Web UI: Visual floorplan editor