initial commit
This commit is contained in:
commit
8de962eeb8
44 changed files with 4843 additions and 0 deletions
409
DeviceModels.swift
Normal file
409
DeviceModels.swift
Normal 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" }
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue