@@ -19,6 +19,8 @@ private struct bleDevice {
1919 var address : String
2020 var uuid : UUID ?
2121 var batteryLevel : [ KeyValue_t ]
22+ var vendorId : Int ? = nil
23+ var productId : Int ? = nil
2224}
2325
2426private struct ioDevice {
@@ -86,7 +88,9 @@ internal class DevicesReader: Reader<[BLEDevice]>, CBCentralManagerDelegate, CBP
8688 self . devices [ idx] . batteryLevel = data. batteryLevel
8789 self . devices [ idx] . isPaired = device. isPaired
8890 self . devices [ idx] . isConnected = device. isConnected
89-
91+ if self . devices [ idx] . vendorId == nil { self . devices [ idx] . vendorId = data. vendorId }
92+ if self . devices [ idx] . productId == nil { self . devices [ idx] . productId = data. productId }
93+
9094 return
9195 }
9296
@@ -97,7 +101,9 @@ internal class DevicesReader: Reader<[BLEDevice]>, CBCentralManagerDelegate, CBP
97101 RSSI: rssi,
98102 batteryLevel: data. batteryLevel,
99103 isConnected: device. isConnected,
100- isPaired: device. isPaired
104+ isPaired: device. isPaired,
105+ vendorId: data. vendorId,
106+ productId: data. productId
101107 ) )
102108 }
103109
@@ -150,18 +156,18 @@ internal class DevicesReader: Reader<[BLEDevice]>, CBCentralManagerDelegate, CBP
150156
151157 if !pmsetName. isEmpty,
152158 let idx = self . devices. firstIndex ( where: {
153- $0. name. trimmingCharacters ( in: . whitespacesAndNewlines) . lowercased ( ) == pmsetName
159+ let deviceName = $0. name. trimmingCharacters ( in: . whitespacesAndNewlines) . lowercased ( )
160+ return deviceName == pmsetName || deviceName. contains ( pmsetName) || pmsetName. contains ( deviceName)
154161 } ) {
155162 if !p. batteryLevel. isEmpty {
156163 self . devices [ idx] . batteryLevel = p. batteryLevel
157164 }
158165 return
159166 }
160167
161- if !p . address . isEmpty ,
168+ if let pVendor = p . vendorId , let pProduct = p . productId ,
162169 let idx = self . devices. firstIndex ( where: {
163- !$0. address. isEmpty &&
164- $0. address. caseInsensitiveCompare ( p. address) == . orderedSame
170+ $0. vendorId == pVendor && $0. productId == pProduct
165171 } ) {
166172 if !p. batteryLevel. isEmpty {
167173 self . devices [ idx] . batteryLevel = p. batteryLevel
@@ -176,7 +182,9 @@ internal class DevicesReader: Reader<[BLEDevice]>, CBCentralManagerDelegate, CBP
176182 RSSI: 100 ,
177183 batteryLevel: p. batteryLevel,
178184 isConnected: true ,
179- isPaired: false
185+ isPaired: false ,
186+ vendorId: p. vendorId,
187+ productId: p. productId
180188 ) )
181189 }
182190
@@ -205,7 +213,9 @@ internal class DevicesReader: Reader<[BLEDevice]>, CBCentralManagerDelegate, CBP
205213 address = addr
206214 }
207215
208- list. append ( bleDevice ( name: name, address: address, uuid: nil , batteryLevel: [ KeyValue_t ( key: " battery " , value: " \( batteryPercent) " ) ] ) )
216+ let vendorId = d. object ( forKey: " VendorID " ) as? Int
217+ let productId = d. object ( forKey: " ProductID " ) as? Int
218+ list. append ( bleDevice ( name: name, address: address, uuid: nil , batteryLevel: [ KeyValue_t ( key: " battery " , value: " \( batteryPercent) " ) ] , vendorId: vendorId, productId: productId) )
209219 }
210220
211221 return list
@@ -371,119 +381,122 @@ internal class DevicesReader: Reader<[BLEDevice]>, CBCentralManagerDelegate, CBP
371381
372382 // MARK: - PMSET data
373383 private func pmsetAccessoryLevels( ) -> [ bleDevice ] {
374- guard let res = process ( path: " /usr/bin/pmset " , arguments: [ " -g " , " accps " ] ) else { return [ ] }
384+ guard let res = process ( path: " /usr/bin/pmset " , arguments: [ " -g " , " accps " , " -xml " ] ) else { return [ ] }
375385
376- struct Entry {
377- let originalName : String
378- let normalizedName : String
379- let percent : Int
380- let id : String
381- let isCase : Bool
382- let state : String ? // "charging" | "discharging"
383- }
386+ let plists = res. components ( separatedBy: " <?xml " )
387+ . filter { !$0. trimmingCharacters ( in: . whitespacesAndNewlines) . isEmpty }
388+ . compactMap { chunk -> [ String : Any ] ? in
389+ let xml = " <?xml " + chunk
390+ guard let data = xml. data ( using: . utf8) ,
391+ let plist = try ? PropertyListSerialization . propertyList ( from: data, format: nil ) as? [ String : Any ] else {
392+ return nil
393+ }
394+ return plist
395+ }
384396
385- var grouped : [ String : [ Entry ] ] = [ : ]
386- var displayNameForGroup : [ String : String ] = [ : ]
397+ struct PmsetEntry {
398+ let name : String
399+ let capacity : Int
400+ let accessoryIdentifier : String
401+ let partIdentifier : String ?
402+ let groupIdentifier : String ?
403+ let category : String ?
404+ let isCharging : Bool
405+ let vendorId : Int ?
406+ let productId : Int ?
407+ let combinedParts : [ [ String : Any ] ] ?
408+ }
387409
388- for raw in res. components ( separatedBy: . newlines) {
389- let line = raw. trimmingCharacters ( in: . whitespacesAndNewlines)
390- guard line. hasPrefix ( " - " ) , let tabIdx = line. firstIndex ( of: " \t " ) else { continue }
391-
392- var namePart = String ( line [ line. index ( after: line. startIndex) ..< tabIdx] ) . trimmingCharacters ( in: . whitespaces)
393-
394- var parsedID = " "
395- if let idMatch = namePart. range ( of: #"(?<=\(id=)\d+(?=\))"# , options: . regularExpression) {
396- parsedID = String ( namePart [ idMatch] )
397- }
398- if let idRange = namePart. range ( of: #"\s*\(id=\d+\)$"# , options: . regularExpression) {
399- namePart. removeSubrange ( idRange)
400- }
401- guard !namePart. isEmpty else { continue }
410+ var entries : [ PmsetEntry ] = [ ]
411+ for dict in plists {
412+ guard let name = dict [ " Name " ] as? String ,
413+ let capacity = dict [ " Current Capacity " ] as? Int ,
414+ let accessoryId = dict [ " Accessory Identifier " ] as? String else { continue }
402415
403- let details = String ( line [ line. index ( after: tabIdx) ... ] ) . trimmingCharacters ( in: . whitespaces)
404- guard let first = details. split ( separator: " ; " ) . first else { continue }
405-
406- let pctString = first. replacingOccurrences ( of: " % " , with: " " ) . trimmingCharacters ( in: . whitespaces)
407- guard let pct = Int ( pctString) else { continue }
408-
409- let normalized = namePart. lowercased ( )
410- let isCase = normalized. contains ( " etui " ) || normalized. contains ( " case " )
411-
412- if !isCase && details. range ( of: #"\bremaining\b"# , options: . regularExpression) != nil {
413- continue
414- }
415-
416- let groupKey = normalized
417- . replacingOccurrences ( of: #"^\s*(etui|case)\s+"# , with: " " , options: . regularExpression)
418- . trimmingCharacters ( in: . whitespaces)
419-
420- let state : String ?
421- if details. range ( of: #"\bcharging\b"# , options: . regularExpression) != nil {
422- state = " charging "
423- } else if details. range ( of: #"\bdischarging\b"# , options: . regularExpression) != nil {
424- state = " discharging "
416+ let isCharging : Bool
417+ if let charging = dict [ " Is Charging " ] as? Bool {
418+ isCharging = charging
419+ } else if let state = dict [ " Power Source State " ] as? String {
420+ isCharging = state == " AC Power "
425421 } else {
426- state = nil
422+ isCharging = false
427423 }
428424
429- grouped [ groupKey, default: [ ] ] . append ( Entry (
430- originalName: namePart,
431- normalizedName: normalized,
432- percent: pct,
433- id: parsedID,
434- isCase: isCase,
435- state: state
425+ entries. append ( PmsetEntry (
426+ name: name,
427+ capacity: capacity,
428+ accessoryIdentifier: accessoryId,
429+ partIdentifier: dict [ " Part Identifier " ] as? String ,
430+ groupIdentifier: dict [ " Group Identifier " ] as? String ,
431+ category: dict [ " Accessory Category " ] as? String ,
432+ isCharging: isCharging,
433+ vendorId: dict [ " Vendor ID " ] as? Int ,
434+ productId: dict [ " Product ID " ] as? Int ,
435+ combinedParts: dict [ " Combined Parts " ] as? [ [ String : Any ] ]
436436 ) )
437-
438- if displayNameForGroup [ groupKey] == nil {
439- let display = namePart
440- . replacingOccurrences ( of: #"^\s*(?i:etui|case)\s+"# , with: " " , options: . regularExpression)
441- . trimmingCharacters ( in: . whitespaces)
442- displayNameForGroup [ groupKey] = display
437+ }
438+
439+ var grouped : [ String : [ PmsetEntry ] ] = [ : ]
440+ var standalone : [ PmsetEntry ] = [ ]
441+ for entry in entries {
442+ if let groupId = entry. groupIdentifier {
443+ grouped [ groupId, default: [ ] ] . append ( entry)
444+ } else {
445+ standalone. append ( entry)
443446 }
444447 }
445448
446449 var out : [ bleDevice ] = [ ]
447450
448- for (groupKey, entries) in grouped {
449- let displayName = displayNameForGroup [ groupKey] ?? entries. first? . originalName ?? groupKey
451+ for entry in standalone {
452+ let state = entry. isCharging ? " charging " : " discharging "
453+ out. append ( bleDevice (
454+ name: entry. name,
455+ address: entry. accessoryIdentifier,
456+ uuid: nil ,
457+ batteryLevel: [ KeyValue_t ( key: " battery " , value: " \( entry. capacity) " , additional: state) ] ,
458+ vendorId: entry. vendorId,
459+ productId: entry. productId
460+ ) )
461+ }
462+
463+ for (_, group) in grouped {
464+ let combinedEntry = group. first ( where: { $0. partIdentifier == " Combined " } )
465+ let caseEntry = group. first ( where: { $0. partIdentifier == " Case " || $0. category == " Audio Battery Case " } )
466+ let displayName = combinedEntry? . name ?? group. first ( where: { !( $0. category ?? " " ) . contains ( " Case " ) } ) ? . name ?? group. first? . name ?? " "
467+ let accessoryId = combinedEntry? . accessoryIdentifier ?? group. first? . accessoryIdentifier ?? " "
468+
450469 var kv : [ KeyValue_t ] = [ ]
451470
452- if entries. count == 1 , let e = entries. first {
453- kv = [ KeyValue_t ( key: " battery " , value: " \( e. percent) " , additional: e. state) ]
454- } else {
455- if let c = entries. first ( where: { $0. isCase } ) {
456- kv. append ( KeyValue_t ( key: " case " , value: " \( c. percent) " , additional: c. state) )
457- }
458-
459- let buds = entries
460- . filter { !$0. isCase }
461- . sorted { lhs, rhs in
462- let li = Int ( lhs. id) ?? Int . max
463- let ri = Int ( rhs. id) ?? Int . max
464- if li != ri { return li < ri }
465- return lhs. id < rhs. id
466- }
467-
468- if buds. count >= 1 {
469- kv. append ( KeyValue_t ( key: " first " , value: " \( buds [ 0 ] . percent) " , additional: buds [ 0 ] . state) )
470- }
471- if buds. count >= 2 {
472- kv. append ( KeyValue_t ( key: " second " , value: " \( buds [ 1 ] . percent) " , additional: buds [ 1 ] . state) )
473- }
474-
475- if kv. isEmpty, let first = entries. first {
476- kv = [ KeyValue_t ( key: " battery " , value: " \( first. percent) " , additional: first. state) ]
471+ if let c = caseEntry {
472+ let state = c. isCharging ? " charging " : " discharging "
473+ kv. append ( KeyValue_t ( key: " case " , value: " \( c. capacity) " , additional: state) )
474+ }
475+
476+ if let parts = combinedEntry? . combinedParts {
477+ for part in parts {
478+ guard let partId = part [ " Part Identifier " ] as? String ,
479+ let cap = part [ " Current Capacity " ] as? Int else { continue }
480+ let charging = ( part [ " Is Charging " ] as? Bool ) ?? false
481+ let state = charging ? " charging " : " discharging "
482+ kv. append ( KeyValue_t ( key: partId. lowercased ( ) , value: " \( cap) " , additional: state) )
477483 }
478484 }
479485
480- let mergedAddress = entries. map { $0. id } . sorted ( ) . joined ( separator: " x " )
486+ if kv. isEmpty, let e = combinedEntry ?? group. first {
487+ let state = e. isCharging ? " charging " : " discharging "
488+ kv. append ( KeyValue_t ( key: " battery " , value: " \( e. capacity) " , additional: state) )
489+ }
481490
491+ let vendorId = combinedEntry? . vendorId ?? group. first? . vendorId
492+ let productId = combinedEntry? . productId ?? group. first? . productId
482493 out. append ( bleDevice (
483494 name: displayName,
484- address: mergedAddress ,
495+ address: accessoryId ,
485496 uuid: nil ,
486- batteryLevel: kv
497+ batteryLevel: kv,
498+ vendorId: vendorId,
499+ productId: productId
487500 ) )
488501 }
489502
0 commit comments