Device Resolution - External Device Matching
The device resolution system maps internal Broadford Living devices to their external counterparts on SmartThings, Yale, and Hive platforms. This is a critical component of the commissioning workflow.
Key Files
crates/bf_resident/src/handlers/it_setup.rs- Discovery and matching logiccrates/bf_resident_db/src/operations/external_device_ids.rs- ID mapping storagecrates/bf_smart/src/smartthings/mod.rs- SmartThings API clientcrates/bf_smart/src/yale/mod.rs- Yale API client
Device Discovery Flow
┌─────────────────────────────────────────────────────────────────────────┐
│ discover_devices() │
│ │
│ 1. Fetch SmartThings devices from commissioning location │
│ 2. Fetch Yale devices (locks, doorbells, keypads) │
│ 3. For each internal device: │
│ - Try SmartThings matching │
│ - Try Yale matching (for Yale device types) │
│ - Save external IDs or create error records │
│ │
└─────────────────────────────────────────────────────────────────────────┘
SmartThings Device Matching
SmartThings matching uses a multi-step algorithm with both name-based and type-based matching.
Matching Algorithm
fn find_smartthings_match() -> MatchResult {
// Step 1: Exact name match (case-insensitive)
for device in smartthings_devices {
if st_name.to_lowercase() == internal_name.to_lowercase() {
// Verify BOTH device_model AND capabilities match
if has_expected_device_model(device) && has_capabilities(device) {
return Match(device, ("exact_name", 100));
} else {
return TypeMismatch(device, failure_reason);
}
}
}
// Step 2: Unique type match (only if one device of this type exists)
if count_internal_devices_of_type == 1 {
for device in smartthings_devices {
if has_expected_device_model(device) && has_capabilities(device) {
return Match(device, ("unique_type", 90));
}
}
}
return NoMatch;
}
Match Confidence Levels
| Match Type | Confidence | Description |
|---|---|---|
exact_name | 100% | Name matches exactly (case-insensitive), type verified |
multichannel_1 | 100% | Special case for energy meters with " 1" suffix |
unique_type | 90% | Only device of this type in property |
Device Model Verification
Each internal device type maps to expected SmartThings device models:
fn get_expected_device_models(device_type: DeviceType) -> Vec<&'static str> {
match device_type {
DeviceType::SmartLock => vec!["Lock"],
DeviceType::SmartThingsHub => vec!["V3_HUB"],
DeviceType::ContactSensor => vec!["multi"],
// Other types use capability-only matching
_ => vec![],
}
}
Capability Verification
Devices must have the expected SmartThings capabilities:
fn get_expected_capabilities(device_type: DeviceType) -> Vec<&'static str> {
match device_type {
DeviceType::SmartLock => vec!["lock"],
DeviceType::ContactSensor => vec!["contactSensor"],
DeviceType::MotionSensor => vec!["motionSensor"],
DeviceType::WaterLeakSensor => vec!["waterSensor"],
DeviceType::HomeEnergyMeter => vec!["powerMeter", "energyMeter", "refresh"],
DeviceType::Thermostat => vec!["thermostatMode", "thermostatHeatingSetpoint"],
DeviceType::SmartThingsHub => vec!["bridge"],
DeviceType::VideoDoorbellPro => vec!["webrtc"],
DeviceType::Oven => vec!["ovenMode", "ovenSetpoint"],
DeviceType::Dishwasher => vec!["dishwasherOperatingState"],
DeviceType::WasherDryer => vec!["washerOperatingState"],
DeviceType::FridgeFreezer => vec!["refrigeration"],
DeviceType::Extractor => vec!["fanSpeed"],
DeviceType::InductionHob => vec!["switch"],
_ => vec![],
}
}
Not Applicable Devices
Some device types don't have SmartThings representation:
fn is_not_applicable_device_type_for_st(device_type: DeviceType) -> bool {
matches!(device_type,
DeviceType::DoorbellChime |
DeviceType::AlarmKeypad |
DeviceType::IndoorSiren
)
}
These are marked with smartthings_id = "N/A".
Match Failure Handling
When a name matches but verification fails, the system creates detailed error records:
enum SmartThingsMatchFailure {
CapabilityMismatch {
device_name: String,
expected_capabilities: Vec<String>,
found_capabilities: Vec<String>,
},
DeviceModelMismatch {
device_name: String,
expected_device_models: Vec<String>,
found_device_model: String,
},
TypeMismatch { /* combined failure */ },
}
Yale Device Matching
Yale matching handles locks, doorbells, and keypads with a similar algorithm.
Matching Algorithm
fn find_yale_match() -> YaleMatchResult {
// Only match Yale device types
if !is_yale_device_type(internal_type) {
return NotApplicable;
}
let expected_yale_type = match internal_type {
SmartLock => "lock",
SmartKeypad => "keypad",
VideoDoorbellPro => "doorbell",
};
// Step 1: Exact name match (case-insensitive)
for device in yale_devices.filter(type == expected_yale_type) {
if yale_name.to_lowercase() == internal_name.to_lowercase() {
return Match { match_method: "exact_name" };
}
}
// Step 2: Partial name match
for device in yale_devices.filter(type == expected_yale_type) {
// Yale name contains internal name
if yale_name.contains(internal_name) {
return Match { match_method: "partial_name_yale_contains" };
}
// Internal name contains Yale name
if internal_name.contains(yale_name) {
return Match { match_method: "partial_name_internal_contains" };
}
}
// Step 3: Unique device match
if matching_type_devices.len() == 1 {
return Match { match_method: "unique_yale_device" };
}
// Step 4: Unique internal device match
if count_internal_devices_of_type == 1 {
return Match { match_method: "unique_internal_device" };
}
return NoMatch;
}
Yale Match Methods
| Match Method | Description |
|---|---|
exact_name | Names match exactly (case-insensitive) |
partial_name_yale_contains | Yale name contains internal device name |
partial_name_internal_contains | Internal name contains Yale device name |
unique_yale_device | Only one Yale device of this type |
unique_internal_device | Only one internal device needing Yale match |
Smart Lock Requirements
Smart locks require both SmartThings and Yale IDs:
// Smart locks must have a Yale ID
if device_type == DeviceType::SmartLock && yale_id.is_none() {
create_installer_error(
"Smart lock matched SmartThings but has no Yale ID.
Yale integration is required for smart locks."
);
return unmatched;
}
This is because:
- SmartThings provides lock status events
- Yale provides PIN code management and remote lock/unlock
External Device ID Storage
Database Schema
CREATE TABLE external_device_id (
id TEXT PRIMARY KEY,
device_id TEXT NOT NULL,
smartthings_id TEXT,
yale_id TEXT,
yale_type TEXT, -- "lock", "doorbell", "keypad"
hive_id TEXT,
hive_type TEXT,
created_at INTEGER,
updated_at INTEGER
);
Resolution Functions
// Resolve from any platform
pub async fn resolve_device_id(
conn: &mut D1Connection,
platform: ExternalPlatform,
external_id: &str,
) -> Option<String> {
match platform {
SmartThings => get_device_id_by_smartthings_id(conn, external_id),
Yale => get_device_id_by_yale_id(conn, external_id),
Hive => get_device_id_by_hive_id(conn, external_id),
}
}
Multichannel Energy Meter Handling
Home energy meters often appear in SmartThings as multiple channels:
Multichannel 1- Main channel (used)Multichannel 2- Secondary channel (ignored)Multichannel 3- Tertiary channel (ignored)
The matching logic specifically handles this:
// For multichannel meters, match against name with " 1" appended
let multichannel_match_name = if is_multichannel_meter {
format!("{} 1", internal_name_lower)
} else {
String::new()
};
// Track matched prefixes to suppress warnings for other channels
if match_confidence.0.starts_with("multichannel_1") {
matched_multichannel_prefixes.insert(prefix);
}
Unmatched Device Handling
Unmatched Internal Devices
Internal devices that can't be matched to external devices create installer errors:
InstallerError {
error_type: InstallerErrorType::UnmatchedDevice,
error_message: "No matching device found...",
device_id: Some(internal_device.id),
resolved: false, // Blocking error
}
Unmatched External Devices
External devices not matched to any internal device create warnings:
InstallerError {
error_type: InstallerErrorType::UnmatchedExternalDevice,
error_message: "SmartThings device exists but is not matched...",
device_id: None, // Not associated with internal device
resolved: true, // Warning, not blocking
}
Mock Device System
In mock mode (#[cfg(feature = "mock")]), device discovery:
- Creates mock devices in
bf_smart - Uses internal device ID as mock SmartThings ID
- Triggers mock commissioning
- Marks all devices as matched
#[cfg(feature = "mock")]
async fn mock_discover_devices() {
for device in internal_devices {
// Use device.id as mock SmartThings ID
upsert_external_device_id(CreateExternalDeviceIdRequest {
device_id: device.id.clone(),
smartthings_id: Some(device.id.clone()), // Self-reference
..
});
}
}
Capability Execution
Once devices are resolved, capabilities are executed through the appropriate platform:
pub async fn execute_capability(
device_id: &str,
capability: DeviceCapability,
) -> Result<DeviceState> {
// 1. Get external IDs for device
let external_ids = get_external_device_id_by_device(conn, device_id)?;
// 2. Categorize capability to determine platform
let platform = match capability {
Lock | Unlock | CreatePinCode | DeletePinCode => Platform::Yale,
SetAlarmHubMode | SilenceAlarm => Platform::Yale,
Refresh | GetContactStatus | GetMotionStatus => Platform::SmartThings,
SetHeatingSetpoint | SetCoolingSetpoint => Platform::Hive,
_ => Platform::SmartThings, // Default
};
// 3. Execute on appropriate platform
match platform {
Platform::Yale => {
let yale_id = external_ids.yale_id
.ok_or("No Yale ID for this device")?;
yale_client.execute(yale_id, capability).await
}
Platform::SmartThings => {
let st_id = external_ids.smartthings_id
.ok_or("No SmartThings ID for this device")?;
smartthings_client.execute(st_id, capability).await
}
Platform::Hive => {
let hive_id = external_ids.hive_id
.ok_or("No Hive ID for this device")?;
hive_client.execute(hive_id, capability).await
}
}
}
See Also
- smart-device-abstraction.md - bf_smart architecture and capability system
- webhook-handling.md - External event processing
- it-setup-flow.md - Commissioning workflow