Files
iOS/Sources/Shared/API/Webhook/Sensors/ConnectivitySensor.swift
2025-07-14 23:08:37 +02:00

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
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
}