devices.go 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578
  1. package store
  2. import (
  3. "context"
  4. "database/sql"
  5. "errors"
  6. "fmt"
  7. "strings"
  8. "time"
  9. )
  10. type contextExecer interface {
  11. ExecContext(context.Context, string, ...any) (sql.Result, error)
  12. }
  13. func (s *Store) UpsertDevice(ctx context.Context, value Device) error {
  14. return upsertDevice(ctx, s.db, value)
  15. }
  16. // SaveDeviceState stores configuration and the supplied runtime snapshots in
  17. // one transaction. A nil runtime leaves that snapshot untouched.
  18. func (s *Store) SaveDeviceState(
  19. ctx context.Context,
  20. value Device,
  21. runtime *DeviceRuntime,
  22. vowifi *VoWiFiRuntime,
  23. ) error {
  24. tx, err := s.db.BeginTx(ctx, nil)
  25. if err != nil {
  26. return fmt.Errorf("begin device state update: %w", err)
  27. }
  28. defer tx.Rollback()
  29. if err := upsertDevice(ctx, tx, value); err != nil {
  30. return err
  31. }
  32. if runtime != nil {
  33. snapshot := *runtime
  34. if snapshot.DeviceID == "" {
  35. snapshot.DeviceID = value.ID
  36. }
  37. if snapshot.DeviceID != value.ID {
  38. return errors.New("device runtime belongs to a different device")
  39. }
  40. if err := upsertDeviceRuntime(ctx, tx, snapshot); err != nil {
  41. return err
  42. }
  43. }
  44. if vowifi != nil {
  45. snapshot := *vowifi
  46. if snapshot.DeviceID == "" {
  47. snapshot.DeviceID = value.ID
  48. }
  49. if snapshot.DeviceID != value.ID {
  50. return errors.New("VoWiFi runtime belongs to a different device")
  51. }
  52. if err := upsertVoWiFiRuntime(ctx, tx, snapshot); err != nil {
  53. return err
  54. }
  55. }
  56. if err := tx.Commit(); err != nil {
  57. return fmt.Errorf("commit device state update: %w", err)
  58. }
  59. return nil
  60. }
  61. func upsertDevice(ctx context.Context, executor contextExecer, value Device) error {
  62. value.ID = strings.TrimSpace(value.ID)
  63. value.Name = strings.TrimSpace(value.Name)
  64. if value.ID == "" {
  65. return errors.New("device id is required")
  66. }
  67. if value.Name == "" {
  68. return errors.New("device name is required")
  69. }
  70. if value.ProxyPort < 0 || value.ProxyPort > 65535 {
  71. return errors.New("device proxy port must be between 0 and 65535")
  72. }
  73. if value.BaudRate == 0 {
  74. value.BaudRate = 115200
  75. }
  76. if value.BaudRate < 1 {
  77. return errors.New("device baud rate must be positive")
  78. }
  79. if value.DataBits == 0 {
  80. value.DataBits = 8
  81. }
  82. if value.DataBits < 5 || value.DataBits > 8 {
  83. return errors.New("device data bits must be between 5 and 8")
  84. }
  85. if value.StopBits == 0 {
  86. value.StopBits = 1
  87. }
  88. if value.StopBits != 1 && value.StopBits != 2 {
  89. return errors.New("device stop bits must be 1 or 2")
  90. }
  91. if value.Parity == "" {
  92. value.Parity = "none"
  93. }
  94. value.Parity = strings.ToLower(strings.TrimSpace(value.Parity))
  95. switch value.Parity {
  96. case "none", "even", "odd", "mark", "space":
  97. default:
  98. return fmt.Errorf("unsupported device parity %q", value.Parity)
  99. }
  100. if value.DeviceBackend == "" {
  101. value.DeviceBackend = "at"
  102. }
  103. value.DeviceBackend = strings.ToLower(strings.TrimSpace(value.DeviceBackend))
  104. if value.DeviceBackend != "at" && value.DeviceBackend != "qmi" {
  105. return fmt.Errorf("unsupported device backend %q", value.DeviceBackend)
  106. }
  107. if value.ESIMTransport == "" {
  108. value.ESIMTransport = "at"
  109. }
  110. value.ESIMTransport = strings.ToLower(strings.TrimSpace(value.ESIMTransport))
  111. if value.ESIMTransport != "at" && value.ESIMTransport != "qmi" {
  112. return fmt.Errorf("unsupported eSIM transport %q", value.ESIMTransport)
  113. }
  114. extra, err := normalizeJSONObject(value.Extra)
  115. if err != nil {
  116. return fmt.Errorf("normalize device extra data: %w", err)
  117. }
  118. now := time.Now().UTC()
  119. createdAt := value.CreatedAt
  120. if createdAt.IsZero() {
  121. createdAt = now
  122. }
  123. updatedAt := value.UpdatedAt
  124. if updatedAt.IsZero() {
  125. updatedAt = now
  126. }
  127. _, err = executor.ExecContext(ctx, `
  128. INSERT INTO devices (
  129. id, name, interface, control_device, at_port, usb_path,
  130. audio_device, modem_imei, apn, proxy_port, baud_rate,
  131. data_bits, stop_bits, parity, device_backend, esim_transport,
  132. qmi_use_proxy, qmi_proxy_path, qmi_proxy_executable,
  133. network_enabled, sms_enabled, vowifi_enabled, extra_json,
  134. created_at, updated_at
  135. ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  136. ON CONFLICT(id) DO UPDATE SET
  137. name = excluded.name,
  138. interface = excluded.interface,
  139. control_device = excluded.control_device,
  140. at_port = excluded.at_port,
  141. usb_path = excluded.usb_path,
  142. audio_device = excluded.audio_device,
  143. modem_imei = excluded.modem_imei,
  144. apn = excluded.apn,
  145. proxy_port = excluded.proxy_port,
  146. baud_rate = excluded.baud_rate,
  147. data_bits = excluded.data_bits,
  148. stop_bits = excluded.stop_bits,
  149. parity = excluded.parity,
  150. device_backend = excluded.device_backend,
  151. esim_transport = excluded.esim_transport,
  152. qmi_use_proxy = excluded.qmi_use_proxy,
  153. qmi_proxy_path = excluded.qmi_proxy_path,
  154. qmi_proxy_executable = excluded.qmi_proxy_executable,
  155. network_enabled = excluded.network_enabled,
  156. sms_enabled = excluded.sms_enabled,
  157. vowifi_enabled = excluded.vowifi_enabled,
  158. extra_json = excluded.extra_json,
  159. updated_at = excluded.updated_at
  160. `,
  161. value.ID, value.Name, value.Interface, value.ControlDevice, value.ATPort,
  162. value.USBPath, value.AudioDevice, value.ModemIMEI, value.APN,
  163. value.ProxyPort, value.BaudRate, value.DataBits, value.StopBits,
  164. value.Parity, value.DeviceBackend, value.ESIMTransport,
  165. boolInt(value.QMIUseProxy), value.QMIProxyPath, value.QMIProxyExecutable,
  166. boolInt(value.NetworkEnabled), boolInt(value.SMSEnabled),
  167. boolInt(value.VoWiFiEnabled), string(extra), createdAt.Unix(),
  168. updatedAt.Unix(),
  169. )
  170. if err != nil {
  171. return fmt.Errorf("upsert device %q: %w", value.ID, err)
  172. }
  173. return nil
  174. }
  175. func (s *Store) Device(ctx context.Context, id string) (Device, error) {
  176. return scanDevice(s.db.QueryRowContext(ctx, deviceSelect+` WHERE id = ?`, id))
  177. }
  178. func (s *Store) ListDevices(ctx context.Context) ([]Device, error) {
  179. rows, err := s.db.QueryContext(ctx, deviceSelect+` ORDER BY name COLLATE NOCASE, id`)
  180. if err != nil {
  181. return nil, fmt.Errorf("list devices: %w", err)
  182. }
  183. defer rows.Close()
  184. values := make([]Device, 0)
  185. for rows.Next() {
  186. value, err := scanDevice(rows)
  187. if err != nil {
  188. return nil, fmt.Errorf("scan device: %w", err)
  189. }
  190. values = append(values, value)
  191. }
  192. if err := rows.Err(); err != nil {
  193. return nil, fmt.Errorf("iterate devices: %w", err)
  194. }
  195. return values, nil
  196. }
  197. func (s *Store) DeleteDevice(ctx context.Context, id string) error {
  198. result, err := s.db.ExecContext(ctx, `DELETE FROM devices WHERE id = ?`, id)
  199. if err != nil {
  200. return fmt.Errorf("delete device %q: %w", id, err)
  201. }
  202. return requireAffected(result)
  203. }
  204. const deviceSelect = `
  205. SELECT id, name, interface, control_device, at_port, usb_path,
  206. audio_device, modem_imei, apn, proxy_port, baud_rate, data_bits,
  207. stop_bits, parity, device_backend, esim_transport, qmi_use_proxy,
  208. qmi_proxy_path, qmi_proxy_executable, network_enabled, sms_enabled,
  209. vowifi_enabled, extra_json, created_at, updated_at
  210. FROM devices`
  211. func scanDevice(row rowScanner) (Device, error) {
  212. var value Device
  213. var qmiUseProxy, networkEnabled, smsEnabled, vowifiEnabled int
  214. var extra string
  215. var createdAt, updatedAt int64
  216. err := row.Scan(
  217. &value.ID, &value.Name, &value.Interface, &value.ControlDevice,
  218. &value.ATPort, &value.USBPath, &value.AudioDevice, &value.ModemIMEI,
  219. &value.APN, &value.ProxyPort, &value.BaudRate, &value.DataBits,
  220. &value.StopBits, &value.Parity, &value.DeviceBackend,
  221. &value.ESIMTransport, &qmiUseProxy, &value.QMIProxyPath,
  222. &value.QMIProxyExecutable, &networkEnabled, &smsEnabled,
  223. &vowifiEnabled, &extra, &createdAt, &updatedAt,
  224. )
  225. if errors.Is(err, sql.ErrNoRows) {
  226. return Device{}, ErrNotFound
  227. }
  228. if err != nil {
  229. return Device{}, err
  230. }
  231. value.QMIUseProxy = qmiUseProxy != 0
  232. value.NetworkEnabled = networkEnabled != 0
  233. value.SMSEnabled = smsEnabled != 0
  234. value.VoWiFiEnabled = vowifiEnabled != 0
  235. value.Extra = []byte(extra)
  236. value.CreatedAt = time.Unix(createdAt, 0).UTC()
  237. value.UpdatedAt = time.Unix(updatedAt, 0).UTC()
  238. return value, nil
  239. }
  240. func (s *Store) UpsertDeviceRuntime(ctx context.Context, value DeviceRuntime) error {
  241. return upsertDeviceRuntime(ctx, s.db, value)
  242. }
  243. func upsertDeviceRuntime(
  244. ctx context.Context,
  245. executor contextExecer,
  246. value DeviceRuntime,
  247. ) error {
  248. if strings.TrimSpace(value.DeviceID) == "" {
  249. return errors.New("device runtime device id is required")
  250. }
  251. traffic, err := normalizeJSONObject(value.Traffic)
  252. if err != nil {
  253. return fmt.Errorf("normalize device traffic: %w", err)
  254. }
  255. extra, err := normalizeJSONObject(value.Extra)
  256. if err != nil {
  257. return fmt.Errorf("normalize device runtime extra data: %w", err)
  258. }
  259. updatedAt := value.UpdatedAt
  260. if updatedAt.IsZero() {
  261. updatedAt = time.Now().UTC()
  262. }
  263. _, err = executor.ExecContext(ctx, `
  264. INSERT INTO device_runtime (
  265. device_id, running, healthy, control_online, physical_present,
  266. worker_running, data_connected, radio_registered, network_connected,
  267. flight_mode, lifecycle_phase, lifecycle_reason, public_ip, private_ip,
  268. operator, native_mcc, native_mnc, native_spn, network_mode,
  269. network_duplex, radio_band, radio_channel, signal_dbm, signal_rsrp,
  270. signal_rsrq, signal_sinr, imei, iccid, imsi, firmware, reg_status,
  271. reg_status_text, ps_attached, sim_inserted, operating_mode,
  272. phone_number, phone_number_source, traffic_json, extra_json, updated_at
  273. ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  274. ON CONFLICT(device_id) DO UPDATE SET
  275. running = excluded.running,
  276. healthy = excluded.healthy,
  277. control_online = excluded.control_online,
  278. physical_present = excluded.physical_present,
  279. worker_running = excluded.worker_running,
  280. data_connected = excluded.data_connected,
  281. radio_registered = excluded.radio_registered,
  282. network_connected = excluded.network_connected,
  283. flight_mode = excluded.flight_mode,
  284. lifecycle_phase = excluded.lifecycle_phase,
  285. lifecycle_reason = excluded.lifecycle_reason,
  286. public_ip = excluded.public_ip,
  287. private_ip = excluded.private_ip,
  288. operator = excluded.operator,
  289. native_mcc = excluded.native_mcc,
  290. native_mnc = excluded.native_mnc,
  291. native_spn = excluded.native_spn,
  292. network_mode = excluded.network_mode,
  293. network_duplex = excluded.network_duplex,
  294. radio_band = excluded.radio_band,
  295. radio_channel = excluded.radio_channel,
  296. signal_dbm = excluded.signal_dbm,
  297. signal_rsrp = excluded.signal_rsrp,
  298. signal_rsrq = excluded.signal_rsrq,
  299. signal_sinr = excluded.signal_sinr,
  300. imei = excluded.imei,
  301. iccid = excluded.iccid,
  302. imsi = excluded.imsi,
  303. firmware = excluded.firmware,
  304. reg_status = excluded.reg_status,
  305. reg_status_text = excluded.reg_status_text,
  306. ps_attached = excluded.ps_attached,
  307. sim_inserted = excluded.sim_inserted,
  308. operating_mode = excluded.operating_mode,
  309. phone_number = excluded.phone_number,
  310. phone_number_source = excluded.phone_number_source,
  311. traffic_json = excluded.traffic_json,
  312. extra_json = excluded.extra_json,
  313. updated_at = excluded.updated_at
  314. `,
  315. value.DeviceID, boolInt(value.Running), boolInt(value.Healthy),
  316. boolInt(value.ControlOnline), boolInt(value.PhysicalPresent),
  317. boolInt(value.WorkerRunning), boolInt(value.DataConnected),
  318. boolInt(value.RadioRegistered), boolInt(value.NetworkConnected),
  319. boolInt(value.FlightMode), value.LifecyclePhase, value.LifecycleReason,
  320. value.PublicIP, value.PrivateIP, value.Operator, value.NativeMCC,
  321. value.NativeMNC, value.NativeSPN, value.NetworkMode,
  322. value.NetworkDuplex, value.RadioBand, value.RadioChannel,
  323. value.SignalDBM, nullableInt(value.SignalRSRP), nullableInt(value.SignalRSRQ),
  324. nullableInt(value.SignalSINR), value.IMEI, value.ICCID, value.IMSI,
  325. value.Firmware, value.RegStatus, value.RegStatusText,
  326. nullableBool(value.PSAttached), nullableBool(value.SIMInserted),
  327. nullableInt(value.OperatingMode), value.PhoneNumber,
  328. value.PhoneNumberSource, string(traffic), string(extra), updatedAt.Unix(),
  329. )
  330. if err != nil {
  331. return fmt.Errorf("upsert runtime for device %q: %w", value.DeviceID, err)
  332. }
  333. return nil
  334. }
  335. func (s *Store) DeviceRuntime(ctx context.Context, deviceID string) (DeviceRuntime, error) {
  336. return scanDeviceRuntime(s.db.QueryRowContext(ctx, deviceRuntimeSelect+` WHERE device_id = ?`, deviceID))
  337. }
  338. const deviceRuntimeSelect = `
  339. SELECT device_id, running, healthy, control_online, physical_present,
  340. worker_running, data_connected, radio_registered, network_connected,
  341. flight_mode, lifecycle_phase, lifecycle_reason, public_ip, private_ip,
  342. operator, native_mcc, native_mnc, native_spn, network_mode,
  343. network_duplex, radio_band, radio_channel, signal_dbm, signal_rsrp,
  344. signal_rsrq, signal_sinr, imei, iccid, imsi, firmware, reg_status,
  345. reg_status_text, ps_attached, sim_inserted, operating_mode,
  346. phone_number, phone_number_source, traffic_json, extra_json, updated_at
  347. FROM device_runtime`
  348. func scanDeviceRuntime(row rowScanner) (DeviceRuntime, error) {
  349. var value DeviceRuntime
  350. var running, healthy, controlOnline, physicalPresent int
  351. var workerRunning, dataConnected, radioRegistered, networkConnected int
  352. var flightMode int
  353. var signalRSRP, signalRSRQ, signalSINR sql.NullInt64
  354. var psAttached, simInserted, operatingMode sql.NullInt64
  355. var traffic, extra string
  356. var updatedAt int64
  357. err := row.Scan(
  358. &value.DeviceID, &running, &healthy, &controlOnline, &physicalPresent,
  359. &workerRunning, &dataConnected, &radioRegistered, &networkConnected,
  360. &flightMode, &value.LifecyclePhase, &value.LifecycleReason,
  361. &value.PublicIP, &value.PrivateIP, &value.Operator, &value.NativeMCC,
  362. &value.NativeMNC, &value.NativeSPN, &value.NetworkMode,
  363. &value.NetworkDuplex, &value.RadioBand, &value.RadioChannel,
  364. &value.SignalDBM, &signalRSRP, &signalRSRQ, &signalSINR, &value.IMEI,
  365. &value.ICCID, &value.IMSI, &value.Firmware, &value.RegStatus,
  366. &value.RegStatusText, &psAttached, &simInserted, &operatingMode,
  367. &value.PhoneNumber, &value.PhoneNumberSource, &traffic, &extra, &updatedAt,
  368. )
  369. if errors.Is(err, sql.ErrNoRows) {
  370. return DeviceRuntime{}, ErrNotFound
  371. }
  372. if err != nil {
  373. return DeviceRuntime{}, err
  374. }
  375. value.Running = running != 0
  376. value.Healthy = healthy != 0
  377. value.ControlOnline = controlOnline != 0
  378. value.PhysicalPresent = physicalPresent != 0
  379. value.WorkerRunning = workerRunning != 0
  380. value.DataConnected = dataConnected != 0
  381. value.RadioRegistered = radioRegistered != 0
  382. value.NetworkConnected = networkConnected != 0
  383. value.FlightMode = flightMode != 0
  384. value.SignalRSRP = nullIntPointer(signalRSRP)
  385. value.SignalRSRQ = nullIntPointer(signalRSRQ)
  386. value.SignalSINR = nullIntPointer(signalSINR)
  387. value.PSAttached = nullBoolPointer(psAttached)
  388. value.SIMInserted = nullBoolPointer(simInserted)
  389. value.OperatingMode = nullIntPointer(operatingMode)
  390. value.Traffic = []byte(traffic)
  391. value.Extra = []byte(extra)
  392. value.UpdatedAt = time.Unix(updatedAt, 0).UTC()
  393. return value, nil
  394. }
  395. func (s *Store) DeleteDeviceRuntime(ctx context.Context, deviceID string) error {
  396. result, err := s.db.ExecContext(ctx, `DELETE FROM device_runtime WHERE device_id = ?`, deviceID)
  397. if err != nil {
  398. return fmt.Errorf("delete runtime for device %q: %w", deviceID, err)
  399. }
  400. return requireAffected(result)
  401. }
  402. func (s *Store) UpsertVoWiFiRuntime(ctx context.Context, value VoWiFiRuntime) error {
  403. return upsertVoWiFiRuntime(ctx, s.db, value)
  404. }
  405. func upsertVoWiFiRuntime(
  406. ctx context.Context,
  407. executor contextExecer,
  408. value VoWiFiRuntime,
  409. ) error {
  410. if strings.TrimSpace(value.DeviceID) == "" {
  411. return errors.New("VoWiFi runtime device id is required")
  412. }
  413. tunnel, err := normalizeJSONObject(value.Tunnel)
  414. if err != nil {
  415. return fmt.Errorf("normalize VoWiFi tunnel state: %w", err)
  416. }
  417. imscore, err := normalizeJSONObject(value.IMSCore)
  418. if err != nil {
  419. return fmt.Errorf("normalize VoWiFi IMS state: %w", err)
  420. }
  421. smsip, err := normalizeJSONObject(value.SMSIP)
  422. if err != nil {
  423. return fmt.Errorf("normalize VoWiFi SMS state: %w", err)
  424. }
  425. extra, err := normalizeJSONObject(value.Extra)
  426. if err != nil {
  427. return fmt.Errorf("normalize VoWiFi extra state: %w", err)
  428. }
  429. updatedAt := value.UpdatedAt
  430. if updatedAt.IsZero() {
  431. updatedAt = time.Now().UTC()
  432. }
  433. _, err = executor.ExecContext(ctx, `
  434. INSERT INTO vowifi_runtime (
  435. device_id, phase, dataplane_mode, iccid, imsi, sim_ready,
  436. access_ready, tunnel_ready, ims_ready, sms_ready, reg_status,
  437. reg_status_text, network_mode, local_phone, phone_number_source,
  438. last_error_class, last_error, last_reason, tunnel_json,
  439. imscore_json, smsip_json, extra_json, updated_at
  440. ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  441. ON CONFLICT(device_id) DO UPDATE SET
  442. phase = excluded.phase,
  443. dataplane_mode = excluded.dataplane_mode,
  444. iccid = excluded.iccid,
  445. imsi = excluded.imsi,
  446. sim_ready = excluded.sim_ready,
  447. access_ready = excluded.access_ready,
  448. tunnel_ready = excluded.tunnel_ready,
  449. ims_ready = excluded.ims_ready,
  450. sms_ready = excluded.sms_ready,
  451. reg_status = excluded.reg_status,
  452. reg_status_text = excluded.reg_status_text,
  453. network_mode = excluded.network_mode,
  454. local_phone = excluded.local_phone,
  455. phone_number_source = excluded.phone_number_source,
  456. last_error_class = excluded.last_error_class,
  457. last_error = excluded.last_error,
  458. last_reason = excluded.last_reason,
  459. tunnel_json = excluded.tunnel_json,
  460. imscore_json = excluded.imscore_json,
  461. smsip_json = excluded.smsip_json,
  462. extra_json = excluded.extra_json,
  463. updated_at = excluded.updated_at
  464. `,
  465. value.DeviceID, value.Phase, value.DataplaneMode, value.ICCID,
  466. value.IMSI, boolInt(value.SIMReady), boolInt(value.AccessReady),
  467. boolInt(value.TunnelReady), boolInt(value.IMSReady),
  468. boolInt(value.SMSReady), value.RegStatus, value.RegStatusText,
  469. value.NetworkMode, value.LocalPhone, value.PhoneNumberSource,
  470. value.LastErrorClass, value.LastError, value.LastReason,
  471. string(tunnel), string(imscore), string(smsip), string(extra),
  472. updatedAt.Unix(),
  473. )
  474. if err != nil {
  475. return fmt.Errorf("upsert VoWiFi runtime for device %q: %w", value.DeviceID, err)
  476. }
  477. return nil
  478. }
  479. func (s *Store) VoWiFiRuntime(ctx context.Context, deviceID string) (VoWiFiRuntime, error) {
  480. return scanVoWiFiRuntime(s.db.QueryRowContext(ctx, vowifiRuntimeSelect+` WHERE device_id = ?`, deviceID))
  481. }
  482. const vowifiRuntimeSelect = `
  483. SELECT device_id, phase, dataplane_mode, iccid, imsi, sim_ready,
  484. access_ready, tunnel_ready, ims_ready, sms_ready, reg_status,
  485. reg_status_text, network_mode, local_phone, phone_number_source,
  486. last_error_class, last_error, last_reason, tunnel_json, imscore_json,
  487. smsip_json, extra_json, updated_at
  488. FROM vowifi_runtime`
  489. func scanVoWiFiRuntime(row rowScanner) (VoWiFiRuntime, error) {
  490. var value VoWiFiRuntime
  491. var simReady, accessReady, tunnelReady, imsReady, smsReady int
  492. var tunnel, imscore, smsip, extra string
  493. var updatedAt int64
  494. err := row.Scan(
  495. &value.DeviceID, &value.Phase, &value.DataplaneMode, &value.ICCID,
  496. &value.IMSI, &simReady, &accessReady, &tunnelReady, &imsReady,
  497. &smsReady, &value.RegStatus, &value.RegStatusText, &value.NetworkMode,
  498. &value.LocalPhone, &value.PhoneNumberSource, &value.LastErrorClass,
  499. &value.LastError, &value.LastReason, &tunnel, &imscore, &smsip,
  500. &extra, &updatedAt,
  501. )
  502. if errors.Is(err, sql.ErrNoRows) {
  503. return VoWiFiRuntime{}, ErrNotFound
  504. }
  505. if err != nil {
  506. return VoWiFiRuntime{}, err
  507. }
  508. value.SIMReady = simReady != 0
  509. value.AccessReady = accessReady != 0
  510. value.TunnelReady = tunnelReady != 0
  511. value.IMSReady = imsReady != 0
  512. value.SMSReady = smsReady != 0
  513. value.Tunnel = []byte(tunnel)
  514. value.IMSCore = []byte(imscore)
  515. value.SMSIP = []byte(smsip)
  516. value.Extra = []byte(extra)
  517. value.UpdatedAt = time.Unix(updatedAt, 0).UTC()
  518. return value, nil
  519. }
  520. func (s *Store) DeleteVoWiFiRuntime(ctx context.Context, deviceID string) error {
  521. result, err := s.db.ExecContext(ctx, `DELETE FROM vowifi_runtime WHERE device_id = ?`, deviceID)
  522. if err != nil {
  523. return fmt.Errorf("delete VoWiFi runtime for device %q: %w", deviceID, err)
  524. }
  525. return requireAffected(result)
  526. }
  527. func requireAffected(result sql.Result) error {
  528. affected, err := result.RowsAffected()
  529. if err != nil {
  530. return err
  531. }
  532. if affected == 0 {
  533. return ErrNotFound
  534. }
  535. return nil
  536. }
  537. func nullIntPointer(value sql.NullInt64) *int {
  538. if !value.Valid {
  539. return nil
  540. }
  541. result := int(value.Int64)
  542. return &result
  543. }
  544. func nullBoolPointer(value sql.NullInt64) *bool {
  545. if !value.Valid {
  546. return nil
  547. }
  548. result := value.Int64 != 0
  549. return &result
  550. }