feat: expand companion platform, device controls, and memoir workflows
ci / verify (push) Canceled after 0s

Add customer and operator login, BLE provisioning, device fleet controls, call memory, memoir generation, and refreshed family and maintenance portals. Include API contracts, migrations, firmware sources, tests, assets, and deployment documentation.
This commit is contained in:
Codex Local
2026-10-08 10:06:44 +08:00
parent 98fb2965d0
commit bf1233d5df
861 changed files with 94614 additions and 3209 deletions
+108 -126
View File
@@ -9,6 +9,7 @@ import (
"encoding/json"
"errors"
"fmt"
"net/url"
"strings"
"time"
@@ -17,6 +18,7 @@ import (
"data-terminal-cloud/internal/deviceauth"
"data-terminal-cloud/internal/devicebinding"
"data-terminal-cloud/internal/identity"
"data-terminal-cloud/internal/onboarding"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
@@ -24,24 +26,35 @@ import (
)
var (
ErrAccessDenied = errors.New("operator access denied")
ErrInvalid = errors.New("invalid device management input")
ErrNotFound = errors.New("device not found")
ErrVersionConflict = errors.New("device version conflict")
ErrClaimUnavailable = errors.New("device claim is unavailable")
ErrActivationConflict = errors.New("device activation transition conflict")
ErrActivationNotReady = errors.New("device activation prerequisites are not ready")
ErrAccessDenied = errors.New("operator access denied")
ErrInvalid = errors.New("invalid device management input")
ErrNotFound = errors.New("device not found")
ErrVersionConflict = errors.New("device version conflict")
ErrClaimUnavailable = errors.New("device claim is unavailable")
ErrActivationConflict = errors.New("device activation transition conflict")
ErrActivationNotReady = errors.New("device activation prerequisites are not ready")
ErrProvisioningUnavailable = errors.New("device provisioning is unavailable")
)
type Service struct {
pool *pgxpool.Pool
credential *deviceauth.DatabaseSecretResolver
audit *audit.Repository
pool *pgxpool.Pool
credential *deviceauth.DatabaseSecretResolver
audit *audit.Repository
onboardingMasterKey string
deviceOrigin string
}
type Options struct {
OnboardingMasterKey string
DeviceOrigin string
}
type Device struct {
ID string `json:"id"`
TenantID string `json:"tenant_id"`
FactoryID string `json:"factory_id"`
EnrollmentMode string `json:"enrollment_mode"`
Bound bool `json:"bound"`
Model string `json:"model"`
HardwareRevision *string `json:"hardware_revision,omitempty"`
Status string `json:"status"`
@@ -76,19 +89,7 @@ type AssignmentOption struct {
SubjectName string `json:"subject_name"`
}
type CreateInput struct {
Model string
HardwareRevision string
MinimumSecurityVersion string
MinimumFirmwareVersion string
HouseholdID string
SubjectID string
VoiceRuntime string
HardwareAgentBotID string
HardwareAgentTransport string
HardwareAgentMinimumSDKVersion string
InitialActivationState string
}
type CreateInput struct{ Model string }
type ActivationTransitionInput struct {
TargetState string
@@ -124,16 +125,16 @@ type UpdateInput struct {
}
type CredentialIssue struct {
DeviceID string `json:"device_id"`
Secret string `json:"secret"`
KeyVersion int64 `json:"key_version"`
IssuedAt time.Time `json:"issued_at"`
FactoryID string `json:"factory_id"`
Model string `json:"model"`
Origin string `json:"origin"`
DeviceKeyHex string `json:"device_key_hex"`
KeyVersion int64 `json:"key_version"`
}
type CreateResult struct {
Device Device `json:"device"`
Credential CredentialIssue `json:"credential"`
Claim *DeviceClaimIssue `json:"claim,omitempty"`
Device Device `json:"device"`
Credential CredentialIssue `json:"credential"`
}
type DeviceClaimIssue struct {
@@ -151,11 +152,26 @@ type queryRow interface {
QueryRow(context.Context, string, ...any) pgx.Row
}
func NewService(pool *pgxpool.Pool, credential *deviceauth.DatabaseSecretResolver, auditRepository *audit.Repository) (*Service, error) {
func NewService(pool *pgxpool.Pool, credential *deviceauth.DatabaseSecretResolver, auditRepository *audit.Repository, option ...Options) (*Service, error) {
if pool == nil || credential == nil || auditRepository == nil {
return nil, errors.New("operations device service dependencies are required")
}
return &Service{pool: pool, credential: credential, audit: auditRepository}, nil
if len(option) > 1 {
return nil, errors.New("only one operations device service option is allowed")
}
var configured Options
if len(option) == 1 {
configured = option[0]
}
origin := strings.TrimSpace(configured.DeviceOrigin)
if origin != "" {
parsed, err := url.Parse(origin)
if err != nil || parsed.Host == "" || (parsed.Scheme != "https" && parsed.Scheme != "http") {
return nil, errors.New("device origin must be an absolute HTTP URL")
}
origin = parsed.Scheme + "://" + parsed.Host
}
return &Service{pool: pool, credential: credential, audit: auditRepository, onboardingMasterKey: strings.TrimSpace(configured.OnboardingMasterKey), deviceOrigin: origin}, nil
}
func (s *Service) List(ctx context.Context, principal identity.Principal, tenantPublicID string) ([]Device, error) {
@@ -244,51 +260,32 @@ func (s *Service) ListActivationTransitions(ctx context.Context, principal ident
func (s *Service) Create(ctx context.Context, principal identity.Principal, tenantPublicID, requestID string, input CreateInput) (CreateResult, error) {
input.Model = strings.TrimSpace(input.Model)
input.HardwareRevision = strings.TrimSpace(input.HardwareRevision)
input.MinimumSecurityVersion = strings.TrimSpace(input.MinimumSecurityVersion)
input.MinimumFirmwareVersion = strings.TrimSpace(input.MinimumFirmwareVersion)
input.VoiceRuntime = strings.TrimSpace(input.VoiceRuntime)
input.HardwareAgentBotID = strings.TrimSpace(input.HardwareAgentBotID)
input.HardwareAgentTransport = strings.TrimSpace(input.HardwareAgentTransport)
input.HardwareAgentMinimumSDKVersion = strings.TrimSpace(input.HardwareAgentMinimumSDKVersion)
input.InitialActivationState = strings.ToUpper(strings.TrimSpace(input.InitialActivationState))
if input.VoiceRuntime == "" {
input.VoiceRuntime = "legacy_voicechat"
}
if input.HardwareAgentTransport == "" {
input.HardwareAgentTransport = "high_quality"
}
activationState := deviceactivation.Claimable
if input.HouseholdID != "" {
activationState = deviceactivation.Bound
}
if input.InitialActivationState != "" {
parsed, err := deviceactivation.Parse(input.InitialActivationState)
if err != nil || (parsed != deviceactivation.Factory && parsed != activationState) {
return CreateResult{}, ErrInvalid
}
activationState = parsed
}
if activationState == deviceactivation.Factory && input.HouseholdID != "" {
if input.Model == "" || len([]rune(input.Model)) > 120 {
return CreateResult{}, ErrInvalid
}
if input.Model == "" || len([]rune(input.Model)) > 120 || len([]rune(input.HardwareRevision)) > 80 ||
len([]rune(input.MinimumSecurityVersion)) > 80 || len([]rune(input.MinimumFirmwareVersion)) > 80 ||
((input.HouseholdID == "") != (input.SubjectID == "")) || !validVoiceRuntime(input.VoiceRuntime) {
return CreateResult{}, ErrInvalid
if s.onboardingMasterKey == "" || s.deviceOrigin == "" {
return CreateResult{}, ErrProvisioningUnavailable
}
secretBytes := make([]byte, 32)
serialBytes := make([]byte, 32)
if _, err := rand.Read(secretBytes); err != nil {
return CreateResult{}, fmt.Errorf("generate device secret: %w", err)
}
if _, err := rand.Read(serialBytes); err != nil {
return CreateResult{}, fmt.Errorf("generate device serial material: %w", err)
factoryBytes := make([]byte, 6)
if _, err := rand.Read(factoryBytes); err != nil {
return CreateResult{}, fmt.Errorf("generate factory device ID: %w", err)
}
secret := hex.EncodeToString(secretBytes)
serialDigest := sha256.Sum256(serialBytes)
factoryMAC := strings.ToUpper(hex.EncodeToString(factoryBytes))
factoryID := "KP01-" + factoryMAC
serialDigest := sha256.Sum256([]byte(factoryID))
deviceID := uuid.NewString()
publicID := uuid.NewString()
credentialRef := "device-secret://" + publicID
credentialRef := "onboarding://" + factoryID
const keyVersion int64 = 1
ciphertext, err := onboarding.SealDeviceKey(s.onboardingMasterKey, deviceID, keyVersion, secretBytes)
if err != nil {
return CreateResult{}, fmt.Errorf("seal onboarding device key: %w", err)
}
tx, err := s.pool.Begin(ctx)
if err != nil {
@@ -299,63 +296,29 @@ func (s *Service) Create(ctx context.Context, principal identity.Principal, tena
if err != nil {
return CreateResult{}, err
}
if input.HouseholdID != "" {
if err := validateAssignment(ctx, tx, access.tenantID, input.HouseholdID, input.SubjectID); err != nil {
return CreateResult{}, err
}
}
var deviceID string
err = tx.QueryRow(ctx, `
INSERT INTO device(public_id,tenant_id,serial_digest,model,hardware_revision,status,credential_ref,minimum_security_version,minimum_firmware_version,activation_state,activation_reason)
VALUES($1::uuid,$2::uuid,$3,$4,NULLIF($5,''),'registered',$6,NULLIF($7,''),NULLIF($8,''),$9,$10) RETURNING id::text`,
publicID, access.tenantID, hex.EncodeToString(serialDigest[:]), input.Model, input.HardwareRevision, credentialRef,
input.MinimumSecurityVersion, input.MinimumFirmwareVersion, activationState, "ops-device-create").Scan(&deviceID)
_, err = tx.Exec(ctx, `
INSERT INTO device(id,public_id,tenant_id,serial_digest,model,status,credential_ref,activation_state,activation_reason,enrollment_mode,factory_device_id,factory_mac,onboarding_released_at)
VALUES($1::uuid,$2::uuid,$3::uuid,$4,$5,'registered',$6,'CLAIMABLE','ops-device-create','physical_confirm',$7,$8,now())`,
deviceID, publicID, access.tenantID, hex.EncodeToString(serialDigest[:]), input.Model, credentialRef, factoryID, factoryMAC)
if err != nil {
return CreateResult{}, fmt.Errorf("insert device: %w", err)
}
if _, err := tx.Exec(ctx, `INSERT INTO device_activation_transition(tenant_id,device_id,from_state,to_state,reason,source,actor_type,actor_public_id,request_id,activation_revision)
VALUES($1::uuid,$2::uuid,NULL,$3,'ops-device-create','operator','user',$4::uuid,NULLIF($5,''),1)`, access.tenantID, deviceID, activationState, access.userPublicID, requestID); err != nil {
VALUES($1::uuid,$2::uuid,NULL,'CLAIMABLE','ops-device-create','operator','user',$3::uuid,NULLIF($4,''),1)`, access.tenantID, deviceID, access.userPublicID, requestID); err != nil {
return CreateResult{}, fmt.Errorf("record initial device activation: %w", err)
}
desiredConfig, _ := json.Marshal(map[string]string{"voice_runtime": input.VoiceRuntime})
if _, err := tx.Exec(ctx, `INSERT INTO device_twin(device_id,tenant_id,desired_config) VALUES($1::uuid,$2::uuid,$3::jsonb)`, deviceID, access.tenantID, desiredConfig); err != nil {
if _, err := tx.Exec(ctx, `INSERT INTO device_twin(device_id,tenant_id,desired_config) VALUES($1::uuid,$2::uuid,'{}'::jsonb)`, deviceID, access.tenantID); err != nil {
return CreateResult{}, fmt.Errorf("insert device twin: %w", err)
}
if input.HouseholdID != "" {
if err := insertBinding(ctx, tx, access, deviceID, input.HouseholdID, input.SubjectID, "运维端登记设备"); err != nil {
return CreateResult{}, err
}
}
keyVersion, err := s.credential.ProvisionTx(ctx, tx, publicID, []byte(secret), false)
if err != nil {
return CreateResult{}, err
}
if activationState == deviceactivation.Bound && input.VoiceRuntime == "legacy_voicechat" {
if _, err := deviceactivation.Apply(ctx, tx, deviceactivation.Input{DeviceID: deviceID, TenantID: access.tenantID,
Target: deviceactivation.Provisioning, Reason: "旧版语音运行时平台凭据预配", Source: "platform-provisioning", ActorType: "user",
ActorPublicID: &access.userPublicID, RequestID: requestID}); err != nil {
return CreateResult{}, mapActivationError(err)
}
if _, err := deviceactivation.Apply(ctx, tx, deviceactivation.Input{DeviceID: deviceID, TenantID: access.tenantID,
Target: deviceactivation.Active, Reason: "旧版语音运行时已具备绑定与平台凭据", Source: "platform-provisioning", ActorType: "user",
ActorPublicID: &access.userPublicID, RequestID: requestID}); err != nil {
return CreateResult{}, mapActivationError(err)
}
activationState = deviceactivation.Active
if _, err := tx.Exec(ctx, `INSERT INTO device_onboarding_key(device_id,tenant_id,key_version,ciphertext,status,activated_at)
VALUES($1::uuid,$2::uuid,$3,$4,'ACTIVE',now())`, deviceID, access.tenantID, keyVersion, ciphertext); err != nil {
return CreateResult{}, fmt.Errorf("insert onboarding device key: %w", err)
}
if err := s.audit.Record(ctx, tx, audit.Event{TenantID: &access.tenantID, ActorType: "user", ActorPublicID: &access.userPublicID,
Action: "ops.device.create", ResourceType: "device", ResourceID: &publicID, RequestID: requestID, Result: "success",
Detail: map[string]any{"model": input.Model, "assigned": input.HouseholdID != "", "key_version": keyVersion, "activation_state": activationState}}); err != nil {
Detail: map[string]any{"factory_id": factoryID, "model": input.Model, "key_version": keyVersion, "enrollment_mode": "physical_confirm"}}); err != nil {
return CreateResult{}, err
}
var claim *DeviceClaimIssue
if activationState == deviceactivation.Claimable {
issued, err := s.issueDeviceClaimTx(ctx, tx, access, publicID, deviceID, requestID)
if err != nil {
return CreateResult{}, err
}
claim = &issued
}
item, err := readDevice(ctx, tx, access.tenantID, publicID)
if err != nil {
return CreateResult{}, err
@@ -363,7 +326,7 @@ func (s *Service) Create(ctx context.Context, principal identity.Principal, tena
if err := tx.Commit(ctx); err != nil {
return CreateResult{}, fmt.Errorf("commit device registration: %w", err)
}
return CreateResult{Device: item, Credential: CredentialIssue{DeviceID: publicID, Secret: secret, KeyVersion: keyVersion, IssuedAt: time.Now().UTC()}, Claim: claim}, nil
return CreateResult{Device: item, Credential: CredentialIssue{FactoryID: factoryID, Model: input.Model, Origin: s.deviceOrigin, DeviceKeyHex: secret, KeyVersion: keyVersion}}, nil
}
func (s *Service) IssueDeviceClaim(ctx context.Context, principal identity.Principal, tenantPublicID, devicePublicID, requestID string) (DeviceClaimIssue, error) {
@@ -622,9 +585,15 @@ func (s *Service) Delete(ctx context.Context, principal identity.Principal, tena
if _, err := tx.Exec(ctx, `UPDATE device_credential SET status='revoked',revoked_at=COALESCE(revoked_at,now()) WHERE device_id=$1::uuid`, deviceID); err != nil {
return Device{}, fmt.Errorf("revoke device credential: %w", err)
}
if _, err := tx.Exec(ctx, `UPDATE device_onboarding_key SET status='REVOKED' WHERE device_id=$1::uuid AND status IN ('ACTIVE','PENDING')`, deviceID); err != nil {
return Device{}, fmt.Errorf("revoke onboarding device key: %w", err)
}
if _, err := tx.Exec(ctx, `UPDATE device_binding SET unbound_at=now(),unbound_by=$2::uuid,reason='运维端删除设备' WHERE device_id=$1::uuid AND unbound_at IS NULL`, deviceID, access.userID); err != nil {
return Device{}, fmt.Errorf("close retired device binding: %w", err)
}
if _, err := tx.Exec(ctx, `UPDATE device_account_binding SET released_at=now() WHERE device_id=$1::uuid AND released_at IS NULL`, deviceID); err != nil {
return Device{}, fmt.Errorf("close retired device account binding: %w", err)
}
if _, err := tx.Exec(ctx, `UPDATE device_claim SET revoked_at=now() WHERE device_id=$1::uuid AND used_at IS NULL AND revoked_at IS NULL`, deviceID); err != nil {
return Device{}, fmt.Errorf("revoke retired device claims: %w", err)
}
@@ -734,7 +703,6 @@ func (s *Service) RotateCredential(ctx context.Context, principal identity.Princ
if _, err := rand.Read(secretBytes); err != nil {
return CredentialIssue{}, fmt.Errorf("generate device credential: %w", err)
}
secret := hex.EncodeToString(secretBytes)
tx, err := s.pool.Begin(ctx)
if err != nil {
return CredentialIssue{}, fmt.Errorf("begin credential rotation: %w", err)
@@ -744,19 +712,29 @@ func (s *Service) RotateCredential(ctx context.Context, principal identity.Princ
if err != nil {
return CredentialIssue{}, err
}
var rotationDeviceID string
if err := tx.QueryRow(ctx, `SELECT id::text FROM device WHERE public_id=$1::uuid AND tenant_id=$2::uuid FOR UPDATE`, devicePublicID, access.tenantID).Scan(&rotationDeviceID); err != nil {
var rotationDeviceID, enrollmentMode, factoryID, model string
if err := tx.QueryRow(ctx, `SELECT id::text,enrollment_mode,COALESCE(factory_device_id,''),model FROM device WHERE public_id=$1::uuid AND tenant_id=$2::uuid AND status='registered' AND activation_state NOT IN ('LOST','REVOKED') FOR UPDATE`, devicePublicID, access.tenantID).Scan(&rotationDeviceID, &enrollmentMode, &factoryID, &model); err != nil {
return CredentialIssue{}, ErrNotFound
}
if err := devicebinding.RequireLegacy(ctx, tx, rotationDeviceID); err != nil {
return CredentialIssue{}, err
if enrollmentMode != "physical_confirm" || factoryID == "" || s.onboardingMasterKey == "" || s.deviceOrigin == "" {
return CredentialIssue{}, ErrProvisioningUnavailable
}
version, err := s.credential.ProvisionTx(ctx, tx, devicePublicID, []byte(secret), true)
if errors.Is(err, deviceauth.ErrDeviceNotFound) {
return CredentialIssue{}, ErrNotFound
var version int64
if err := tx.QueryRow(ctx, `SELECT COALESCE(max(key_version),0)+1 FROM device_onboarding_key WHERE device_id=$1::uuid`, rotationDeviceID).Scan(&version); err != nil {
return CredentialIssue{}, fmt.Errorf("read onboarding key version: %w", err)
}
ciphertext, err := onboarding.SealDeviceKey(s.onboardingMasterKey, rotationDeviceID, version, secretBytes)
if err != nil {
return CredentialIssue{}, err
return CredentialIssue{}, fmt.Errorf("seal rotated onboarding device key: %w", err)
}
if _, err := tx.Exec(ctx, `UPDATE device_onboarding_key SET status='REVOKED' WHERE device_id=$1::uuid AND status='ACTIVE'`, rotationDeviceID); err != nil {
return CredentialIssue{}, fmt.Errorf("revoke previous onboarding device key: %w", err)
}
if _, err := tx.Exec(ctx, `INSERT INTO device_onboarding_key(device_id,tenant_id,key_version,ciphertext,status,activated_at) VALUES($1::uuid,$2::uuid,$3,$4,'ACTIVE',now())`, rotationDeviceID, access.tenantID, version, ciphertext); err != nil {
return CredentialIssue{}, fmt.Errorf("insert rotated onboarding device key: %w", err)
}
if _, err := tx.Exec(ctx, `UPDATE device SET onboarding_revision=onboarding_revision+1,version=version+1,updated_at=now() WHERE id=$1::uuid`, rotationDeviceID); err != nil {
return CredentialIssue{}, fmt.Errorf("update onboarding key revision: %w", err)
}
if err := s.audit.Record(ctx, tx, audit.Event{TenantID: &access.tenantID, ActorType: "user", ActorPublicID: &access.userPublicID,
Action: "ops.device.credential.rotate", ResourceType: "device", ResourceID: &devicePublicID, RequestID: requestID, Result: "success",
@@ -766,7 +744,7 @@ func (s *Service) RotateCredential(ctx context.Context, principal identity.Princ
if err := tx.Commit(ctx); err != nil {
return CredentialIssue{}, fmt.Errorf("commit credential rotation: %w", err)
}
return CredentialIssue{DeviceID: devicePublicID, Secret: secret, KeyVersion: version, IssuedAt: time.Now().UTC()}, nil
return CredentialIssue{FactoryID: factoryID, Model: model, Origin: s.deviceOrigin, DeviceKeyHex: hex.EncodeToString(secretBytes), KeyVersion: version}, nil
}
func requireAccess(ctx context.Context, q queryRow, principal identity.Principal, tenantPublicID string, mutate bool) (operatorAccess, error) {
@@ -841,23 +819,27 @@ func mapActivationError(err error) error {
}
const deviceSelectSQL = `
SELECT d.public_id::text,t.public_id::text,d.model,d.hardware_revision,d.status,d.activation_state,d.activation_revision,d.activation_reason,d.activation_updated_at,d.minimum_security_version,d.minimum_firmware_version,
h.public_id::text,h.name,sp.public_id::text,sp.preferred_name,COALESCE(dc.status,'missing'),COALESCE(dc.key_version,0),
SELECT d.public_id::text,t.public_id::text,COALESCE(d.factory_device_id,''),d.enrollment_mode,
(EXISTS(SELECT 1 FROM device_binding current_binding WHERE current_binding.device_id=d.id AND current_binding.unbound_at IS NULL)
OR EXISTS(SELECT 1 FROM device_account_binding account_binding WHERE account_binding.device_id=d.id AND account_binding.released_at IS NULL AND account_binding.generation=d.binding_generation)),
d.model,d.hardware_revision,d.status,d.activation_state,d.activation_revision,d.activation_reason,d.activation_updated_at,d.minimum_security_version,d.minimum_firmware_version,
h.public_id::text,h.name,sp.public_id::text,sp.preferred_name,COALESCE(lower(ok.status),dc.status,'missing'),COALESCE(ok.key_version,dc.key_version,0),
dt.firmware_version,dt.last_reported_at,d.registered_at,d.updated_at,d.version,
COALESCE(dt.desired_config->>'voice_runtime','legacy_voicechat'),COALESCE(hb.bot_id,''),COALESCE(hb.transport,''),COALESCE(hb.minimum_sdk_version,''),COALESCE(hb.status,'')
COALESCE(dt.desired_config->>'voice_runtime',CASE WHEN d.enrollment_mode='physical_confirm' THEN 'volc_hardware_agent' ELSE 'legacy_voicechat' END),COALESCE(hb.bot_id,''),COALESCE(hb.transport,''),COALESCE(hb.minimum_sdk_version,''),COALESCE(hb.status,'')
FROM device d JOIN tenant t ON t.id=d.tenant_id
LEFT JOIN device_twin dt ON dt.device_id=d.id AND dt.tenant_id=d.tenant_id
LEFT JOIN device_binding b ON b.device_id=d.id AND b.tenant_id=d.tenant_id AND b.unbound_at IS NULL
LEFT JOIN household h ON h.id=b.household_id AND h.tenant_id=b.tenant_id
LEFT JOIN subject_profile sp ON sp.id=b.subject_id AND sp.tenant_id=b.tenant_id
LEFT JOIN device_credential dc ON dc.device_id=d.id AND dc.tenant_id=d.tenant_id
LEFT JOIN device_onboarding_key ok ON ok.device_id=d.id AND ok.tenant_id=d.tenant_id AND ok.status='ACTIVE'
LEFT JOIN hardware_agent_device_binding hb ON hb.device_id=d.id AND hb.tenant_id=d.tenant_id`
type scanner interface{ Scan(...any) error }
func scanDevice(row scanner) (Device, error) {
var item Device
if err := row.Scan(&item.ID, &item.TenantID, &item.Model, &item.HardwareRevision, &item.Status, &item.ActivationState, &item.ActivationRevision, &item.ActivationReason, &item.ActivationUpdatedAt, &item.MinimumSecurityVersion, &item.MinimumFirmwareVersion,
if err := row.Scan(&item.ID, &item.TenantID, &item.FactoryID, &item.EnrollmentMode, &item.Bound, &item.Model, &item.HardwareRevision, &item.Status, &item.ActivationState, &item.ActivationRevision, &item.ActivationReason, &item.ActivationUpdatedAt, &item.MinimumSecurityVersion, &item.MinimumFirmwareVersion,
&item.HouseholdID, &item.HouseholdName, &item.SubjectID, &item.SubjectName, &item.CredentialStatus, &item.KeyVersion,
&item.FirmwareVersion, &item.LastReportedAt, &item.RegisteredAt, &item.UpdatedAt, &item.Version,
&item.VoiceRuntime, &item.HardwareAgentBotID, &item.HardwareAgentTransport, &item.HardwareAgentMinimumSDKVersion, &item.HardwareAgentStatus); err != nil {