openapi: 3.1.0
info:
  title: Rocket Home API (generalized, group/version)
  version: 1.0.0
  description: >
    Клиент-нейтральный OAuth2 API умного дома Rocket Home для MCP, Node-RED и любых
    будущих клиентов. Версионирование — в стиле Kubernetes API: каждая группа ресурсов
    (`identity`, `structure`, `devices`, `automations`, `mqtt`) несёт свою версию и
    бампается независимо. Путь: `/api/{group}/{version}/...`; локация = namespace.


    Discovery: `GET /api` (группы), `GET /api/{group}` (версии+ресурсы),
    `/.well-known/oauth-authorization-server` (RFC 8414),
    `/.well-known/oauth-protected-resource` (RFC 9728).
    Динамическая регистрация клиентов — `POST /oauth/register` (RFC 7591), гейтнута
    initial access token.


    Гранулярный ACL: scopes `rh.<resource>:<action>`. Любой scope может нести
    qualifier-суффикс, сужающий грант до локации/комнаты (подмножество базы, выдаётся
    на consent-экране): `rh.devices:control@room:{uuid}`, `rh.structure:read@location:{uuid}`.

servers:
  - url: /

security:
  - oauth2Auth: []

tags:
  - name: discovery
  - name: oauth
  - name: identity
  - name: structure
  - name: devices
  - name: automations
  - name: mqtt

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
    oauth2Auth:
      type: oauth2
      description: >
        authorization_code + PKCE (S256). Базовые scopes ниже; к любому из них на
        consent-экране может добавляться qualifier `@location:{uuid}` / `@room:{uuid}`.
      flows:
        authorizationCode:
          authorizationUrl: /oauth/authorize
          tokenUrl: /oauth/token
          refreshUrl: /oauth/token
          scopes:
            rh.profile:read: Read your profile and granted scopes
            rh.structure:read: Read structure (locations, rooms, devices)
            rh.devices:read: Read device state
            rh.devices:control: Control devices (via MQTT)
            rh.automations:read: Read automations
            rh.automations:write: Create and modify automations
            rh.mqtt:connect: Connect to the MQTT broker
  schemas:
    Error:
      type: object
      properties:
        message: { type: string }
        required_scope: { type: string }
    DiscoveryGroups:
      type: object
      properties:
        groups:
          type: array
          items:
            type: object
            properties:
              name: { type: string, example: structure }
              preferredVersion: { type: string, example: v1 }
              versions:
                type: array
                items: { type: string }
    GroupDetail:
      type: object
      properties:
        name: { type: string }
        preferredVersion: { type: string }
        versions:
          type: object
          additionalProperties:
            type: object
            properties:
              resources:
                type: array
                items:
                  type: object
                  properties:
                    resource: { type: string }
                    scopes: { type: array, items: { type: string } }
                    namespaced: { type: boolean }
                    verbs: { type: array, items: { type: string } }
    AuthorizationServerMetadata:
      type: object
      properties:
        issuer: { type: string }
        authorization_endpoint: { type: string }
        token_endpoint: { type: string }
        registration_endpoint: { type: string }
        revocation_endpoint: { type: string }
        scopes_supported: { type: array, items: { type: string } }
        response_types_supported: { type: array, items: { type: string } }
        grant_types_supported: { type: array, items: { type: string } }
        code_challenge_methods_supported: { type: array, items: { type: string } }
    ClientRegistrationRequest:
      type: object
      required: [client_name, redirect_uris]
      properties:
        client_name: { type: string }
        redirect_uris:
          type: array
          items: { type: string, description: https или loopback http (127.0.0.1/localhost) }
        grant_types:
          type: array
          items: { type: string, enum: [authorization_code, refresh_token] }
        token_endpoint_auth_method:
          type: string
          enum: [none, client_secret_basic, client_secret_post]
        scope: { type: string, description: space-separated rh.* scopes }
    ClientRegistrationResponse:
      type: object
      properties:
        client_id: { type: string }
        client_secret: { type: string, description: только для confidential }
        client_id_issued_at: { type: integer }
        client_name: { type: string }
        redirect_uris: { type: array, items: { type: string } }
        grant_types: { type: array, items: { type: string } }
        token_endpoint_auth_method: { type: string }
        scope: { type: string }
    Me:
      type: object
      properties:
        id: { type: string, format: uuid }
        sub: { type: string, format: uuid, description: идентификатор тенанта (= id) }
        name: { type: string }
        email: { type: string }
        allowedLocations:
          type: array
          items:
            type: object
            properties:
              id: { type: string, format: uuid }
              title: { type: string }
        scopes: { type: array, items: { type: string } }
    MqttToken:
      type: object
      properties:
        token: { type: string, description: короткоживущий RS256 JWT }
        name: { type: string, description: MQTT username (= locationId) }
        locationId: { type: string, format: uuid }
        expiresIn: { type: integer }
        expiresAt: { type: string, format: date-time }
  parameters:
    LocationId:
      name: location
      in: path
      required: true
      schema: { type: string, format: uuid }
    RoomId:
      name: room
      in: path
      required: true
      schema: { type: string, format: uuid }
  responses:
    Forbidden:
      description: insufficient_scope или нет доступа к ресурсу
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }

paths:
  /api:
    get:
      tags: [discovery]
      summary: Список групп API (K8s-style)
      security: []
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DiscoveryGroups' }
  /api/{group}:
    get:
      tags: [discovery]
      summary: Версии и ресурсы группы
      security: []
      parameters:
        - name: group
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/GroupDetail' }
        '404': { description: Unknown group }
  /.well-known/oauth-authorization-server:
    get:
      tags: [oauth]
      summary: Метаданные authorization server (RFC 8414)
      security: []
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AuthorizationServerMetadata' }
  /.well-known/oauth-protected-resource:
    get:
      tags: [oauth]
      summary: Метаданные protected resource (RFC 9728)
      security: []
      responses:
        '200': { description: OK }
  /oauth/register:
    post:
      tags: [oauth]
      summary: Dynamic Client Registration (RFC 7591)
      description: Требует initial access token (Bearer). Без него — 404.
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ClientRegistrationRequest' }
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ClientRegistrationResponse' }
        '400': { description: invalid_redirect_uri / invalid_scope }
        '401': { description: invalid_token }
        '404': { description: registration_disabled }
  /api/identity/v1/me:
    get:
      tags: [identity]
      summary: Профиль текущего пользователя
      security:
        - oauth2Auth: [rh.profile:read]
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Me' }
        '403': { $ref: '#/components/responses/Forbidden' }
  /api/structure/v1/locations:
    get:
      tags: [structure]
      summary: Доступные локации
      security:
        - oauth2Auth: [rh.structure:read]
      responses:
        '200': { description: OK }
        '403': { $ref: '#/components/responses/Forbidden' }
  /api/structure/v1/locations/{location}:
    get:
      tags: [structure]
      summary: Локация + статистика
      security:
        - oauth2Auth: [rh.structure:read]
      parameters: [ { $ref: '#/components/parameters/LocationId' } ]
      responses:
        '200': { description: OK }
        '403': { $ref: '#/components/responses/Forbidden' }
  /api/structure/v1/locations/{location}/rooms:
    get:
      tags: [structure]
      summary: Комнаты локации
      security:
        - oauth2Auth: [rh.structure:read]
      parameters: [ { $ref: '#/components/parameters/LocationId' } ]
      responses:
        '200': { description: OK }
  /api/structure/v1/locations/{location}/rooms/{room}:
    get:
      tags: [structure]
      summary: Комната
      security:
        - oauth2Auth: [rh.structure:read]
      parameters:
        - { $ref: '#/components/parameters/LocationId' }
        - { $ref: '#/components/parameters/RoomId' }
      responses:
        '200': { description: OK }
        '404': { description: комната не принадлежит локации }
  /api/devices/v1/locations/{location}/devices:
    get:
      tags: [devices]
      summary: Устройства локации (read-only)
      security:
        - oauth2Auth: [rh.devices:read]
      parameters:
        - { $ref: '#/components/parameters/LocationId' }
        - { name: room_id, in: query, schema: { type: string, format: uuid } }
        - { name: type, in: query, schema: { type: string } }
        - { name: search, in: query, schema: { type: string } }
      responses:
        '200': { description: OK }
        '403': { $ref: '#/components/responses/Forbidden' }
  /api/devices/v1/locations/{location}/rooms/{room}/devices:
    get:
      tags: [devices]
      summary: Устройства комнаты
      security:
        - oauth2Auth: [rh.devices:read]
      parameters:
        - { $ref: '#/components/parameters/LocationId' }
        - { $ref: '#/components/parameters/RoomId' }
      responses:
        '200': { description: OK }
  /api/devices/v1/locations/{location}/devices/{device}:
    get:
      tags: [devices]
      summary: Устройство
      security:
        - oauth2Auth: [rh.devices:read]
      parameters:
        - { $ref: '#/components/parameters/LocationId' }
        - { name: device, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        '200': { description: OK }
  /api/automations/v2/locations/{location}/automations:
    get:
      tags: [automations]
      summary: Список автоматизаций локации
      security:
        - oauth2Auth: [rh.automations:read]
      parameters:
        - { $ref: '#/components/parameters/LocationId' }
        - { name: active, in: query, schema: { type: boolean } }
      responses:
        '200': { description: OK }
    post:
      tags: [automations]
      summary: Создать автоматизацию
      security:
        - oauth2Auth: [rh.automations:write]
      parameters: [ { $ref: '#/components/parameters/LocationId' } ]
      responses:
        '201': { description: Created }
        '403': { $ref: '#/components/responses/Forbidden' }
  /api/automations/v2/automations/{automation}:
    description: Одиночные операции по глобальному uuid (namespace не требуется).
    get:
      tags: [automations]
      summary: Автоматизация
      security:
        - oauth2Auth: [rh.automations:read]
      parameters:
        - { name: automation, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        '200': { description: OK }
    put:
      tags: [automations]
      summary: Обновить автоматизацию
      security:
        - oauth2Auth: [rh.automations:write]
      parameters:
        - { name: automation, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        '200': { description: OK }
    delete:
      tags: [automations]
      summary: Удалить автоматизацию
      security:
        - oauth2Auth: [rh.automations:write]
      parameters:
        - { name: automation, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        '204': { description: No Content }
  /api/automations/v2/automations/{automation}/toggle:
    post:
      tags: [automations]
      summary: Включить/выключить автоматизацию
      security:
        - oauth2Auth: [rh.automations:write]
      parameters:
        - { name: automation, in: path, required: true, schema: { type: string, format: uuid } }
      responses:
        '200': { description: OK }
  /api/mqtt/v1/token:
    get:
      tags: [mqtt]
      summary: Короткоживущий MQTT-JWT для дефолтной локации (cluster-scoped)
      description: Для клиентов с одной локацией (напр. Node-RED). Локация — дефолтная для пользователя.
      security:
        - oauth2Auth: [rh.mqtt:connect]
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/MqttToken' }
        '403': { $ref: '#/components/responses/Forbidden' }
  /api/mqtt/v1/locations/{location}/token:
    get:
      tags: [mqtt]
      summary: Короткоживущий MQTT-JWT для локации
      description: >
        Требует `rh.mqtt:connect`. `rh.devices:control@location/@room` сужает выдачу
        (room-claim несётся в токене; broker-side enforcement room-уровня — follow-up в hmq2).
      security:
        - oauth2Auth: [rh.mqtt:connect]
      parameters: [ { $ref: '#/components/parameters/LocationId' } ]
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/MqttToken' }
        '403': { $ref: '#/components/responses/Forbidden' }
