Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion Config/BuildSettings.local.example.xcconfig
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
// archives. Do not commit the local file.

APP_DEVELOPMENT_TEAM = YOURTEAMID
APP_IPHONEOS_CODE_SIGN_IDENTITY =
APP_BUNDLE_ID = com.example.shellbee
APP_WIDGET_BUNDLE_SUFFIX = widgets
APP_WIDGET_BUNDLE_ID = $(APP_BUNDLE_ID).$(APP_WIDGET_BUNDLE_SUFFIX)
Expand Down
1 change: 0 additions & 1 deletion Config/BuildSettings.xcconfig
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
// which is ignored by git and should be used for device builds and App Store archives.

APP_DEVELOPMENT_TEAM =
APP_IPHONEOS_CODE_SIGN_IDENTITY =
APP_BUNDLE_ID = dev.echodb.shellbee
APP_WIDGET_BUNDLE_SUFFIX = shellbeeWidgets
APP_WIDGET_BUNDLE_ID = $(APP_BUNDLE_ID).$(APP_WIDGET_BUNDLE_SUFFIX)
Expand Down
10 changes: 4 additions & 6 deletions Shellbee.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -846,7 +846,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.6.2;
MARKETING_VERSION = 1.6.3;
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_ID)";
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = YES;
Expand All @@ -871,7 +871,6 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
CODE_SIGN_ENTITLEMENTS = Shellbee.entitlements;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "$(APP_IPHONEOS_CODE_SIGN_IDENTITY)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "$(APP_DEVELOPMENT_TEAM)";
Expand All @@ -885,7 +884,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.6.2;
MARKETING_VERSION = 1.6.3;
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_BUNDLE_ID)";
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = YES;
Expand Down Expand Up @@ -923,7 +922,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.6.2;
MARKETING_VERSION = 1.6.3;
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_WIDGET_BUNDLE_ID)";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
Expand All @@ -948,7 +947,6 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CODE_SIGN_ENTITLEMENTS = ShellbeeWidgets.entitlements;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "$(APP_IPHONEOS_CODE_SIGN_IDENTITY)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "$(APP_DEVELOPMENT_TEAM)";
Expand All @@ -963,7 +961,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.6.2;
MARKETING_VERSION = 1.6.3;
PRODUCT_BUNDLE_IDENTIFIER = "$(APP_WIDGET_BUNDLE_ID)";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
Expand Down
19 changes: 19 additions & 0 deletions Shellbee/Core/Models/Device.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ struct Device: Codable, Identifiable, Sendable, Equatable, Hashable {
var manufacturer: String?
var interviewCompleted: Bool
var interviewing: Bool
var interviewState: InterviewState?
var softwareBuildId: String?
var dateCode: String?
var endpoints: [String: JSONValue]?
Expand Down Expand Up @@ -44,6 +45,16 @@ struct Device: Codable, Identifiable, Sendable, Equatable, Hashable {
}
}

/// Single source of truth for interview progress across the app. Prefers the
/// modern `interview_state` enum that recent Z2M emits and falls back to the
/// legacy `interviewing` / `interview_completed` boolean pair for older bridges.
var isInterviewing: Bool {
if let state = interviewState {
return state == .pending || state == .inProgress
}
return interviewing || !interviewCompleted
}

func hash(into hasher: inout Hasher) { hasher.combine(id) }
static func == (lhs: Device, rhs: Device) -> Bool { lhs.ieeeAddress == rhs.ieeeAddress }

Expand All @@ -55,12 +66,20 @@ struct Device: Codable, Identifiable, Sendable, Equatable, Hashable {
case powerSource = "power_source"
case modelId = "model_id"
case interviewCompleted = "interview_completed"
case interviewState = "interview_state"
case softwareBuildId = "software_build_id"
case dateCode = "date_code"
case endpoints, options, availability
}
}

enum InterviewState: String, Codable, Sendable, Equatable, Hashable {
case pending = "PENDING"
case inProgress = "IN_PROGRESS"
case successful = "SUCCESSFUL"
case failed = "FAILED"
}

enum DeviceType: String, Codable, Sendable, Equatable, ChipRepresentable {
case router = "Router"
case endDevice = "EndDevice"
Expand Down
2 changes: 1 addition & 1 deletion Shellbee/Core/Models/NetworkAnalysis.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ enum DeviceCondition: String, CaseIterable, Sendable {
case .availabilityOff: return availabilityStatus == .untracked
case .batteryLow: return (state.battery ?? 100) < DesignTokens.Threshold.lowBattery
case .weakSignal: return (state.linkQuality ?? 999) < DesignTokens.Threshold.weakSignal
case .interviewing: return device.interviewing || !device.interviewCompleted
case .interviewing: return device.isInterviewing
case .unsupported: return !device.supported
}
}
Expand Down
5 changes: 4 additions & 1 deletion Shellbee/Core/Store/AppStore+Events.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ extension AppStore {
// freshly added.
for device in list where device.type != .coordinator {
guard deviceFirstSeen[device.ieeeAddress] == nil else { continue }
if device.interviewing || !device.interviewCompleted {
if device.isInterviewing {
recordFirstSeen(ieee: device.ieeeAddress)
}
}
Expand Down Expand Up @@ -122,9 +122,11 @@ extension AppStore {
case "started":
devices[idx].interviewing = true
devices[idx].interviewCompleted = false
devices[idx].interviewState = .inProgress
case "successful":
devices[idx].interviewing = false
devices[idx].interviewCompleted = true
devices[idx].interviewState = .successful
// A device that just finished interviewing is by
// definition online — Z2M only completes interview
// when the device responds. Optimistically reflect
Expand All @@ -139,6 +141,7 @@ extension AppStore {
case "failed":
devices[idx].interviewing = false
devices[idx].interviewCompleted = false
devices[idx].interviewState = .failed
default:
break
}
Expand Down
2 changes: 1 addition & 1 deletion Shellbee/Features/Devices/DeviceListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,7 @@ private struct DeviceListContent: View {
return environment.allDevices
.filter { $0.device.type != .coordinator }
.filter { bound in
if bound.device.interviewing { return true }
if bound.device.isInterviewing { return true }
let store = environment.registry.session(for: bound.bridgeID)?.store
if let joined = store?.deviceFirstSeen[bound.device.ieeeAddress], joined >= cutoff {
return true
Expand Down
2 changes: 1 addition & 1 deletion Shellbee/Features/Devices/DeviceListViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ final class DeviceListViewModel {
return store.devices
.filter { $0.type != .coordinator }
.filter { device in
if device.interviewing { return true }
if device.isInterviewing { return true }
if let joined = store.deviceFirstSeen[device.ieeeAddress], joined >= cutoff {
return true
}
Expand Down
4 changes: 1 addition & 3 deletions Shellbee/Features/Devices/DeviceRowView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,7 @@ struct DeviceRowView: View {
.padding(.vertical, DesignTokens.Spacing.xs)
}

private var isInterviewing: Bool {
device.interviewing || !device.interviewCompleted
}
private var isInterviewing: Bool { device.isInterviewing }

@ViewBuilder
private var rightDetailView: some View {
Expand Down
2 changes: 1 addition & 1 deletion Shellbee/Features/Home/HomeSnapshot.swift
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ struct HomeSnapshot: Sendable {
guard let quality = (states[$0.friendlyName] ?? [:]).linkQuality else { return false }
return quality < DesignTokens.Threshold.weakSignal
}.count
interviewingDevices = nonCoordinatorDevices.filter { $0.interviewing || !$0.interviewCompleted }.count
interviewingDevices = nonCoordinatorDevices.filter { $0.isInterviewing }.count
let lqiValues = nonCoordinatorDevices.compactMap { states[$0.friendlyName]?.linkQuality }
averageLinkQuality = lqiValues.isEmpty ? nil : lqiValues.reduce(0, +) / lqiValues.count

Expand Down
7 changes: 7 additions & 0 deletions Shellbee/Features/Pairing/PairingWizardModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ final class PairingWizardModel {
}

func interviewStatus(for device: Device) -> InterviewStatus {
if let state = device.interviewState {
switch state {
case .inProgress: return .running
case .successful: return .completed
case .pending, .failed: return .pending
}
}
if device.interviewing { return .running }
if device.interviewCompleted { return .completed }
return .pending
Expand Down
6 changes: 3 additions & 3 deletions Shellbee/Shared/Components/DeviceCard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ struct DeviceCard: View {
}
}

if device.interviewing {
if device.isInterviewing {
return "Interviewing"
}

Expand All @@ -312,14 +312,14 @@ struct DeviceCard: View {

private var statusColor: Color {
if otaStatus?.isActive == true { return .blue }
if device.interviewing { return .orange }
if device.isInterviewing { return .orange }
if !device.availabilityTrackingEnabled { return .secondary }
return isAvailable ? .green : .red
}

private var statusIcon: String {
if otaStatus?.isActive == true { return "arrow.triangle.2.circlepath.circle.fill" }
if device.interviewing { return "dot.radiowaves.left.and.right" }
if device.isInterviewing { return "dot.radiowaves.left.and.right" }
if !device.availabilityTrackingEnabled { return "minus.circle.fill" }
return isAvailable ? "checkmark.circle.fill" : "xmark.circle.fill"
}
Expand Down
4 changes: 2 additions & 2 deletions Shellbee/Shared/Components/DeviceCardFooterBar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ struct DeviceCardFooterBar: View {
}
}

if device.interviewing {
if device.isInterviewing {
return "Interviewing"
}

Expand All @@ -94,7 +94,7 @@ struct DeviceCardFooterBar: View {

private var statusColor: Color {
if otaStatus?.isActive == true { return .blue }
if device.interviewing { return .orange }
if device.isInterviewing { return .orange }
if !device.availabilityTrackingEnabled { return .secondary }
return isAvailable ? .green : .red
}
Expand Down
1 change: 0 additions & 1 deletion ci_scripts/ci_post_clone.sh
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ TARGET="$REPO_ROOT/Config/BuildSettings.local.xcconfig"

{
echo "APP_DEVELOPMENT_TEAM = $SHELLBEE_TEAM_ID"
echo "APP_IPHONEOS_CODE_SIGN_IDENTITY = Apple Distribution"
echo "APP_BUNDLE_ID = $SHELLBEE_BUNDLE_ID"
echo "APP_WIDGET_BUNDLE_SUFFIX = $SHELLBEE_WIDGET_SUFFIX"
echo "APP_WIDGET_BUNDLE_ID = \$(APP_BUNDLE_ID).\$(APP_WIDGET_BUNDLE_SUFFIX)"
Expand Down
3 changes: 3 additions & 0 deletions docker/seeder/control.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ def _build_device(model: str, name: str, ieee: str) -> tuple[dict, dict]:
"manufacturer": m["vendor"],
"interview_completed": False,
"interviewing": True,
"interview_state": "IN_PROGRESS",
"software_build_id": None,
"date_code": None,
"endpoints": {"1": {"inputClusters": [], "outputClusters": [],
Expand Down Expand Up @@ -258,6 +259,7 @@ def run():
with seeder._lock:
device["interview_completed"] = False
device["interviewing"] = False
device["interview_state"] = "FAILED"
seeder._publish_devices(client)
seeder._emit_event(client, "device_interview", {
"friendly_name": name, "ieee_address": ieee,
Expand All @@ -268,6 +270,7 @@ def run():
with seeder._lock:
device["interview_completed"] = True
device["interviewing"] = False
device["interview_state"] = "SUCCESSFUL"
seeder._publish_devices(client)
seeder._emit_event(client, "device_interview", {
"friendly_name": name, "ieee_address": ieee,
Expand Down
1 change: 1 addition & 0 deletions docker/seeder/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ def device(
"manufacturer": m["vendor"],
"interview_completed": True,
"interviewing": False,
"interview_state": "SUCCESSFUL",
"software_build_id": None,
"date_code": None,
"endpoints": {"1": {"inputClusters": [], "outputClusters": [],
Expand Down
1 change: 1 addition & 0 deletions docker/seeder/seeder.py
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,7 @@ def run():
with _lock:
device["interview_completed"] = True
device["interviewing"] = False
device["interview_state"] = "SUCCESSFUL"
_emit_event(client, "device_interview", {
"friendly_name": name, "status": "successful", "ieee_address": ieee,
"supported": True,
Expand Down
Loading