initial commit

This commit is contained in:
Jon Vanvik 2026-05-31 14:11:29 +02:00
commit 8de962eeb8
44 changed files with 4843 additions and 0 deletions

409
DeviceModels.swift Normal file
View file

@ -0,0 +1,409 @@
import Foundation
// MARK: - Enums (tolerant decoding with `.unknown` fallback)
/// Device states for PON Controllers, OLTs, and ONUs (dashboard-level).
enum DeviceState: String, Decodable, CaseIterable {
case online = "Online"
case offline = "Offline"
case unattached = "Unattached"
case preProvisioned = "Pre-provisioned"
case unknown
init(from decoder: Decoder) throws {
let raw = try decoder.singleValueContainer().decode(String.self)
self = DeviceState(rawValue: raw) ?? .unknown
}
}
/// ONU registration state. These are the buckets MCMS keeps on the **OLT-STATE**
/// document under `"ONU States"` not a field on ONU-STATE (dev guide
/// Procedure 2). Raw values match the bucket names exactly.
enum ONULifecycleState: String, Decodable, CaseIterable {
case registered = "Registered"
case unspecified = "Unspecified"
case unprovisioned = "Unprovisioned"
case deregistered = "Deregistered"
case dyingGasp = "Dying Gasp"
case disabled = "Disabled"
case disallowedAdmin = "Disallowed Admin"
case disallowedError = "Disallowed Error"
case disallowedRegID = "Disallowed Reg ID"
case unknown
init(from decoder: Decoder) throws {
let raw = try decoder.singleValueContainer().decode(String.self)
self = ONULifecycleState(rawValue: raw) ?? .unknown
}
/// Per dev guide Procedure 2: an ONU in `Registered` or `Unspecified` is online.
var isOnline: Bool { self == .registered || self == .unspecified }
}
/// Automation roll-up states shown on the dashboard.
enum AutomationState: String, Decodable, CaseIterable {
case done = "Done"
case error = "Error"
case waitingForInput = "Waiting For Input"
case waitingForDevice = "Waiting For Device"
case disabled = "Disabled"
case unknown
init(from decoder: Decoder) throws {
let raw = try decoder.singleValueContainer().decode(String.self)
self = AutomationState(rawValue: raw) ?? .unknown
}
}
/// Syslog-style alarm/severity levels (lower rank = more severe).
enum AlarmSeverity: String, Decodable, CaseIterable, Comparable {
case emergency, alert, critical, error, warning, notice, info, debug
case unknown
var rank: Int {
switch self {
case .emergency: return 0
case .alert: return 1
case .critical: return 2
case .error: return 3
case .warning: return 4
case .notice: return 5
case .info: return 6
case .debug: return 7
case .unknown: return 8
}
}
init(from decoder: Decoder) throws {
let raw = try decoder.singleValueContainer().decode(String.self)
self = AlarmSeverity(rawValue: raw.lowercased()) ?? .unknown
}
static func < (lhs: AlarmSeverity, rhs: AlarmSeverity) -> Bool { lhs.rank < rhs.rank }
}
// MARK: - Flexible documents
//
// Full MongoDB document schemas (ONU-CFG, ONU-STATE, OLT-CFG, CNTL-CFG, ) are
// large and version-specific. Rather than hand-transcribe every field (and risk
// getting it wrong), each document decodes its full body into `raw: JSONValue`
// and exposes a few convenience accessors. The accessor key paths below are
// best-effort guesses RECONCILE THEM WITH `openapi.json` and a live response,
// then promote the fields you actually use into typed properties.
protocol MongoDocument: Decodable, Identifiable {
var raw: JSONValue { get }
}
extension MongoDocument {
var documentId: String? { raw["_id"]?.stringValue }
}
/// ONU state document (`ONU-STATE`).
struct ONUStateDoc: MongoDocument {
let raw: JSONValue
init(from decoder: Decoder) throws { raw = try JSONValue(from: decoder) }
var id: String { raw["_id"]?.stringValue ?? UUID().uuidString }
var serialNumber: String? { raw["_id"]?.stringValue }
/// Parent OLT MAC, from `OLT.MAC Address` (dev guide Procedure 2 §2).
var parentOLT: String? { raw["OLT"]?["MAC Address"]?.stringValue }
var lastUpdate: String? { raw["Time"]?.stringValue }
/// Registration state is **not** carried on ONU-STATE it lives in the
/// OLT-STATE buckets. The list/dashboard view models resolve it via
/// `OLTRepository.registrationMap()` and stamp it here. `nil` until resolved.
var resolvedLifecycle: ONULifecycleState?
var lifecycle: ONULifecycleState { resolvedLifecycle ?? .unknown }
/// Operator-set label from ONU-CFG (`ONU.Name`); resolved + stamped by the
/// list VM via `ONURepository.nameMap()`. `nil` until resolved / if unset.
var resolvedName: String?
/// Best label to show: the operator name if set, else the serial/MAC id.
var displayName: String {
if let name = resolvedName, !name.isEmpty { return name }
return id
}
// Live optical + FEC. Current values live on ONU-STATE.STATS, not on the
// /onus/stats/ time-series (field guide §3.12 / §7).
private func ponStat(_ pon: String, _ key: String) -> Double? {
raw["STATS"]?[pon]?[key]?.doubleValue
}
var rxOpticalDBm: Double? { ponStat("ONU-PON", "RX Optical Level") }
var txOpticalDBm: Double? { ponStat("ONU-PON", "TX Optical Level") }
var onuPreFEC: Double? { ponStat("ONU-PON", "RX Pre-FEC BER") }
var onuPostFEC: Double? { ponStat("ONU-PON", "RX Post-FEC BER") }
var oltPreFEC: Double? { ponStat("OLT-PON", "RX Pre-FEC BER") }
var oltPostFEC: Double? { ponStat("OLT-PON", "RX Post-FEC BER") }
}
/// ONU config document (`ONU-CFG`).
struct ONUConfigDoc: MongoDocument {
let raw: JSONValue
init(from decoder: Decoder) throws { raw = try JSONValue(from: decoder) }
var id: String { raw["_id"]?.stringValue ?? UUID().uuidString }
/// Operator-set label (`ONU.Name`, §5.1).
var name: String? { raw["ONU"]?["Name"]?.stringValue }
var netconfName: String? { raw["Netconf"]?["Name"]?.stringValue }
}
/// OLT config document (`OLT-CFG`).
struct OLTConfigDoc: MongoDocument {
let raw: JSONValue
init(from decoder: Decoder) throws { raw = try JSONValue(from: decoder) }
var id: String { raw["_id"]?.stringValue ?? UUID().uuidString } // MAC address
/// Operator label from `OLT.Name` (confirmed against a live OLT-CFG, e.g.
/// "OLT69-RIN"). Note `NETCONF.Name` defaults to the MAC, so it's *not* the
/// friendly name. Returns nil when unset.
var name: String? {
guard let value = raw["OLT"]?["Name"]?.stringValue, !value.isEmpty else { return nil }
return value
}
}
/// OLT state document (`OLT-STATE`). Carries the per-OLT ONU registration
/// buckets used to resolve each ONU's lifecycle (dev guide Procedure 2).
struct OLTStateDoc: MongoDocument {
let raw: JSONValue
init(from decoder: Decoder) throws { raw = try JSONValue(from: decoder) }
var id: String { raw["_id"]?.stringValue ?? UUID().uuidString } // OLT MAC address
var lastUpdate: String? { raw["Time"]?.stringValue }
/// Operator name from OLT-CFG (`Netconf.Name`); resolved + stamped by the
/// list VM via `OLTRepository.nameMap()`. `nil` until resolved / if unset.
var resolvedName: String?
/// Best label to show: the operator name if set, else the MAC id.
var displayName: String {
if let name = resolvedName, !name.isEmpty { return name }
return id
}
/// `"ONU States"` bucket name [ONU IDs].
var onuStatesByBucket: [String: [String]] {
guard let obj = raw["ONU States"]?.objectValue else { return [:] }
return obj.mapValues { $0.arrayValue?.compactMap(\.stringValue) ?? [] }
}
var totalONUs: Int { onuStatesByBucket.values.reduce(0) { $0 + $1.count } }
/// Online = Registered + Unspecified (dev guide Procedure 2).
var onlineONUs: Int {
onuStatesByBucket.reduce(0) { sum, entry in
(ONULifecycleState(rawValue: entry.key)?.isOnline ?? false) ? sum + entry.value.count : sum
}
}
/// Non-empty registration buckets, ordered by lifecycle severity.
var populatedBuckets: [(state: ONULifecycleState, ids: [String])] {
onuStatesByBucket
.filter { !$0.value.isEmpty }
.map { (state: ONULifecycleState(rawValue: $0.key) ?? .unknown, ids: $0.value.sorted()) }
.sorted {
let order = ONULifecycleState.allCases
return (order.firstIndex(of: $0.state) ?? .max) < (order.firstIndex(of: $1.state) ?? .max)
}
}
}
/// PON Controller config document (`CNTL-CFG`).
struct ControllerConfigDoc: MongoDocument {
let raw: JSONValue
init(from decoder: Decoder) throws { raw = try JSONValue(from: decoder) }
var id: String { raw["_id"]?.stringValue ?? UUID().uuidString } // MAC address
var version: String? { raw["CNTL"]?["Version"]?.stringValue }
}
/// Alarm-history document (`*-ALARM-HIST-STATE`).
struct AlarmHistoryDoc: MongoDocument {
let raw: JSONValue
init(from decoder: Decoder) throws { raw = try JSONValue(from: decoder) }
var id: String { raw["_id"]?.stringValue ?? UUID().uuidString }
// TODO: confirm key paths.
var severity: AlarmSeverity {
guard let s = raw["Severity"]?.stringValue ?? raw["severity"]?.stringValue
else { return .unknown }
return AlarmSeverity(rawValue: s.lowercased()) ?? .unknown
}
var message: String? { raw["Message"]?.stringValue ?? raw["message"]?.stringValue }
var raisedAt: String? { raw["Time"]?.stringValue ?? raw["timestamp"]?.stringValue }
}
/// One statistics sample (`STATS-*`). Timestamp + arbitrary counters.
struct StatSample: MongoDocument {
let raw: JSONValue
init(from decoder: Decoder) throws { raw = try JSONValue(from: decoder) }
var id: String { raw["_id"]?.stringValue ?? UUID().uuidString }
var timestamp: String? { raw["Time"]?.stringValue ?? raw["timestamp"]?.stringValue }
/// Pull a numeric counter by key for charting.
func metric(_ key: String) -> Double? {
switch raw[key] {
case .some(.double(let d)): return d
case .some(.int(let i)): return Double(i)
default: return nil
}
}
}
/// Syslog line (`SYSLOG-*`).
struct LogEntry: MongoDocument {
let raw: JSONValue
init(from decoder: Decoder) throws { raw = try JSONValue(from: decoder) }
var id: String { raw["_id"]?.stringValue ?? UUID().uuidString }
var severity: AlarmSeverity {
guard let s = raw["Severity"]?.stringValue ?? raw["severity"]?.stringValue
else { return .unknown }
return AlarmSeverity(rawValue: s.lowercased()) ?? .unknown
}
var message: String? { raw["Message"]?.stringValue ?? raw["message"]?.stringValue }
var time: String? { raw["Time"]?.stringValue ?? raw["timestamp"]?.stringValue }
}
/// A CPE behind an ONU, with its DHCP lease from the `/cpe/onu/<id>/` helper.
struct CPEState: MongoDocument {
let raw: JSONValue
init(from decoder: Decoder) throws { raw = try JSONValue(from: decoder) }
init(raw: JSONValue) { self.raw = raw }
var id: String { raw["_id"]?.stringValue ?? UUID().uuidString }
var mac: String? { raw["_id"]?.stringValue }
// IPv4 (DHCP)
var dhcpState: String? { nonEmpty(raw["DHCP"]?["Client State"]?.stringValue) }
var ipv4: String? { nonEmpty(raw["DHCP"]?["Client IP Addr"]?.stringValue) }
var dhcpServer: String? { nonEmpty(raw["DHCP"]?["Server ID"]?.stringValue) }
var dhcpLeaseSeconds: Int? { raw["DHCP"]?["Lease Time"]?.intValue }
var circuitId: String? { nonEmpty(raw["DHCP"]?["Circuit ID"]?.stringValue) }
var dhcpLastSuccess: String? { nonEmpty(raw["DHCP"]?["Last Success Time"]?.stringValue) }
// IPv6 (DHCPv6)
var dhcpv6State: String? { nonEmpty(raw["DHCPV6"]?["Client State"]?.stringValue) }
var ipv6: String? { nonEmpty(raw["DHCPV6"]?["Client IP Addr"]?.stringValue) }
var ipv6ValidLifetime: String? { nonEmpty(raw["DHCPV6"]?["Valid Lifetime"]?.stringValue) }
var ipv6Prefix: String? {
guard let entry = raw["DHCPV6"]?["Prefix Delegation"]?.arrayValue?.first,
let prefixObj = entry["Prefixes"]?.arrayValue?.first,
let prefix = prefixObj["Prefix"]?.stringValue,
let len = prefixObj["Prefix Length"]?.intValue else { return nil }
return "\(prefix)/\(len)"
}
var hasLease: Bool { ipv4 != nil || ipv6 != nil }
private func nonEmpty(_ value: String?) -> String? { (value?.isEmpty == false) ? value : nil }
}
// MARK: - ONU-STATE detail accessors (status / firmware / UNI / traffic)
/// One UNI port parsed from a `UNI-ETH N` / `UNI-POTS N` entry on ONU-STATE.
struct UNIPort: Identifiable {
let name: String
let raw: JSONValue
var id: String { name }
var isEthernet: Bool { name.localizedCaseInsensitiveContains("ETH") }
var enabled: Bool? { raw["Enable"]?.boolValue }
var speed: String? { raw["Speed"]?.stringValue }
var duplex: String? { raw["Duplex"]?.stringValue }
/// Resolved link state string on ETH ports, e.g. "10G Ethernet Full Duplex".
var state: String? { raw["State"]?.stringValue }
}
/// Per-service traffic parsed from an `OLT-PON Service N` entry on ONU-STATE.STATS.
struct PONServiceStats: Identifiable {
let name: String
let raw: JSONValue
var id: String { name }
var rxRateBps: Double? { raw["RX Rate bps"]?.doubleValue }
var txRateBps: Double? { raw["TX Rate bps"]?.doubleValue }
var rxTotalOctets: Double? { raw["RX Total Octets"]?.doubleValue }
var txTotalOctets: Double? { raw["TX Total Octets"]?.doubleValue }
}
extension ONUStateDoc {
private var onu: JSONValue? { raw["ONU"] }
// Status / identity
var registrationState: String? { onu?["Registration State"]?.stringValue }
var equipmentId: String? { onu?["Equipment ID"]?.stringValue }
var vendor: String? { onu?["Vendor"]?.stringValue }
var ponMode: String? { onu?["PON Mode"]?.stringValue }
var onlineTime: String? { onu?["Online Time"]?.stringValue }
var temperatureC: Double? { raw["GPON"]?["Temperature"]?.doubleValue }
// Firmware
var fwVersion: String? { onu?["FW Version"]?.stringValue }
var fwBankVersions: [String] { onu?["FW Bank Versions"]?.arrayValue?.compactMap(\.stringValue) ?? [] }
var fwBankPtr: Int? { onu?["FW Bank Ptr"]?.intValue }
/// Non-empty only while an upgrade is in progress / recorded.
var fwUpgradeStatus: [String: JSONValue]? {
let obj = onu?["FW Upgrade Status"]?.objectValue
return (obj?.isEmpty == false) ? obj : nil
}
/// UNI ports, from the top-level `UNI-*` entries.
var uniPorts: [UNIPort] {
guard let obj = raw.objectValue else { return [] }
return obj.keys.filter { $0.hasPrefix("UNI-") }.sorted()
.compactMap { key in obj[key].map { UNIPort(name: key, raw: $0) } }
}
/// Per-service traffic, from `STATS["OLT-PON Service N"]`.
var ponServices: [PONServiceStats] {
guard let stats = raw["STATS"]?.objectValue else { return [] }
return stats.keys.filter { $0.hasPrefix("OLT-PON Service ") }.sorted()
.compactMap { key in stats[key].map { PONServiceStats(name: key, raw: $0) } }
}
}
// MARK: - OLT-STATE detail accessors (uptime / traffic / environment / uplink)
extension OLTStateDoc {
private var olt: JSONValue? { raw["OLT"] }
private var ponStats: JSONValue? { raw["STATS"]?["OLT-PON"] }
var onlineTime: String? { olt?["Online Time"]?.stringValue }
var firmwareVersion: String? { olt?["FW Version"]?.stringValue }
var serialNumber: String? { olt?["Serial Number"]?.stringValue }
var model: String? { olt?["Model"]?.stringValue }
var ponMode: String? { olt?["PON Mode"]?.stringValue }
var laserShutdown: String? { olt?["Laser Shutdown"]?.stringValue }
/// True when the OLT laser isn't "Laser ON" (shut down / inactive). Only
/// meaningful when the field is present.
var isLaserOff: Bool {
guard let value = laserShutdown else { return false }
return value.caseInsensitiveCompare("Laser ON") != .orderedSame
}
var statsTotalONUs: Int? { ponStats?["Total ONUs Count"]?.intValue }
var statsOnlineONUs: Int? { ponStats?["Online ONUs Count"]?.intValue }
var statsOfflineONUs: Int? { ponStats?["Offline ONUs Count"]?.intValue }
var rxUtilPercent: Int? { ponStats?["RX BW Total Util"]?.intValue }
var txUtilPercent: Int? { ponStats?["TX BW Total Util"]?.intValue }
var rxRateBps: Double? { ponStats?["RX BW Ethernet Rate bps"]?.doubleValue }
var txRateBps: Double? { ponStats?["TX BW Ethernet Rate bps"]?.doubleValue }
var asicTempC: Int? { raw["STATS"]?["OLT-TEMP"]?["ASIC"]?.intValue }
var voltage: Double? { raw["STATS"]?["OLT-ENV"]?["Voltage"]?.doubleValue }
// Upstream switch (LLDP neighbour).
var switchName: String? { nonEmpty(raw["Switch"]?["System Name"]?.stringValue) }
var switchPort: String? { nonEmpty(raw["Switch"]?["Port ID"]?.stringValue) }
var switchAddress: String? { nonEmpty(raw["Switch"]?["IPv4 Address"]?.stringValue) }
private func nonEmpty(_ s: String?) -> String? { (s?.isEmpty == false) ? s : nil }
}
// MARK: - OLT action payloads
/// Body for `PUT /olts/<OLT>/disable-onu/<ONU>/` sent WITHOUT the data wrapper.
struct DisableONURequest: Encodable {
let disable: Bool
}
/// OLT action responses return `{ "Pending": true|false }`.
struct PendingStatus: Decodable {
let pending: Bool
enum CodingKeys: String, CodingKey { case pending = "Pending" }
}