openapi: 3.1.0
info:
  title: Mowlog Public API
  version: "1.0.0"
  description: |
    Token-authenticated REST API for logging mowing activity and firmware
    changes on behalf of a Mowlog user. See https://www.mowlog.com/developers
    for the narrative guide and Home Assistant quick start.

    ## Tolerant reader
    Clients MUST ignore unknown response fields and unknown enum values.
    Mowlog will add fields and enum values within v1 without notice.
servers:
  - url: https://www.mowlog.com/api/public/v1
    description: Production
security:
  - BearerToken: []
components:
  securitySchemes:
    BearerToken:
      type: http
      scheme: bearer
      bearerFormat: mowlog_pat
  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              enum:
                - invalid_token
                - insufficient_scope
                - mower_not_allowed
                - not_found
                - unsupported_media_type
                - validation_error
                - rate_limited
                - payload_too_large
                - internal
            message: { type: string }
            fields:
              type: object
              additionalProperties: { type: string }
    Me:
      type: object
      properties:
        handle: { type: string, nullable: true }
        token_name: { type: string }
        scopes:
          type: array
          items: { type: string }
        mower_id: { type: string, format: uuid, nullable: true }
    FirmwareComponent:
      type: object
      required: [component, version]
      properties:
        component: { type: string }
        version: { type: string }
    Mower:
      type: object
      required: [id, brand, model, positioning, firmware]
      properties:
        id: { type: string, format: uuid }
        nickname: { type: string, nullable: true }
        brand: { type: string }
        model: { type: string }
        positioning:
          type: string
          # Values injected from src/lib/api-vocabulary.ts (POSITIONING_SYSTEMS)
          enum: [wire, rtk, vision, internet, other]
        firmware:
          type: array
          items: { $ref: "#/components/schemas/FirmwareComponent" }
    Source:
      type: object
      properties:
        kind:
          type: string
          # Values injected from src/lib/api-vocabulary.ts (SOURCE_KINDS)
          enum: [manual, home_assistant, script, import, other]
        client: { type: string }
        version: { type: string }
    EntryCreate:
      type: object
      required: [mower_id, type, occurred_at]
      properties:
        mower_id: { type: string, format: uuid }
        type:
          type: string
          description: Entry type. Values from src/lib/api-vocabulary.ts (ENTRY_TYPES).
          enum:
            - auto_mow
            - auto_user
            - manual_mow
            - cleaning
            - repair
            - offline
            - blade_change
            - firmware_update
        occurred_at: { type: string, format: date-time }
        ended_at: { type: string, format: date-time, nullable: true }
        status:
          type: string
          description: Status. Values from src/lib/api-vocabulary.ts (STATUSES).
          enum: [success, partial, failure, warning, maintenance, inconclusive]
        failure_category: { type: string, nullable: true }
        error_code: { type: string, maxLength: 64, nullable: true }
        notes: { type: string, maxLength: 2000, nullable: true }
        description: { type: string, maxLength: 2000, nullable: true }
        data:
          type: object
          additionalProperties: true
          description: Free-form JSON payload up to 8 KB.
        source: { $ref: "#/components/schemas/Source" }
        idempotency_key: { type: string, maxLength: 128, nullable: true }
    Entry:
      type: object
      properties:
        id: { type: string, format: uuid }
        mower_id: { type: string, format: uuid }
        type: { type: string }
        occurred_at: { type: string, format: date-time }
        source: { $ref: "#/components/schemas/Source" }
        url: { type: string, format: uri }
    FirmwareUpdate:
      type: object
      required: [components]
      properties:
        components:
          type: array
          minItems: 1
          items: { $ref: "#/components/schemas/FirmwareComponent" }
        detected_at: { type: string, format: date-time }
        source: { $ref: "#/components/schemas/Source" }
    MetaEnums:
      type: object
      properties:
        entry_types: { type: array, items: { type: string } }
        statuses: { type: array, items: { type: string } }
        positioning_systems: { type: array, items: { type: string } }
        weathers: { type: array, items: { type: string } }
        grass_moistures: { type: array, items: { type: string } }
        source_kinds: { type: array, items: { type: string } }
        scopes: { type: array, items: { type: string } }
        failure_category_suggestions:
          type: object
          additionalProperties:
            type: array
            items: { type: string }
paths:
  /me:
    get:
      summary: Identify the current token
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Me" }
        "401":
          description: Unauthorized
          content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
  /mowers:
    get:
      summary: List mowers (scope mowers:read)
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  mowers:
                    type: array
                    items: { $ref: "#/components/schemas/Mower" }
  /mowers/{id}:
    get:
      summary: Get a single mower (scope mowers:read)
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string, format: uuid }
      responses:
        "200":
          description: OK
          content: { application/json: { schema: { $ref: "#/components/schemas/Mower" } } }
        "404":
          description: Not found
          content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
  /mowers/{id}/firmware:
    put:
      summary: Sync firmware versions (scope firmware:write)
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string, format: uuid }
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/FirmwareUpdate" }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  changed: { type: boolean }
                  entries_created:
                    type: array
                    items: { type: string, format: uuid }
                  firmware:
                    type: array
                    items: { $ref: "#/components/schemas/FirmwareComponent" }
  /entries:
    post:
      summary: Create a logbook entry (scope entries:write)
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/EntryCreate" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  entry: { $ref: "#/components/schemas/Entry" }
        "200":
          description: |
            Returned when an existing entry is replayed by idempotency_key.
            The response header `X-Idempotent-Replay: true` is set. The
            original stored entry is returned; the replayed payload is ignored.
          headers:
            X-Idempotent-Replay:
              schema: { type: string, enum: ["true"] }
          content:
            application/json:
              schema:
                type: object
                properties:
                  entry: { $ref: "#/components/schemas/Entry" }
        "422":
          description: Validation error
          content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
  /meta/enums:
    get:
      summary: Current vocabulary
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/MetaEnums" }
