Skip to main content

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 handlers
  • crates/bf_resident_db/src/operations/it_setup.rs - IT setup database operations
  • crates/bf_smart/src/smartthings_client.rs - SmartThings Enterprise API client
  • crates/bf_smart/src/yale_client.rs - Yale API client

Overview

IT setup configures a property's connection to external platforms:

  1. SmartThings Enterprise - Hub, sensors, and device management
  2. Yale - Smart locks via Yale Access API
  3. Hive - Thermostats and heating control (legacy)
  4. Daikin - Altherma heat pumps via Onecta Cloud API (conditional — only if property has DaikinThermostat devices)
  5. 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

  1. Verify property exists and is in Setup status
  2. Check IT setup doesn't already exist
  3. Verify SmartThings Site ID isn't in use by another property
  4. Generate Yale email via bf_auth credential service
  5. Generate Hive email via bf_auth credential service
  6. Create IT setup record with Pending status
// 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

  1. Verify IT setup is in Pending status
  2. Transition to SmartthingsSetup status

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:

  1. Generate OAuth URL: Backend creates authorization URL
  2. User Authorization: IT Admin logs in via Yale
  3. Callback: Yale redirects with authorization code
  4. Token Exchange: Backend exchanges code for tokens
  5. 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

  1. Check Requirement: If property has no DaikinThermostat devices, skip to device discovery
  2. Generate OAuth URL: Backend creates authorization URL for Daikin consent flow
  3. User Authorization: IT Admin logs in via Daikin Onecta portal
  4. Callback: Daikin redirects with authorization code
  5. Token Exchange: Backend exchanges code for tokens
  6. 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_id is 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).

  1. Resolve MAC — read the short-lived KV cache (AI_FLOOR_PLAN_CACHE, key iparcelbox:provisioned:v1:serial:{serial}, 1-day TTL). On a hit, reuse the cached DeviceID; on a miss, POST /provision with the master-account headers and {"SerialNo": serial}, require HTTP 200, capture the DeviceID (MAC), and cache it. Idempotent — a re-provision returns Client is already owner of {DeviceID}, treated as normal success. (Provisioning also starts the 90-day Premium trial that enables the cloud API.)
  2. DB uniqueness — reject if this MAC is already attached to a different device/property. Re-running for the same device is fine.
  3. Persist creds — upsert external_device_id: parcelbox_serial, parcelbox_device_id (MAC), parcelbox_device_password_encrypted, reusing any existing parcelbox_webhook_token so a retry never invalidates a registered webhook.
  4. Confirm onlinegetStatus with a single 10s wake retry (boxes sleep/just-rebooted). If still connected: false, return a resumable "waiting for box online" state so the installer can finish wifi (third-party Espressif BLE app) and retry.
  5. Already secured? — if getStatus.webhook == expected_url and data.localAPI_enabled == false, skip both commands and mark secured. This is what stops a retry re-calling setWebhook once the box already reports our URL.
  6. Register webhook (only if it differs) — setWebhook to {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.
  7. Disable local API — if localAPI_enabled != false, wait 10s if the box is asleep, call localapidisable, then re-read status and require localAPI_enabled == false before securing. We must not leave the on-LAN local API open.
  8. 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

  1. Exchange SmartThings API key for JWT token
  2. Fetch devices from SmartThings API
  3. Fetch devices from Yale API (if Yale configured)
  4. Match internal devices by name and type
  5. Store external IDs in external_device_ids table
  6. 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

  1. Confirm password reset cutover for Yale, Hive if configured, and Aidoo if connected
  2. Move SmartThings devices to room locations
  3. Set up SmartThings Enterprise webhook subscriptions
  4. Register Yale webhooks for lock events
  5. Transition property to Commissioned status

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

EndpointMethodDescription
/it-admin/setupsGETList all IT setups
/it-admin/properties/{id}/setupGETGet IT setup by property
/it-admin/properties/{id}/setupDELETEDelete IT setup
/it-admin/properties/{id}/setup/initializePOSTInitialize IT setup
/it-admin/properties/{id}/setup/confirm-smartthingsPOSTConfirm SmartThings
/it-admin/properties/{id}/yale/oauth-urlPOSTGenerate Yale OAuth URL
/it-admin/properties/{id}/setup/provision-parcelboxPOSTProvision an iParcelBox device (conditional)
/it-admin/properties/{id}/setup/complete-cloud-transferPOSTComplete setup
/it-admin/st-pro/api/{path}ANYProxy to SmartThings Pro
/properties/{id}/discover-devicesPOSTDiscover and match devices
/properties/{id}/errorsGETGet installer errors

See Also