ソースを参照

readding openapi-yaml

Medowar 1 ヶ月 前
コミット
3f94cf4c42
1 ファイル変更743 行追加0 行削除
  1. 743 0
      api-docs/openapi.yaml

+ 743 - 0
api-docs/openapi.yaml

@@ -0,0 +1,743 @@
+openapi: 3.1.0
+info:
+  title: Getränkeautomat Monitor API
+  version: 1.0.0
+  summary: HTTP API for ingesting sensor readings and retrieving vending machine status.
+  description: |
+    Handwritten OpenAPI specification for the Getränkeautomat Monitor application.
+
+    The API currently exposes two endpoints:
+    - `POST /api/v1/readings.php` for ingesting one sensor measurement
+    - `GET /api/v1/status.php` for retrieving the aggregated application state
+
+    The readings endpoint uses Bearer token authentication. The status endpoint is
+    intentionally public so the dashboard can poll it without a login.
+servers:
+  - url: ./
+    description: Same-origin deployment
+tags:
+  - name: Readings
+    description: Receive one sensor reading and update the persisted slot state.
+  - name: Status
+    description: Retrieve the current aggregated machine, slot, and alert status.
+paths:
+  /api/v1/readings.php:
+    post:
+      tags:
+        - Readings
+      summary: Submit one reading
+      description: |
+        Accepts exactly one sensor measurement for one configured machine slot.
+
+        Processing behavior:
+        - Requires a Bearer token in the `Authorization` header
+        - Accepts JSON request bodies
+        - Updates `data/state.json` on success
+        - Treats `distance_mm: -1` as a client-side measurement error, keeps
+          the last good slot value, and stores input-error metadata for the UI
+        - Triggers alerts only when the slot state changes from `ok` to `critical`
+          or from `critical` to `ok`
+      operationId: submitReading
+      security:
+        - bearerAuth: []
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/ReadingRequest'
+            examples:
+              lobbySlot:
+                summary: Reading for the A1 slot in the lobby machine
+                value:
+                  machine_id: automat-lobby
+                  sensor_id: fach-a1
+                  distance_mm: 184
+                  measured_at: '2026-04-15T19:20:00Z'
+              clientError:
+                summary: Client-side measurement error for the A1 slot
+                value:
+                  machine_id: automat-lobby
+                  sensor_id: fach-a1
+                  distance_mm: -1
+                  measured_at: '2026-04-15T19:22:09Z'
+      responses:
+        '200':
+          description: Reading processed successfully.
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ReadingSuccessResponse'
+              examples:
+                ok:
+                  value:
+                    ok: true
+                    machine_id: automat-lobby
+                    sensor_id: fach-a1
+                    slot_label: A1
+                    units_estimated: 4
+                    fill_percent: 63
+                    state: ok
+                clientError:
+                  value:
+                    ok: true
+                    machine_id: automat-lobby
+                    sensor_id: fach-a1
+                    slot_label: A1
+                    units_estimated: 4
+                    fill_percent: 63
+                    state: ok
+                    input_error:
+                      received_at: '2026-04-15T19:22:10+00:00'
+                      measured_at: '2026-04-15T19:22:09+00:00'
+                      reported_value: -1
+                      message: Client meldete einen Messfehler.
+                      last_good:
+                        distance_mm: 184
+                        fill_percent: 63
+                        units_estimated: 4
+                        max_units: 7
+                        state: ok
+                        measured_at: '2026-04-15T19:20:00+00:00'
+        '400':
+          description: Invalid JSON body or malformed request payload.
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ErrorResponse'
+              examples:
+                invalidJson:
+                  value:
+                    ok: false
+                    error: Ungültiger JSON-Body.
+        '401':
+          description: Missing or invalid Bearer token.
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ErrorResponse'
+              examples:
+                unauthorized:
+                  value:
+                    ok: false
+                    error: Nicht autorisiert.
+        '404':
+          description: The referenced machine or sensor is not configured.
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ErrorResponse'
+              examples:
+                unknownMachineOrSensor:
+                  value:
+                    ok: false
+                    error: Unbekannter Automat oder Sensor.
+        '405':
+          description: Only POST is supported for this endpoint.
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ErrorResponse'
+              examples:
+                wrongMethod:
+                  value:
+                    ok: false
+                    error: Nur POST ist erlaubt.
+        '422':
+          description: Semantic validation failed.
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ErrorResponse'
+              examples:
+                missingIdentifiers:
+                  value:
+                    ok: false
+                    error: machine_id und sensor_id sind erforderlich.
+                nonNumericDistance:
+                  value:
+                    ok: false
+                    error: distance_mm muss numerisch sein.
+                invalidTimestamp:
+                  value:
+                    ok: false
+                    error: measured_at ist kein gültiger ISO-Zeitstempel.
+        '500':
+          description: Unexpected internal server error.
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ErrorResponse'
+              examples:
+                internalError:
+                  value:
+                    ok: false
+                    error: Interner Fehler.
+    options:
+      tags:
+        - Readings
+      summary: CORS preflight for readings
+      description: |
+        Preflight handler for browser-based clients. The endpoint responds with
+        `204 No Content` and emits `Access-Control-Allow-Methods` and
+        `Access-Control-Allow-Headers`.
+      operationId: readingsPreflight
+      responses:
+        '204':
+          description: Preflight accepted without a response body.
+          headers:
+            Access-Control-Allow-Methods:
+              description: Allowed methods for this endpoint.
+              schema:
+                type: string
+                example: POST, OPTIONS
+            Access-Control-Allow-Headers:
+              description: Allowed request headers for this endpoint.
+              schema:
+                type: string
+                example: Authorization, Content-Type
+  /api/v1/status.php:
+    get:
+      tags:
+        - Status
+      summary: Retrieve the current application status
+      description: |
+        Returns the aggregated state for the dashboard and admin panel.
+
+        The response contains:
+        - app metadata
+        - machine and slot status
+        - a summary section
+        - the most recent alert log entries
+      operationId: getStatus
+      responses:
+        '200':
+          description: Aggregated status generated successfully.
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/StatusResponse'
+              examples:
+                dashboard:
+                  value:
+                    ok: true
+                    generated_at: '2026-04-15T20:10:00+00:00'
+                    app:
+                      name: Getränkeautomat Monitor
+                      dashboard_refresh_seconds: 15
+                    summary:
+                      machine_count: 2
+                      slot_count: 3
+                      critical_count: 1
+                    machines:
+                      - id: automat-lobby
+                        name: Lobby Automat
+                        location: Erdgeschoss
+                        slots:
+                          - machine_id: automat-lobby
+                            machine_name: Lobby Automat
+                            sensor_id: fach-a1
+                            slot_label: A1
+                            product_name: Cola 0,5l
+                            fill_percent: 63
+                            units_estimated: 4
+                            max_units: 7
+                            distance_mm: 184
+                            state: ok
+                            measured_at: '2026-04-15T19:20:00+00:00'
+                            updated_at: '2026-04-15T19:20:02+00:00'
+                            alert_below_units: 2
+                    alerts:
+                      - id: alert_680004979d8512.07480974
+                        created_at: '2026-04-15T19:20:02+00:00'
+                        payload:
+                          event: critical
+                          machine_id: automat-lobby
+                          machine_name: Lobby Automat
+                          sensor_id: fach-a1
+                          slot_label: A1
+                          product_name: Cola 0,5l
+                          distance_mm: 320
+                          units_estimated: 1
+                          max_units: 7
+                          fill_percent: 14
+                          state: critical
+                          previous_state: ok
+                          measured_at: '2026-04-15T19:19:59+00:00'
+                        deliveries:
+                          webhooks:
+                            - label: lager-webhook
+                              success: false
+                              message: Webhook nicht gefunden oder deaktiviert.
+                          emails:
+                            - label: lager-team
+                              success: true
+                              message: Email versendet.
+        '405':
+          description: Only GET is supported for this endpoint.
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ErrorResponse'
+              examples:
+                wrongMethod:
+                  value:
+                    ok: false
+                    error: Nur GET ist erlaubt.
+components:
+  securitySchemes:
+    bearerAuth:
+      type: http
+      scheme: bearer
+      bearerFormat: opaque token
+      description: Bearer token stored in config.json under api.bearer_token.
+  schemas:
+    ReadingRequest:
+      type: object
+      additionalProperties: false
+      required:
+        - machine_id
+        - sensor_id
+        - distance_mm
+      properties:
+        machine_id:
+          type: string
+          description: Configured machine identifier from config.json.
+          minLength: 1
+          example: automat-lobby
+        sensor_id:
+          type: string
+          description: Slot or sensor identifier within the machine.
+          minLength: 1
+          example: fach-a1
+        distance_mm:
+          type: number
+          description: Measured distance in millimeters. The sentinel value `-1` reports a client-side measurement error and does not replace the last good slot value.
+          example: 184
+        measured_at:
+          type:
+            - string
+            - 'null'
+          description: |
+            Optional measurement timestamp. When omitted or empty, the server uses
+            the current time. Values are parsed with PHP `strtotime()` and returned
+            as an ISO-8601 timestamp.
+          format: date-time
+          example: '2026-04-15T19:20:00Z'
+    ReadingSuccessResponse:
+      type: object
+      additionalProperties: false
+      required:
+        - ok
+        - machine_id
+        - sensor_id
+        - slot_label
+        - units_estimated
+        - fill_percent
+        - state
+      properties:
+        ok:
+          type: boolean
+          const: true
+        machine_id:
+          type: string
+          example: automat-lobby
+        sensor_id:
+          type: string
+          example: fach-a1
+        slot_label:
+          type: string
+          example: A1
+        units_estimated:
+          type:
+            - integer
+            - 'null'
+          example: 4
+        fill_percent:
+          type:
+            - integer
+            - 'null'
+          example: 63
+        state:
+          $ref: '#/components/schemas/SlotState'
+        input_error:
+          oneOf:
+            - $ref: '#/components/schemas/InputError'
+            - type: 'null'
+    ErrorResponse:
+      type: object
+      additionalProperties: false
+      required:
+        - ok
+        - error
+      properties:
+        ok:
+          type: boolean
+          const: false
+        error:
+          type: string
+          example: Nicht autorisiert.
+    StatusResponse:
+      type: object
+      additionalProperties: false
+      required:
+        - ok
+        - generated_at
+        - app
+        - summary
+        - machines
+        - alerts
+      properties:
+        ok:
+          type: boolean
+          const: true
+        generated_at:
+          type: string
+          format: date-time
+          description: Timestamp when the response was generated.
+        app:
+          $ref: '#/components/schemas/AppStatus'
+        summary:
+          $ref: '#/components/schemas/StatusSummary'
+        machines:
+          type: array
+          items:
+            $ref: '#/components/schemas/MachineStatus'
+        alerts:
+          type: array
+          items:
+            $ref: '#/components/schemas/AlertEvent'
+    AppStatus:
+      type: object
+      additionalProperties: false
+      required:
+        - name
+        - dashboard_refresh_seconds
+      properties:
+        name:
+          type: string
+          example: Getränkeautomat Monitor
+        dashboard_refresh_seconds:
+          type: integer
+          minimum: 1
+          example: 15
+    StatusSummary:
+      type: object
+      additionalProperties: false
+      required:
+        - machine_count
+        - slot_count
+        - critical_count
+      properties:
+        machine_count:
+          type: integer
+          minimum: 0
+          example: 2
+        slot_count:
+          type: integer
+          minimum: 0
+          example: 3
+        critical_count:
+          type: integer
+          minimum: 0
+          example: 1
+    MachineStatus:
+      type: object
+      additionalProperties: false
+      required:
+        - id
+        - name
+        - location
+        - slots
+      properties:
+        id:
+          type: string
+          example: automat-lobby
+        name:
+          type: string
+          example: Lobby Automat
+        location:
+          type: string
+          example: Erdgeschoss
+        slots:
+          type: array
+          items:
+            $ref: '#/components/schemas/SlotStatus'
+    SlotStatus:
+      type: object
+      additionalProperties: false
+      required:
+        - machine_id
+        - machine_name
+        - sensor_id
+        - slot_label
+        - product_name
+        - fill_percent
+        - units_estimated
+        - max_units
+        - distance_mm
+        - state
+        - measured_at
+        - updated_at
+        - alert_below_units
+      properties:
+        machine_id:
+          type: string
+          example: automat-lobby
+        machine_name:
+          type: string
+          example: Lobby Automat
+        sensor_id:
+          type: string
+          example: fach-a1
+        slot_label:
+          type: string
+          example: A1
+        product_name:
+          type: string
+          example: Cola 0,5l
+        fill_percent:
+          type:
+            - integer
+            - 'null'
+          minimum: 0
+          maximum: 100
+          description: Null until a first reading has been received.
+          example: 63
+        units_estimated:
+          type:
+            - integer
+            - 'null'
+          minimum: 0
+          description: Null until a first reading has been received.
+          example: 4
+        max_units:
+          type: integer
+          minimum: 0
+          example: 7
+        distance_mm:
+          type:
+            - number
+            - 'null'
+          description: Last measured distance in millimeters.
+          example: 184
+        state:
+          $ref: '#/components/schemas/SlotState'
+        measured_at:
+          type:
+            - string
+            - 'null'
+          format: date-time
+          example: '2026-04-15T19:20:00+00:00'
+        updated_at:
+          type:
+            - string
+            - 'null'
+          format: date-time
+          example: '2026-04-15T19:20:02+00:00'
+        alert_below_units:
+          type: integer
+          minimum: 0
+          example: 2
+        input_error:
+          oneOf:
+            - $ref: '#/components/schemas/InputError'
+            - type: 'null'
+          description: Present when the most recent submitted value for this slot was `-1`.
+    InputError:
+      type: object
+      additionalProperties: false
+      required:
+        - received_at
+        - measured_at
+        - reported_value
+        - message
+        - last_good
+      properties:
+        received_at:
+          type: string
+          format: date-time
+          description: Server time when the `-1` value was received.
+          example: '2026-04-15T19:22:10+00:00'
+        measured_at:
+          type: string
+          format: date-time
+          description: Timestamp supplied by the client for the failed reading, or the server time when omitted.
+          example: '2026-04-15T19:22:09+00:00'
+        reported_value:
+          type: integer
+          const: -1
+        message:
+          type: string
+          example: Client meldete einen Messfehler.
+        last_good:
+          oneOf:
+            - $ref: '#/components/schemas/LastGoodReading'
+            - type: 'null'
+          description: Last successfully calculated slot data, if any exists.
+    LastGoodReading:
+      type: object
+      additionalProperties: false
+      required:
+        - distance_mm
+        - fill_percent
+        - units_estimated
+        - max_units
+        - state
+        - measured_at
+      properties:
+        distance_mm:
+          type: number
+          example: 184
+        fill_percent:
+          type: integer
+          minimum: 0
+          maximum: 100
+          example: 63
+        units_estimated:
+          type: integer
+          minimum: 0
+          example: 4
+        max_units:
+          type: integer
+          minimum: 0
+          example: 7
+        state:
+          $ref: '#/components/schemas/SlotState'
+        measured_at:
+          type: string
+          format: date-time
+          example: '2026-04-15T19:20:00+00:00'
+    SlotState:
+      type: string
+      enum:
+        - ok
+        - critical
+        - unknown
+      example: ok
+    AlertEvent:
+      type: object
+      additionalProperties: false
+      required:
+        - id
+        - created_at
+        - payload
+        - deliveries
+      properties:
+        id:
+          type: string
+          description: Unique alert log entry ID generated with PHP uniqid().
+          example: alert_680004979d8512.07480974
+        created_at:
+          type: string
+          format: date-time
+          example: '2026-04-15T19:20:02+00:00'
+        payload:
+          $ref: '#/components/schemas/AlertPayload'
+        deliveries:
+          $ref: '#/components/schemas/AlertDeliveries'
+    AlertPayload:
+      type: object
+      additionalProperties: false
+      required:
+        - event
+        - machine_id
+        - machine_name
+        - sensor_id
+        - slot_label
+        - product_name
+        - distance_mm
+        - units_estimated
+        - max_units
+        - fill_percent
+        - state
+        - previous_state
+        - measured_at
+      properties:
+        event:
+          type: string
+          enum:
+            - critical
+            - recovered
+          example: critical
+        machine_id:
+          type: string
+          example: automat-lobby
+        machine_name:
+          type: string
+          example: Lobby Automat
+        sensor_id:
+          type: string
+          example: fach-a1
+        slot_label:
+          type: string
+          example: A1
+        product_name:
+          type: string
+          example: Cola 0,5l
+        distance_mm:
+          type:
+            - number
+            - 'null'
+          example: 320
+        units_estimated:
+          type:
+            - integer
+            - 'null'
+          example: 1
+        max_units:
+          type:
+            - integer
+            - 'null'
+          example: 7
+        fill_percent:
+          type:
+            - integer
+            - 'null'
+          example: 14
+        state:
+          $ref: '#/components/schemas/SlotState'
+        previous_state:
+          type:
+            - string
+            - 'null'
+          description: Previous slot state before the transition.
+          example: ok
+        measured_at:
+          type:
+            - string
+            - 'null'
+          format: date-time
+          example: '2026-04-15T19:19:59+00:00'
+    AlertDeliveries:
+      type: object
+      additionalProperties: false
+      required:
+        - webhooks
+        - emails
+      properties:
+        webhooks:
+          type: array
+          items:
+            $ref: '#/components/schemas/DeliveryResult'
+        emails:
+          type: array
+          items:
+            $ref: '#/components/schemas/DeliveryResult'
+    DeliveryResult:
+      type: object
+      additionalProperties: false
+      required:
+        - id
+        - success
+        - message
+      properties:
+        id:
+          type: string
+          example: lager-team
+        success:
+          type: boolean
+          example: true
+        message:
+          type: string
+          example: Email versendet.