openapi.yaml 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654
  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. webhook_ids:
  217. - lager-webhook
  218. email_ids:
  219. - lager-team
  220. alerts:
  221. - id: alert_680004979d8512.07480974
  222. created_at: '2026-04-15T19:20:02+00:00'
  223. payload:
  224. event: critical
  225. machine_id: automat-lobby
  226. machine_name: Lobby Automat
  227. sensor_id: fach-a1
  228. slot_label: A1
  229. product_name: Cola 0,5l
  230. distance_mm: 320
  231. units_estimated: 1
  232. max_units: 7
  233. fill_percent: 14
  234. state: critical
  235. previous_state: ok
  236. measured_at: '2026-04-15T19:19:59+00:00'
  237. deliveries:
  238. webhooks:
  239. - id: lager-webhook
  240. success: false
  241. message: Webhook nicht gefunden oder deaktiviert.
  242. emails:
  243. - id: lager-team
  244. success: true
  245. message: Email versendet.
  246. '405':
  247. description: Only GET is supported for this endpoint.
  248. content:
  249. application/json:
  250. schema:
  251. $ref: '#/components/schemas/ErrorResponse'
  252. examples:
  253. wrongMethod:
  254. value:
  255. ok: false
  256. error: Nur GET ist erlaubt.
  257. components:
  258. securitySchemes:
  259. bearerAuth:
  260. type: http
  261. scheme: bearer
  262. bearerFormat: opaque token
  263. description: Bearer token stored in config.json under api.bearer_token.
  264. schemas:
  265. ReadingRequest:
  266. type: object
  267. additionalProperties: false
  268. required:
  269. - machine_id
  270. - sensor_id
  271. - distance_mm
  272. properties:
  273. machine_id:
  274. type: string
  275. description: Configured machine identifier from config.json.
  276. minLength: 1
  277. example: automat-lobby
  278. sensor_id:
  279. type: string
  280. description: Slot or sensor identifier within the machine.
  281. minLength: 1
  282. example: fach-a1
  283. distance_mm:
  284. type: number
  285. description: Measured distance in millimeters.
  286. example: 184
  287. measured_at:
  288. type:
  289. - string
  290. - 'null'
  291. description: |
  292. Optional measurement timestamp. When omitted or empty, the server uses
  293. the current time. Values are parsed with PHP `strtotime()` and returned
  294. as an ISO-8601 timestamp.
  295. format: date-time
  296. example: '2026-04-15T19:20:00Z'
  297. ReadingSuccessResponse:
  298. type: object
  299. additionalProperties: false
  300. required:
  301. - ok
  302. - machine_id
  303. - sensor_id
  304. - slot_label
  305. - units_estimated
  306. - fill_percent
  307. - state
  308. properties:
  309. ok:
  310. type: boolean
  311. const: true
  312. machine_id:
  313. type: string
  314. example: automat-lobby
  315. sensor_id:
  316. type: string
  317. example: fach-a1
  318. slot_label:
  319. type: string
  320. example: A1
  321. units_estimated:
  322. type: integer
  323. example: 4
  324. fill_percent:
  325. type: integer
  326. example: 63
  327. state:
  328. $ref: '#/components/schemas/SlotState'
  329. ErrorResponse:
  330. type: object
  331. additionalProperties: false
  332. required:
  333. - ok
  334. - error
  335. properties:
  336. ok:
  337. type: boolean
  338. const: false
  339. error:
  340. type: string
  341. example: Nicht autorisiert.
  342. StatusResponse:
  343. type: object
  344. additionalProperties: false
  345. required:
  346. - ok
  347. - generated_at
  348. - app
  349. - summary
  350. - machines
  351. - alerts
  352. properties:
  353. ok:
  354. type: boolean
  355. const: true
  356. generated_at:
  357. type: string
  358. format: date-time
  359. description: Timestamp when the response was generated.
  360. app:
  361. $ref: '#/components/schemas/AppStatus'
  362. summary:
  363. $ref: '#/components/schemas/StatusSummary'
  364. machines:
  365. type: array
  366. items:
  367. $ref: '#/components/schemas/MachineStatus'
  368. alerts:
  369. type: array
  370. items:
  371. $ref: '#/components/schemas/AlertEvent'
  372. AppStatus:
  373. type: object
  374. additionalProperties: false
  375. required:
  376. - name
  377. - dashboard_refresh_seconds
  378. properties:
  379. name:
  380. type: string
  381. example: Getränkeautomat Monitor
  382. dashboard_refresh_seconds:
  383. type: integer
  384. minimum: 1
  385. example: 15
  386. StatusSummary:
  387. type: object
  388. additionalProperties: false
  389. required:
  390. - machine_count
  391. - slot_count
  392. - critical_count
  393. properties:
  394. machine_count:
  395. type: integer
  396. minimum: 0
  397. example: 2
  398. slot_count:
  399. type: integer
  400. minimum: 0
  401. example: 3
  402. critical_count:
  403. type: integer
  404. minimum: 0
  405. example: 1
  406. MachineStatus:
  407. type: object
  408. additionalProperties: false
  409. required:
  410. - id
  411. - name
  412. - location
  413. - slots
  414. properties:
  415. id:
  416. type: string
  417. example: automat-lobby
  418. name:
  419. type: string
  420. example: Lobby Automat
  421. location:
  422. type: string
  423. example: Erdgeschoss
  424. slots:
  425. type: array
  426. items:
  427. $ref: '#/components/schemas/SlotStatus'
  428. SlotStatus:
  429. type: object
  430. additionalProperties: false
  431. required:
  432. - machine_id
  433. - machine_name
  434. - sensor_id
  435. - slot_label
  436. - product_name
  437. - fill_percent
  438. - units_estimated
  439. - max_units
  440. - distance_mm
  441. - state
  442. - measured_at
  443. - updated_at
  444. - alert_below_units
  445. - webhook_ids
  446. - email_ids
  447. properties:
  448. machine_id:
  449. type: string
  450. example: automat-lobby
  451. machine_name:
  452. type: string
  453. example: Lobby Automat
  454. sensor_id:
  455. type: string
  456. example: fach-a1
  457. slot_label:
  458. type: string
  459. example: A1
  460. product_name:
  461. type: string
  462. example: Cola 0,5l
  463. fill_percent:
  464. type:
  465. - integer
  466. - 'null'
  467. minimum: 0
  468. maximum: 100
  469. description: Null until a first reading has been received.
  470. example: 63
  471. units_estimated:
  472. type:
  473. - integer
  474. - 'null'
  475. minimum: 0
  476. description: Null until a first reading has been received.
  477. example: 4
  478. max_units:
  479. type: integer
  480. minimum: 0
  481. example: 7
  482. distance_mm:
  483. type:
  484. - number
  485. - 'null'
  486. description: Last measured distance in millimeters.
  487. example: 184
  488. state:
  489. $ref: '#/components/schemas/SlotState'
  490. measured_at:
  491. type:
  492. - string
  493. - 'null'
  494. format: date-time
  495. example: '2026-04-15T19:20:00+00:00'
  496. updated_at:
  497. type:
  498. - string
  499. - 'null'
  500. format: date-time
  501. example: '2026-04-15T19:20:02+00:00'
  502. alert_below_units:
  503. type: integer
  504. minimum: 0
  505. example: 2
  506. webhook_ids:
  507. type: array
  508. items:
  509. type: string
  510. example:
  511. - lager-webhook
  512. email_ids:
  513. type: array
  514. items:
  515. type: string
  516. example:
  517. - lager-team
  518. SlotState:
  519. type: string
  520. enum:
  521. - ok
  522. - critical
  523. - unknown
  524. example: ok
  525. AlertEvent:
  526. type: object
  527. additionalProperties: false
  528. required:
  529. - id
  530. - created_at
  531. - payload
  532. - deliveries
  533. properties:
  534. id:
  535. type: string
  536. description: Unique alert log entry ID generated with PHP uniqid().
  537. example: alert_680004979d8512.07480974
  538. created_at:
  539. type: string
  540. format: date-time
  541. example: '2026-04-15T19:20:02+00:00'
  542. payload:
  543. $ref: '#/components/schemas/AlertPayload'
  544. deliveries:
  545. $ref: '#/components/schemas/AlertDeliveries'
  546. AlertPayload:
  547. type: object
  548. additionalProperties: false
  549. required:
  550. - event
  551. - machine_id
  552. - machine_name
  553. - sensor_id
  554. - slot_label
  555. - product_name
  556. - distance_mm
  557. - units_estimated
  558. - max_units
  559. - fill_percent
  560. - state
  561. - previous_state
  562. - measured_at
  563. properties:
  564. event:
  565. type: string
  566. enum:
  567. - critical
  568. - recovered
  569. example: critical
  570. machine_id:
  571. type: string
  572. example: automat-lobby
  573. machine_name:
  574. type: string
  575. example: Lobby Automat
  576. sensor_id:
  577. type: string
  578. example: fach-a1
  579. slot_label:
  580. type: string
  581. example: A1
  582. product_name:
  583. type: string
  584. example: Cola 0,5l
  585. distance_mm:
  586. type:
  587. - number
  588. - 'null'
  589. example: 320
  590. units_estimated:
  591. type:
  592. - integer
  593. - 'null'
  594. example: 1
  595. max_units:
  596. type:
  597. - integer
  598. - 'null'
  599. example: 7
  600. fill_percent:
  601. type:
  602. - integer
  603. - 'null'
  604. example: 14
  605. state:
  606. $ref: '#/components/schemas/SlotState'
  607. previous_state:
  608. type:
  609. - string
  610. - 'null'
  611. description: Previous slot state before the transition.
  612. example: ok
  613. measured_at:
  614. type:
  615. - string
  616. - 'null'
  617. format: date-time
  618. example: '2026-04-15T19:19:59+00:00'
  619. AlertDeliveries:
  620. type: object
  621. additionalProperties: false
  622. required:
  623. - webhooks
  624. - emails
  625. properties:
  626. webhooks:
  627. type: array
  628. items:
  629. $ref: '#/components/schemas/DeliveryResult'
  630. emails:
  631. type: array
  632. items:
  633. $ref: '#/components/schemas/DeliveryResult'
  634. DeliveryResult:
  635. type: object
  636. additionalProperties: false
  637. required:
  638. - id
  639. - success
  640. - message
  641. properties:
  642. id:
  643. type: string
  644. example: lager-team
  645. success:
  646. type: boolean
  647. example: true
  648. message:
  649. type: string
  650. example: Email versendet.