Skip to content

Commit d6ea6c4

Browse files
authored
Prompt in onboarding for new device name if already taken (#1895)
Fixes #415. ## Summary When the device name is already taken by a different unique ID than the current device (in other words, when registering would produce a duplicate device name; login->logout is not affected), prompt for a replacement device name to use instead. This will hopefully eliminate cases where the notify service is confused, and prevent sensor naming conflicts.
1 parent 4c39d86 commit d6ea6c4

File tree

8 files changed

+484
-1
lines changed

8 files changed

+484
-1
lines changed

HomeAssistant.xcodeproj/project.pbxproj

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@
227227
1185DFAF271FF53800ED7D9A /* OnboardingAuthStepRegister.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1185DFA8271FF53800ED7D9A /* OnboardingAuthStepRegister.swift */; };
228228
1185DFB0271FF53800ED7D9A /* OnboardingAuthStepConnectivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1185DFA9271FF53800ED7D9A /* OnboardingAuthStepConnectivity.swift */; };
229229
1185DFB1271FF53800ED7D9A /* OnboardingAuthStepNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1185DFAA271FF53800ED7D9A /* OnboardingAuthStepNotify.swift */; };
230+
1185DFB2271FF53800ED7D9A /* OnboardingAuthStepDuplicate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1185DFAB271FF53800ED7D9A /* OnboardingAuthStepDuplicate.swift */; };
230231
1185DFB3271FF53800ED7D9A /* OnboardingAuthStepSensors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1185DFAC271FF53800ED7D9A /* OnboardingAuthStepSensors.swift */; };
231232
1185DFB4271FF53800ED7D9A /* OnboardingAuthStepModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1185DFAD271FF53800ED7D9A /* OnboardingAuthStepModels.swift */; };
232233
1187DE4224D77CCC00F0A6A6 /* NFCTagViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1187DE4124D77CCC00F0A6A6 /* NFCTagViewController.swift */; };
@@ -399,6 +400,8 @@
399400
11EFCDDC24F6065F00314D85 /* AboutSceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11EFCDDB24F6065F00314D85 /* AboutSceneDelegate.swift */; };
400401
11EFCDE024F60E5900314D85 /* BasicSceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11EFCDDF24F60E5900314D85 /* BasicSceneDelegate.swift */; };
401402
11EFD3BE27253504000AF78B /* OnboardingAuthStepConnectivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11EFD3BD27253504000AF78B /* OnboardingAuthStepConnectivity.swift */; };
403+
11EFD3C027261AA4000AF78B /* OnboardingAuthStepDuplicate.test.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11EFD3BF27261AA4000AF78B /* OnboardingAuthStepDuplicate.test.swift */; };
404+
11EFD3C327264306000AF78B /* UIAlertAction+Additions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11EFD3C227264306000AF78B /* UIAlertAction+Additions.swift */; };
402405
11F01A80263D050D002AC33B /* NotificationLoadingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11F01A7F263D050D002AC33B /* NotificationLoadingViewController.swift */; };
403406
11F2F1EC2586ED6100F61F7C /* NotificationAttachmentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11F2F1EB2586ED6100F61F7C /* NotificationAttachmentManager.swift */; };
404407
11F2F1ED2586ED6100F61F7C /* NotificationAttachmentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11F2F1EB2586ED6100F61F7C /* NotificationAttachmentManager.swift */; };
@@ -1271,6 +1274,7 @@
12711274
1185DFA8271FF53800ED7D9A /* OnboardingAuthStepRegister.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnboardingAuthStepRegister.swift; sourceTree = "<group>"; };
12721275
1185DFA9271FF53800ED7D9A /* OnboardingAuthStepConnectivity.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnboardingAuthStepConnectivity.swift; sourceTree = "<group>"; };
12731276
1185DFAA271FF53800ED7D9A /* OnboardingAuthStepNotify.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnboardingAuthStepNotify.swift; sourceTree = "<group>"; };
1277+
1185DFAB271FF53800ED7D9A /* OnboardingAuthStepDuplicate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnboardingAuthStepDuplicate.swift; sourceTree = "<group>"; };
12741278
1185DFAC271FF53800ED7D9A /* OnboardingAuthStepSensors.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnboardingAuthStepSensors.swift; sourceTree = "<group>"; };
12751279
1185DFAD271FF53800ED7D9A /* OnboardingAuthStepModels.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnboardingAuthStepModels.swift; sourceTree = "<group>"; };
12761280
1187DE4124D77CCC00F0A6A6 /* NFCTagViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NFCTagViewController.swift; sourceTree = "<group>"; };
@@ -1408,6 +1412,8 @@
14081412
11EFCDDB24F6065F00314D85 /* AboutSceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutSceneDelegate.swift; sourceTree = "<group>"; };
14091413
11EFCDDF24F60E5900314D85 /* BasicSceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicSceneDelegate.swift; sourceTree = "<group>"; };
14101414
11EFD3BD27253504000AF78B /* OnboardingAuthStepConnectivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingAuthStepConnectivity.swift; sourceTree = "<group>"; };
1415+
11EFD3BF27261AA4000AF78B /* OnboardingAuthStepDuplicate.test.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingAuthStepDuplicate.test.swift; sourceTree = "<group>"; };
1416+
11EFD3C227264306000AF78B /* UIAlertAction+Additions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertAction+Additions.swift"; sourceTree = "<group>"; };
14111417
11F01A7F263D050D002AC33B /* NotificationLoadingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationLoadingViewController.swift; sourceTree = "<group>"; };
14121418
11F2F1EB2586ED6100F61F7C /* NotificationAttachmentManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationAttachmentManager.swift; sourceTree = "<group>"; };
14131419
11F2F2082586FB0C00F61F7C /* NotificationAttachmentManager.test.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationAttachmentManager.test.swift; sourceTree = "<group>"; };
@@ -2257,6 +2263,7 @@
22572263
116D3A3C2724D83300EF5D21 /* OnboardingAuth.test.swift */,
22582264
116D3A4527252C3200EF5D21 /* OnboardingAuthStepConfig.test.swift */,
22592265
11EFD3BD27253504000AF78B /* OnboardingAuthStepConnectivity.swift */,
2266+
11EFD3BF27261AA4000AF78B /* OnboardingAuthStepDuplicate.test.swift */,
22602267
11ED43952726599D00B5FD45 /* OnboardingAuthStepModels.test.swift */,
22612268
11ED439727265B9C00B5FD45 /* OnboardingAuthStepNotify.test.swift */,
22622269
11ED439927265DE800B5FD45 /* OnboardingAuthStepRegister.test.swift */,
@@ -2334,6 +2341,7 @@
23342341
children = (
23352342
1185DFA7271FF53800ED7D9A /* OnboardingAuthStepConfig.swift */,
23362343
1185DFA9271FF53800ED7D9A /* OnboardingAuthStepConnectivity.swift */,
2344+
1185DFAB271FF53800ED7D9A /* OnboardingAuthStepDuplicate.swift */,
23372345
1185DFAD271FF53800ED7D9A /* OnboardingAuthStepModels.swift */,
23382346
1185DFAA271FF53800ED7D9A /* OnboardingAuthStepNotify.swift */,
23392347
1185DFA8271FF53800ED7D9A /* OnboardingAuthStepRegister.swift */,
@@ -2627,6 +2635,14 @@
26272635
path = Scenes;
26282636
sourceTree = "<group>";
26292637
};
2638+
11EFD3C1272642FC000AF78B /* Additions */ = {
2639+
isa = PBXGroup;
2640+
children = (
2641+
11EFD3C227264306000AF78B /* UIAlertAction+Additions.swift */,
2642+
);
2643+
path = Additions;
2644+
sourceTree = "<group>";
2645+
};
26302646
11F2F21725871C1700F61F7C /* NotificationAttachments */ = {
26312647
isa = PBXGroup;
26322648
children = (
@@ -3026,6 +3042,7 @@
30263042
B657A8FF1CA646EB00121384 /* App */ = {
30273043
isa = PBXGroup;
30283044
children = (
3045+
11EFD3C1272642FC000AF78B /* Additions */,
30293046
116D3A3B2724D81C00EF5D21 /* Auth */,
30303047
11AD2EA7252900AA00FBC437 /* Resources */,
30313048
11A71C7424A5023200D9565F /* ZoneManager */,
@@ -5207,6 +5224,7 @@
52075224
1164DA3225FBF5D600515E8A /* UITextView+CodeRow.swift in Sources */,
52085225
11A48D8324CA9D010021BDD9 /* RealmSection.swift in Sources */,
52095226
116D3A442724EFFB00EF5D21 /* OnboardingAuthTokenExchange.swift in Sources */,
5227+
1185DFB2271FF53800ED7D9A /* OnboardingAuthStepDuplicate.swift in Sources */,
52105228
11EFCDDA24F5FE0600314D85 /* SceneActivity.swift in Sources */,
52115229
11DA6B4D2713900E008ADFAF /* OnboardingPermissionWorkflowController.swift in Sources */,
52125230
1185DFB1271FF53800ED7D9A /* OnboardingAuthStepNotify.swift in Sources */,
@@ -5235,8 +5253,10 @@
52355253
11ED43A027279AFA00B5FD45 /* OnboardingAuthLoginImpl.test.swift in Sources */,
52365254
11EFD3BE27253504000AF78B /* OnboardingAuthStepConnectivity.swift in Sources */,
52375255
11ED439A27265DE800B5FD45 /* OnboardingAuthStepRegister.test.swift in Sources */,
5256+
11EFD3C027261AA4000AF78B /* OnboardingAuthStepDuplicate.test.swift in Sources */,
52385257
11A71C9124A598AB00D9565F /* ZoneManagerProcessor.test.swift in Sources */,
52395258
11ED439827265B9C00B5FD45 /* OnboardingAuthStepNotify.test.swift in Sources */,
5259+
11EFD3C327264306000AF78B /* UIAlertAction+Additions.swift in Sources */,
52405260
11A71C8F24A5946B00D9565F /* FakeCLLocationManager.swift in Sources */,
52415261
11EF62DA24C3687D00BABB64 /* ZoneManagerRegionFilter.test.swift in Sources */,
52425262
11A71C8724A5074E00D9565F /* ZoneManager.test.swift in Sources */,

Sources/App/Onboarding/API/OnboardingAuth.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class OnboardingAuth {
1919
OnboardingAuthStepConnectivity.self,
2020
]
2121
var postSteps: [OnboardingAuthPostStep.Type] = [
22+
OnboardingAuthStepDuplicate.self,
2223
OnboardingAuthStepConfig.self,
2324
OnboardingAuthStepSensors.self,
2425
OnboardingAuthStepModels.self,
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import HAKit
2+
import PromiseKit
3+
import Shared
4+
5+
struct OnboardingAuthStepDuplicate: OnboardingAuthPostStep {
6+
init(
7+
connection: HAConnection,
8+
api: HomeAssistantAPI,
9+
sender: UIViewController
10+
) {
11+
self.connection = connection
12+
self.api = api
13+
self.sender = sender
14+
}
15+
16+
var connection: HAConnection
17+
var api: HomeAssistantAPI
18+
var sender: UIViewController
19+
20+
static var supportedPoints: Set<OnboardingAuthStepPoint> {
21+
Set([.beforeRegister])
22+
}
23+
24+
var timeout: TimeInterval = 30.0
25+
26+
func perform(point: OnboardingAuthStepPoint) -> Promise<Void> {
27+
let devices = firstly { () -> Promise<[HAData]> in
28+
connection.send(.init(type: "config/device_registry/list")).promise.map {
29+
if case let .array(value) = $0 {
30+
return value
31+
} else {
32+
throw HomeAssistantAPI.APIError.invalidResponse
33+
}
34+
}
35+
}.compactMapValues { value -> RegisteredDevice? in
36+
try? RegisteredDevice(data: value)
37+
}
38+
39+
let timeout: Promise<[RegisteredDevice]> = after(seconds: timeout).then { () -> Promise<[RegisteredDevice]> in
40+
switch connection.state {
41+
case let .disconnected(reason: .waitingToReconnect(lastError: .some(error), atLatest: _, retryCount: _)):
42+
throw error
43+
default:
44+
throw OnboardingAuthError(kind: .invalidURL, data: nil)
45+
}
46+
}
47+
48+
// racing the request, not the whole flow, importantly.
49+
// otherwise we'd fail out before the user finished typing.
50+
51+
return race(timeout, devices).then { [self] registeredDevices -> Promise<Void> in
52+
guard !registeredDevices.contains(where: { $0.id == Current.settingsStore.integrationDeviceID }) else {
53+
// if the integration is registered already, we will take over that one, so we don't need to look
54+
return .value(())
55+
}
56+
57+
// this can be removed once the mobile_app notify service stops being device name specific
58+
return promptForDeviceName(
59+
deviceName: Current.device.deviceName(),
60+
registeredDevices: registeredDevices,
61+
sender: sender
62+
)
63+
}
64+
}
65+
66+
private struct RegisteredDevice {
67+
var name: String
68+
var id: String
69+
70+
init?(data: HAData) throws {
71+
self.name = try data.decode("name")
72+
self.id = try {
73+
let identifiers: [[String]] = try data.decode("identifiers")
74+
for identifier in identifiers {
75+
if identifier.count == 2, identifier.starts(with: ["mobile_app"]) {
76+
return identifier[1]
77+
}
78+
}
79+
80+
throw HADataError.couldntTransform(key: "identifiers")
81+
}()
82+
}
83+
84+
func matches(name other: String) -> Bool {
85+
name.lowercased() == other.lowercased()
86+
}
87+
}
88+
89+
private func promptForDeviceName(
90+
deviceName: String,
91+
registeredDevices: [RegisteredDevice],
92+
sender: UIViewController
93+
) -> Promise<Void> {
94+
guard registeredDevices.contains(where: { $0.matches(name: deviceName) }) else {
95+
// if the device name is not already taken, we can safely use it and don't need to prompt
96+
return .value(())
97+
}
98+
99+
return Promise<Void> { seal in
100+
let alert = UIAlertController(
101+
title: L10n.Onboarding.DeviceNameCheck.Error.title(deviceName),
102+
message: L10n.Onboarding.DeviceNameCheck.Error.prompt,
103+
preferredStyle: .alert
104+
)
105+
106+
alert.addTextField { textField in
107+
textField.keyboardType = .default
108+
textField.placeholder = deviceName
109+
textField.text = deviceName
110+
textField.enablesReturnKeyAutomatically = true
111+
textField.autocapitalizationType = .words
112+
}
113+
114+
alert.addAction(.init(title: L10n.cancelLabel, style: .cancel, handler: { _ in
115+
seal.reject(PMKError.cancelled)
116+
}))
117+
118+
alert.addAction(.init(
119+
title: L10n.Onboarding.DeviceNameCheck.Error.renameAction,
120+
style: .default,
121+
handler: { [self] _ in
122+
let name = alert.textFields?.first?.text?.trimmingCharacters(in: .whitespaces)
123+
124+
guard let name = name, name.isEmpty == false,
125+
!registeredDevices.contains(where: { $0.matches(name: name) }) else {
126+
promptForDeviceName(
127+
deviceName: deviceName,
128+
registeredDevices: registeredDevices,
129+
sender: sender
130+
).pipe(to: seal.resolve)
131+
return
132+
}
133+
134+
Current.settingsStore.overrideDeviceName = name
135+
seal.fulfill(())
136+
}
137+
))
138+
139+
sender.present(alert, animated: true, completion: nil)
140+
}
141+
}
142+
}

Sources/App/Resources/en.lproj/Localizable.strings

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -727,4 +727,7 @@ Home Assistant is free and open source home automation software with a focus on
727727
"widgets.open_page.description" = "Open a frontend page in Home Assistant.";
728728
"widgets.open_page.not_configured" = "No Pages Available";
729729
"widgets.open_page.title" = "Open Page";
730-
"yes_label" = "Yes";
730+
"yes_label" = "Yes";
731+
"onboarding.device_name_check.error.title" = "A device already exists with the name '%1$@'";
732+
"onboarding.device_name_check.error.prompt" = "What device name should be used instead?";
733+
"onboarding.device_name_check.error.rename_action" = "Rename";

Sources/Shared/Resources/Swiftgen/Strings.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -721,6 +721,18 @@ public enum L10n {
721721
public static var description: String { return L10n.tr("Localizable", "onboarding.connection_test_result.client_certificate.description") }
722722
}
723723
}
724+
public enum DeviceNameCheck {
725+
public enum Error {
726+
/// What device name should be used instead?
727+
public static var prompt: String { return L10n.tr("Localizable", "onboarding.device_name_check.error.prompt") }
728+
/// Rename
729+
public static var renameAction: String { return L10n.tr("Localizable", "onboarding.device_name_check.error.rename_action") }
730+
/// A device already exists with the name '%1$@'
731+
public static func title(_ p1: Any) -> String {
732+
return L10n.tr("Localizable", "onboarding.device_name_check.error.title", String(describing: p1))
733+
}
734+
}
735+
}
724736
public enum ManualSetup {
725737
/// Connect
726738
public static var connect: String { return L10n.tr("Localizable", "onboarding.manual_setup.connect") }
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import UIKit
2+
3+
extension UIAlertAction {
4+
typealias Handler = @convention(block) (UIAlertAction) -> Void
5+
6+
var ha_handler: Handler {
7+
// https://stackoverflow.com/questions/36173740/trigger-uialertaction-on-uialertcontroller-programmatically
8+
let block = value(forKey: "handler")
9+
return unsafeBitCast(block as AnyObject, to: Handler.self)
10+
}
11+
}

Tests/App/Auth/OnboardingAuth.test.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class OnboardingAuthTests: XCTestCase {
4343

4444
XCTAssertTrue(pre.contains(.init(OnboardingAuthStepConnectivity.self)))
4545

46+
XCTAssertTrue(post.contains(.init(OnboardingAuthStepDuplicate.self)))
4647
XCTAssertTrue(post.contains(.init(OnboardingAuthStepConfig.self)))
4748
XCTAssertTrue(post.contains(.init(OnboardingAuthStepSensors.self)))
4849
XCTAssertTrue(post.contains(.init(OnboardingAuthStepModels.self)))

0 commit comments

Comments
 (0)