openapi: 3.0.3
info:
  title: Smart Home MCP API
  version: 1.0.0
  description: >
    OAuth2-based API for MCP agents to access and control the smart home:
    user profile, locations/rooms/devices,
    scenes (scenarios) and schedules.

servers:
  - url: /api/v1

security:
  - bearerAuth: []

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
    oauth2Auth:
      type: oauth2
      flows:
        authorizationCode:
          authorizationUrl: /oauth/authorize
          tokenUrl: /oauth/token
          refreshUrl: /oauth/token
          scopes:
            smarthome.profile.read: Read basic user profile and OAuth scopes for smart home MCP agents
            smarthome.structure.read: Read locations, rooms and devices structure for smart home MCP agents
            smarthome.scenes.manage: Manage smart home scenarios via MCP agents
            smarthome.schedules.manage: Manage smart home schedules via MCP agents
            smarthome.mqtt.connect: Obtain short-lived JWT for MQTT connection

  schemas:
    OAuthTokenResponse:
      type: object
      required: [token_type, expires_in, access_token, refresh_token]
      properties:
        token_type:
          type: string
          example: Bearer
        expires_in:
          type: integer
          example: 31536000
        access_token:
          type: string
        refresh_token:
          type: string

    OAuthAuthorizationCodeTokenRequest:
      type: object
      required: [grant_type, client_id, client_secret, redirect_uri, code]
      properties:
        grant_type:
          type: string
          enum: [authorization_code]
        client_id:
          type: string
        client_secret:
          type: string
        redirect_uri:
          type: string
          format: uri
        code:
          type: string

    OAuthRefreshTokenRequest:
      type: object
      required: [grant_type, refresh_token, client_id, client_secret]
      properties:
        grant_type:
          type: string
          enum: [refresh_token]
        refresh_token:
          type: string
        client_id:
          type: string
        client_secret:
          type: string
        scope:
          type: string
          nullable: true
          description: Optional. Omit to keep previously granted scopes.

    OAuthRevokeResponse:
      type: object
      required: [message]
      properties:
        message:
          type: string
          example: Token revoked successfully. Authorization will be required on next login.

    ErrorResponse:
      type: object
      properties:
        message:
          type: string

    Me:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        email:
          type: string
          format: email
        allowedLocations:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
                format: uuid
              title:
                type: string
        scopes:
          type: array
          items:
            type: string

    MqttTokenResponse:
      type: object
      required: [token, locationId, expiresIn]
      properties:
        token:
          type: string
          description: Short-lived JWT for MQTT connection (use as password with prefix "token=")
        locationId:
          type: string
          format: uuid
          description: Location ID to use as MQTT username
        expiresIn:
          type: integer
          description: Token lifetime in seconds
        expiresAt:
          type: string
          format: date-time
          description: Token expiration time (ISO 8601)

    Location:
      type: object
      properties:
        id:
          type: string
          format: uuid
        title:
          type: string
        protected:
          type: boolean
        latitude:
          type: number
          nullable: true
        longitude:
          type: number
          nullable: true
        stats:
          type: object
          nullable: true
          properties:
            rooms:
              type: integer
            devices:
              type: integer
            scenarios:
              type: integer
            schedules:
              type: integer

    Room:
      type: object
      properties:
        id:
          type: string
          format: uuid
        location_id:
          type: string
          format: uuid
        title:
          type: string
        is_hidden:
          type: boolean
          nullable: true

    Device:
      type: object
      properties:
        id:
          type: string
          format: uuid
        room_id:
          type: string
          format: uuid
        type:
          type: string
        title:
          type: string
        description:
          type: string
          nullable: true
        custom_data:
          type: object
          nullable: true
        privileged:
          type: boolean
          nullable: true
        is_hidden:
          type: boolean
          nullable: true
        topic:
          type: string
          nullable: true

    Scene:
      type: object
      properties:
        id:
          type: string
          format: uuid
        location_id:
          type: string
          format: uuid
        user_id:
          type: string
          format: uuid
        name:
          type: string
        when_triggers:
          type: array
          description: Scenario triggers (schedule/device)
          items:
            type: object
        condition_blocks:
          type: array
          items:
            type: object
        is_active:
          type: boolean
        created_at:
          type: string
          format: date-time
          nullable: true
        updated_at:
          type: string
          format: date-time
          nullable: true

    CreateSceneRequest:
      type: object
      required: [locationId, name, when_triggers, condition_blocks]
      properties:
        locationId:
          type: string
          format: uuid
        name:
          type: string
        when_triggers:
          type: array
          items:
            type: object
        condition_blocks:
          type: array
          items:
            type: object
        is_active:
          type: boolean

    Schedule:
      type: object
      properties:
        id:
          type: string
          format: uuid
        user_id:
          type: string
          format: uuid
        location_id:
          type: string
          format: uuid
        device_id:
          type: string
          format: uuid
          nullable: true
        scenario_id:
          type: string
          format: uuid
          nullable: true
        title:
          type: string
          description: Display title. Always present in response; if not set in DB, computed from scenario/device/room and payload (same as list display).
        topic:
          type: string
        payload:
          type: string
          nullable: true
        schedule_type:
          type: string
          enum: [cron, one_time]
        cron_expression:
          type: string
          nullable: true
        timezone:
          type: string
          nullable: true
        one_time_at:
          type: string
          format: date-time
          nullable: true
        next_due_at_utc:
          type: string
          format: date-time
          nullable: true
        is_active:
          type: boolean
        delete_after_run:
          type: boolean
        time_mode:
          type: string
          enum: [fixed, sunrise, sunset]
        solar_offset_minutes:
          type: integer
          nullable: true
        solar_latitude:
          type: number
          nullable: true
        solar_longitude:
          type: number
          nullable: true
        created_at:
          type: string
          format: date-time
          nullable: true
        updated_at:
          type: string
          format: date-time
          nullable: true

    CreateScheduleRequest:
      type: object
      required: [locationId, schedule_type]
      properties:
        locationId:
          type: string
          format: uuid
        deviceId:
          type: string
          format: uuid
          nullable: true
        scenarioId:
          type: string
          format: uuid
          nullable: true
        title:
          type: string
          nullable: true
        topic:
          type: string
          nullable: true
        payload:
          type: string
          nullable: true
        schedule_type:
          type: string
          enum: [cron, one_time]
        cron_expression:
          type: string
          nullable: true
        timezone:
          type: string
          nullable: true
        one_time_at:
          type: string
          format: date-time
          nullable: true
        is_active:
          type: boolean
        delete_after_run:
          type: boolean
        time_mode:
          type: string
          enum: [fixed, sunrise, sunset]
        solar_offset_minutes:
          type: integer
          nullable: true
        solar_latitude:
          type: number
          nullable: true
        solar_longitude:
          type: number
          nullable: true

paths:
  /oauth/authorize:
    get:
      summary: Start OAuth2 authorization code flow for MCP API
      description: >
        Browser endpoint. Redirect the user here to obtain an authorization code for MCP API access.
        After user approval, the callback URL receives `?code=...`.
      servers:
        - url: /
      parameters:
        - in: query
          name: client_id
          required: true
          schema:
            type: string
        - in: query
          name: redirect_uri
          required: true
          schema:
            type: string
            format: uri
        - in: query
          name: response_type
          required: true
          schema:
            type: string
            enum: [code]
        - in: query
          name: scope
          required: false
          schema:
            type: string
          description: Space-separated scopes requested by the MCP client.
        - in: query
          name: state
          required: false
          schema:
            type: string
      responses:
        '200':
          description: Authorization approval page
        '302':
          description: Redirect to callback URL with authorization code

  /oauth/token:
    post:
      summary: Exchange authorization code or refresh token
      description: >
        Standard OAuth2 token endpoint for MCP API. Use `grant_type=authorization_code`
        to exchange an authorization code for tokens, or `grant_type=refresh_token`
        to obtain a new access token.
      servers:
        - url: /
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              oneOf:
                - $ref: '#/components/schemas/OAuthAuthorizationCodeTokenRequest'
                - $ref: '#/components/schemas/OAuthRefreshTokenRequest'
            examples:
              authorization_code:
                summary: Exchange authorization code
                value:
                  grant_type: authorization_code
                  client_id: your-client-id
                  client_secret: your-client-secret
                  redirect_uri: https://example.com/callback
                  code: received-authorization-code
              refresh_token:
                summary: Refresh access token
                value:
                  grant_type: refresh_token
                  refresh_token: previous-refresh-token
                  client_id: your-client-id
                  client_secret: your-client-secret
      responses:
        '200':
          description: Access token response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OAuthTokenResponse'
        '400':
          description: Invalid grant or malformed request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Invalid client credentials
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /api/oauth/revoke:
    post:
      summary: Revoke current OAuth token set
      description: >
        Revokes the current OAuth access token and associated refresh tokens for the same user/client pair.
        Use the current MCP API bearer token in the `Authorization` header.
      servers:
        - url: /
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Token revoked successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OAuthRevokeResponse'
        '401':
          description: Unauthenticated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: No active token found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /me:
    get:
      summary: Get current user profile for MCP
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Current user info and scopes
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Me'
        '403':
          description: Missing smarthome.profile.read scope

  /token:
    get:
      summary: Get short-lived JWT for MQTT connection
      description: >
        Requires scope smarthome.mqtt.connect. Returns a JWT to connect to MQTT broker
        (username = locationId, password = "token=" + token). Token is short-lived (e.g. 1 hour).
      security:
        - bearerAuth: []
      parameters:
        - in: query
          name: location_id
          schema:
            type: string
            format: uuid
          required: false
          description: Location to get MQTT token for; if omitted, user's default location is used
      responses:
        '200':
          description: MQTT JWT and connection info
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MqttTokenResponse'
        '403':
          description: Missing smarthome.mqtt.connect scope or location not allowed for token
        '404':
          description: Location not found

  /locations:
    get:
      summary: List locations available to the token
      security:
        - bearerAuth: []
      parameters:
        - in: query
          name: location_id
          schema:
            type: string
            format: uuid
          required: false
      responses:
        '200':
          description: List of locations
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Location'

  /locations/{locationId}:
    get:
      summary: Get location details and basic stats
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: locationId
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Location details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Location'

  /locations/{locationId}/rooms:
    get:
      summary: List rooms for a location
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: locationId
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Rooms for location
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Room'

  /rooms/{roomId}:
    get:
      summary: Get room details
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: roomId
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Room
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Room'

  /locations/{locationId}/devices:
    get:
      summary: List devices in location
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: locationId
          required: true
          schema:
            type: string
            format: uuid
        - in: query
          name: room_id
          schema:
            type: string
            format: uuid
        - in: query
          name: type
          schema:
            type: string
        - in: query
          name: search
          schema:
            type: string
      responses:
        '200':
          description: Devices list
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Device'

  /rooms/{roomId}/devices:
    get:
      summary: List devices in room
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: roomId
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Devices in room
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Device'

  /devices/{deviceId}:
    get:
      summary: Get device details
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: deviceId
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Device
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Device'

  /scenes:
    get:
      summary: List scenes (scenarios) across accessible locations
      security:
        - bearerAuth: []
      parameters:
        - in: query
          name: location_id
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Scenes list
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Scene'
    post:
      summary: Create new scene
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateSceneRequest'
      responses:
        '201':
          description: Created scene
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Scene'

  /scenes/{sceneId}:
    get:
      summary: Get scene
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: sceneId
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Scene
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Scene'
    put:
      summary: Update scene
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: sceneId
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateSceneRequest'
      responses:
        '200':
          description: Updated scene
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Scene'
    patch:
      summary: Partially update scene
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: sceneId
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
      responses:
        '200':
          description: Updated scene
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Scene'
    delete:
      summary: Delete scene
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: sceneId
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '204':
          description: Deleted

  /scenes/{sceneId}/run:
    post:
      summary: Record manual run of a scene
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: sceneId
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Run created
          content:
            application/json:
              schema:
                type: object
                properties:
                  runId:
                    type: string
                    format: uuid
                  status:
                    type: string

  /schedules:
    get:
      summary: List schedules across accessible locations
      security:
        - bearerAuth: []
      parameters:
        - in: query
          name: location_id
          schema:
            type: string
            format: uuid
        - in: query
          name: active
          schema:
            type: boolean
        - in: query
          name: scenario_id
          schema:
            type: string
            format: uuid
        - in: query
          name: device_id
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Schedules list
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Schedule'
    post:
      summary: Create new schedule
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateScheduleRequest'
      responses:
        '201':
          description: Created schedule
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Schedule'

  /schedules/{scheduleId}:
    get:
      summary: Get schedule
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: scheduleId
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Schedule
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Schedule'
    put:
      summary: Update schedule
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: scheduleId
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
      responses:
        '200':
          description: Updated schedule
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Schedule'
    patch:
      summary: Partially update schedule
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: scheduleId
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
      responses:
        '200':
          description: Updated schedule
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Schedule'
    delete:
      summary: Delete schedule
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: scheduleId
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '204':
          description: Deleted

  /schedules/{scheduleId}/toggle:
    post:
      summary: Toggle schedule active state
      security:
        - bearerAuth: []
      parameters:
        - in: path
          name: scheduleId
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                is_active:
                  type: boolean
      responses:
        '200':
          description: Updated schedule
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Schedule'

