openapi.yaml 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636
  1. openapi: 3.1.0
  2. info:
  3. title: Getränkeautomat Monitor API
  4. version: 1.0.0
  5. summary: HTTP API for ingesting sensor readings and retrieving vending machine status.
  6. description: |
  7. Handwritten OpenAPI specification for the Getränkeautomat Monitor application.
  8. The API currently exposes two endpoints:
  9. - `POST /api/v1/readings.php` for ingesting one sensor measurement
  10. - `GET /api/v1/status.php` for retrieving the aggregated application state
  11. The readings endpoint uses Bearer token authentication. The status endpoint is
  12. intentionally public so the dashboard can poll it without a login.
  13. servers:
  14. - url: ./
  15. description: Same-origin deployment
  16. tags:
  17. - name: Readings
  18. description: Receive one sensor reading and update the persisted slot state.
  19. - name: Status
  20. description: Retrieve the current aggregated machine, slot, and alert status.
  21. paths:
  22. /api/v1/readings.php:
  23. post:
  24. tags:
  25. - Readings
  26. summary: Submit one reading
  27. description: |
  28. Accepts exactly one sensor measurement for one configured machine slot.
  29. Processing behavior:
  30. - Requires a Bearer token in the `Authorization` header
  31. - Accepts JSON request bodies
  32. - Updates `data/state.json` on success
  33. - Triggers alerts only when the slot state changes from `ok` to `critical`
  34. or from `critical` to `ok`
  35. operationId: submitReading
  36. security:
  37. - bearerAuth: []
  38. requestBody:
  39. required: true
  40. content:
  41. application/json:
  42. schema:
  43. $ref: '#/components/schemas/ReadingRequest'
  44. examples:
  45. lobbySlot:
  46. summary: Reading for the A1 slot in the lobby machine
  47. value:
  48. machine_id: automat-lobby
  49. sensor_id: fach-a1
  50. distance_mm: 184
  51. measured_at: '2026-04-15T19:20:00Z'
  52. responses:
  53. '200':
  54. description: Reading processed successfully.
  55. content:
  56. application/json:
  57. schema:
  58. $ref: '#/components/schemas/ReadingSuccessResponse'
  59. examples:
  60. ok:
  61. value:
  62. ok: true
  63. machine_id: automat-lobby
  64. sensor_id: fach-a1
  65. slot_label: A1
  66. units_estimated: 4
  67. fill_percent: 63
  68. state: ok
  69. '400':
  70. description: Invalid JSON body or malformed request payload.
  71. content:
  72. application/json:
  73. schema:
  74. $ref: '#/components/schemas/ErrorResponse'
  75. examples:
  76. invalidJson:
  77. value:
  78. ok: false
  79. error: Ungültiger JSON-Body.
  80. '401':
  81. description: Missing or invalid Bearer token.
  82. content:
  83. application/json:
  84. schema:
  85. $ref: '#/components/schemas/ErrorResponse'
  86. examples:
  87. unauthorized:
  88. value:
  89. ok: false
  90. error: Nicht autorisiert.
  91. '404':
  92. description: The referenced machine or sensor is not configured.
  93. content:
  94. application/json:
  95. schema:
  96. $ref: '#/components/schemas/ErrorResponse'
  97. examples:
  98. unknownMachineOrSensor:
  99. value:
  100. ok: false
  101. error: Unbekannter Automat oder Sensor.
  102. '405':
  103. description: Only POST is supported for this endpoint.
  104. content:
  105. application/json:
  106. schema:
  107. $ref: '#/components/schemas/ErrorResponse'
  108. examples:
  109. wrongMethod:
  110. value:
  111. ok: false
  112. error: Nur POST ist erlaubt.
  113. '422':
  114. description: Semantic validation failed.
  115. content:
  116. application/json:
  117. schema:
  118. $ref: '#/components/schemas/ErrorResponse'
  119. examples:
  120. missingIdentifiers:
  121. value:
  122. ok: false
  123. error: machine_id und sensor_id sind erforderlich.
  124. nonNumericDistance:
  125. value:
  126. ok: false
  127. error: distance_mm muss numerisch sein.
  128. invalidTimestamp:
  129. value:
  130. ok: false
  131. error: measured_at ist kein gültiger ISO-Zeitstempel.
  132. '500':
  133. description: Unexpected internal server error.
  134. content:
  135. application/json:
  136. schema:
  137. $ref: '#/components/schemas/ErrorResponse'
  138. examples:
  139. internalError:
  140. value:
  141. ok: false
  142. error: Interner Fehler.
  143. options:
  144. tags:
  145. - Readings
  146. summary: CORS preflight for readings
  147. description: |
  148. Preflight handler for browser-based clients. The endpoint responds with
  149. `204 No Content` and emits `Access-Control-Allow-Methods` and
  150. `Access-Control-Allow-Headers`.
  151. operationId: readingsPreflight
  152. responses:
  153. '204':
  154. description: Preflight accepted without a response body.
  155. headers:
  156. Access-Control-Allow-Methods:
  157. description: Allowed methods for this endpoint.
  158. schema:
  159. type: string
  160. example: POST, OPTIONS
  161. Access-Control-Allow-Headers:
  162. description: Allowed request headers for this endpoint.
  163. schema:
  164. type: string
  165. example: Authorization, Content-Type
  166. /api/v1/status.php:
  167. get:
  168. tags:
  169. - Status
  170. summary: Retrieve the current application status
  171. description: |
  172. Returns the aggregated state for the dashboard and admin panel.
  173. The response contains:
  174. - app metadata
  175. - machine and slot status
  176. - a summary section
  177. - the most recent alert log entries
  178. operationId: getStatus
  179. responses:
  180. '200':
  181. description: Aggregated status generated successfully.
  182. content:
  183. application/json:
  184. schema:
  185. $ref: '#/components/schemas/StatusResponse'
  186. examples:
  187. dashboard:
  188. value:
  189. ok: true
  190. generated_at: '2026-04-15T20:10:00+00:00'
  191. app:
  192. name: Getränkeautomat Monitor
  193. dashboard_refresh_seconds: 15
  194. summary:
  195. machine_count: 2
  196. slot_count: 3
  197. critical_count: 1
  198. machines:
  199. - id: automat-lobby
  200. name: Lobby Automat
  201. location: Erdgeschoss
  202. slots:
  203. - machine_id: automat-lobby
  204. machine_name: Lobby Automat
  205. sensor_id: fach-a1
  206. slot_label: A1
  207. product_name: Cola 0,5l
  208. fill_percent: 63
  209. units_estimated: 4
  210. max_units: 7
  211. distance_mm: 184
  212. state: ok
  213. measured_at: '2026-04-15T19:20:00+00:00'
  214. updated_at: '2026-04-15T19:20:02+00:00'
  215. alert_below_units: 2
  216. alerts:
  217. - id: alert_680004979d8512.07480974
  218. created_at: '2026-04-15T19:20:02+00:00'
  219. payload:
  220. event: critical
  221. machine_id: automat-lobby
  222. machine_name: Lobby Automat
  223. sensor_id: fach-a1
  224. slot_label: A1
  225. product_name: Cola 0,5l
  226. distance_mm: 320
  227. units_estimated: 1
  228. max_units: 7
  229. fill_percent: 14
  230. state: critical
  231. previous_state: ok
  232. measured_at: '2026-04-15T19:19:59+00:00'
  233. deliveries:
  234. webhooks:
  235. - label: lager-webhook
  236. success: false
  237. message: Webhook nicht gefunden oder deaktiviert.
  238. emails:
  239. - label: lager-team
  240. success: true
  241. message: Email versendet.
  242. '405':
  243. description: Only GET is supported for this endpoint.
  244. content:
  245. application/json:
  246. schema:
  247. $ref: '#/components/schemas/ErrorResponse'
  248. examples:
  249. wrongMethod:
  250. value:
  251. ok: false
  252. error: Nur GET ist erlaubt.
  253. components:
  254. securitySchemes:
  255. bearerAuth:
  256. type: http
  257. scheme: bearer
  258. bearerFormat: opaque token
  259. description: Bearer token stored in config.json under api.bearer_token.
  260. schemas:
  261. ReadingRequest:
  262. type: object
  263. additionalProperties: false
  264. required:
  265. - machine_id
  266. - sensor_id
  267. - distance_mm
  268. properties:
  269. machine_id:
  270. type: string
  271. description: Configured machine identifier from config.json.
  272. minLength: 1
  273. example: automat-lobby
  274. sensor_id:
  275. type: string
  276. description: Slot or sensor identifier within the machine.
  277. minLength: 1
  278. example: fach-a1
  279. distance_mm:
  280. type: number
  281. description: Measured distance in millimeters.
  282. example: 184
  283. measured_at:
  284. type:
  285. - string
  286. - 'null'
  287. description: |
  288. Optional measurement timestamp. When omitted or empty, the server uses
  289. the current time. Values are parsed with PHP `strtotime()` and returned
  290. as an ISO-8601 timestamp.
  291. format: date-time
  292. example: '2026-04-15T19:20:00Z'
  293. ReadingSuccessResponse:
  294. type: object
  295. additionalProperties: false
  296. required:
  297. - ok
  298. - machine_id
  299. - sensor_id
  300. - slot_label
  301. - units_estimated
  302. - fill_percent
  303. - state
  304. properties:
  305. ok:
  306. type: boolean
  307. const: true
  308. machine_id:
  309. type: string
  310. example: automat-lobby
  311. sensor_id:
  312. type: string
  313. example: fach-a1
  314. slot_label:
  315. type: string
  316. example: A1
  317. units_estimated:
  318. type: integer
  319. example: 4
  320. fill_percent:
  321. type: integer
  322. example: 63
  323. state:
  324. $ref: '#/components/schemas/SlotState'
  325. ErrorResponse:
  326. type: object
  327. additionalProperties: false
  328. required:
  329. - ok
  330. - error
  331. properties:
  332. ok:
  333. type: boolean
  334. const: false
  335. error:
  336. type: string
  337. example: Nicht autorisiert.
  338. StatusResponse:
  339. type: object
  340. additionalProperties: false
  341. required:
  342. - ok
  343. - generated_at
  344. - app
  345. - summary
  346. - machines
  347. - alerts
  348. properties:
  349. ok:
  350. type: boolean
  351. const: true
  352. generated_at:
  353. type: string
  354. format: date-time
  355. description: Timestamp when the response was generated.
  356. app:
  357. $ref: '#/components/schemas/AppStatus'
  358. summary:
  359. $ref: '#/components/schemas/StatusSummary'
  360. machines:
  361. type: array
  362. items:
  363. $ref: '#/components/schemas/MachineStatus'
  364. alerts:
  365. type: array
  366. items:
  367. $ref: '#/components/schemas/AlertEvent'
  368. AppStatus:
  369. type: object
  370. additionalProperties: false
  371. required:
  372. - name
  373. - dashboard_refresh_seconds
  374. properties:
  375. name:
  376. type: string
  377. example: Getränkeautomat Monitor
  378. dashboard_refresh_seconds:
  379. type: integer
  380. minimum: 1
  381. example: 15
  382. StatusSummary:
  383. type: object
  384. additionalProperties: false
  385. required:
  386. - machine_count
  387. - slot_count
  388. - critical_count
  389. properties:
  390. machine_count:
  391. type: integer
  392. minimum: 0
  393. example: 2
  394. slot_count:
  395. type: integer
  396. minimum: 0
  397. example: 3
  398. critical_count:
  399. type: integer
  400. minimum: 0
  401. example: 1
  402. MachineStatus:
  403. type: object
  404. additionalProperties: false
  405. required:
  406. - id
  407. - name
  408. - location
  409. - slots
  410. properties:
  411. id:
  412. type: string
  413. example: automat-lobby
  414. name:
  415. type: string
  416. example: Lobby Automat
  417. location:
  418. type: string
  419. example: Erdgeschoss
  420. slots:
  421. type: array
  422. items:
  423. $ref: '#/components/schemas/SlotStatus'
  424. SlotStatus:
  425. type: object
  426. additionalProperties: false
  427. required:
  428. - machine_id
  429. - machine_name
  430. - sensor_id
  431. - slot_label
  432. - product_name
  433. - fill_percent
  434. - units_estimated
  435. - max_units
  436. - distance_mm
  437. - state
  438. - measured_at
  439. - updated_at
  440. - alert_below_units
  441. properties:
  442. machine_id:
  443. type: string
  444. example: automat-lobby
  445. machine_name:
  446. type: string
  447. example: Lobby Automat
  448. sensor_id:
  449. type: string
  450. example: fach-a1
  451. slot_label:
  452. type: string
  453. example: A1
  454. product_name:
  455. type: string
  456. example: Cola 0,5l
  457. fill_percent:
  458. type:
  459. - integer
  460. - 'null'
  461. minimum: 0
  462. maximum: 100
  463. description: Null until a first reading has been received.
  464. example: 63
  465. units_estimated:
  466. type:
  467. - integer
  468. - 'null'
  469. minimum: 0
  470. description: Null until a first reading has been received.
  471. example: 4
  472. max_units:
  473. type: integer
  474. minimum: 0
  475. example: 7
  476. distance_mm:
  477. type:
  478. - number
  479. - 'null'
  480. description: Last measured distance in millimeters.
  481. example: 184
  482. state:
  483. $ref: '#/components/schemas/SlotState'
  484. measured_at:
  485. type:
  486. - string
  487. - 'null'
  488. format: date-time
  489. example: '2026-04-15T19:20:00+00:00'
  490. updated_at:
  491. type:
  492. - string
  493. - 'null'
  494. format: date-time
  495. example: '2026-04-15T19:20:02+00:00'
  496. alert_below_units:
  497. type: integer
  498. minimum: 0
  499. example: 2
  500. SlotState:
  501. type: string
  502. enum:
  503. - ok
  504. - critical
  505. - unknown
  506. example: ok
  507. AlertEvent:
  508. type: object
  509. additionalProperties: false
  510. required:
  511. - id
  512. - created_at
  513. - payload
  514. - deliveries
  515. properties:
  516. id:
  517. type: string
  518. description: Unique alert log entry ID generated with PHP uniqid().
  519. example: alert_680004979d8512.07480974
  520. created_at:
  521. type: string
  522. format: date-time
  523. example: '2026-04-15T19:20:02+00:00'
  524. payload:
  525. $ref: '#/components/schemas/AlertPayload'
  526. deliveries:
  527. $ref: '#/components/schemas/AlertDeliveries'
  528. AlertPayload:
  529. type: object
  530. additionalProperties: false
  531. required:
  532. - event
  533. - machine_id
  534. - machine_name
  535. - sensor_id
  536. - slot_label
  537. - product_name
  538. - distance_mm
  539. - units_estimated
  540. - max_units
  541. - fill_percent
  542. - state
  543. - previous_state
  544. - measured_at
  545. properties:
  546. event:
  547. type: string
  548. enum:
  549. - critical
  550. - recovered
  551. example: critical
  552. machine_id:
  553. type: string
  554. example: automat-lobby
  555. machine_name:
  556. type: string
  557. example: Lobby Automat
  558. sensor_id:
  559. type: string
  560. example: fach-a1
  561. slot_label:
  562. type: string
  563. example: A1
  564. product_name:
  565. type: string
  566. example: Cola 0,5l
  567. distance_mm:
  568. type:
  569. - number
  570. - 'null'
  571. example: 320
  572. units_estimated:
  573. type:
  574. - integer
  575. - 'null'
  576. example: 1
  577. max_units:
  578. type:
  579. - integer
  580. - 'null'
  581. example: 7
  582. fill_percent:
  583. type:
  584. - integer
  585. - 'null'
  586. example: 14
  587. state:
  588. $ref: '#/components/schemas/SlotState'
  589. previous_state:
  590. type:
  591. - string
  592. - 'null'
  593. description: Previous slot state before the transition.
  594. example: ok
  595. measured_at:
  596. type:
  597. - string
  598. - 'null'
  599. format: date-time
  600. example: '2026-04-15T19:19:59+00:00'
  601. AlertDeliveries:
  602. type: object
  603. additionalProperties: false
  604. required:
  605. - webhooks
  606. - emails
  607. properties:
  608. webhooks:
  609. type: array
  610. items:
  611. $ref: '#/components/schemas/DeliveryResult'
  612. emails:
  613. type: array
  614. items:
  615. $ref: '#/components/schemas/DeliveryResult'
  616. DeliveryResult:
  617. type: object
  618. additionalProperties: false
  619. required:
  620. - id
  621. - success
  622. - message
  623. properties:
  624. id:
  625. type: string
  626. example: lager-team
  627. success:
  628. type: boolean
  629. example: true
  630. message:
  631. type: string
  632. example: Email versendet.