IT Setup & Commissioning Flow
This document covers the IT Admin commissioning process for connecting properties to external smart home platforms.
Key Files
crates/bf_resident/src/handlers/it_setup.rs- IT setup handlerscrates/bf_resident_db/src/operations/it_setup.rs- IT setup database operationscrates/bf_smart/src/smartthings_client.rs- SmartThings Enterprise API clientcrates/bf_smart/src/yale_client.rs- Yale API client
Overview
IT setup configures a property's connection to external platforms:
- SmartThings Enterprise - Hub, sensors, and device management
- Yale - Smart locks via Yale Access API
- Hive - Thermostats and heating control (legacy)
- Daikin - Altherma heat pumps via Onecta Cloud API (conditional — only if property has DaikinThermostat devices)
- iParcelBox - Smart parcel delivery boxes via the iParcelBox Cloud API (conditional — only if property has ParcelBox devices; no OAuth, provisioned per-box)
┌─────────────────────────────────────────────────────────────────────┐
│ IT Setup Status Flow │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Property (Setup) ──────────────────────────────────────────────────│
│ │ │
│ ▼ │
│ ┌─────────────┐ │
│ │ Pending │ IT Admin creates setup record │
│ │ │ • SmartThings Site ID + API Key │
│ │ │ • Yale email generated │
│ │ │ • Hive email generated │
│ └──────┬──────┘ │
│ │ confirm-smartthings │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ SmartthingsSetup │ SmartThings Pro group confirmed │
│ │ │ • Devices appear in location │
│ └──────────┬──────────┘ │
│ │ yale-oauth-complete │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ YaleValidated │ Yale OAuth completed │
│ │ │ • Tokens stored in bf_auth │
│ └──────────┬──────────┘ │
│ │ hive-oauth-complete │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ HiveValidated │ Hive OAuth completed │
│ │ │ • Tokens stored in bf_auth │
│ └──────────┬──────────┘ │
│ │ daikin-oauth-complete (conditional) │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ DaikinValidated │ Daikin OAuth completed (if applicable) │
│ │ │ • Tokens stored in bf_auth │
│ │ │ • Skipped if no DaikinThermostat devices │
│ └──────────┬──────────┘ │
│ │ discover-devices │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ Commissioning │ Devices discovered and matched │
│ │ │ • External IDs mapped │
│ │ │ • Devices moved to locations │
│ └──────────┬──────────┘ │
│ │ complete-cloud-transfer │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ Commissioned │ Ready for tenancy │
│ │ │ • Webhooks registered │
│ │ │ • Device events flowing │
│ └─────────────────────┘ │
│ │ │
│ Property (Commissioning → Commissioned) │
│ │
└─────────────────────────────────────────────────────────────────────┘
ItSetupStatus Enum
pub enum ItSetupStatus {
Pending, // Initial state after creation
SmartthingsSetup, // SmartThings Pro confirmed
YaleValidated, // Yale OAuth complete
HiveValidated, // Hive OAuth complete
DaikinValidated, // Daikin OAuth complete (conditional)
Commissioning, // Device discovery in progress
Commissioned, // All setup complete
}
Step 1: Initialize IT Setup
Creates the IT setup record with SmartThings credentials and generates service emails.
Endpoint
POST /it-admin/properties/{property_id}/setup/initialize
Request
pub struct InitializeRequest {
/// SmartThings Site ID for this property
pub smartthings_site_id: String,
/// SmartThings API Key (will be encrypted)
pub smartthings_api_key: String,
}
Process
- Verify property exists and is in
Setupstatus - Check IT setup doesn't already exist
- Verify SmartThings Site ID isn't in use by another property
- Generate Yale email via bf_auth credential service
- Generate Hive email via bf_auth credential service
- Create IT setup record with
Pendingstatus
// crates/bf_resident/src/handlers/it_setup.rs:311
let yale_creds = bf_auth_client
.generate_credentials(GenerateCredentialsRequest {
service: "yale".to_string(),
unique_id: property_id.clone(),
environment: environment.clone(),
})
.await?;
let hive_creds = bf_auth_client
.generate_credentials(GenerateCredentialsRequest {
service: "hive".to_string(),
unique_id: property_id.clone(),
environment: environment.clone(),
})
.await?;
Step 2: SmartThings Pro Setup
After the IT Admin configures SmartThings Pro (via the ST Pro portal or API):
Endpoint
POST /it-admin/properties/{property_id}/setup/confirm-smartthings
Process
- Verify IT setup is in
Pendingstatus - Transition to
SmartthingsSetupstatus
The SmartThings Pro API proxy allows direct interaction:
GET /it-admin/st-pro/api/{path}
POST /it-admin/st-pro/api/{path}
Step 3: Yale OAuth Flow
Yale uses OAuth 2.0 for authorization. The flow:
- Generate OAuth URL: Backend creates authorization URL
- User Authorization: IT Admin logs in via Yale
- Callback: Yale redirects with authorization code
- Token Exchange: Backend exchanges code for tokens
- Token Storage: Tokens encrypted and stored in bf_auth
OAuth URL Generation
POST /it-admin/properties/{property_id}/yale/oauth-url
Response
pub struct GenerateYaleOAuthUrlResponse {
/// The OAuth authorization URL to redirect to
pub url: String,
}
Step 4: Hive OAuth Flow
Similar to Yale, Hive uses OAuth for thermostat access.
Step 4b: Daikin OAuth Flow (Conditional)
Daikin OAuth is only required when the property has DaikinThermostat devices configured. If no DaikinThermostat devices exist, this step is skipped entirely.
Token Endpoint
https://idp.onecta.daikineurope.com/v1/oidc/token
Scopes
openid onecta:basic.integration
Process
- Check Requirement: If property has no DaikinThermostat devices, skip to device discovery
- Generate OAuth URL: Backend creates authorization URL for Daikin consent flow
- User Authorization: IT Admin logs in via Daikin Onecta portal
- Callback: Daikin redirects with authorization code
- Token Exchange: Backend exchanges code for tokens
- Token Storage: Tokens encrypted and stored in bf_auth DO with
Service::Daikin
Step 4c: Multi-Lock Validation
During commissioning, the system validates that a SmartLock device named "Front Door Lock" exists. This lock is used as the primary lock for:
- Security status (door locked/closed state)
- Default PIN code targeting (when no
lock_idis specified) - Notification context (e.g., "Front Door Lock Locked")
Additional locks (e.g., "Parcel Lock") are supported but don't require model validation.
Step 4d: iParcelBox Provisioning (Conditional)
Required only when the property has a ParcelBox device. Unlike Yale/Daikin there is no OAuth and no separate state transition. A parcel box is not SmartThings-discovered — device discovery marks ParcelBox SmartThings-not-applicable (so it never double-claims an ST device and never blocks commissioning) and reports Requires parcel box QR/serial setup / Resolved by installer-submitted parcel box setup from the stored creds. The box is resolved by installer-submitted serial/QR, because only the installer sees the serial on the delivered box. The endpoint is allowed in HiveValidated, Commissioning, and Commissioned, and is re-runnable and phase-based (the box reboots mid-flow and is often offline at first wifi connect).
Endpoint
POST /it-admin/properties/{property_id}/setup/provision-parcelbox
Request
pub struct ProvisionParcelBoxRequest {
/// Box serial (from the Setup-Information QR `name`, or the vendor CSV)
pub serial: String,
/// Per-device password (QR `pop` field, or the vendor CSV). Required —
/// every cloud command needs `x-device-password`.
pub device_password: String,
/// Optional raw QR payload, for server-side audit only. Never logged.
pub qr_payload: Option<String>,
}
The setup UI parses the QR client-side via the shared parseIparcelboxSetupQr helper (@platform/ui): it validates ver/username/transport, strips the exact iParcelBox- prefix from name to get the serial, and reads pop as the device password. The QR photo is decoded locally (native BarcodeDetector, falling back to paste) — never uploaded, since pop is a credential.
Process (phase-based, re-runnable)
provision_parcelbox mirrors validate_daikin's state-guard + audit-log + DO-sync shape and the Yale webhook-registration outbound pattern. Every iParcelBox envelope is parsed in full (top-level result plus data); HTTP 2xx with result: false is an error, and a missing data.boxStatus is an error (never defaulted to Locked).
- Resolve MAC — read the short-lived KV cache (
AI_FLOOR_PLAN_CACHE, keyiparcelbox:provisioned:v1:serial:{serial}, 1-day TTL). On a hit, reuse the cachedDeviceID; on a miss,POST /provisionwith the master-account headers and{"SerialNo": serial}, require HTTP 200, capture theDeviceID(MAC), and cache it. Idempotent — a re-provision returnsClient is already owner of {DeviceID}, treated as normal success. (Provisioning also starts the 90-day Premium trial that enables the cloud API.) - DB uniqueness — reject if this MAC is already attached to a different device/property. Re-running for the same device is fine.
- Persist creds — upsert
external_device_id:parcelbox_serial,parcelbox_device_id(MAC),parcelbox_device_password_encrypted, reusing any existingparcelbox_webhook_tokenso a retry never invalidates a registered webhook. - Confirm online —
getStatuswith a single 10s wake retry (boxes sleep/just-rebooted). If stillconnected: false, return a resumable "waiting for box online" state so the installer can finish wifi (third-party Espressif BLE app) and retry. - Already secured? — if
getStatus.webhook == expected_urlanddata.localAPI_enabled == false, skip both commands and mark secured. This is what stops a retry re-callingsetWebhookonce the box already reports our URL. - Register webhook (only if it differs) —
setWebhookto{bf_notify_base}/webhooks/iparcelbox/{token}, then wait 60s for the box to reboot and re-read status. Still offline → resumable "webhook registered, waiting for reboot" state. - Disable local API — if
localAPI_enabled != false, wait 10s if the box isasleep, calllocalapidisable, then re-read status and requirelocalAPI_enabled == falsebefore securing. We must not leave the on-LAN local API open. - Set
it_setup.parcelbox_connected = true(the secured/usable marker) only after the box confirms the webhook + disabled local API, write an audit log, and (if commissioned) sync the device password + MAC to the User DO.
Each "resumable" return is a 200 with parcelbox_connected: false, so the installer simply retries to finish a partial setup. See optional-credentials.md for the credential model and webhook-handling.md for the unsigned, URL-token webhook (which emits only a StateUpdated snapshot — semantic events are derived from the state transition in bf_user).
Step 5: Device Discovery
Maps internal devices to external platform devices.
Endpoint
POST /properties/{property_id}/discover-devices
Process
- Exchange SmartThings API key for JWT token
- Fetch devices from SmartThings API
- Fetch devices from Yale API (if Yale configured)
- Match internal devices by name and type
- Store external IDs in
external_device_idstable - Create installer errors for unmatched devices
Matching Algorithm
// Device types that don't need external IDs
fn is_not_applicable_device_type_for_st(device_type: DeviceType) -> bool {
matches!(
device_type,
DeviceType::Router | DeviceType::Unknown | ...
)
}
// SmartThings matching by name similarity and capabilities
fn find_smartthings_match(
device_name: &str,
device_type: DeviceType,
st_devices: &[SmartThingsDevice],
internal_devices: &[Device],
) -> MatchResult
// Yale matching by device name and type
fn find_yale_match(
device_name: &str,
device_type: DeviceType,
yale_devices: &[YaleDevice],
internal_devices: &[Device],
) -> YaleMatchResult
Response
pub struct DiscoverDevicesResponse {
pub property_id: String,
pub total_devices: usize,
pub matched_count: usize,
pub not_applicable_count: usize,
pub unmatched_count: usize,
pub devices: Vec<DiscoveredDeviceMatch>,
pub smartthings_devices_found: usize,
pub yale_devices_found: usize,
pub unmatched_smartthings_devices: Vec<UnmatchedSmartThingsDevice>,
pub unmatched_yale_devices: Vec<UnmatchedYaleDevice>,
}
Step 6: Complete Cloud Transfer
Finalizes setup by moving devices to room locations and setting up webhooks.
Endpoint
POST /it-admin/properties/{property_id}/setup/complete-cloud-transfer
Request
pub struct CompleteCloudTransferRequest {
/// Confirmation that Yale admin password has been reset
pub yale_password_reset_confirmed: bool,
/// Confirmation that Hive admin password has been reset (only required if Hive is configured)
#[serde(default)]
pub hive_password_reset_confirmed: bool,
/// Confirmation that Aidoo admin password has been reset (only required if Aidoo is connected)
#[serde(default)]
pub aidoo_password_reset_confirmed: bool,
}
Process
- Confirm password reset cutover for Yale, Hive if configured, and Aidoo if connected
- Move SmartThings devices to room locations
- Set up SmartThings Enterprise webhook subscriptions
- Register Yale webhooks for lock events
- Transition property to
Commissionedstatus
Device Location Assignment
Devices are moved to SmartThings Locations matching their room names:
// crates/bf_resident/src/handlers/it_setup.rs:785
async fn move_devices_to_target_locations(
jwt_token: &str,
conn: &mut D1Connection,
property_id: &str,
) -> Result<DeviceMoveResult> {
// Build location name -> ID map
let locations = fetch_all_enterprise_locations(jwt_token).await?;
let location_map: HashMap<String, String> = locations
.iter()
.map(|loc| (loc.name.to_lowercase(), loc.location_id.clone()))
.collect();
// Move each device to its target location
for device in &devices {
let (room_name, floor_name) = room_info.get(&device.room_id)?;
let target_location_id = location_map
.get(&room_name.to_lowercase())
.or_else(|| location_map.get(&floor_name.to_lowercase()));
move_device_to_location(jwt_token, smartthings_id, target_location_id).await?;
}
}
SmartThings Enterprise Webhooks
Two subscriptions are created:
// PushDeviceEvents - capability-based filtering.
// Capability list is defined in REQUIRED_DEVICE_EVENT_CAPABILITIES
// (crates/bf_resident/src/handlers/it_setup/handlers.rs).
let body = json!({
"name": "PushDeviceEvents",
"sinkId": sink_id,
"scope": "ACCOUNT",
"filters": [{
"type": "CAPABILITY",
"capabilityFilter": {
"capabilities": [
{"capability": "contactSensor", "attribute": "contact"},
{"capability": "lock", "attribute": "lock"},
{"capability": "button", "attribute": "button"},
{"capability": "battery", "attribute": "battery"},
{"capability": "temperatureMeasurement", "attribute": "temperature"},
{"capability": "motionSensor", "attribute": "motion"},
{"capability": "waterSensor", "attribute": "water"},
{"capability": "smokeDetector", "attribute": "smoke"},
// Yale AlarmHub:
{"capability": "securitySystem", "attribute": "securitySystemStatus"},
{"capability": "alarm", "attribute": "alarm"},
{"capability": "switch", "attribute": "switch"},
]
}
}]
});
// OpsEvents - lifecycle events
let body = json!({
"name": "OpsEvents",
"sinkId": sink_id,
"scope": "ACCOUNT",
"filters": [{
"type": "EVENT",
"eventFilter": {
"eventTypes": [
"DEVICE_HEALTH_EVENT",
"HUB_HEALTH_EVENT",
"DEVICE_LIFECYCLE_EVENT",
"HUB_LIFECYCLE_EVENT"
]
}
}]
});
ensure_enterprise_subscriptions is self-healing: when PushDeviceEvents
already exists for the sink, it fetches the subscription's current capability
filter and compares it against REQUIRED_DEVICE_EVENT_CAPABILITIES. If any
required (capability, attribute) pair is missing, the old subscription is
deleted and a fresh one is created. This means the standard
complete-cloud-transfer flow will repair stale subscriptions automatically
on re-run. To trigger the refresh on an already-Commissioned property
without re-running the full cloud transfer, call:
POST /it-admin/properties/{property_id}/setup/refresh-smartthings-subscriptions
The endpoint is idempotent — when subscriptions are already current it logs "PushDeviceEvents subscription is current" and makes no destructive calls.
Yale Webhook Registration
// crates/bf_resident/src/handlers/it_setup.rs:1488
async fn register_yale_webhooks(
env: &Env,
conn: &mut D1Connection,
property_id: &str,
yale_email: &str,
) -> Result<()> {
let yale_client = YaleClient::new(yale_api_key, yale_token);
let registration = YaleClient::build_lock_webhook_registration(
&webhook_url,
&webhook_header,
&webhook_token,
&client_id,
);
for ext_id in yale_devices {
let url = yale_client.url_register_lock_webhook(&yale_id);
// Register webhook via Yale API
}
}
Installer Errors
Device discovery creates errors for issues that need resolution:
pub struct InstallerError {
pub id: String,
pub device_id: Option<String>,
pub error_type: InstallerErrorType,
pub error_message: String,
pub metadata: Option<serde_json::Value>,
pub resolved: bool, // false = error, true = warning
pub created_at: i64,
pub resolved_at: Option<i64>,
}
pub enum InstallerErrorType {
UnmatchedDevice, // Device not found in external APIs
CapabilityMismatch, // Name matches but wrong capabilities
UnmatchedExternalDevice, // External device not in internal list
YaleMultipleUsers, // Yale account has multiple users
}
Error Retrieval
GET /properties/{property_id}/errors
External Device ID Mapping
The external_device_ids table stores platform mappings:
CREATE TABLE external_device_ids (
device_id TEXT PRIMARY KEY,
smartthings_id TEXT,
yale_id TEXT,
yale_type TEXT,
hive_id TEXT,
hive_type TEXT,
daikin_id TEXT,
daikin_type TEXT,
-- iParcelBox (resolved at provision time, not device discovery)
parcelbox_serial TEXT, -- reference / provisioning input
parcelbox_device_id TEXT, -- MAC; cloud ?DeviceID= + webhook routing
parcelbox_device_password_encrypted BLOB, -- x-device-password (encrypted)
parcelbox_webhook_token TEXT -- unguessable webhook URL token
);
Special value <not-applicable> marks devices that don't need external IDs (e.g., routers).
IT Admin Endpoints Summary
| Endpoint | Method | Description |
|---|---|---|
/it-admin/setups | GET | List all IT setups |
/it-admin/properties/{id}/setup | GET | Get IT setup by property |
/it-admin/properties/{id}/setup | DELETE | Delete IT setup |
/it-admin/properties/{id}/setup/initialize | POST | Initialize IT setup |
/it-admin/properties/{id}/setup/confirm-smartthings | POST | Confirm SmartThings |
/it-admin/properties/{id}/yale/oauth-url | POST | Generate Yale OAuth URL |
/it-admin/properties/{id}/setup/provision-parcelbox | POST | Provision an iParcelBox device (conditional) |
/it-admin/properties/{id}/setup/complete-cloud-transfer | POST | Complete setup |
/it-admin/st-pro/api/{path} | ANY | Proxy to SmartThings Pro |
/properties/{id}/discover-devices | POST | Discover and match devices |
/properties/{id}/errors | GET | Get installer errors |
See Also
- Credential Management - Email/password generation
- Smart Device Abstraction - SmartClient trait
- Webhook Handling - Event ingress
- Property Lifecycle - Status transitions