feat: expand companion platform, device controls, and memoir workflows
ci / verify (push) Canceled after 0s
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:
@@ -17,6 +17,7 @@ import (
|
||||
)
|
||||
|
||||
type ManagedDevice struct {
|
||||
AccountBindingID string `json:"account_binding_id,omitempty"`
|
||||
ID string `json:"id"`
|
||||
BindingID string `json:"binding_id"`
|
||||
HouseholdID string `json:"household_id"`
|
||||
@@ -65,6 +66,8 @@ type DevicePresence struct {
|
||||
Source string `json:"source"`
|
||||
}
|
||||
|
||||
var ErrDevicePowerUnsupported = errors.New("device has not reported power control support")
|
||||
|
||||
func (s *Service) ListDevices(ctx context.Context, principal identity.Principal) ([]ManagedDevice, error) {
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT d.public_id::text,b.public_id::text,h.public_id::text,sp.public_id::text,sp.preferred_name,
|
||||
@@ -99,7 +102,9 @@ func (s *Service) ListDevices(ctx context.Context, principal identity.Principal)
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("iterate managed devices: %w", err)
|
||||
}
|
||||
return result, nil
|
||||
rows.Close()
|
||||
owned, err := s.listAccountDevices(ctx, principal)
|
||||
return append(result, owned...), err
|
||||
}
|
||||
|
||||
func (s *Service) GetDevice(ctx context.Context, principal identity.Principal, devicePublicID string) (ManagedDevice, error) {
|
||||
@@ -126,6 +131,16 @@ func (s *Service) PatchDeviceTwin(ctx context.Context, principal identity.Princi
|
||||
if err := concurrency.RequireVersion(expectedRevision, access.desiredRevision); err != nil {
|
||||
return ErrVersionConflict
|
||||
}
|
||||
if _, changing := desired["power"]; changing {
|
||||
twin, err := readDeviceTwin(ctx, tx, access)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
power, ok := twin.ReportedConfig["power"].(map[string]any)
|
||||
if !ok || (power["mode"] != "active" && power["mode"] != "standby" && power["mode"] != "transition") {
|
||||
return ErrDevicePowerUnsupported
|
||||
}
|
||||
}
|
||||
desiredJSON, _ := json.Marshal(desired)
|
||||
if _, err := tx.Exec(ctx, `UPDATE device_twin SET desired_config=desired_config || $1::jsonb,desired_revision=desired_revision+1,updated_at=now() WHERE device_id=$2::uuid AND tenant_id=$3::uuid`, desiredJSON, access.deviceID, access.tenantID); err != nil {
|
||||
return fmt.Errorf("update desired device configuration: %w", err)
|
||||
@@ -149,6 +164,24 @@ func (s *Service) GetDevicePresence(ctx context.Context, principal identity.Prin
|
||||
return DevicePresence{}, err
|
||||
}
|
||||
result := DevicePresence{DeviceID: devicePublicID, Online: false, Status: "offline", LastSeenAt: device.LastReportedAt, Firmware: device.FirmwareVersion, Source: "database"}
|
||||
// Factory-authenticated hardware reports presence through MQTT, not the
|
||||
// legacy conversation telemetry Redis key. Use the same source as mini OTA.
|
||||
var fleetSeen *time.Time
|
||||
var fleetOnline bool
|
||||
err = s.pool.QueryRow(ctx, `SELECT COALESCE(connected AND last_seen_at>now()-interval '90 seconds',false),last_seen_at
|
||||
FROM fleet_presence WHERE device_id=$1`, access.deviceID).Scan(&fleetOnline, &fleetSeen)
|
||||
if err == nil {
|
||||
result.Online = fleetOnline
|
||||
if fleetOnline {
|
||||
result.Status = "online"
|
||||
}
|
||||
result.LastSeenAt = fleetSeen
|
||||
result.Source = "mqtt"
|
||||
return result, nil
|
||||
}
|
||||
if !errors.Is(err, pgx.ErrNoRows) {
|
||||
return DevicePresence{}, err
|
||||
}
|
||||
if s.redis == nil {
|
||||
return result, nil
|
||||
}
|
||||
@@ -214,7 +247,7 @@ func resolveDevice(ctx context.Context, q queryRow, principal identity.Principal
|
||||
&device.Status, &device.MinimumSecurityVersion, &device.MinimumFirmwareVersion, &device.FirmwareVersion, &device.DesiredRevision, &device.ReportedRevision,
|
||||
&device.LastReportedAt, &device.BoundAt, &device.RegisteredAt, &device.Version)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return deviceAccess{}, ManagedDevice{}, ErrAccessDenied
|
||||
return resolveAccountDevice(ctx, q, principal, publicID, capability, lock)
|
||||
}
|
||||
if err != nil {
|
||||
return deviceAccess{}, ManagedDevice{}, fmt.Errorf("resolve device access: %w", err)
|
||||
|
||||
Reference in New Issue
Block a user