openapi.yaml 22 KB

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