mirror of
https://github.com/home-assistant/iOS.git
synced 2026-02-16 18:46:05 -06:00
## Summary BSSID sensor reported MAC addresses without leading zeros (e.g., `18:e8:29:a7:e9:b`), inconsistent with Android companion app and standard MAC address notation. This breaks cross-platform room presence tracking. **Changes:** - Added `String.formattedBSSID` extension that pads hex octets to 2 characters - Applied formatting to BSSID sensor state in `ConnectivitySensor` - Added test coverage for formatting edge cases ```swift // Before: "18:e8:29:a7:e9:b" // After: "18:e8:29:a7:e9:0b" sensor.State = bssid.formattedBSSID ``` ## Screenshots N/A - Sensor value formatting change only ## Link to pull request in Documentation repository Documentation: home-assistant/companion.home-assistant# ## Any other notes **Breaking change:** Users with BSSID-based automations must update to the new zero-padded format. This aligns iOS with Android, standard MAC notation, and network management tools like UniFi. <!-- START COPILOT ORIGINAL PROMPT --> <details> <summary>Original prompt</summary> > > ---- > > *This section details on the original issue you should resolve* > > <issue_title>BSSID mac address format isn't consistant with Android Companion app</issue_title> > <issue_description>**iOS device model, version and app version** > <!-- Please include your device 'Model Name' and 'Software Version' as listed in iOS Settings>General>About. Please also give the app version listed beneath "Home Assistant Companion" in the App Configuration>About menu within the app, please include the number in brackets --> > > Model Name: IPhone 6S > Software Version: IOS 14.4.1 > App version: 2021.77 > > **Home Assistant Core Version** > version | core-2021.3.4 > -- | -- > installation_type | Home Assistant Container > dev | false > hassio | false > docker | true > virtualenv | false > python_version | 3.8.7 > os_name | Linux > os_version | 5.3.18-3-pve > arch | x86_64 > timezone | Europe/Paris > > <details><summary>Home Assistant Community Store</summary> > > GitHub API | ok > -- | -- > Github API Calls Remaining | 4998 > Installed Version | 1.11.0 > Stage | running > Available Repositories | 850 > Installed Repositories | 8 > > </details> > > <details><summary>Home Assistant Cloud</summary> > > logged_in | false > -- | -- > can_reach_cert_server | ok > can_reach_cloud_auth | ok > can_reach_cloud | ok > > </details> > > <details><summary>Lovelace</summary> > > dashboards | 9 > -- | -- > resources | 6 > views | 31 > mode | storage > > </details> > > **Describe the bug** > The BSSID sensor reports access point mac address with leading zeros omitted > example: > > ``` > 18:e8:29:a7:e9:b > ``` > The last number is 0x0b > > An adroid device connected the the same access point returns > > ``` > 18:e8:29:a7:e9:0b > ``` > **To Reproduce** > You need to have an access point with mac bytes lower then 0x10 > > **Expected behavior** > Having same BSSID representation than reported by Android App companion > > **Screenshots** > Iphone: >  > > Android phone >  > > > **Additional context** > I'm using BSSID to track room presence based on connected AP. I'm trying to share BSSID --> access point for all household mobile. > > I'd prefer the version with leading 0 witch is aligned with Unify management tool. > > </issue_description> > > ## Comments on the Issue (you are @copilot in this section) > > <comments> > <comment_new><author>@zacwest</author><body> > We're just slurping the value we read from the system, so we'd need to sanitize the input to change it. It would also be a breaking change for modifying what values are sent up, which is less-than-ideal since users get auto-updated in the apps. I'm not sure there's a great path forward for resolving this.</body></comment_new> > </comments> > </details> <!-- START COPILOT CODING AGENT SUFFIX --> - Fixes home-assistant/iOS#1563 <!-- START COPILOT CODING AGENT TIPS --> --- 💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more [Copilot coding agent tips](https://gh.io/copilot-coding-agent-tips) in the docs. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: bgoncal <5808343+bgoncal@users.noreply.github.com>
265 lines
8.7 KiB
Swift
265 lines
8.7 KiB
Swift
import Foundation
|
|
import PromiseKit
|
|
#if os(iOS)
|
|
import CoreTelephony
|
|
import Reachability
|
|
#endif
|
|
|
|
final class ConnectivitySensorUpdateSignaler: SensorProviderUpdateSignaler, SensorObserver {
|
|
private var isObserving = false
|
|
let signal: () -> Void
|
|
|
|
#if DEBUG
|
|
/// Used for unit test to identify when observation is ready
|
|
var notifyObservation: (() -> Void)?
|
|
#endif
|
|
|
|
init(signal: @escaping () -> Void) {
|
|
self.signal = signal
|
|
Current.sensors.register(observer: self)
|
|
}
|
|
|
|
@objc private func connectivityDidChange(_ note: Notification) {
|
|
signal()
|
|
}
|
|
|
|
private func observe() {
|
|
guard !isObserving else { return }
|
|
NotificationCenter.default.addObserver(
|
|
self,
|
|
selector: #selector(connectivityDidChange(_:)),
|
|
name: Current.connectivity.connectivityDidChangeNotification(),
|
|
object: nil
|
|
)
|
|
isObserving = true
|
|
|
|
#if DEBUG
|
|
notifyObservation?()
|
|
#endif
|
|
}
|
|
|
|
private func stopObserving() {
|
|
guard isObserving else { return }
|
|
NotificationCenter.default.removeObserver(
|
|
self,
|
|
name: Current.connectivity.connectivityDidChangeNotification(),
|
|
object: nil
|
|
)
|
|
isObserving = false
|
|
}
|
|
|
|
func sensorContainer(_ container: SensorContainer, didUpdate update: SensorObserverUpdate) {
|
|
update.sensors.done { [weak self] sensors in
|
|
let activeRelatedSensors = sensors.filter({ sensor in
|
|
sensor.UniqueID == WebhookSensorId.connectivitySSID.rawValue ||
|
|
sensor.UniqueID == WebhookSensorId.connectivityBSID.rawValue ||
|
|
sensor.UniqueID == WebhookSensorId.connectivityConnectionType.rawValue
|
|
})
|
|
|
|
let activeSensors = activeRelatedSensors.filter({ sensor in
|
|
Current.sensors.isEnabled(sensor: sensor)
|
|
})
|
|
|
|
if activeSensors.isEmpty {
|
|
self?.stopObserving()
|
|
} else {
|
|
self?.observe()
|
|
}
|
|
}
|
|
}
|
|
|
|
func sensorContainer(
|
|
_ container: SensorContainer,
|
|
didSignalForUpdateBecause reason: SensorContainerUpdateReason,
|
|
lastUpdate: SensorObserverUpdate?
|
|
) {
|
|
/* no-op */
|
|
}
|
|
}
|
|
|
|
public class ConnectivitySensor: SensorProvider {
|
|
public enum ConnectivityError: Error {
|
|
case unsupportedPlatform
|
|
case noCarriers
|
|
}
|
|
|
|
public let request: SensorProviderRequest
|
|
public required init(request: SensorProviderRequest) {
|
|
self.request = request
|
|
}
|
|
|
|
public func sensors() -> Promise<[WebhookSensor]> {
|
|
#if os(iOS)
|
|
let sensors: Promise<[WebhookSensor]> = firstly { () -> Guarantee<[Result<[WebhookSensor]>]> in
|
|
var sensors = [Promise<[WebhookSensor]>]()
|
|
|
|
sensors.append(ssid())
|
|
sensors.append(connectionType())
|
|
#if !targetEnvironment(macCatalyst)
|
|
sensors.append(cellularProviders())
|
|
#endif
|
|
|
|
return when(resolved: sensors)
|
|
}.map { sensors -> [WebhookSensor] in
|
|
sensors.compactMap { (result: Result<[WebhookSensor]>) -> [WebhookSensor]? in
|
|
if case let .fulfilled(value) = result {
|
|
return value
|
|
} else {
|
|
return nil
|
|
}
|
|
}.flatMap { $0 }
|
|
}
|
|
|
|
// Set up our observer
|
|
let _: ConnectivitySensorUpdateSignaler = request.dependencies.updateSignaler(for: self)
|
|
|
|
return sensors
|
|
#else
|
|
return .init(error: ConnectivityError.unsupportedPlatform)
|
|
#endif
|
|
}
|
|
|
|
#if os(iOS)
|
|
|
|
private func ssid() -> Promise<[WebhookSensor]> {
|
|
guard Current.connectivity.hasWiFi() else {
|
|
return .init(error: ConnectivityError.unsupportedPlatform)
|
|
}
|
|
|
|
return .value([
|
|
with(WebhookSensor(name: "SSID", uniqueID: WebhookSensorId.connectivitySSID.rawValue)) { sensor in
|
|
if let ssid = Current.connectivity.currentWiFiSSID() {
|
|
sensor.State = ssid
|
|
sensor.Icon = "mdi:wifi"
|
|
} else {
|
|
sensor.State = "Not Connected"
|
|
sensor.Icon = "mdi:wifi-off"
|
|
}
|
|
},
|
|
with(WebhookSensor(name: "BSSID", uniqueID: WebhookSensorId.connectivityBSID.rawValue)) { sensor in
|
|
if let bssid = Current.connectivity.currentWiFiBSSID() {
|
|
sensor.State = bssid.formattedBSSID
|
|
sensor.Icon = "mdi:wifi-star"
|
|
} else {
|
|
sensor.State = "Not Connected"
|
|
sensor.Icon = "mdi:wifi-off"
|
|
}
|
|
},
|
|
])
|
|
}
|
|
|
|
#endif
|
|
|
|
#if os(iOS)
|
|
|
|
private func connectionType() -> Promise<[WebhookSensor]> {
|
|
let simple = Current.connectivity.simpleNetworkType()
|
|
|
|
return .value([
|
|
with(WebhookSensor(
|
|
name: "Connection Type",
|
|
uniqueID: WebhookSensorId.connectivityConnectionType.rawValue
|
|
)) { sensor in
|
|
sensor.State = simple.description
|
|
sensor.Icon = simple.icon
|
|
|
|
var attributes = Current.connectivity.networkAttributes()
|
|
|
|
if case .cellular = simple {
|
|
let cellular = Current.connectivity.cellularNetworkType()
|
|
attributes["Cellular Technology"] = cellular.description
|
|
}
|
|
|
|
sensor.Attributes = attributes
|
|
},
|
|
])
|
|
}
|
|
|
|
#if !targetEnvironment(macCatalyst)
|
|
private func cellularProviders() -> Promise<[WebhookSensor]> {
|
|
let networkInfo = Current.connectivity.telephonyCarriers()
|
|
let radioTech = Current.connectivity.telephonyRadioAccessTechnology()
|
|
|
|
if let networkInfo {
|
|
return when(fulfilled: networkInfo.map {
|
|
carrierSensor(
|
|
carrier: $0.value,
|
|
radioTech: radioTech?[$0.key],
|
|
key: $0.key
|
|
)
|
|
})
|
|
} else {
|
|
return .init(error: ConnectivityError.noCarriers)
|
|
}
|
|
}
|
|
|
|
private func carrierSensor(
|
|
carrier: CTCarrier,
|
|
radioTech: String?,
|
|
key: String
|
|
) -> Guarantee<WebhookSensor> {
|
|
let sensor: WebhookSensor
|
|
|
|
let id = key.last ?? "?"
|
|
sensor = WebhookSensor(
|
|
name: "SIM \(id)",
|
|
uniqueID: "connectivity_sim_\(id)",
|
|
icon: "mdi:sim",
|
|
state: "Unknown"
|
|
)
|
|
|
|
sensor.State = carrier.carrierName ?? "N/A"
|
|
sensor.Attributes = [
|
|
"Carrier ID": key,
|
|
"Carrier Name": carrier.carrierName ?? "N/A",
|
|
"Mobile Country Code": carrier.mobileCountryCode ?? "N/A",
|
|
"Mobile Network Code": carrier.mobileNetworkCode ?? "N/A",
|
|
"ISO Country Code": carrier.isoCountryCode ?? "N/A",
|
|
"Allows VoIP": carrier.allowsVOIP,
|
|
]
|
|
|
|
if let radioTech {
|
|
sensor.Attributes?["Current Radio Technology"] = Self.getRadioTechName(radioTech)
|
|
}
|
|
|
|
return .value(sensor)
|
|
}
|
|
|
|
private static func getRadioTechName(_ radioTech: String) -> String? {
|
|
switch radioTech {
|
|
case CTRadioAccessTechnologyGPRS:
|
|
return "General Packet Radio Service (GPRS)"
|
|
case CTRadioAccessTechnologyEdge:
|
|
return "Enhanced Data rates for GSM Evolution (EDGE)"
|
|
case CTRadioAccessTechnologyCDMA1x:
|
|
return "Code Division Multiple Access (CDMA 1X)"
|
|
case CTRadioAccessTechnologyWCDMA:
|
|
return "Wideband Code Division Multiple Access (WCDMA)"
|
|
case CTRadioAccessTechnologyHSDPA:
|
|
return "High Speed Downlink Packet Access (HSDPA)"
|
|
case CTRadioAccessTechnologyHSUPA:
|
|
return "High Speed Uplink Packet Access (HSUPA)"
|
|
case CTRadioAccessTechnologyCDMAEVDORev0:
|
|
return "Code Division Multiple Access Evolution-Data Optimized Revision 0 (CDMA EV-DO Rev. 0)"
|
|
case CTRadioAccessTechnologyCDMAEVDORevA:
|
|
return "Code Division Multiple Access Evolution-Data Optimized Revision A (CDMA EV-DO Rev. A)"
|
|
case CTRadioAccessTechnologyCDMAEVDORevB:
|
|
return "Code Division Multiple Access Evolution-Data Optimized Revision B (CDMA EV-DO Rev. B)"
|
|
case CTRadioAccessTechnologyeHRPD:
|
|
return "High Rate Packet Data (HRPD)"
|
|
case CTRadioAccessTechnologyLTE:
|
|
return "Long-Term Evolution (LTE)"
|
|
default:
|
|
switch radioTech {
|
|
case CTRadioAccessTechnologyNR:
|
|
return "5G"
|
|
case CTRadioAccessTechnologyNRNSA:
|
|
return "5G Non-Standalone"
|
|
default: return nil
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
#endif
|
|
}
|