openapi: 3.1.1
info:
  title: 'Voltic Hotel Integration API v1'
  version: 1.0.0
  description: |
    Contrato genérico para integrar hoteles con el inventario, estados y series de Voltic.

    Los módulos opcionales de ocupación histórica, runtime y preclimatización requieren
    sus permisos y features vigentes. Publicarlos no los habilita para todas las cuentas.
    Las capacidades `x-voltic-proposed-capability` son propuestas para un perfil futuro;
    no son scopes OAuth ni restricciones nuevas de los tokens actuales.

    Usa los UUID de inventario para mapear hoteles, plantas, habitaciones y dispositivos.
    El alias o número de habitación es un dato de mapeo. En series con `scope=device`,
    `scopeId` es el identificador físico `hardware_id` (o IEEE legacy), no necesariamente
    el UUID de inventario. Las respuestas conservan campos técnicos y relaciones actuales.
    `fields` e `include` seleccionan información; no son una barrera de autorización.
    Los ejemplos mínimos no limitan los campos que puede devolver el servidor.

    Los instantes se expresan en UTC; la zona IANA del edificio determina la presentación
    y los buckets de calendario. En series, `from` es inclusivo y `to` exclusivo.
    Consulta los límites, unidades, agregaciones, calidad y valores ausentes de cada operación:
    no existe una ventana ni frecuencia de polling uniforme para todo el catálogo.
    Los listados de inventario actuales devuelven colecciones sin paginación configurable.
    La energía es consumo por intervalo, no el valor bruto del contador acumulado;
    el runtime y sus estimaciones no equivalen a una medición eléctrica.

    Las denegaciones por feature usan `403` con `error=feature_unavailable`;
    las de rol/perímetro conservan sus formatos actuales. Una escritura puede responder
    `403` con `error=subscription_read_only` mientras las lecturas siguen disponibles.
    Respeta `Retry-After` cuando se emita. Los errores no tienen un envelope único.
    Reintenta las lecturas según su estado; repetir preclimatización renueva la expiración.
servers:
  -
    url: 'https://api.voltic.es/api/v1/bms'
    description: Producción
security:
  -
    BearerAuth: []
tags:
  -
    name: Buildings
  -
    name: Floors
  -
    name: Rooms
  -
    name: Devices
  -
    name: Zones
  -
    name: 'Time Series'
  -
    name: Occupancy
  -
    name: Preconditioning
x-voltic-profile: hotel-integrations-v1
x-voltic-authentication: |
  API token Bearer emitido por Voltic para una cuenta autorizada y los edificios acordados.
  Envía `Authorization: Bearer <token>`; solicita el alta y los permisos a Voltic.
  Se mantienen las comprobaciones de pertenencia, rol, ámbito del token y features.
  El catálogo no publica el login interno ni crea un perfil nuevo de credenciales.
paths:
  /buildings:
    get:
      operationId: listBuildings
      tags:
        - Buildings
      summary: 'Listar edificios'
      description: |
        Devuelve los edificios a los que el usuario autenticado tiene acceso. Las suscripciones en
        modo `billing_only` también aparecen para que el usuario pueda descubrir el building, consultar
        sus relaciones de lectura y recuperar su suscripción; las escrituras y demás superficies siguen
        bloqueadas.
        Usa `include` para anidar relaciones y `fields` para devolver solo ciertos atributos.
        `timezone` siempre está presente, incluso con `fields`; úsala para construir los rangos
        analíticos desde el listado, sin esperar a consultar el detalle del edificio.
        Los campos opcionales y las relaciones solo se calculan cuando se solicitan.
      security:
        -
          BearerAuth: []
      parameters:
        -
          $ref: '#/components/parameters/IncludeQueryParam'
        -
          $ref: '#/components/parameters/FieldsQueryParam'
      responses:
        '200':
          description: 'Colección de edificios visibles para la cuenta y el ámbito del token; sin paginación configurable.'
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Building'
              examples:
                basic:
                  summary: 'Edificios visibles'
                  value:
                    data:
                      -
                        uuid: 0194b243-d8dd-7112-b6f9-e9f088d50ba3
                        name: 'Hotel Solaris'
                        type: hotel
                        demo_kind: null
                        timezone: Europe/Madrid
                      -
                        uuid: 0194b251-2312-70c4-a800-0b6de9a55d1b
                        name: 'Campus Norte'
                        type: office
                        demo_kind: null
                        timezone: Atlantic/Canary
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          $ref: '#/components/responses/InternalError'
      x-voltic-stability: stable
      x-voltic-integration-module: core
      x-voltic-proposed-capability: 'inventory:read'
      x-voltic-route-name: bms.buildings.index
  '/buildings/{uuid}':
    get:
      operationId: getBuilding
      tags:
        - Buildings
      summary: 'Obtener un edificio por UUID'
      description: |
        Devuelve los datos detallados de un edificio accesible para el usuario. En `billing_only`
        devuelve las relaciones de lectura solicitadas mediante `include` (rooms, hubs, floors, zones
        y devices) y permite abrir la recuperación de suscripción; las escrituras siguen bloqueadas.
        `timezone` siempre se incluye, también con `fields`. Los campos opcionales no solicitados
        no se calculan; los permisos de lectura se comprueban en cada petición.
      security:
        -
          BearerAuth: []
      parameters:
        -
          $ref: '#/components/parameters/BuildingUuid'
        -
          $ref: '#/components/parameters/IncludeQueryParam'
        -
          $ref: '#/components/parameters/FieldsQueryParam'
      responses:
        '200':
          description: 'Edificio encontrado'
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Building'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/FeatureUnavailable'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'
      x-voltic-stability: stable
      x-voltic-integration-module: core
      x-voltic-proposed-capability: 'inventory:read'
      x-voltic-route-name: bms.buildings.show
  '/buildings/{buildingUuid}/floors':
    get:
      operationId: listFloorsByBuilding
      tags:
        - Floors
      summary: 'Listar plantas de un edificio'
      description: 'Devuelve las plantas asociadas al edificio indicado.'
      security:
        -
          BearerAuth: []
      parameters:
        -
          $ref: '#/components/parameters/BuildingUuidParam'
        -
          $ref: '#/components/parameters/IncludeQueryParam'
        -
          $ref: '#/components/parameters/FieldsQueryParam'
      responses:
        '200':
          description: 'Plantas encontradas.'
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Floor'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/FeatureUnavailable'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'
      x-voltic-stability: stable
      x-voltic-integration-module: core
      x-voltic-proposed-capability: 'inventory:read'
      x-voltic-route-name: bms.buildings.floors.index
  '/floors/{uuid}':
    get:
      operationId: getFloor
      tags:
        - Floors
      summary: 'Obtener planta'
      description: 'Devuelve una planta por su UUID.'
      security:
        -
          BearerAuth: []
      parameters:
        -
          $ref: '#/components/parameters/FloorUuid'
        -
          $ref: '#/components/parameters/IncludeQueryParam'
        -
          $ref: '#/components/parameters/FieldsQueryParam'
      responses:
        '200':
          description: 'Planta encontrada.'
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Floor'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/FeatureUnavailable'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'
      x-voltic-stability: stable
      x-voltic-integration-module: core
      x-voltic-proposed-capability: 'inventory:read'
      x-voltic-route-name: bms.floors.show
  '/buildings/{buildingUuid}/rooms':
    get:
      operationId: listRoomsByBuilding
      tags:
        - Rooms
      summary: 'Listar habitaciones de un edificio'
      description: 'Devuelve las habitaciones asociadas al edificio indicado. La autorización se comprueba antes de servir la caché de la variante solicitada. El autor de instalación se resuelve solo con fields=installer_updated_by; una lectura de contexto del dashboard no consulta usuarios instaladores para cada habitación.'
      security:
        -
          BearerAuth: []
      parameters:
        -
          $ref: '#/components/parameters/BuildingUuidParam'
        -
          $ref: '#/components/parameters/IncludeQueryParam'
        -
          $ref: '#/components/parameters/FieldsQueryParam'
        -
          name: tag_uuids
          in: query
          required: false
          description: 'Lista separada por comas de UUIDs de tags. Devuelve habitaciones que tengan cualquiera de esos tags.'
          schema:
            type: string
            example: '123e4567-e89b-12d3-a456-426614174999,123e4567-e89b-12d3-a456-426614174998'
      responses:
        '200':
          description: 'Habitaciones encontradas.'
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Room'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'
      x-voltic-stability: stable
      x-voltic-integration-module: core
      x-voltic-proposed-capability: 'inventory:read'
      x-voltic-route-name: bms.buildings.rooms.index
  '/buildings/{buildingUuid}/room-occupancy-states':
    get:
      tags:
        - Occupancy
      summary: 'Consultar la ocupación actual de las habitaciones'
      description: |
        Snapshot del último estado físico de ocupación conocido de las habitaciones del hotel;
        no representa reservas ni ocupación PMS. `occupancy: null` significa desconocido,
        nunca habitación vacía. `generated_at` indica cuándo se construyó la respuesta,
        no cuándo se observó cada habitación. Los snapshots de hubs se cachean 10 segundos.
        Un hub o subsistema no disponible, o un sensor sin estado fiable, produce null.
        `changed_at` puede ser null aunque la ocupación sea conocida: el hub solo informa
        del inicio de un periodo ocupado. Todos los instantes son UTC.
        La ruta limita actualmente a 30 peticiones por minuto con el bucket bms-occupancy;
        no establece una recomendación universal de polling para otros endpoints.
      parameters:
        -
          name: buildingUuid
          in: path
          required: true
          description: 'UUID del edificio en Voltic.'
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: 'Snapshot actual de ocupación'
          content:
            application/json:
              schema:
                type: object
                required:
                  - generated_at
                  - timezone
                  - data
                properties:
                  generated_at:
                    type: string
                    format: date-time
                  timezone:
                    type: string
                    description: 'Zona horaria IANA del edificio.'
                    example: Europe/Madrid
                  data:
                    type: array
                    items:
                      type: object
                      required:
                        - room_uuid
                        - occupancy
                        - changed_at
                      properties:
                        room_uuid:
                          type: string
                          format: uuid
                        occupancy:
                          type:
                            - boolean
                            - 'null'
                          description: 'true con presencia, false sin presencia y null si se desconoce.'
                        changed_at:
                          type:
                            - string
                            - 'null'
                          format: date-time
                          description: 'Instante UTC en el que comenzó el estado actual; null si no se conoce ese instante, aunque se conozca la ocupación.'
              example:
                generated_at: '2026-08-04T10:15:30Z'
                timezone: Europe/Madrid
                data:
                  -
                    room_uuid: 0194b251-2312-70c4-a800-0b6de9a55d20
                    occupancy: true
                    changed_at: '2026-08-04T10:14:52Z'
                  -
                    room_uuid: 0194b251-2312-70c4-a800-0b6de9a55d21
                    occupancy: null
                    changed_at: null
        '400':
          description: 'Identificador no válido'
          content:
            application/json:
              schema:
                type: object
                required:
                  - code
                  - message
                properties:
                  code:
                    type: string
                  message:
                    type: string
        '401':
          description: 'API token ausente, inválido o revocado'
          content:
            application/json:
              schema:
                type: object
                required:
                  - code
                  - message
                properties:
                  code:
                    type: string
                  message:
                    type: string
        '403':
          description: 'Edificio no accesible o feature de inventario/plataforma no disponible.'
          content:
            application/json:
              schema:
                anyOf:
                  -
                    type: object
                    required:
                      - code
                      - message
                    properties:
                      code:
                        type: string
                      message:
                        type: string
                  -
                    $ref: '#/components/schemas/FeatureUnavailableError'
        '404':
          description: 'Edificio o habitación no encontrado'
          content:
            application/json:
              schema:
                type: object
                required:
                  - code
                  - message
                properties:
                  code:
                    type: string
                  message:
                    type: string
        '429':
          description: 'Límite de peticiones superado; esperar antes de reintentar.'
          headers:
            Retry-After:
              description: 'Segundos hasta poder reintentar, cuando lo emite el middleware de throttling.'
              schema:
                type: integer
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
        '500':
          description: 'Error interno'
          content:
            application/json:
              schema:
                type: object
                required:
                  - code
                  - message
                properties:
                  code:
                    type: string
                  message:
                    type: string
      operationId: listBuildingRoomOccupancyStates
      security:
        -
          BearerAuth: []
      x-voltic-stability: stable
      x-voltic-integration-module: core
      x-voltic-proposed-capability: 'occupancy:read'
      x-voltic-route-name: bms.buildings.room_occupancy_states
  '/rooms/{roomUuid}/actions/precondition':
    post:
      tags:
        - Preconditioning
      summary: 'Activar el modo confort durante 15 minutos'
      description: |
        Solicita el modo confort configurado durante 15 minutos desde cada petición aceptada.
        Voltic decide enfriar/calentar y las consignas; no admite duración o consigna arbitrarias.
        Cada repetición válida renueva `expires_at`: la acción no es idempotente.
        `202` confirma aceptación, no que se haya alcanzado la temperatura deseada.
        Al vencer, se solicita volver a `eco`; la restauración queda pendiente hasta observar
        ese modo. Un hub desconectado puede retrasar la confirmación remota del retorno.
        Requiere permiso de control, features de control y una suscripción que permita escritura.
        La ruta limita actualmente a 10 peticiones por minuto con el bucket bms-precondition.
      parameters:
        -
          name: roomUuid
          in: path
          required: true
          description: 'UUID de la habitación en Voltic.'
          schema:
            type: string
            format: uuid
      responses:
        '202':
          description: 'Solicitud aceptada para su ejecución; no confirma que se haya alcanzado la temperatura de confort'
          content:
            application/json:
              schema:
                type: object
                required:
                  - room_uuid
                  - status
                  - expires_at
                properties:
                  room_uuid:
                    type: string
                    format: uuid
                  status:
                    type: string
                    enum:
                      - accepted
                  expires_at:
                    type: string
                    format: date-time
              example:
                room_uuid: 0194b251-2312-70c4-a800-0b6de9a55d20
                status: accepted
                expires_at: '2026-08-04T10:30:30Z'
        '400':
          description: 'Identificador no válido'
          content:
            application/json:
              schema:
                type: object
                required:
                  - code
                  - message
                properties:
                  code:
                    type: string
                  message:
                    type: string
        '401':
          description: 'API token ausente, inválido o revocado'
          content:
            application/json:
              schema:
                type: object
                required:
                  - code
                  - message
                properties:
                  code:
                    type: string
                  message:
                    type: string
        '403':
          description: 'Rol sin control, feature no disponible o suscripción con escritura bloqueada.'
          content:
            application/json:
              schema:
                anyOf:
                  -
                    type: object
                    required:
                      - error
                      - permission
                      - message
                    properties:
                      error:
                        type: string
                        const: permission_denied
                      permission:
                        type: string
                      message:
                        type: string
                  -
                    $ref: '#/components/schemas/FeatureUnavailableError'
                  -
                    $ref: '#/components/schemas/SubscriptionReadOnlyError'
        '404':
          description: 'Edificio o habitación no encontrado'
          content:
            application/json:
              schema:
                type: object
                required:
                  - code
                  - message
                properties:
                  code:
                    type: string
                  message:
                    type: string
        '409':
          description: 'La habitación no admite preclimatización en este momento'
          content:
            application/json:
              schema:
                type: object
                required:
                  - code
                  - message
                properties:
                  code:
                    type: string
                  message:
                    type: string
              example:
                code: preconditioning_unavailable
                message: 'La climatización de la habitación no está disponible.'
        '503':
          description: 'No se ha podido confirmar el envío de la solicitud'
          content:
            application/json:
              schema:
                type: object
                required:
                  - code
                  - message
                properties:
                  code:
                    type: string
                  message:
                    type: string
        '429':
          description: 'Límite de peticiones superado; repetir después renueva la expiración si se acepta.'
          headers:
            Retry-After:
              description: 'Segundos hasta poder reintentar, cuando lo emite el middleware de throttling.'
              schema:
                type: integer
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
        '500':
          description: 'Error interno'
          content:
            application/json:
              schema:
                type: object
                required:
                  - code
                  - message
                properties:
                  code:
                    type: string
                  message:
                    type: string
      operationId: startRoomPreconditioning
      security:
        -
          BearerAuth: []
      x-voltic-stability: stable
      x-voltic-integration-module: optional
      x-voltic-proposed-capability: 'rooms:precondition'
      x-voltic-route-name: bms.rooms.precondition
  '/floors/{floorUuid}/rooms':
    get:
      operationId: listRoomsByFloor
      tags:
        - Rooms
      summary: 'Listar habitaciones de una planta'
      description: 'Devuelve las habitaciones asociadas a la planta indicada.'
      security:
        -
          BearerAuth: []
      parameters:
        -
          $ref: '#/components/parameters/FloorUuidParam'
        -
          $ref: '#/components/parameters/IncludeQueryParam'
        -
          $ref: '#/components/parameters/FieldsQueryParam'
        -
          name: tag_uuids
          in: query
          required: false
          description: 'Lista separada por comas de UUIDs de tags. Devuelve habitaciones que tengan cualquiera de esos tags.'
          schema:
            type: string
            example: 123e4567-e89b-12d3-a456-426614174999
      responses:
        '200':
          description: 'Habitaciones encontradas.'
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Room'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/FeatureUnavailable'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'
      x-voltic-stability: stable
      x-voltic-integration-module: core
      x-voltic-proposed-capability: 'inventory:read'
      x-voltic-route-name: bms.floors.rooms.index
  '/rooms/{uuid}':
    get:
      operationId: getRoom
      tags:
        - Rooms
      summary: 'Obtener habitación'
      description: |
        Devuelve una habitación por UUID.

        Campos recomendados para este caso de uso:
        - `fields=metadata` para ver el estado persistido bruto
        - `fields=fan_coil_profile,room_energy_model,energy_model` para obtener una vista lista para frontend

        El campo `energy_model` devuelve:
        - coeficientes derivados
        - coeficientes efectivos finales
        - prioridad aplicada (`manual > derived > building > default`)
        - factores y supuestos usados en la estimación
      security:
        -
          BearerAuth: []
      parameters:
        -
          $ref: '#/components/parameters/RoomUuid'
        -
          $ref: '#/components/parameters/IncludeQueryParam'
        -
          $ref: '#/components/parameters/FieldsQueryParam'
      responses:
        '200':
          description: 'Habitación encontrada.'
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Room'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/FeatureUnavailable'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'
      x-voltic-stability: stable
      x-voltic-integration-module: core
      x-voltic-proposed-capability: 'inventory:read'
      x-voltic-route-name: bms.rooms.show
  '/devices/{uuid}/state':
    get:
      operationId: getDeviceState
      tags:
        - Devices
      summary: 'Obtener estado actual de un dispositivo'
      description: |
        Devuelve únicamente el estado cacheado del dispositivo y, si existe, su disponibilidad
        online/offline cacheada junto con `last_seen_at`. No devuelve el recurso completo del dispositivo.
      security:
        -
          BearerAuth: []
      parameters:
        -
          $ref: '#/components/parameters/DeviceUuid'
      responses:
        '200':
          description: 'Estado actual del dispositivo.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeviceStateResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/FeatureUnavailable'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'
      x-voltic-stability: stable
      x-voltic-integration-module: core
      x-voltic-proposed-capability: 'states:read'
      x-voltic-route-name: bms.devices.states.show
  '/devices/{uuid}':
    get:
      operationId: getDevice
      tags:
        - Devices
      summary: 'Obtener dispositivo'
      description: 'Devuelve un dispositivo por UUID si el usuario tiene acceso.'
      security:
        -
          BearerAuth: []
      parameters:
        -
          $ref: '#/components/parameters/DeviceUuid'
        -
          $ref: '#/components/parameters/IncludeQueryParam'
        -
          $ref: '#/components/parameters/FieldsQueryParam'
      responses:
        '200':
          description: 'Dispositivo encontrado.'
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Device'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/FeatureUnavailable'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'
      x-voltic-stability: stable
      x-voltic-integration-module: core
      x-voltic-proposed-capability: 'inventory:read'
      x-voltic-route-name: bms.devices.show
  '/zones/{uuid}':
    get:
      operationId: getZone
      tags:
        - Zones
      summary: 'Obtener zona'
      description: 'Devuelve una zona por UUID.'
      security:
        -
          BearerAuth: []
      parameters:
        -
          $ref: '#/components/parameters/ZoneUuid'
        -
          $ref: '#/components/parameters/IncludeQueryParam'
        -
          $ref: '#/components/parameters/FieldsQueryParam'
      responses:
        '200':
          description: 'Zona encontrada.'
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Zone'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/FeatureUnavailable'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'
      x-voltic-stability: stable
      x-voltic-integration-module: core
      x-voltic-proposed-capability: 'inventory:read'
      x-voltic-route-name: bms.zones.show
  '/timeseries/{scope}/{scopeId}/{metric}':
    get:
      operationId: getTimeSeries
      tags:
        - 'Time Series'
      summary: 'Consultar datos de series temporales o estado'
      description: |
        Devuelve mediciones agregadas o eventos de estado (SoC) para edificios, plantas, habitaciones o dispositivos.
        - Para métricas de **series temporales** (`temperature`, `humidity`, `energy`, `voltage`, etc.) puedes usar `interval` y `stats`.
        - Los nombres de los campos y los identificadores de `scope`/`metric` se documentan en minúscula y `snake_case`. `scope` y `metric` se normalizan sin distinguir mayúsculas/minúsculas, por lo que `Temperature` y `temperature` son equivalentes; se recomienda usar siempre la forma minúscula.
        - En `scope=device`, `temperature` y `humidity` resuelven automáticamente la fuente según la categoría del dispositivo. Los sensores CoolingPatrol se identifican por su identificador físico canónico (`cp:{sensor_id}`) o por un IEEE legacy, sin exponer su tecnología de conexión.
        - El identificador físico resuelve el dispositivo y sus permisos; las temperaturas y humedades CoolingPatrol se consultan internamente por `sensor_uuid`, también en las submétricas `.avg`.
        - Para `scope=building`, `floor` o `room`, usa `category=coldroom_sensor` para consultar CoolingPatrol con filtros de ubicación por UUID. Sin este parámetro se conserva la fuente de termostatos.
        - Intervalos admitidos: `15m`, `1h`, `4h`, `1d`, `1w` y `1mo`.
        - Límites por petición: `15m` hasta 14 días, `1h` hasta 100 días, `4h` y `1d` hasta 370 días; `1w` y `1mo` no tienen un límite adicional, pero todos los intervalos están sujetos al máximo absoluto de 5 años.
        - Si el rango excede el límite del intervalo, la API devuelve `400`, también en `energy` con `enrich`; `422` queda reservado para parámetros incompatibles o errores propios del enriquecimiento.
        - Para métricas de **estado** (`switch_state`, `thermostat_state`) se devuelven eventos sin agregación.
        - `from` es inclusivo y `to` exclusivo. En este endpoint ambos son opcionales: si faltan, se consulta el último día (`to` por defecto es ahora y `from` es `to - 1 día`).
        - En `device`, `scopeId` acepta tanto el identificador físico canónico (`hardware_id`) como el IEEE legacy.
        - `energy` siempre representa **consumo dentro del periodo consultado**, calculado como delta positivo entre lecturas consecutivas del contador acumulado. No devuelve el valor bruto del contador.
        - Para no perder el primer delta del rango, la API toma una lectura semilla anterior a `from` cuando la métrica es acumulativa.
        - En `scope=device`, `energy` y `power` se resuelven según la categoría del dispositivo.
        - En `scope=device` con `category=switch`, `energy`, `power`, `voltage` y `current` usan las métricas monofásicas del switch.
        - En `scope=device` con `category=power_meter`, `energy` devuelve el consumo total del contador trifásico y `power` devuelve la potencia total del contador.
        - En `scope=device`, los `power_meter` trifásicos exponen `energy/voltage/current/power` por fase con métricas explícitas como `energy_phase_a`, `voltage_phase_ab`, `current_phase_a` o `power_phase_b`.
        - En `scope=device`, `voltage` y `current` genéricos no están disponibles para `power_meter`; deben consultarse por fase.
        - En `scope=room|floor|building`, `energy` suma los deltas de `switch` y `power_meter`. Si un dispositivo declara `metadata.metering.scope=room_total`, el total de esa habitación usa únicamente ese contador desde `valid_from` (o para todo el histórico sin fecha); antes se suman los demás dispositivos. No se rellenan sus huecos con los circuitos hijos. Las consultas `scope=device` conservan cada lectura individual. El resto de métricas no mezclan categorías.
        - `15m`, `1h` y `4h` se alinean en UTC; para `interval=1d|1w|1mo`, los buckets se alinean con la timezone del building asociado al scope. Si no está definida, se usa `Europe/Madrid`.
        - Para `1d|1w|1mo`, el rango efectivo se ajusta a buckets de calendario completos: `from` se redondea al inicio local y `to` al final exclusivo del último bucket completo.
        - Todos los timestamps de `data` son instantes UTC serializados con sufijo `Z`; el campo `timezone` sirve para que el frontend los presente en hora local.
        - `energy` admite `enrich=cost`, `enrich=co2`, `enrich=electrical_adjustments` o combinaciones CSV entre ellos.
        - `energy?enrich=electrical_adjustments` solo está disponible para `scope=room|floor|building` y suma cargas opcionales configuradas en analytics settings (`fan_coil_kwh_per_runtime_hour`, `fridge_kwh_15m`).
        - El cálculo eléctrico opcional del fancoil usa por defecto `electrical_adjustment_fan_runtime_basis=valve_open`, es decir, el tiempo en que la válvula ha estado abierta.
        - Si se quiere imputar el ventilador por tiempo de termostato encendido, se puede pedir `electrical_adjustment_fan_runtime_basis=thermostat_on`.
        - Si `electrical_adjustments` se combina con `cost` o `co2`, esos cálculos se aplican sobre el `value` final ajustado.
        - En `pricing_model=tou`, las `schedule_rules.time_ranges` se aplican por buckets de 15 minutos.
        - `start` debe caer en `:00`, `:15`, `:30` o `:45`.
        - `end` acepta tanto fronteras exactas (`:00`, `:15`, `:30`, `:45`) como shorthand inclusivo de cierre de bucket (`:14`, `:29`, `:44`, `:59`).
        - Ejemplo: `start=00:00`, `end=23:59` cubre el día completo hasta `00:00`; `start=09:00`, `end=14:59` cubre hasta el cierre del bucket de `14:45`.
        - El enriquecimiento de `energy` calcula internamente siempre en buckets de 15 minutos y luego reagrupa al `interval` pedido.
        - Sin enriquecimiento, las consultas exactas de `energy`, `humidity` y `temperature` reutilizan filas en Redis durante 60 segundos. Se distinguen los filtros, parámetros y estadísticas de la consulta validada; se comprueban los permisos en cada petición. Las nuevas lecturas pueden tardar hasta ese TTL en aparecer.
        - En ese camino, `Server-Timing` expone el estado `cache` (`hit|miss`) y la duración total en milisegundos. Esta caché no almacena respuestas de procesamiento ni errores.
      security:
        -
          BearerAuth: []
      parameters:
        -
          $ref: '#/components/parameters/TimeSeriesScopeParam'
        -
          $ref: '#/components/parameters/TimeSeriesScopeIdParam'
        -
          $ref: '#/components/parameters/TimeSeriesMetricParam'
        -
          $ref: '#/components/parameters/TimeSeriesIntervalQueryParam'
        -
          $ref: '#/components/parameters/TimeSeriesStatsQueryParam'
        -
          $ref: '#/components/parameters/TimeSeriesEnrichQueryParam'
        -
          $ref: '#/components/parameters/TimeSeriesElectricalAdjustmentFanRuntimeBasisQueryParam'
        -
          $ref: '#/components/parameters/TimeSeriesFromQueryParam'
        -
          $ref: '#/components/parameters/TimeSeriesToQueryParam'
        -
          name: category
          in: query
          description: 'Selecciona CoolingPatrol para temperature/humidity (incluidas submétricas) en building, floor o room.'
          schema:
            type: string
            enum:
              - coldroom_sensor
      responses:
        '200':
          description: 'Datos devueltos.'
          headers:
            Server-Timing:
              schema:
                type: string
              description: 'Estado hit/miss y duración total para las lecturas sin enriquecimiento; no está presente en todos los caminos de este endpoint.'
          content:
            application/json:
              schema:
                oneOf:
                  -
                    $ref: '#/components/schemas/TimeSeriesResponse'
                  -
                    $ref: '#/components/schemas/StateResponse'
              examples:
                energyInRoomSimple:
                  summary: 'Energía en habitación (intervalo 15m)'
                  value:
                    scope:
                      type: room
                      uuid: 0194b251-2312-70c4-a800-0b6de9a55d1b
                      name: '206'
                    metric: energy
                    unit: kWh
                    interval: 15m
                    timezone: Europe/Madrid
                    stat: []
                    data:
                      -
                        - '2025-07-01T10:30:00.000Z'
                        -
                          value: 46.25
                      -
                        - '2025-07-01T10:45:00.000Z'
                        -
                          value: 0.009998
                energyInRoom:
                  summary: 'Energía en habitación (1h, sum)'
                  value:
                    scope:
                      type: room
                      uuid: 0194b251-2312-70c4-a800-0b6de9a55d1b
                      name: '206'
                    metric: energy
                    unit: kWh
                    interval: 1h
                    timezone: Europe/Madrid
                    stat:
                      - sum
                    data:
                      -
                        - '2025-07-01T10:00:00.000Z'
                        -
                          value: 46.26
                      -
                        - '2025-07-01T11:00:00.000Z'
                        -
                          value: 0.03
                energyInBuildingSum1h:
                  summary: 'Energía agregada de edificio (1h, sum)'
                  value:
                    scope:
                      type: building
                      uuid: 0194b243-d8dd-7112-b6f9-e9f088d50ba3
                      name: test
                    metric: energy
                    unit: kWh
                    interval: 1h
                    timezone: Europe/Madrid
                    stat:
                      - sum
                    data:
                      -
                        - '2025-06-28T23:00:00.000Z'
                        -
                          value: 126.69
                      -
                        - '2025-07-01T08:00:00.000Z'
                        -
                          value: 270.16
                energyWithCost:
                  summary: 'Energía con coste eléctrico por bucket'
                  value:
                    scope:
                      type: building
                      uuid: 0194b243-d8dd-7112-b6f9-e9f088d50ba3
                      name: 'Hotel Solaris'
                    metric: energy
                    unit: kWh
                    interval: 15m
                    timezone: Europe/Madrid
                    stat:
                      - sum
                    from: '2026-01-01T00:00:00.000Z'
                    to: '2026-01-01T00:15:00.000Z'
                    kwh_total: 10
                    currency_code: EUR
                    cost_variable_total: 2.2
                    cost_fixed_total: 0.01
                    cost_total: 2.21
                    derived_units:
                      cost_variable: EUR
                      cost_fixed: EUR
                      cost_total: EUR
                    tariff_breakdown:
                      -
                        period_label: 'Flat Jan 2026'
                        period_valid_from: '2026-01-01'
                        period_valid_to: null
                        pricing_model: flat
                        billing_unit: kWh
                        flat_rate: 0.22
                        kwh_total: 10
                        cost_variable_total: 2.2
                        cost_fixed_total: 0.01
                        cost_total: 2.21
                    data:
                      -
                        - '2026-01-01T00:00:00.000Z'
                        -
                          value: 10
                          cost_variable: 2.2
                          cost_fixed: 0.01
                          cost_total: 2.21
                energyWithCostAndCo2:
                  summary: 'Energía con coste y CO2 por bucket agregado'
                  value:
                    scope:
                      type: building
                      uuid: 0194b243-d8dd-7112-b6f9-e9f088d50ba3
                      name: 'Hotel Solaris'
                    metric: energy
                    unit: kWh
                    interval: 1h
                    timezone: Europe/Madrid
                    stat:
                      - sum
                    from: '2026-01-01T09:00:00.000Z'
                    to: '2026-01-01T10:00:00.000Z'
                    kwh_total: 4
                    currency_code: EUR
                    cost_variable_total: 0.8
                    cost_fixed_total: 0
                    cost_total: 0.8
                    co2_tonnes_total: 0.0004
                    co2_baseline_tonnes_total: 0.0006
                    co2_avoided_tonnes_total: 0.0002
                    derived_units:
                      cost_variable: EUR
                      cost_fixed: EUR
                      cost_total: EUR
                      co2_tonnes: tCO2e
                      co2_baseline_tonnes: tCO2e
                      co2_avoided_tonnes: tCO2e
                    tariff_breakdown:
                      -
                        period_label: 'TOU Jan 2026'
                        period_valid_from: '2026-01-01'
                        period_valid_to: null
                        pricing_model: tou
                        billing_unit: kWh
                        kwh_total: 4
                        cost_variable_total: 0.8
                        cost_fixed_total: 0
                        cost_total: 0.8
                        band_breakdown:
                          -
                            band_code: P1
                            band_label: Punta
                            rate: 0.3
                            kwh_total: 2
                            cost_variable_total: 0.6
                          -
                            band_code: P2
                            band_label: Valle
                            rate: 0.1
                            kwh_total: 2
                            cost_variable_total: 0.2
                    data:
                      -
                        - '2026-01-01T09:00:00.000Z'
                        -
                          value: 4
                          cost_variable: 0.8
                          cost_fixed: 0
                          cost_total: 0.8
                          co2_tonnes: 0.0004
                          co2_baseline_tonnes: 0.0006
                          co2_avoided_tonnes: 0.0002
                energyWithElectricalAdjustments:
                  summary: 'Energía con cargas opcionales añadidas'
                  value:
                    scope:
                      type: room
                      uuid: 0194b251-2312-70c4-a800-0b6de9a55d1b
                      name: '206'
                    metric: energy
                    unit: kWh
                    interval: 15m
                    timezone: Europe/Madrid
                    stat:
                      - sum
                    from: '2026-01-01T00:00:00.000Z'
                    to: '2026-01-01T00:15:00.000Z'
                    kwh_total: 1.6
                    kwh_metered_total: 1
                    kwh_adjustment_total: 0.6
                    derived_units:
                      metered_value: kWh
                      electrical_adjustment_value: kWh
                    data:
                      -
                        - '2026-01-01T00:00:00.000Z'
                        -
                          value: 1.6
                          metered_value: 1
                          electrical_adjustment_value: 0.6
                energyWithElectricalAdjustmentsThermostatOn:
                  summary: 'Energía con cargas opcionales y base del fancoil por thermostat on'
                  value:
                    scope:
                      type: room
                      uuid: 0194b251-2312-70c4-a800-0b6de9a55d1b
                      name: '206'
                    metric: energy
                    unit: kWh
                    interval: 15m
                    timezone: Europe/Madrid
                    stat:
                      - sum
                    from: '2026-01-01T00:00:00.000Z'
                    to: '2026-01-01T00:15:00.000Z'
                    kwh_total: 1.6
                    kwh_metered_total: 1
                    kwh_adjustment_total: 0.6
                    derived_units:
                      metered_value: kWh
                      electrical_adjustment_value: kWh
                    data:
                      -
                        - '2026-01-01T00:00:00.000Z'
                        -
                          value: 1.6
                          metered_value: 1
                          electrical_adjustment_value: 0.6
                voltageInRoomOHLC:
                  summary: 'Voltaje en habitación (OHLC)'
                  value:
                    scope:
                      type: room
                      uuid: 0194b251-2312-70c4-a800-0b6de9a55d1b
                      name: '206'
                    metric: voltage
                    unit: V
                    interval: 1h
                    timezone: Europe/Madrid
                    stat:
                      - ohlc
                    data:
                      -
                        - '2025-07-01T10:00:00.000Z'
                        -
                          open: 229
                          close: 232
                          high: 232
                          low: 229
                          avg: 230
                      -
                        - '2025-07-01T11:00:00.000Z'
                        -
                          open: 232
                          close: 229
                          high: 232
                          low: 229
                          avg: 229.575
                linkqualityForDevice:
                  summary: 'Calidad de enlace para un dispositivo'
                  value:
                    scope:
                      type: device
                      uuid: 0194ef9b-d977-731f-ba81-bbeae9f7adb6
                      name: Termostato
                      category: thermostat
                    metric: linkquality
                    unit: lqi
                    interval: 1d
                    timezone: Europe/Madrid
                    stat:
                      - avg
                    data:
                      -
                        - '2025-07-01T00:00:00.000Z'
                        -
                          value: 82.61
                mixedEnergyInRoom:
                  summary: 'Energía agregada mixta de habitación (switch + power meter)'
                  value:
                    scope:
                      type: room
                      uuid: 0194b251-2312-70c4-a800-0b6de9a55d1b
                      name: '206'
                    metric: energy
                    unit: kWh
                    interval: 1h
                    timezone: Europe/Madrid
                    stat:
                      - sum
                    data:
                      -
                        - '2025-07-01T10:00:00.000Z'
                        -
                          value: 1.42
                      -
                        - '2025-07-01T11:00:00.000Z'
                        -
                          value: 1.37
                energyForPowerMeterDevice:
                  summary: 'Consumo de energía de power meter trifásico por bucket'
                  value:
                    scope:
                      type: device
                      uuid: 0194ef9b-d977-731f-ba81-bbeae9f7adb6
                      name: 'Power Meter'
                      category: power_meter
                    metric: energy
                    unit: kWh
                    interval: 1h
                    timezone: Europe/Madrid
                    stat:
                      - sum
                    data:
                      -
                        - '2025-07-01T10:00:00.000Z'
                        -
                          value: 1.18
                      -
                        - '2025-07-01T11:00:00.000Z'
                        -
                          value: 1.07
                powerForPowerMeterDevice:
                  summary: 'Potencia total de power meter trifásico (OHLC)'
                  value:
                    scope:
                      type: device
                      uuid: 0194ef9b-d977-731f-ba81-bbeae9f7adb6
                      name: 'Power Meter'
                      category: power_meter
                    metric: power
                    unit: W
                    interval: 1h
                    timezone: Europe/Madrid
                    stat:
                      - ohlc
                    data:
                      -
                        - '2025-07-01T10:00:00.000Z'
                        -
                          open: 245.2
                          close: 241.8
                          high: 252.4
                          low: 238.9
                          avg: 244.1
                voltagePhaseAbForPowerMeterDevice:
                  summary: 'Voltaje entre fases A-B de power meter trifásico (OHLC)'
                  value:
                    scope:
                      type: device
                      uuid: 0194ef9b-d977-731f-ba81-bbeae9f7adb6
                      name: 'Power Meter'
                      category: power_meter
                    metric: voltage_phase_ab
                    unit: V
                    interval: 1h
                    timezone: Europe/Madrid
                    stat:
                      - ohlc
                    data:
                      -
                        - '2025-07-01T10:00:00.000Z'
                        -
                          open: 398.4
                          close: 401.2
                          high: 402.1
                          low: 397.9
                          avg: 400.1
                thermostatState:
                  summary: 'Estado de termostatos en habitación'
                  value:
                    scope:
                      type: room
                      uuid: 0194b251-2312-70c4-a800-0b6de9a55d1b
                      name: '206'
                    metric: thermostat_state
                    unit: state
                    interval: event
                    timezone: Europe/Madrid
                    stat: []
                    data:
                      -
                        - '2025-07-02T11:52:30.000Z'
                        -
                          ieee: '0x583bc2fffe1c8755'
                          state: true
                          setpoint: 23
                          fan_mode: high
                          system_mode: cool
                switchStateForDevice:
                  summary: 'Estado de interruptor por dispositivo'
                  value:
                    scope:
                      type: device
                      uuid: '0x583bc2fffe1c8755'
                      name: 'Switch - Baño'
                    metric: switch_state
                    unit: state
                    interval: event
                    timezone: Europe/Madrid
                    stat: []
                    data:
                      -
                        - '2025-07-02T11:52:30.000Z'
                        -
                          ieee: '0x583bc2fffe1c8755'
                          state: true
                      -
                        - '2025-07-02T12:01:00.000Z'
                        -
                          ieee: '0xABCDEF1234567890'
                          state: false
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/FeatureUnavailable'
        '404':
          $ref: '#/components/responses/NotFound'
        '400':
          description: 'Intervalo o rango temporal inválido para la serie solicitada.'
        '422':
          description: 'Parámetros incompatibles, métrica no disponible para el scope o error de validación/configuración de `energy` con `enrich`.'
        '500':
          $ref: '#/components/responses/InternalError'
      x-voltic-stability: stable
      x-voltic-integration-module: core
      x-voltic-proposed-capability: 'telemetry:read'
      x-voltic-route-name: bms.timeseries.get_metric_data
  '/timeseries/room/{roomUuid}/occupancy-events':
    get:
      operationId: getRoomOccupancyEvents
      tags:
        - 'Time Series'
      summary: 'Consultar eventos exactos de presencia de una habitación'
      description: |
        Devuelve el último estado conocido anterior a `from` y todos los cambios de presencia
        ocurridos en `[from, to)`, ordenados ascendentemente.

        Reconstrucción de periodos:

        * `initial_state` está vigente desde `from` hasta el primer evento.
        * cada evento está vigente desde su `timestamp` hasta el siguiente evento.
        * el último evento está vigente hasta `to`.
        * si `initial_state` es `null`, el estado entre `from` y el primer evento es desconocido.
        * `from` es inclusivo y `to` exclusivo.
        * los timestamps son instantes UTC serializados con sufijo `Z`; `timezone` indica cómo presentarlos en hora local.
        * el rango máximo es de 14 días por petición; rangos mayores devuelven `422`.

        A diferencia de `room-state-timeline`, este endpoint no crea buckets ni agrega duraciones:
        conserva los timestamps exactos almacenados en `bms.room_occupancy_state`.
      security:
        -
          BearerAuth: []
      parameters:
        -
          name: roomUuid
          in: path
          required: true
          description: 'UUID de la habitación.'
          schema:
            type: string
            format: uuid
        -
          name: from
          in: query
          required: true
          description: 'Inicio inclusivo en ISO8601 / RFC3339.'
          schema:
            type: string
            format: date-time
        -
          name: to
          in: query
          required: true
          description: 'Fin exclusivo en ISO8601 / RFC3339.'
          schema:
            type: string
            format: date-time
      responses:
        '200':
          description: 'Semilla anterior al rango y eventos exactos ordenados.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RoomOccupancyEventsResponse'
              example:
                scope:
                  type: room
                  uuid: 0194b251-2312-70c4-a800-0b6de9a55d1b
                  name: '206'
                metric: room_occupancy_events
                timezone: Europe/Madrid
                from: '2026-07-20T08:00:00.000Z'
                to: '2026-07-20T12:00:00.000Z'
                initial_state:
                  timestamp: '2026-07-20T07:54:12.000Z'
                  occupancy: false
                  occupancy_state: unknown
                  occupancy_confidence: 0.72
                events:
                  -
                    timestamp: '2026-07-20T08:17:23.000Z'
                    occupancy: true
                    occupancy_state: active
                    occupancy_confidence: 0.94
                  -
                    timestamp: '2026-07-20T09:42:10.000Z'
                    occupancy: false
                    occupancy_state: unknown
                    occupancy_confidence: 0.81
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          description: 'Faltan parámetros, el rango es inválido o supera 14 días.'
        '500':
          $ref: '#/components/responses/InternalError'
      x-voltic-stability: stable
      x-voltic-integration-module: optional
      x-voltic-proposed-capability: 'occupancy:read'
      x-voltic-route-name: bms.timeseries.get_room_occupancy_events
  '/timeseries/{scope}/{scopeId}/thermostat-runtime':
    get:
      operationId: getThermostatRuntime
      tags:
        - 'Time Series'
      summary: 'Consultar tiempo de uso de termostatos'
      description: |
        Los Aidoo de expansión directa/VRV comparten `h_heat_open`, `h_cool_open` y sus segundos
        con fancoil: representan demanda térmica estimada, no marcha medida del compresor.
        El modelo manual usa histéresis ±1 °C. En `auto` infiere dirección desde la temperatura,
        retiene el modo y espera 20 minutos de demanda satisfecha antes de invertirlo; es una
        aproximación que no reproduce todas las restricciones de la máquina exterior.
        `dry`, modos desconocidos, temperaturas ausentes y consignas ausentes quedan sin estimar.
        `state_covered_seconds`/`state_expected_seconds` incluyen también esta cobertura Aidoo;
        no interpretar las horas desconocidas como equipo parado. Las horas se suman por equipo.
        La temperatura se aproxima dentro de cada bucket interpolando apertura/cierre; no permite
        reconstruir ciclos reales inferiores a 15 minutos. El endpoint energético Aidoo es `hvac-energy`.

        Calcula el tiempo de apertura de válvulas de calefacción y refrigeración a partir de eventos
        store-on-change en `bms.thermostat_state`, usando un cálculo interno fijo en buckets de 15
        minutos y devolviendo el resultado agregado por periodo.

        Además de los tiempos de uso, devuelve degree-hours interiores y kWh térmicos estimados
        con el modelo de batería de agua v2. Los degree-hours comparan temperatura interior y
        consigna durante el tiempo activo; se conservan como indicador independiente y no generan kWh.
        La energía usa temperatura y humedad agregadas en `bms.thermostat_agg_15min`, velocidad,
        condiciones del agua y parámetros efectivos del fan coil.

        En fan coils de agua, los estados previos solo aportan cobertura durante el
        `max_state_hours` efectivo (24 horas por defecto, configurable por habitación/edificio).
        El cálculo frío limita la búsqueda de la última semilla a esa validez, conservando
        la mayor ventana de los equipos de cada lote. Un estado caducado queda sin cobertura;
        no se interpreta como apagado. Los cálculos internos que arrastran estados para
        reconstruir episodios de ahorro conservan la búsqueda histórica completa.

        Para `room`, `floor` y `building`, el backend calcula el runtime agregando los termostatos
        pertenecientes al scope solicitado. En rangos amplios, la disponibilidad y el rendimiento
        pueden depender de la estrategia de cálculo y de la preparación previa de datos históricos.

        Restricciones y requisitos:

        * `scope` admite `device`, `room`, `floor` y `building`
        * para `room`, `floor` y `building`, el backend resuelve los termostatos del scope y agrega sus resultados
        * en `device`, el recurso debe ser un termostato
        * `interval` admite `15m`, `1h`, `4h`, `1d`, `1w` y `1mo`
        * `15m` solo admite rangos de hasta 7 días completamente comprendidos en los últimos 7 días y no permite timestamps futuros
        * en `15m`, los buckets devueltos siempre se alinean a cuartos de hora aunque `from` y `to` no caigan exactamente en esos límites
        * `1h` y `4h` se obtienen reagrupando el cálculo base de 15 minutos; primero se ajusta la ventana a horas UTC completas y las respuestas incluyen `calculation_interval: 15m`
        * `1h` admite como máximo 100 días, `4h` y `1d` 370 días, `1w` 12 semanas y `1mo` 12 meses por petición
        * un rango que exceda su límite devuelve `400` y debe fragmentarse o solicitarse con un intervalo mayor
        * el agrupado diario, semanal y mensual usa la timezone del building asociado al scope; si no existe, se usa `Europe/Madrid`
        * para todos los intervalos, `from` y `to` de la respuesta contienen los límites efectivos, inclusivo y exclusivo respectivamente, después de alinear la ventana
        * los timestamps de `data` son UTC con sufijo `Z`; `timezone` indica cómo mostrarlos en hora local
        * el agrupado semanal usa semana ISO, de lunes a domingo
        * `metadata.thermostat_energy_coefficients` se conserva por compatibilidad, pero no interviene
          en el cálculo v2; los parámetros ausentes se heredan de habitación, edificio o referencia
        * cuando faltan días completos de rollup en `room`, `floor` o `building` y está activada la cola asíncrona, el endpoint responde `202 Accepted`, encola un job diario por día ausente y el cliente debe reintentar más tarde
        * en `building&interval=15m`, el resultado completo se reutiliza desde Redis durante diez minutos si incluye hoy y una hora si es histórico. Se distingue rango efectivo, zona horaria, configuración y versiones de los días HVAC; los cambios de esas versiones invalidan la lectura
        * el resultado disponible se devuelve inmediatamente y se renueva en segundo plano a partir de dos minutos para hoy y treinta minutos para históricos; solo se encola un refresco por clave. Si el refresco falla, el resultado previo se conserva hasta su caducidad máxima
        * las peticiones concurrentes al mismo rango frío comparten un único cálculo: mientras otra petición o job lo calcula, las demás reciben `202` con los días pendientes, sin bloquear un worker esperando el resultado. El bloqueo expira a los cinco minutos y se libera también si el cálculo falla
        * la autorización y validación del rango se mantienen en cada petición; no se guardan respuestas `202` ni excepciones en esta caché. El primer cálculo puede seguir siendo costoso. Esta caché incluye edificios con Aidoo y mixtos, evitando repetir también sus consultas de demanda térmica
      security:
        -
          BearerAuth: []
      parameters:
        -
          $ref: '#/components/parameters/ThermostatRuntimeScopeParam'
        -
          $ref: '#/components/parameters/ThermostatRuntimeScopeIdParam'
        -
          $ref: '#/components/parameters/ThermostatRuntimeIntervalQueryParam'
        -
          $ref: '#/components/parameters/TimeSeriesFromQueryParam'
        -
          $ref: '#/components/parameters/TimeSeriesToQueryParam'
      responses:
        '200':
          description: 'Tiempo de uso agregado para el scope indicado.'
          headers:
            Server-Timing:
              schema:
                type: string
              description: 'Duración total y estado hit/miss de la caché del resultado completo; miss no descarta reutilizar cachés internas.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ThermostatRuntimeResponse'
              examples:
                deviceDaily:
                  summary: 'Uso diario de un termostato'
                  value:
                    scope:
                      type: device
                      uuid: '0x583bc2fffe85fe9e'
                      name: 'Termostato 206'
                      category: thermostat
                    metric: thermostat_runtime
                    interval: 1d
                    timezone: Europe/Madrid
                    calculation_interval: 15m
                    from: '2025-06-30T22:00:00.000Z'
                    to: '2025-07-02T22:00:00.000Z'
                    units:
                      sec_heat_open: s
                      sec_cool_open: s
                      h_heat_open: h
                      h_cool_open: h
                      degree_hours_heat: 'degC*h'
                      degree_hours_cool: 'degC*h'
                      kwh_heat_est: kWh
                      kwh_cool_est: kWh
                      kwh_total_est: kWh
                    data:
                      -
                        - '2025-07-01T00:00:00.000Z'
                        -
                          sec_heat_open: 14400
                          sec_cool_open: 0
                          h_heat_open: 4
                          h_cool_open: 0
                          degree_hours_heat: 6.3
                          degree_hours_cool: 0
                          kwh_heat_est: 5.04
                          kwh_cool_est: 0
                          kwh_total_est: 5.04
                      -
                        - '2025-07-02T00:00:00.000Z'
                        -
                          sec_heat_open: 7200
                          sec_cool_open: 0
                          h_heat_open: 2
                          h_cool_open: 0
                          degree_hours_heat: 2.1
                          degree_hours_cool: 0
                          kwh_heat_est: 1.68
                          kwh_cool_est: 0
                          kwh_total_est: 1.68
        '202':
          description: 'El backend ha encolado la materialización o una petición concurrente ya está calculando el mismo rango frío. Expone el contrato uniforme de progreso; `data` está vacío hasta completar el cálculo.'
          headers:
            Retry-After:
              schema:
                type: integer
                const: 10
            Server-Timing:
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AsyncMaterializationResponse'
              examples:
                processing:
                  summary: 'Cálculo asíncrono de broad scope'
                  value:
                    status: processing
                    message: 'Thermostat runtime data is being calculated for the requested range. Retry in a moment.'
                    retry_after_seconds: 10
                    completed_days: []
                    pending_days: []
                    failed_days: []
                    jobs: []
                    progress:
                      total_days: 0
                      completed_days: 0
                      pending_days: 0
                      failed_days: 0
                      percent: 0
                    data: []
                    scope:
                      type: building
                      uuid: f3a8b809-93a2-4f16-9c8b-34b1bd866c12
                      name: 'Edificio HQ'
                    metric: thermostat_runtime
                    interval: 1w
        '400':
          description: 'Intervalo o rango temporal inválido, incluido cualquier rango que exceda los límites documentados.'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/FeatureUnavailable'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          description: 'El recurso no cumple los requisitos del endpoint. Por ejemplo, un scope no soportado o un `device` que no es de categoría `thermostat`.'
        '500':
          $ref: '#/components/responses/InternalError'
      x-voltic-stability: stable
      x-voltic-integration-module: optional
      x-voltic-proposed-capability: 'telemetry:read'
      x-voltic-route-name: bms.timeseries.get_thermostat_runtime
  '/buildings/{buildingUuid}/devices':
    get:
      operationId: listDevicesByBuilding
      tags:
        - Devices
      summary: 'Listar dispositivos de un edificio'
      description: 'Devuelve los dispositivos asociados al edificio indicado.'
      security:
        -
          BearerAuth: []
      parameters:
        -
          $ref: '#/components/parameters/BuildingUuidParam'
        -
          $ref: '#/components/parameters/IncludeQueryParam'
        -
          $ref: '#/components/parameters/FieldsQueryParam'
      responses:
        '200':
          description: 'Dispositivos encontrados.'
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Device'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/FeatureUnavailable'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'
      x-voltic-stability: stable
      x-voltic-integration-module: core
      x-voltic-proposed-capability: 'inventory:read'
      x-voltic-route-name: bms.buildings.devices.index
  '/buildings/{buildingUuid}/device-states':
    get:
      operationId: listDeviceStatesByBuilding
      tags:
        - Devices
      summary: 'Listar estado actual de los dispositivos de un edificio'
      description: |
        Devuelve un listado mínimo orientado a polling rápido con el estado cacheado de cada
        dispositivo del edificio.

        Cada elemento incluye:

        * `uuid`
        * `payload`: último estado cacheado del dispositivo
        * `availability`: última disponibilidad online/offline cacheada, si existe
        * `last_seen_at`: última comunicación real resuelta del dispositivo
      security:
        -
          BearerAuth: []
      parameters:
        -
          $ref: '#/components/parameters/BuildingUuidParam'
      responses:
        '200':
          description: 'Estado actual de los dispositivos del edificio.'
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/DeviceStateListItem'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/FeatureUnavailable'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'
      x-voltic-stability: stable
      x-voltic-integration-module: core
      x-voltic-proposed-capability: 'states:read'
      x-voltic-route-name: bms.buildings.device_states.index
  '/rooms/{roomUuid}/devices':
    get:
      operationId: listDevicesByRoom
      tags:
        - Devices
      summary: 'Listar dispositivos de una habitación'
      description: 'Devuelve los dispositivos asociados a la habitación indicada.'
      security:
        -
          BearerAuth: []
      parameters:
        -
          $ref: '#/components/parameters/RoomUuidParam'
        -
          $ref: '#/components/parameters/IncludeQueryParam'
        -
          $ref: '#/components/parameters/FieldsQueryParam'
      responses:
        '200':
          description: 'Dispositivos encontrados.'
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Device'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/FeatureUnavailable'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'
      x-voltic-stability: stable
      x-voltic-integration-module: core
      x-voltic-proposed-capability: 'inventory:read'
      x-voltic-route-name: bms.rooms.devices.index
  '/buildings/{buildingUuid}/zones':
    get:
      operationId: listZonesByBuilding
      tags:
        - Zones
      summary: 'Listar zonas de un edificio'
      description: 'Devuelve las zonas asociadas al edificio indicado.'
      security:
        -
          BearerAuth: []
      parameters:
        -
          $ref: '#/components/parameters/BuildingUuidParam'
        -
          $ref: '#/components/parameters/IncludeQueryParam'
        -
          $ref: '#/components/parameters/FieldsQueryParam'
      responses:
        '200':
          description: 'Zonas encontradas.'
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Zone'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/FeatureUnavailable'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'
      x-voltic-stability: stable
      x-voltic-integration-module: core
      x-voltic-proposed-capability: 'inventory:read'
      x-voltic-route-name: bms.buildings.zones.index
  '/floors/{floorUuid}/zones':
    get:
      operationId: listZonesByFloor
      tags:
        - Zones
      summary: 'Listar zonas de una planta'
      description: 'Devuelve las zonas asociadas a la planta indicada.'
      security:
        -
          BearerAuth: []
      parameters:
        -
          $ref: '#/components/parameters/FloorUuidParam'
        -
          $ref: '#/components/parameters/IncludeQueryParam'
        -
          $ref: '#/components/parameters/FieldsQueryParam'
      responses:
        '200':
          description: 'Zonas encontradas.'
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Zone'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/FeatureUnavailable'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'
      x-voltic-stability: stable
      x-voltic-integration-module: core
      x-voltic-proposed-capability: 'inventory:read'
      x-voltic-route-name: bms.floors.zones.index
components:
  parameters:
    BuildingUuid:
      name: uuid
      in: path
      required: true
      description: 'UUID del edificio.'
      schema:
        type: string
        format: uuid
    BuildingUuidParam:
      name: buildingUuid
      in: path
      required: true
      description: 'UUID del edificio.'
      schema:
        type: string
        format: uuid
    DeviceUuid:
      name: uuid
      in: path
      required: true
      description: 'UUID del dispositivo.'
      schema:
        type: string
        format: uuid
    FieldsQueryParam:
      name: fields
      in: query
      required: false
      description: 'Campos opcionales a incluir (separados por coma).'
      schema:
        type: string
      examples:
        AddressAndType:
          value: 'address,type'
          summary: 'Incluir dirección y tipo'
        Features:
          value: features
          summary: 'Incluir características del dispositivo'
    FloorUuid:
      name: uuid
      in: path
      required: true
      description: 'UUID de la planta.'
      schema:
        type: string
        format: uuid
    FloorUuidParam:
      name: floorUuid
      in: path
      required: true
      description: 'UUID de la planta.'
      schema:
        type: string
        format: uuid
    IncludeQueryParam:
      name: include
      in: query
      required: false
      description: 'Relaciones a incluir (separadas por coma).'
      schema:
        type: string
      examples:
        FloorsAndRooms:
          value: 'floors,rooms'
          summary: 'Incluir plantas y habitaciones'
        Devices:
          value: devices
          summary: 'Incluir dispositivos'
    RoomUuid:
      name: uuid
      in: path
      required: true
      description: 'UUID de la habitación.'
      schema:
        type: string
        format: uuid
    RoomUuidParam:
      name: roomUuid
      in: path
      required: true
      description: 'UUID de la habitación.'
      schema:
        type: string
        format: uuid
    ThermostatRuntimeIntervalQueryParam:
      name: interval
      in: query
      required: false
      description: |
        Intervalo de agregación del resultado. El cálculo interno siempre usa 15 minutos. `15m` solo está disponible para rangos de hasta 7 días completamente comprendidos en los últimos 7 días y no admite timestamps futuros; sus buckets se alinean a cuartos de hora. `1h` admite hasta 100 días, `4h` y `1d` hasta 370 días, `1w` hasta 12 semanas y `1mo` hasta 12 meses por petición. Un rango que exceda su límite devuelve `400` en `thermostat-runtime` y `422` en `room-consumption-heatmap` o `room-benchmark-summary`. El inicio de día/semana/mes se alinea con la timezone del building del scope o `Europe/Madrid` si no está definida.
      schema:
        type: string
        enum:
          - 15m
          - 1h
          - 4h
          - 1d
          - 1w
          - 1mo
        default: 1d
      example: 15m
    ThermostatRuntimeScopeIdParam:
      name: scopeId
      in: path
      required: true
      description: 'UUID del edificio, planta o habitación cuando aplique; IEEE del termostato cuando `scope=device`.'
      schema:
        type: string
      examples:
        BuildingUuid:
          summary: 'UUID de un edificio'
          value: 0194b243-d8dd-7112-b6f9-e9f088d50ba3
        FloorUuid:
          summary: 'UUID de una planta'
          value: 0194b243-db03-72fb-9a52-edd03ffcd7be
        RoomUuid:
          summary: 'UUID de una habitación'
          value: 0194b251-2312-70c4-a800-0b6de9a55d1b
        DeviceIeee:
          summary: 'IEEE de un termostato'
          value: '0x583bc2fffe85fe9e'
    ThermostatRuntimeScopeParam:
      name: scope
      in: path
      required: true
      description: 'Tipo de recurso sobre el que se calcula el runtime.'
      schema:
        type: string
        enum:
          - building
          - floor
          - room
          - device
      examples:
        Building:
          summary: 'Agregación por edificio'
          value: building
        Floor:
          summary: 'Agregación por planta'
          value: floor
        Room:
          summary: 'Agregación por habitación'
          value: room
        Device:
          summary: 'Agregación por termostato'
          value: device
    TimeSeriesElectricalAdjustmentFanRuntimeBasisQueryParam:
      name: electrical_adjustment_fan_runtime_basis
      in: query
      required: false
      description: |
        Solo aplica cuando `metric=energy` y `enrich` incluye `electrical_adjustments`. Define cómo se imputa la carga eléctrica opcional del fancoil: `valve_open` usa el tiempo en que la válvula ha estado abierta y es el valor por defecto; `thermostat_on` usa el tiempo en que el termostato ha permanecido en estado `on`. Afecta solo al cálculo de `fan_coil_kwh_per_runtime_hour`; `fridge_kwh_15m` no cambia.
      schema:
        type: string
        enum:
          - valve_open
          - thermostat_on
        default: valve_open
      style: form
      explode: false
      examples:
        DefaultValveOpen:
          summary: 'Base por tiempo de válvula abierta'
          value: valve_open
        ThermostatOn:
          summary: 'Base por tiempo de termostato en on'
          value: thermostat_on
    TimeSeriesEnrichQueryParam:
      name: enrich
      in: query
      required: false
      description: |
        Enriquecimientos opcionales solo disponibles para `metric=energy`. Acepta una lista CSV con `cost`, `co2` y/o `electrical_adjustments`. `electrical_adjustments` solo está disponible para `scope=room|floor|building` y suma cargas opcionales configuradas en analytics settings (`demo_comparison_electrical_adjustments`), manteniendo `value` como total final en kWh y añadiendo `metered_value` y `electrical_adjustment_value` por bucket. Además expone en la raíz un resumen agregado del rango —no una propiedad anidada `summary`— con `from`, `to` y `kwh_total`. Según los enriquecimientos solicitados puede incluir también `kwh_metered_total`, `kwh_adjustment_total`, `currency_code`, `cost_variable_total`, `cost_fixed_total`, `cost_total`, `tariff_breakdown`, `co2_tonnes_total`, `co2_baseline_tonnes_total` y `co2_avoided_tonnes_total`. `co2_tonnes_total` mide las emisiones del consumo real; `co2_baseline_tonnes_total` aplica el factor de referencia al mismo consumo; y `co2_avoided_tonnes_total` es la diferencia entre ambos, no el ahorro energético atribuido a Voltic. Los totales baseline y avoided de CO₂ pueden ser `null` si no hay cobertura baseline completa. Si se combina con `cost` o `co2`, esos cálculos se aplican sobre el `value` final ya ajustado. Para tarifas `pricing_model=tou`, las reglas horarias se evalúan en buckets de 15 minutos: `start` debe ir alineado a `:00/:15/:30/:45`, mientras que `end` puede enviarse tanto exacto (`:00/:15/:30/:45`) como en shorthand inclusivo de fin de bucket (`:14/:29/:44/:59`). Por ejemplo, `23:59` se interpreta como el cierre del bucket de `23:45`, es decir, hasta `00:00`. El coste se calcula internamente siempre en buckets de 15 minutos antes de reagrupar al `interval` pedido.
      schema:
        type: string
      style: form
      explode: false
      examples:
        Cost:
          summary: 'Añadir coste eléctrico'
          value: cost
        CostAndCo2:
          summary: 'Añadir coste y CO2'
          value: 'cost,co2'
        ElectricalAdjustments:
          summary: 'Añadir cargas eléctricas opcionales'
          value: electrical_adjustments
        ElectricalAdjustmentsAndCost:
          summary: 'Añadir cargas eléctricas opcionales y coste'
          value: 'electrical_adjustments,cost'
    TimeSeriesFromQueryParam:
      name: from
      in: query
      required: false
      description: 'Timestamp inicial inclusivo en ISO8601 / RFC3339. Si se omite, se usa `to - 1 día` (o el instante actual menos un día cuando también se omite `to`).'
      schema:
        type: string
        format: date-time
      example: '2026-03-31T21:59:59.000Z'
    TimeSeriesIntervalQueryParam:
      name: interval
      in: query
      required: false
      description: |
        Intervalo de agregación. Ignorado en métricas de estado. Límites por petición para métricas temporales: `15m` hasta 14 días, `1h` hasta 100 días, `4h` y `1d` hasta 370 días; cualquier intervalo tiene un máximo absoluto de 5 años. Un rango que exceda su límite devuelve `400`. `energy` con `enrich` puede devolver `422` por parámetros incompatibles o configuración de enriquecimiento no válida. `15m`, `1h` y `4h` se alinean en UTC; en `1d|1w|1mo`, el alineado de buckets usa la timezone del building del scope o `Europe/Madrid` si no está definida. Los timestamps de respuesta siempre se serializan como instantes UTC con sufijo `Z`; `timezone` solo indica cómo presentarlos en hora local.
      schema:
        type: string
        enum:
          - 15m
          - 1h
          - 4h
          - 1d
          - 1w
          - 1mo
        default: 15m
      example: 1d
    TimeSeriesMetricParam:
      name: metric
      in: path
      required: true
      description: 'Métrica a consultar. `energy` siempre devuelve consumo dentro del periodo pedido, calculado como delta positivo del contador acumulado, no el valor bruto del contador. En `scope=device`, `energy` y `power` se resuelven según la `category` del dispositivo. En `power_meter`, las magnitudes trifásicas deben consultarse con métricas explícitas por fase como `energy_phase_a`, `voltage_phase_ab`, `current_phase_a` o `power_phase_b`; `voltage` y `current` genéricos no aplican a esa categoría. En `room|floor|building`, solo `energy` agrega el consumo tanto de `switch` como de `power_meter` cuando existen datos.'
      schema:
        type: string
      examples:
        Temperature:
          summary: 'Serie temporal compleja'
          value: temperature
        Energy:
          summary: 'Consumo energético por bucket'
          value: energy
        Power:
          summary: 'Potencia media o category-aware por dispositivo'
          value: power
        EnergyPhaseA:
          summary: 'Consumo energético de la fase A de power meter'
          value: energy_phase_a
        VoltagePhaseAB:
          summary: 'Voltaje entre fases A-B de power meter trifásico'
          value: voltage_phase_ab
        CurrentPhaseA:
          summary: 'Corriente de la fase A de power meter trifásico'
          value: current_phase_a
        PowerFactor:
          summary: 'Factor de potencia para power meters'
          value: power_factor
        ThermostatState:
          summary: 'Evento de estado'
          value: thermostat_state
    TimeSeriesScopeIdParam:
      name: scopeId
      in: path
      required: true
      description: 'UUID del recurso para `building`, `floor` y `room`; identificador físico para `device`. Stoplight muestra todos los ejemplos disponibles y no los filtra automáticamente según el `scope` elegido.'
      schema:
        type: string
      examples:
        BuildingUuid:
          summary: 'UUID de un edificio'
          value: 0194b243-d8dd-7112-b6f9-e9f088d50ba3
        FloorUuid:
          summary: 'UUID de una planta'
          value: 0194b243-db03-72fb-9a52-edd03ffcd7be
        RoomUuid:
          summary: 'UUID de una habitación'
          value: 0194b251-2312-70c4-a800-0b6de9a55d1b
        DeviceIdentifier:
          summary: 'Hardware ID de un dispositivo LoRa o IEEE Zigbee'
          value: 'cp:0000203'
    TimeSeriesScopeParam:
      name: scope
      in: path
      required: true
      description: 'Tipo de recurso sobre el que se calcula la métrica.'
      schema:
        type: string
        enum:
          - building
          - floor
          - room
          - device
      examples:
        Floor:
          summary: 'Métrica agregada de una planta'
          value: floor
        Device:
          summary: 'Métrica de un dispositivo concreto'
          value: device
    TimeSeriesStatsQueryParam:
      name: stats
      in: query
      required: false
      description: 'Lista de estadísticos separada por comas. Ignorado en métricas de estado. En métricas acumulativas como `energy`, la API devuelve consumo por bucket y fija `stat=["sum"]`. En métricas complejas OHLC como `power`, `voltage_phase_ab` o `current_phase_a`, la API responde con `stat=["ohlc"]`.'
      schema:
        type: string
      style: form
      explode: false
      examples:
        Ohlc:
          summary: 'OHLC para métricas complejas'
          value: ohlc
        AverageAndSum:
          summary: 'Varios estadísticos en métricas simples'
          value: 'avg,sum'
    TimeSeriesToQueryParam:
      name: to
      in: query
      required: false
      description: 'Timestamp final exclusivo en ISO8601 / RFC3339. Si se omite, se usa el instante actual.'
      schema:
        type: string
        format: date-time
      example: '2026-04-07T21:59:59.000Z'
    ZoneUuid:
      name: uuid
      in: path
      required: true
      description: 'UUID de la zona.'
      schema:
        type: string
        format: uuid
  responses:
    FeatureUnavailable:
      description: 'La feature no está disponible para el acceso efectivo de la suscripción'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/FeatureUnavailableError'
          examples:
            featureUnavailable:
              summary: 'Feature no incluida en el acceso efectivo'
              value:
                error: feature_unavailable
                feature: analytics_comparison
                message: 'El plan actual no permite consultar esta funcionalidad.'
                upgrade_required: true
    Forbidden:
      description: 'El usuario autenticado no puede ejecutar la operación'
      content:
        application/json:
          schema:
            oneOf:
              -
                $ref: '#/components/schemas/FeatureUnavailableError'
              -
                $ref: '#/components/schemas/SubscriptionReadOnlyError'
              -
                type: object
                required:
                  - error
                additionalProperties: false
                properties:
                  error:
                    type: string
                    description: 'Mensaje de autorización por rol o perímetro del recurso.'
                    example: 'No tienes permisos para gestionar este edificio.'
          examples:
            featureUnavailable:
              summary: 'Feature no disponible'
              value:
                error: feature_unavailable
                feature: subscription_self_service
                message: 'El plan actual no permite gestionar la suscripción desde la API.'
                upgrade_required: true
            roleDenied:
              summary: 'Rol insuficiente'
              value:
                error: 'No tienes permisos para gestionar este edificio.'
    InternalError:
      description: 'Error interno del servidor'
      content:
        application/json:
          schema:
            type: object
            properties:
              status:
                type: string
                example: error
              message:
                type: string
                example: 'Error interno del servidor'
    NotFound:
      description: 'Recurso no encontrado'
      content:
        application/json:
          schema:
            type: object
            properties:
              error:
                type: string
                example: 'Not Found'
              message:
                type: string
                example: 'The requested resource could not be found'
    Unauthorized:
      description: 'No autorizado'
      content:
        application/json:
          schema:
            type: object
            properties:
              error:
                type: string
                example: 'No autorizado'
  schemas:
    AsyncMaterializationResponse:
      type: object
      required:
        - status
        - completed_days
        - pending_days
        - failed_days
        - jobs
        - progress
        - retry_after_seconds
        - data
      properties:
        status:
          type: string
          enum:
            - processing
        metric:
          type: string
          description: 'Métrica solicitada.'
        scope:
          type: object
          properties:
            type:
              type: string
            uuid:
              type: string
            name:
              type: string
        interval:
          type: string
        message:
          type: string
          description: 'Motivo legible del procesamiento asíncrono.'
        completed_days:
          type: array
          description: 'Días locales completos cuyos datos ya pueden aparecer en `data`.'
          items:
            type: string
            format: date
        pending_days:
          type: array
          description: 'Días locales en cola o ejecución. Su longitud indica cuántos rollups quedan.'
          items:
            type: string
            format: date
        failed_days:
          type: array
          description: 'Días que fallaron y no se reencolan automáticamente mediante sondeo.'
          items:
            type: string
            format: date
        jobs:
          type: array
          description: 'Trabajos compartidos activos o ya reservados para los días pendientes.'
          items:
            type: object
            properties:
              job:
                type: string
                description: 'Identificador estable del trabajo compartido.'
              day_local:
                type: string
                format: date
              status:
                type: string
                enum:
                  - launched
                  - pending
                  - queued
                  - running
                  - failed
        progress:
          type: object
          required:
            - total_days
            - completed_days
            - pending_days
            - failed_days
            - percent
          properties:
            total_days:
              type: integer
              minimum: 0
            completed_days:
              type: integer
              minimum: 0
            pending_days:
              type: integer
              minimum: 0
            failed_days:
              type: integer
              minimum: 0
            percent:
              type: number
              minimum: 0
              maximum: 100
              description: 'Porcentaje de días completos sobre el total solicitado.'
        retry_after_seconds:
          type: integer
          const: 10
        data:
          type: array
          description: 'Datos parciales ya disponibles. Puede estar vacío cuando aún no se completó ningún día.'
          items: {  }
        materialization_status:
          type: string
          enum:
            - queued
            - running
            - processing
            - failed
            - completed
        materialization:
          type: object
          description: 'Diagnóstico adicional de la cola cuando el servicio origen lo proporciona.'
          additionalProperties: true
        missing_days:
          type: array
          deprecated: true
          description: 'Alias heredado de `pending_days`; los clientes nuevos deben usar `pending_days`.'
          items:
            type: string
            format: date
        total_days:
          type: integer
          deprecated: true
          description: 'Contador heredado; usar `progress.total_days`.'
        progress_percent:
          type: number
          deprecated: true
          description: 'Porcentaje heredado; usar `progress.percent`.'
        data_quality:
          type: object
          additionalProperties: true
    Building:
      type: object
      required:
        - name
        - type
        - demo_kind
        - timezone
      properties:
        uuid:
          type: string
          format: uuid
        name:
          type: string
        description:
          type: string
          description: 'Opcional: incluir con ?fields=description'
        type:
          type: string
          enum:
            - hotel
            - hotel_demo
            - office
            - residential
            - commercial
          description: 'Opcional: incluir con ?fields=address'
        demo_kind:
          type:
            - string
            - 'null'
          enum:
            - physical
            - synthetic
            - null
          description: 'Finalidad demo del edificio. `physical` se configura en el backend y `synthetic` identifica los edificios virtuales con type=hotel_demo. Siempre se incluye en la respuesta.'
        city:
          type: string
          description: 'Opcional: incluir con ?fields=city'
        state:
          type: string
          description: 'Opcional: incluir con ?fields=state. Se devuelve como código de región.'
        zip:
          type: string
          description: 'Opcional: incluir con ?fields=zip'
        country:
          type: string
          description: 'Opcional: incluir con ?fields=country. Se devuelve como código de país.'
        currency_code:
          type: string
          description: 'Opcional: incluir con ?fields=currency_code.'
        timezone:
          type: string
          description: 'Zona horaria IANA efectiva del edificio. Siempre se incluye, también en el listado.'
        metadata:
          type: object
          description: 'Opcional: incluir con ?fields=metadata. Las demos físicas guardan metadata.demo.kind=physical y las sintéticas su blueprint en metadata.demo_blueprint. También puede incluir overrides analíticos en thermostat_energy_coefficients.*. room_state_energy_baselines.electrical_kwh_15m es un campo heredado de compatibilidad y no interviene en el ahorro automático.'
          properties:
            demo:
              type: object
              properties:
                kind:
                  type: string
                  enum:
                    - physical
            demo_blueprint:
              $ref: '#/components/schemas/BuildingDemoBlueprint'
            thermal_savings_auto_training_enabled:
              type: boolean
              description: 'Si falta, equivale a false. Habilita el entrenamiento inicial al consultar ahorro.'
        position:
          type: object
          description: 'Opcional: incluir con ?fields=position'
        floors_count:
          type: integer
          description: 'Opcional: incluir con ?fields=floors_count'
        total_area:
          type: number
          description: 'Opcional: incluir con ?fields=total_area'
        ifc_model_available:
          type: boolean
          description: 'Opcional: incluir con ?fields=ifc_model_available'
        ifc_model_updated_at:
          type:
            - string
            - 'null'
          format: date-time
          description: 'Opcional: incluir con ?fields=ifc_model_updated_at'
        logo:
          type: string
          description: 'Opcional: incluir con ?fields=logo'
        address:
          type: string
        user_role:
          type: string
          description: 'Rol del usuario autenticado dentro del edificio cuando aplique.'
        user_permissions:
          type: array
          items:
            type: string
          description: 'Permisos RBAC del rol del usuario autenticado en este edificio. No son entitlements de suscripción.'
        subscription:
          $ref: '#/components/schemas/Subscription'
          description: 'Opcional: include=subscription'
        hubs:
          type: array
          items:
            $ref: '#/components/schemas/Hub'
          description: 'Opcional: include=hubs'
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    BuildingDemoBlueprint:
      type: object
      required:
        - floors_count
        - rooms_count
        - room_mix
      additionalProperties: false
      properties:
        floors_count:
          type: integer
          minimum: 1
          example: 3
        rooms_count:
          type: integer
          minimum: 1
          example: 41
        room_mix:
          type: array
          minItems: 1
          description: 'La suma de `room_mix[].count` debe coincidir exactamente con `rooms_count`.'
          items:
            $ref: '#/components/schemas/BuildingDemoRoomMixItem'
    BuildingDemoRoomMixItem:
      type: object
      required:
        - type
        - count
      additionalProperties: false
      properties:
        type:
          type: string
          enum:
            - common
            - room_double
            - room_single
            - room_suite
            - room_duplex
            - office
            - meeting_room
            - hallway
            - storage
            - it_room
            - lobby
            - kitchen
            - machine_room
            - room_commercial
          description: 'Tipo canónico de habitación usado por el modelo Room.'
          example: room_double
        count:
          type: integer
          minimum: 1
          example: 8
        traits:
          type: object
          additionalProperties: true
          description: 'Particularidades libres para ese tipo de room dentro de la demo.'
          example:
            occupancy: 2
            balcony: false
    Company:
      type: object
      required:
        - uuid
        - name
      properties:
        uuid:
          type: string
          format: uuid
        name:
          type: string
        domain:
          type:
            - string
            - 'null'
        email:
          type:
            - string
            - 'null'
          format: email
        email_invoice:
          type:
            - string
            - 'null'
          format: email
        phone:
          type:
            - string
            - 'null'
        address:
          type:
            - string
            - 'null'
        city:
          type:
            - string
            - 'null'
        state:
          type:
            - string
            - 'null'
        country:
          type:
            - string
            - 'null'
        zip:
          type:
            - string
            - 'null'
        vat_number:
          type:
            - string
            - 'null'
        website:
          type:
            - string
            - 'null'
          format: uri
        erp_contact_id:
          type:
            - string
            - 'null'
        stripe_customer_id:
          type:
            - string
            - 'null'
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    Device:
      type: object
      required:
        - uuid
        - name
        - category
        - role
      properties:
        uuid:
          type: string
          format: uuid
        name:
          type: string
        category:
          type: string
          enum:
            - thermostat
            - switch
            - power_meter
            - pir
            - contact_door
            - contact_window
            - garage_door_opener
            - repeater
            - ir_blaster
          description: 'Categoría funcional canónica del dispositivo. `contact_door` y `contact_window` siguen separadas hoy por compatibilidad, aunque se prevé su futura convergencia en una categoría genérica de contacto.'
        role:
          type:
            - string
            - 'null'
          description: 'Rol funcional específico del dispositivo dentro de la habitación o instalación, validado según la categoría. En `thermostat`, el backend infiere hoy `fancoil` por defecto; los roles `aidoo_direct_expansion` y `aidoo_vrv` quedan disponibles para selección manual.'
        type:
          type: string
          description: 'Opcional: incluir con ?fields=type'
        features:
          type: object
          description: 'Opcional: incluir con ?fields=features'
        manufacturer:
          type: string
          description: 'Opcional: incluir con ?fields=manufacturer'
        model:
          type: string
          description: 'Opcional: incluir con ?fields=model'
        firmware_version:
          type: string
          description: 'Opcional: incluir con ?fields=firmware_version'
        connection_type:
          type: string
          enum:
            - zigbee
            - lora
            - legacy_mqtt
          description: '`legacy_mqtt` conserva el protocolo MQTT directo de instalaciones antiguas.'
        power_source:
          type: string
          description: 'Opcional: incluir con ?fields=power_source'
        ieee_address:
          type: string
          description: 'Dirección IEEE única del dispositivo.'
        topic:
          type: string
          description: 'Topic MQTT asociado al dispositivo (fields=topic)'
        status_topic:
          type: string
          description: 'Topic de estado, presente en dispositivos `legacy_mqtt`.'
        availability_topic:
          type: string
          description: 'Topic de disponibilidad, presente en dispositivos `legacy_mqtt`.'
        command_payload:
          type: string
          description: 'Payload escalar de accionamiento, presente en dispositivos `legacy_mqtt`.'
        position:
          type: object
          description: 'Opcional: incluir con ?fields=position'
        metadata:
          type: object
          description: |
            Opcional: incluir con `?fields=metadata`. Para Aidoo, `metadata.hvac_config` contiene
            asociaciones explícitas; no se infiere ninguna relación por compartir habitación y no
            afirma actividad real del compresor. `system_uuid` relaciona el Aidoo con un sistema HVAC
            compartido del mismo edificio. `dedicated_meter_device_uuid` relaciona un contador dedicado
            del mismo edificio; puede ser `category=power_meter` o un `category=switch` con
            `role=power_meter`. Sin contador, Aidoo solo aporta horas comandadas.
          properties:
            metering:
              type:
                - object
                - 'null'
              properties:
                scope:
                  type: string
                  enum:
                    - circuit
                    - room_total
                valid_from:
                  type:
                    - string
                    - 'null'
                  format: date-time
              description: 'Contador general explícito para el consumo de la habitación; configurable desde el backend o PATCH /devices/{uuid}/metadata.'
            legacy_mqtt:
              type: object
              properties:
                status_topic:
                  type: string
                  example: stat/POWER
                availability_topic:
                  type: string
                  example: tele/LWT
                command_payload:
                  type: string
                  example: '1'
            hvac_config:
              type: object
              properties:
                system_uuid:
                  type:
                    - string
                    - 'null'
                  format: uuid
                  description: 'Sistema HVAC compartido del mismo edificio.'
                dedicated_meter_device_uuid:
                  type:
                    - string
                    - 'null'
                  format: uuid
                  description: 'Contador dedicado del mismo edificio; la categoría determina la tabla de energía.'
                active_power_threshold_w:
                  type:
                    - number
                    - 'null'
                  minimum: 0
                  description: 'Umbral de potencia para estimar actividad/ciclos del compresor; no es necesario para calcular kWh medidos.'
                nominal_thermal_capacity_kw:
                  type:
                    - number
                    - 'null'
                  minimum: 0
                  description: 'Capacidad nominal contextual del equipo; no convierte por sí sola el estado Aidoo a kWh.'
                meter_scope:
                  type: string
                  enum:
                    - split_only
                    - including_auxiliary
                  description: 'Indica si el contador cubre solo el split o también cargas auxiliares.'
        commissioning_status:
          type: string
          enum:
            - pending
            - in_progress
            - completed
            - error
          description: 'Estado de la puesta en marcha del dispositivo (fields=commissioning_status)'
        commissioning_progress:
          type: integer
          minimum: 0
          maximum: 100
          description: 'Progreso del commissioning en porcentaje (fields=commissioning_progress)'
        commissioning_steps:
          type: array
          description: |
            Pasos del flujo de commissioning (fields=commissioning_steps).
            Formato recomendado para frontend:
            `[{ "key": "connectivity", "status": "completed", "label": "Conectividad", "payload": { ... } }]`
          items:
            type: object
            required:
              - key
              - status
            properties:
              key:
                type: string
                description: 'Identificador estable del paso dentro del flujo del dispositivo.'
              status:
                type: string
                enum:
                  - pending
                  - in_progress
                  - completed
                  - error
              label:
                type:
                  - string
                  - 'null'
                description: 'Texto opcional para mostrar en UI.'
              updated_at:
                type:
                  - string
                  - 'null'
                format: date-time
                description: 'Marca temporal opcional del último cambio del paso.'
              payload:
                type:
                  - object
                  - 'null'
                additionalProperties: true
                description: 'Datos arbitrarios asociados al paso.'
        commissioning_started_at:
          type:
            - string
            - 'null'
          format: date-time
          description: 'Fecha/hora de inicio de la puesta en marcha (fields=commissioning_started_at)'
        commissioning_last_update_at:
          type:
            - string
            - 'null'
          format: date-time
          description: 'Última actualización del dispositivo en modo instalador (fields=commissioning_last_update_at)'
        commissioning_completed_at:
          type:
            - string
            - 'null'
          format: date-time
          description: 'Fecha/hora de finalización del commissioning (fields=commissioning_completed_at)'
        commissioning_updated_by:
          type:
            - string
            - 'null'
          description: 'Usuario que realizó la última acción en formato legible (`Nombre Apellido - email@dominio`) cuando está disponible (fields=commissioning_updated_by)'
        commissioning_report:
          type:
            - string
            - 'null'
          description: 'Reporte de fallo enviado por el instalador (fields=commissioning_report)'
        hub:
          $ref: '#/components/schemas/Hub'
          description: 'Opcional: include=hub'
        room:
          $ref: '#/components/schemas/Room'
          description: 'Opcional: include=room'
    DeviceStateListItem:
      type: object
      required:
        - uuid
      properties:
        uuid:
          type: string
          format: uuid
        payload:
          description: 'Último payload de estado recibido para el dispositivo. Puede ser cualquier valor JSON.'
        availability:
          oneOf:
            -
              type: string
              enum:
                - online
                - offline
            -
              type: 'null'
          description: 'Última disponibilidad online/offline cacheada para el dispositivo, si existe.'
        last_seen_at:
          oneOf:
            -
              type: string
              format: date-time
            -
              type: 'null'
          description: 'Última comunicación real del dispositivo en UTC priorizando `last_seen`, luego `reported_at` y por último `received_at`.'
      example:
        uuid: 01914b65-cc09-720e-989c-4f4f5fd73b8b
        payload:
          temperature: 20.5
        availability: offline
        last_seen_at: '2026-05-20T10:15:52Z'
    DeviceStateResponse:
      type: object
      description: 'Estado actual cacheado de un dispositivo.'
      properties:
        payload:
          description: 'Último payload de estado recibido para el dispositivo. Puede ser cualquier valor JSON.'
        availability:
          oneOf:
            -
              type: string
              enum:
                - online
                - offline
            -
              type: 'null'
          description: 'Última disponibilidad online/offline cacheada para el dispositivo, si existe.'
        last_seen_at:
          oneOf:
            -
              type: string
              format: date-time
            -
              type: 'null'
          description: 'Última comunicación real del dispositivo en UTC priorizando `last_seen`, luego `reported_at` y por último `received_at`.'
      example:
        payload:
          temperature: 21.4
          occupied: true
        availability: online
        last_seen_at: '2026-05-20T10:15:52Z'
    FanCoilModel:
      type: object
      required:
        - model_version
        - parameters
        - sources
      properties:
        model_version:
          type: integer
          enum:
            - 2
        parameters:
          $ref: '#/components/schemas/FanCoilModelParameters'
        sources:
          type: object
          additionalProperties:
            type: string
            enum:
              - reference_default
              - building
              - room
    FanCoilModelParameters:
      type: object
      additionalProperties: false
      description: |
        Overrides del modelo térmico v2. Null/vacío hereda habitación → edificio → referencia.
        Unidades y fórmulas completas en docs/thermal-coil-model.md.
      properties:
        control:
          type:
            - string
            - 'null'
          enum:
            - valve
            - fan_only
            - null
        fan_only_mode:
          type:
            - string
            - 'null'
          enum:
            - heat
            - cool
            - 'off'
            - null
        water_available:
          type:
            - boolean
            - 'null'
        auto_speed:
          type:
            - string
            - 'null'
          enum:
            - low
            - medium
            - high
            - null
        max_state_hours:
          type:
            - number
            - 'null'
        fallback_air_c:
          type:
            - number
            - 'null'
        fallback_rh_pct:
          type:
            - number
            - 'null'
        pressure_kpa:
          type:
            - number
            - 'null'
        design_rh_pct:
          type:
            - number
            - 'null'
        design_cool_air_c:
          type:
            - number
            - 'null'
        design_heat_air_c:
          type:
            - number
            - 'null'
        design_cool_water_c:
          type:
            - number
            - 'null'
        design_heat_water_c:
          type:
            - number
            - 'null'
        cool_water_c:
          type:
            - number
            - 'null'
        heat_water_c:
          type:
            - number
            - 'null'
        cool_water_lh:
          type:
            - number
            - 'null'
        heat_water_lh:
          type:
            - number
            - 'null'
        airflow_low_m3h:
          type:
            - number
            - 'null'
        cool_kw_low:
          type:
            - number
            - 'null'
        cool_sensible_kw_low:
          type:
            - number
            - 'null'
        heat_kw_low:
          type:
            - number
            - 'null'
        design_cool_water_lh_low:
          type:
            - number
            - 'null'
        design_heat_water_lh_low:
          type:
            - number
            - 'null'
        airflow_medium_m3h:
          type:
            - number
            - 'null'
        cool_kw_medium:
          type:
            - number
            - 'null'
        cool_sensible_kw_medium:
          type:
            - number
            - 'null'
        heat_kw_medium:
          type:
            - number
            - 'null'
        design_cool_water_lh_medium:
          type:
            - number
            - 'null'
        design_heat_water_lh_medium:
          type:
            - number
            - 'null'
        airflow_high_m3h:
          type:
            - number
            - 'null'
        cool_kw_high:
          type:
            - number
            - 'null'
        cool_sensible_kw_high:
          type:
            - number
            - 'null'
        heat_kw_high:
          type:
            - number
            - 'null'
        design_cool_water_lh_high:
          type:
            - number
            - 'null'
        design_heat_water_lh_high:
          type:
            - number
            - 'null'
    FanCoilProfile:
      type: object
      description: |
        Perfil físico y operativo del fancoil asociado a la habitación.
        La estimación HVAC del backend usa esta lógica:

        `thermal_kw = 0.00033 * airflow_m3h * delta_t_air_c`

        `kwh_est = thermal_kw * runtime_hours * calibration_factor`

        Con estrategia recomendada `supply_air_by_fan_mode`, el backend deriva:

        - en frio: `delta_t_air_c = temperatura_habitacion - temperatura_impulsion`
        - en calor: `delta_t_air_c = temperatura_impulsion - temperatura_habitacion`

        Si falta `temperature_avg`, el backend cae al fallback `fixed_delta_t`.
        `calibration_factor` viene de `thermostat_energy_coefficients.*` o, si no existe override manual,
        de la calibración derivada del perfil del fancoil, anclada alrededor de `0.8`.

        Esta salida es energía térmica estimada (`kWh_th`). Para obtener el equivalente eléctrico,
        el endpoint `/{scope}/{scopeId}/hvac-energy` busca el sistema HVAC default del edificio por
        modo (o una asociación explícita de room si existe), y aplica `kWh_e = kWh_th / COP` en calor
        o `kWh_e = kWh_th / EER` en frío cuando el carrier es electricidad. Además puede añadir el
        consumo eléctrico terminal `runtime_hours * fan_coil_kwh_per_runtime_hour`, configurado en
        analytics settings de room con herencia desde building. El perfil físico se guarda en
        `metadata.fan_coil_profile` mediante `PATCH /rooms/{uuid}`; los coeficientes térmicos y el
        ajuste terminal se gestionan mediante `PATCH /rooms/{uuid}/analytics-settings` o sus controles
        equivalentes del backend UI. Estas estimaciones no sustituyen un contador eléctrico real.
      properties:
        heating_kw_nominal:
          type:
            - number
            - 'null'
          format: float
          minimum: 0
          description: 'Capacidad térmica nominal de calefacción de la unidad interior (fancoil o Aidoo), en kW térmicos.'
          example: 2.8
        cooling_kw_nominal:
          type:
            - number
            - 'null'
          format: float
          minimum: 0
          description: 'Capacidad térmica nominal de refrigeración de la unidad interior (fancoil o Aidoo), en kW térmicos.'
          example: 2.4
        part_load_factor:
          type:
            - number
            - 'null'
          format: float
          minimum: 0
          maximum: 1
          description: |
            Factor de carga medio calibrable para Aidoo. Si falta, se usa 1 y el resultado se identifica como escenario a capacidad nominal; no como consumo medido.
          example: 0.6
        airflow_m3h:
          type:
            - number
            - 'null'
          format: float
          minimum: 0
          deprecated: true
          description: 'Campo legado. Si se envía, backend lo usa como alias de `airflow_high_m3h`.'
          example: 420
        airflow_low_m3h:
          type:
            - number
            - 'null'
          format: float
          minimum: 0
          description: 'Caudal de aire cuando el termostato informa `fan_mode=low`.'
          example: 220
        airflow_medium_m3h:
          type:
            - number
            - 'null'
          format: float
          minimum: 0
          description: 'Caudal de aire cuando el termostato informa `fan_mode=medium`.'
          example: 350
        airflow_high_m3h:
          type:
            - number
            - 'null'
          format: float
          minimum: 0
          description: 'Caudal de aire cuando el termostato informa `fan_mode=high`.'
          example: 500
        airflow_auto_m3h:
          type:
            - number
            - 'null'
          format: float
          minimum: 0
          description: |
            Caudal de aire cuando el termostato informa `fan_mode=auto`.
            Si no se envía, backend usa `0.75 * airflow_high_m3h`.
          example: 375
        temperature_strategy:
          type:
            - string
            - 'null'
          enum:
            - supply_air_by_fan_mode
            - fixed_delta_t
            - null
          description: |
            Estrategia para resolver el salto térmico del aire.
            - `supply_air_by_fan_mode`: recomendada. Usa temperatura de impulsion por velocidad.
            - `fixed_delta_t`: compatibilidad. Usa `delta_t_heat_air_c` o `delta_t_cool_air_c`.
            Default backend: `supply_air_by_fan_mode`.
          example: supply_air_by_fan_mode
        supply_air_temp_cool_low_c:
          type:
            - number
            - 'null'
          format: float
          description: 'Temperatura de impulsion estimada en frio con `fan_mode=low`. Default backend: `14`.'
          example: 14
        supply_air_temp_cool_medium_c:
          type:
            - number
            - 'null'
          format: float
          description: 'Temperatura de impulsion estimada en frio con `fan_mode=medium`. Default backend: `12`.'
          example: 12
        supply_air_temp_cool_high_c:
          type:
            - number
            - 'null'
          format: float
          description: 'Temperatura de impulsion estimada en frio con `fan_mode=high`. Default backend: `10`.'
          example: 10
        supply_air_temp_heat_low_c:
          type:
            - number
            - 'null'
          format: float
          description: 'Temperatura de impulsion estimada en calor con `fan_mode=low`. Default backend: `32`.'
          example: 32
        supply_air_temp_heat_medium_c:
          type:
            - number
            - 'null'
          format: float
          description: 'Temperatura de impulsion estimada en calor con `fan_mode=medium`. Default backend: `38`.'
          example: 38
        supply_air_temp_heat_high_c:
          type:
            - number
            - 'null'
          format: float
          description: 'Temperatura de impulsion estimada en calor con `fan_mode=high`. Default backend: `42`.'
          example: 42
        delta_t_heat_air_c:
          type:
            - number
            - 'null'
          format: float
          minimum: 0
          description: |
            Fallback de compatibilidad para calefaccion cuando no puede derivarse la estrategia dinamica.
            Default backend: `10`.
          example: 10
        delta_t_cool_air_c:
          type:
            - number
            - 'null'
          format: float
          minimum: 0
          description: |
            Fallback de compatibilidad para refrigeracion cuando no puede derivarse la estrategia dinamica.
            Default backend: `8`.
          example: 8
        demand_deadband_c:
          type:
            - number
            - 'null'
          format: float
          minimum: 0
          description: |
            Banda muerta usada solo como gate de demanda con temperatura media y setpoint.
            Default backend: `0.5`.
          example: 0.5
        water_system_type:
          type:
            - string
            - 'null'
          enum:
            - 2pipe
            - 4pipe
            - null
          description: |
            Tipo de sistema hidrónico. Se conserva como metadata contextual del activo.
          example: 2pipe
        valve_type:
          type:
            - string
            - 'null'
          enum:
            - modulating
            - on_off
            - null
          description: |
            Tipo de control de válvula. Se conserva como metadata contextual del activo.
          example: modulating
        installation_factor:
          type:
            - number
            - 'null'
          format: float
          minimum: 0.5
          maximum: 1.5
          description: |
            Factor de calibración derivado por defecto cuando hay datos físicos del fancoil.
            `1.0` mantiene la calibración derivada alrededor de `0.8`. Valores por debajo o por encima
            ajustan esa calibración de forma moderada, sin alejarla bruscamente del orden de magnitud histórico.
          example: 1.0
    FeatureUnavailableError:
      type: object
      additionalProperties: false
      required:
        - error
        - feature
        - message
        - upgrade_required
      properties:
        error:
          type: string
          enum:
            - feature_unavailable
        feature:
          type: string
          description: 'Feature requerida por la operación.'
          example: analytics_comparison
        message:
          type: string
          example: 'El plan actual no permite consultar esta funcionalidad.'
        upgrade_required:
          type: boolean
          enum:
            - true
    Floor:
      type: object
      required:
        - name
        - level
      properties:
        uuid:
          type: string
          format: uuid
        name:
          type: string
        level:
          type: integer
        area:
          type: number
          description: 'Opcional: incluir con ?fields=area'
        type:
          type: string
          description: 'Opcional: incluir con ?fields=type'
        is_accessible:
          type: boolean
          description: 'Opcional: incluir con ?fields=is_accessible'
        facilities:
          type: object
          description: 'Opcional: incluir con ?fields=facilities'
        description:
          type: string
          description: 'Opcional: incluir con ?fields=description'
        capacity:
          type: integer
          description: 'Opcional: incluir con ?fields=capacity'
        status:
          type: string
          description: 'Opcional: incluir con ?fields=status'
        last_inspection_date:
          type: string
          format: date-time
          description: 'Opcional: incluir con ?fields=last_inspection_date'
        building:
          $ref: '#/components/schemas/Building'
          description: 'Opcional: include=building'
        rooms:
          type: array
          items:
            $ref: '#/components/schemas/Room'
          description: 'Opcional: include=rooms'
        zones:
          type: array
          items:
            $ref: '#/components/schemas/Zone'
          description: 'Opcional: include=zones'
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    Hub:
      type: object
      required:
        - uuid
        - name
        - eth_mac_address
        - cpu_id
        - usb_serial
      properties:
        uuid:
          type: string
          format: uuid
          example: 123e4567-e89b-12d3-a456-426614174000
        name:
          type: string
          example: Hub-1
        serial_number:
          type: string
          example: ABCD1234
        z2m_topic:
          type:
            - string
            - 'null'
          description: 'Namespace MQTT interno del hub (`zb`, `lora`, etc.).'
          example: lora
        status:
          type: string
          enum:
            - preassigned
            - pairing_devices
            - configured
            - ready
            - maintenance
            - retired
          example: preassigned
        zt_ip:
          type: string
          description: 'IP asignada en ZeroTier.'
          example: 172.16.1.100
        zt_id:
          type: string
          description: 'Identificador del nodo en la red ZeroTier.'
          example: aabbccddee
        eth_mac_address:
          type: string
          example: '00:11:22:33:44:55'
        wlan_mac_address:
          type: string
          example: '00:11:22:33:44:66'
        cpu_id:
          type: string
          description: 'Identificador hardware de 32 caracteres hex.'
          example: 0123456789abcdef0123456789abcdef
        usb_serial:
          type: string
          description: 'Número de serie USB de 32 caracteres hex.'
          example: fedcba9876543210fedcba9876543210
        model:
          type: string
          enum:
            - voltic_hub_W2024
            - voltic_hub_mvp
          example: voltic_hub_W2024
        building_id:
          type: integer
          description: 'ID interno de edificio (cuando está asignado).'
        floor_id:
          type: integer
          description: 'ID interno de planta (cuando está asignado).'
        ip_address:
          type: string
          description: 'IP local reportada por el hub.'
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    InstallerNote:
      type: object
      required:
        - uuid
        - message
      description: |
        Entrada del histórico de notas del instalador a nivel de habitación.
        Se almacena como una estructura JSON con el texto de la nota, quién la escribió
        y cuándo se añadió. Cada nota tiene un UUID estable para permitir editarla o borrarla
        individualmente, y `modified_at` indica la última fecha de edición cuando aplica.
      properties:
        uuid:
          type: string
          format: uuid
          description: 'Identificador estable de la nota dentro del histórico.'
        message:
          type: string
          description: 'Texto de la nota.'
        created_at:
          type:
            - string
            - 'null'
          format: date-time
          description: 'Fecha/hora ISO 8601 en la que se añadió la nota.'
        created_by:
          type:
            - string
            - 'null'
          description: 'Usuario autor de la nota en formato legible (`Nombre Apellido - email@dominio`) cuando está disponible.'
        created_by_user:
          anyOf:
            -
              allOf:
                -
                  $ref: '#/components/schemas/InstallerNoteAuthor'
              description: 'Datos legibles del usuario autor para frontend y backend.'
            -
              type: 'null'
        modified_at:
          type:
            - string
            - 'null'
          format: date-time
          description: 'Fecha/hora ISO 8601 de la última modificación de la nota. `null` si no se ha editado.'
      example:
        uuid: 0195e5fd-8946-7f1c-8df1-4487f3823a1f
        message: 'Pendiente de revisar el fan coil'
        created_at: '2026-04-01T09:45:00Z'
        created_by: 'Juan Pérez - installer.juan@voltic.es'
        created_by_user:
          id: 121
          email: installer.juan@voltic.es
          name: 'Juan Pérez'
        modified_at: '2026-04-01T10:05:00Z'
    InstallerNoteAuthor:
      type: object
      required:
        - id
      properties:
        id:
          type: integer
          description: 'ID del usuario de Winter.'
        email:
          type:
            - string
            - 'null'
          description: 'Email del usuario cuando está disponible.'
        name:
          type:
            - string
            - 'null'
          description: 'Nombre legible del usuario cuando está disponible.'
      example:
        id: 121
        email: installer.juan@voltic.es
        name: 'Juan Pérez'
    Room:
      type: object
      required:
        - uuid
        - name
      description: |
        Room resource. By default, only mandatory fields (uuid, name) are returned.
        Additional fields can be requested using the `fields` query parameter.

        Example: `?fields=type,control_cohort,alias,description`

        Related resources can be included using the `include` parameter.

        Example: `?include=zone,floor,hub,devices,tags`
      properties:
        uuid:
          type: string
          format: uuid
          description: 'Unique identifier for the room'
        name:
          type: string
          description: 'Room name'
        alias:
          type:
            - string
            - 'null'
          description: 'Custom name for the room (fields=alias)'
        type:
          type: string
          description: |
            Room type identifier (fields=type).
            The API appends `-control_cohort` when `metadata.control_cohort` is truthy:
            e.g. `room_double-control_cohort`.
            * `common` - Common areas like lobbies, halls, etc.
            * `room_double` - Double occupancy room
            * `room_single` - Single occupancy room
        control_cohort:
          type: boolean
          description: |
            Control cohort flag (fields=control_cohort).
            Stored in `metadata.control_cohort`.
        description:
          type:
            - string
            - 'null'
          description: 'Detailed description of the room (fields=description)'
        capacity:
          type:
            - integer
            - 'null'
          description: 'Maximum capacity of the room (fields=capacity)'
        area:
          type:
            - number
            - 'null'
          format: float
          description: 'Room area in square meters (fields=area)'
        is_active:
          type: boolean
          description: 'Indicates if the room is currently active (fields=is_active)'
        metadata:
          type:
            - object
            - 'null'
          description: |
            Additional metadata for the room (fields=metadata).
            Includes `control_cohort` when configured.
            También puede incluir:
            - `fan_coil_profile.*` como inputs físicos del equipo
            - `room_energy_model.*` como contexto geométrico y operativo
            - `thermostat_energy_coefficients.*` como snapshot persistido de calibraciones manuales o derivadas
            - `room_state_energy_baselines.electrical_kwh_15m` como campo heredado de compatibilidad; no se usa en el ahorro automático actual
        fan_coil_model:
          $ref: '#/components/schemas/FanCoilModel'
        fan_coil_profile:
          anyOf:
            -
              $ref: '#/components/schemas/FanCoilProfile'
              description: 'Vista normalizada del perfil físico del equipo (fields=fan_coil_profile).'
            -
              type: 'null'
        room_energy_model:
          anyOf:
            -
              $ref: '#/components/schemas/RoomEnergyModelInputs'
              description: 'Vista normalizada de los inputs energéticos de la habitación (fields=room_energy_model).'
            -
              type: 'null'
        energy_model:
          anyOf:
            -
              $ref: '#/components/schemas/RoomEnergyModel'
              description: |
                Resumen calculado del modelo energético (fields=energy_model).
                Expone coeficientes derivados, coeficientes efectivos, fuente aplicada y factores usados.
            -
              type: 'null'
        position:
          type:
            - object
            - 'null'
          description: 'Room position coordinates (fields=position)'
        installer_status:
          type: string
          enum:
            - pending
            - in_progress
            - completed
            - error
          description: 'Estado global de commissioning de la habitación (fields=installer_status). En `/installer/*` se incluye por defecto.'
        installer_progress:
          type: integer
          minimum: 0
          maximum: 100
          description: 'Progreso agregado de instalación de la habitación en porcentaje (fields=installer_progress). En `/installer/*` se incluye por defecto.'
        installer_started_at:
          type:
            - string
            - 'null'
          format: date-time
          description: 'Fecha/hora en la que se inició la instalación (fields=installer_started_at). En `/installer/*` se incluye por defecto.'
        installer_last_update_at:
          type:
            - string
            - 'null'
          format: date-time
          description: 'Última actualización de estado en modo instalador (fields=installer_last_update_at). En `/installer/*` se incluye por defecto.'
        installer_updated_by:
          type:
            - string
            - 'null'
          description: 'Usuario que realizó la última actualización de la habitación en formato legible (`Nombre Apellido - email@dominio`) cuando está disponible (fields=installer_updated_by). En `/installer/*` se incluye por defecto.'
        installer_notes:
          type:
            - array
            - 'null'
          description: 'Histórico de notas del instalador sobre la habitación con UUID, autor relacional y fecha de última edición (fields=installer_notes). En `/installer/*` se incluye por defecto.'
          items:
            $ref: '#/components/schemas/InstallerNote'
        zone:
          anyOf:
            -
              $ref: '#/components/schemas/Zone'
              description: 'Associated zone information (include=zone)'
            -
              type: 'null'
        floor:
          anyOf:
            -
              $ref: '#/components/schemas/Floor'
              description: 'Associated floor information (include=floor)'
            -
              type: 'null'
        hub:
          anyOf:
            -
              $ref: '#/components/schemas/Hub'
              description: 'Associated hub information (include=hub)'
            -
              type: 'null'
        devices:
          type:
            - array
            - 'null'
          items:
            $ref: '#/components/schemas/Device'
          description: 'Associated devices (include=devices)'
        tags:
          type:
            - array
            - 'null'
          items:
            $ref: '#/components/schemas/RoomTag'
          description: 'Manual room grouping tags scoped to the building (include=tags)'
      examples:
        -
          uuid: 123e4567-e89b-12d3-a456-426614174000
          name: '101'
        -
          uuid: 123e4567-e89b-12d3-a456-426614174000
          name: 101-control
          type: room_double-control_cohort
          control_cohort: true
          alias: 'Suite Principal'
          description: 'Habitación doble con vista al mar'
          fan_coil_profile:
            heating_kw_nominal: 2.8
            cooling_kw_nominal: 2.4
            airflow_low_m3h: 220
            airflow_medium_m3h: 350
            airflow_high_m3h: 500
            airflow_auto_m3h: 375
            temperature_strategy: supply_air_by_fan_mode
            supply_air_temp_cool_low_c: 14
            supply_air_temp_cool_medium_c: 12
            supply_air_temp_cool_high_c: 10
            supply_air_temp_heat_low_c: 32
            supply_air_temp_heat_medium_c: 38
            supply_air_temp_heat_high_c: 42
            delta_t_heat_air_c: 10
            delta_t_cool_air_c: 8
            demand_deadband_c: 0.5
            water_system_type: 2pipe
            valve_type: modulating
            installation_factor: 1.0
          room_energy_model:
            room_area_m2: 24
            ceiling_height_m: 2.6
            facade_exposure: medium
            insulation_level: medium
            window_area_m2: 3.5
            orientation: south
            usage_profile: hotel_standard
          energy_model:
            derived_coefficients:
              heat: 0.8
              cool: 0.8
            effective_coefficients:
              heat: 0.8
              cool: 0.8
            coefficient_source:
              heat: derived
              cool: derived
            source: derived
          tags:
            -
              uuid: 123e4567-e89b-12d3-a456-426614174999
              name: 'Bloque A'
              slug: bloque-a
              color: '#2F6FED'
          installer_notes:
            -
              message: 'Habitación revisada'
              uuid: 0195e5fd-8946-7f1c-8df1-4487f3823a1f
              created_at: '2026-04-01T09:45:00Z'
              created_by: 'Juan Pérez - installer.juan@voltic.es'
              created_by_user:
                id: 121
                email: installer.juan@voltic.es
                name: 'Juan Pérez'
              modified_at: null
    RoomEnergyModel:
      type: object
      description: |
        Presentación de compatibilidad: los coeficientes derivados/efectivos son legado v1 y NO
        se aplican a runtime v2. active_model contiene la configuración física efectiva.
        Consulte docs/thermal-coil-model.md para herencia, fórmulas, respaldos y regeneración.
      required:
        - derived_coefficients
        - effective_coefficients
        - coefficient_source
        - source
        - factor_breakdown
        - assumptions
        - stored_coefficients
      properties:
        model_version:
          type: integer
          enum:
            - 2
        legacy_coefficients_applied:
          type: boolean
          enum:
            - false
        active_model:
          $ref: '#/components/schemas/FanCoilModel'
        derived_coefficients:
          type: object
          required:
            - heat
            - cool
          properties:
            heat:
              type:
                - number
                - 'null'
              format: float
              example: 0.8
            cool:
              type:
                - number
                - 'null'
              format: float
              example: 0.8
        effective_coefficients:
          type: object
          required:
            - heat
            - cool
          properties:
            heat:
              type: number
              format: float
              example: 0.8
            cool:
              type: number
              format: float
              example: 0.8
        coefficient_source:
          type: object
          required:
            - heat
            - cool
          properties:
            heat:
              type: string
              enum:
                - manual
                - derived
                - building
                - default
              example: derived
            cool:
              type: string
              enum:
                - manual
                - derived
                - building
                - default
              example: derived
        source:
          type: string
          enum:
            - manual
            - derived
            - building
            - default
            - mixed
          description: 'Resumen agregado de `coefficient_source`.'
          example: derived
        factor_breakdown:
          type: object
          required:
            - heat
            - cool
          properties:
            heat:
              $ref: '#/components/schemas/RoomEnergyModel/$defs/factorBreakdown'
            cool:
              $ref: '#/components/schemas/RoomEnergyModel/$defs/factorBreakdown'
        assumptions:
          type: array
          description: 'Supuestos y entradas relevantes usados para explicar la estimación.'
          items:
            type: string
          example:
            - airflow_high_m3h=500
            - temperature_strategy=supply_air_by_fan_mode
            - supply_air_temp_cool_medium_c=12.00
            - supply_air_temp_heat_medium_c=38.00
            - installation_factor=1.00
            - 'room_area_m2=24.00 (context_only)'
        stored_coefficients:
          $ref: '#/components/schemas/ThermostatEnergyCoefficientsMetadata'
      $defs:
        factorBreakdown:
          type: object
          properties:
            base_calibration_factor:
              type:
                - number
                - 'null'
              format: float
            installation_factor:
              type:
                - number
                - 'null'
              format: float
            derived_calibration_factor:
              type:
                - number
                - 'null'
              format: float
            temperature_strategy:
              type:
                - string
                - 'null'
            reference_airflow_m3h:
              type:
                - number
                - 'null'
              format: float
            reference_supply_air_temp_c:
              type:
                - number
                - 'null'
              format: float
            fallback_delta_t_air_c:
              type:
                - number
                - 'null'
              format: float
            nominal_kw_cap:
              type:
                - number
                - 'null'
              format: float
            thermal_kw_reference:
              type:
                - number
                - 'null'
              format: float
            demand_deadband_c:
              type:
                - number
                - 'null'
              format: float
    RoomEnergyModelInputs:
      type: object
      description: |
        Inputs geométricos y de contexto operativo de la habitación.
        Se conservan para trazabilidad, segmentación y explicación de la demo, pero no alteran la
        fórmula térmica del fancoil.
      properties:
        room_area_m2:
          type:
            - number
            - 'null'
          format: float
          minimum: 0
          description: 'Superficie útil aproximada de la habitación en m2.'
          example: 24
        ceiling_height_m:
          type:
            - number
            - 'null'
          format: float
          minimum: 0
          description: 'Altura libre aproximada en metros.'
          example: 2.6
        facade_exposure:
          type:
            - string
            - 'null'
          enum:
            - low
            - medium
            - high
            - null
          description: 'Exposición relativa a fachada exterior.'
          example: medium
        insulation_level:
          type:
            - string
            - 'null'
          enum:
            - low
            - medium
            - high
            - null
          description: 'Nivel de aislamiento estimado de la habitación.'
          example: medium
        window_area_m2:
          type:
            - number
            - 'null'
          format: float
          minimum: 0
          description: 'Superficie aproximada de ventana en m2.'
          example: 3.5
        orientation:
          type:
            - string
            - 'null'
          enum:
            - north
            - south
            - east
            - west
            - northeast
            - northwest
            - southeast
            - southwest
            - interior
            - unknown
            - null
          description: 'Orientación principal de la estancia.'
          example: south
        usage_profile:
          type:
            - string
            - 'null'
          enum:
            - hotel_standard
            - hotel_suite
            - office_daytime
            - meeting_room
            - residential
            - retail
            - storage
            - null
          description: 'Perfil de uso operacional esperado.'
          example: hotel_standard
    RoomOccupancyEvent:
      type: object
      required:
        - timestamp
        - occupancy
        - occupancy_state
        - occupancy_confidence
      properties:
        timestamp:
          type: string
          format: date-time
          description: 'Instante exacto en el que este estado comenzó a estar vigente.'
        occupancy:
          oneOf:
            -
              type: boolean
            -
              type: 'null'
          description: '`true` con presencia, `false` sin presencia y `null` si el estado es desconocido.'
        occupancy_state:
          oneOf:
            -
              type: string
              enum:
                - active
                - resting
                - sleeping
                - in_bathroom
                - unknown
            -
              type: 'null'
          description: 'Estado categórico de presencia, cuando está disponible.'
        occupancy_confidence:
          oneOf:
            -
              type: number
              format: float
              minimum: 0
              maximum: 1
            -
              type: 'null'
          description: 'Confianza asociada al estado, cuando está disponible.'
    RoomOccupancyEventsResponse:
      type: object
      required:
        - scope
        - metric
        - timezone
        - from
        - to
        - initial_state
        - events
      properties:
        scope:
          type: object
          required:
            - type
            - uuid
            - name
          properties:
            type:
              type: string
              const: room
            uuid:
              type: string
              format: uuid
            name:
              type: string
        metric:
          type: string
          const: room_occupancy_events
        timezone:
          type: string
          description: 'Timezone IANA del building de la habitación.'
        from:
          type: string
          format: date-time
        to:
          type: string
          format: date-time
        initial_state:
          oneOf:
            -
              $ref: '#/components/schemas/RoomOccupancyEvent'
            -
              type: 'null'
          description: 'Último estado conocido anterior a `from`, o `null` si no existe.'
        events:
          type: array
          description: 'Cambios con `from <= timestamp < to`, ordenados ascendentemente.'
          items:
            $ref: '#/components/schemas/RoomOccupancyEvent'
    RoomTag:
      type: object
      required:
        - uuid
        - name
        - slug
      properties:
        uuid:
          type: string
          format: uuid
          description: 'Unique identifier for the room tag'
        name:
          type: string
          description: 'Display label chosen by the hotelier'
        slug:
          type: string
          description: 'Normalized identifier generated by the backend from the tag name'
        color:
          type:
            - string
            - 'null'
          pattern: '^#[0-9A-Fa-f]{6}$'
          description: 'Optional UI accent color in hexadecimal format'
        rooms_count:
          type:
            - integer
            - 'null'
          description: 'Number of rooms currently linked to the tag when the endpoint preloads that count'
    StateDataPoint:
      type: array
      minItems: 2
      maxItems: 2
      prefixItems:
        -
          type: string
          format: date-time
          description: 'Timestamp en formato ISO8601'
        -
          $ref: '#/components/schemas/StateDataPointValue'
    StateDataPointValue:
      type: object
      description: 'Objeto con los valores de estado en un punto del tiempo.'
      properties:
        ieee:
          type: string
          description: 'El IEEE address del dispositivo que generó el evento.'
        state:
          type: boolean
          description: 'Indica si el termostato está encendido (true) o apagado (false).'
        setpoint:
          type: number
          description: 'Temperatura de consigna en grados Celsius.'
        fan_mode:
          type: string
          description: 'Modo del ventilador.'
          enum:
            - low
            - medium
            - high
            - auto
        system_mode:
          type: string
          description: 'Modo de funcionamiento del sistema.'
          enum:
            - cool
            - heat
            - fan_only
            - auto
        valve:
          type: string
          description: 'Estado de la válvula para termostatos.'
          enum:
            - cool_open
            - heat_open
            - closed
      required:
        - ieee
      additionalProperties: true
      example:
        ieee: '0x583bc2fffe1c8755'
        state: true
        setpoint: 23
        fan_mode: auto
        system_mode: cool
        valve: cool_open
    StateResponse:
      type: object
      properties:
        scope:
          type: object
          properties:
            type:
              type: string
              enum:
                - building
                - floor
                - room
                - device
            uuid:
              type: string
              description: 'UUID interno del recurso'
            name:
              type: string
            ieee_address:
              type:
                - string
                - 'null'
            connection_type:
              type:
                - string
                - 'null'
        metric:
          type: string
          example: thermostat_state
        unit:
          type:
            - string
            - 'null'
          example: state
        interval:
          type: string
          description: 'Para métricas de estado, el intervalo es siempre "event".'
          example: event
        stat:
          type:
            - array
            - 'null'
          items:
            type: string
          description: 'Para métricas de estado, no se calculan estadísticas.'
          example: []
        data:
          type: array
          items:
            $ref: '#/components/schemas/StateDataPoint'
    Subscription:
      type: object
      required:
        - uuid
        - type
        - status
        - access
      properties:
        uuid:
          type: string
          format: uuid
        subscription_type_uuid:
          type:
            - string
            - 'null'
          format: uuid
        billing_engine:
          type: string
          enum:
            - winter_stripe
            - odoo_import
          description: 'Indica si la suscripción operativa la orquesta Winter+Stripe o se reconcilió desde Odoo.'
        type:
          type: string
          enum:
            - free
            - paid
        status:
          type: string
          enum:
            - active
            - inactive
          description: 'Estado local de la suscripción. No se convierte automáticamente en `free` cuando falla un cobro.'
        access:
          type: object
          description: |
            Modo efectivo de acceso de la suscripción para la aplicación. Se calcula a partir del plan,
            el estado local y el estado remoto de pago; no implica cambiar el plan contratado.
            Una suscripción `paid` con Stripe `past_due` o `unpaid` permanece en su plan y devuelve
            `read_only`. El frontend debe conservar las superficies de recuperación de pago cuando
            `payment_recovery_available` sea `true`.
          required:
            - mode
            - read_only
            - writes_blocked
            - payment_recovery_available
          properties:
            mode:
              type: string
              enum:
                - full
                - read_only
                - billing_only
                - blocked
              description: |
                `full` permite el plan normal; `read_only` conserva consultas y telemetría de lectura
                pero bloquea escrituras; `billing_only` conserva las lecturas de contexto y MQTT incluidas
                en el plan, además de la consulta y recuperación de la suscripción, pero bloquea
                escrituras y demás superficies, aunque permite tickets básicos de soporte; `blocked` no
                tiene una superficie operativa o de recuperación válida.
            read_only:
              type: boolean
              description: 'True cuando el modo efectivo es `read_only` o `billing_only`.'
            writes_blocked:
              type: boolean
              description: 'True para `read_only` y `billing_only`.'
            payment_recovery_available:
              type: boolean
              description: 'Indica que el cliente puede actualizar el pago, cambiar de plan o cancelar.'
        stripe_subscription_status:
          type:
            - string
            - 'null'
          description: |
            Estado devuelto por Stripe (`trialing`, `active`, `past_due`, `unpaid`, `canceled`,
            `incomplete`, `incomplete_expired`, etc.). `past_due` y `unpaid` producen `access.mode=read_only`
            cuando existe una relación de pago `paid`.
        stripe_customer_id:
          type: string
        stripe_payment_method_id:
          type: string
        auto_renew:
          type: boolean
        start_date:
          type: string
          format: date-time
        end_date:
          type:
            - string
            - 'null'
          format: date-time
        cancel_at:
          type:
            - string
            - 'null'
          format: date-time
        canceled_at:
          type:
            - string
            - 'null'
          format: date-time
        odoo_partner_id:
          type:
            - integer
            - 'null'
        odoo_subscription_id:
          type:
            - integer
            - 'null'
        odoo_invoice_id:
          type:
            - integer
            - 'null'
        billing_metadata:
          type: object
          additionalProperties: true
        items:
          type: array
          description: 'Productos recurrentes activos o pendientes incluidos en la suscripción.'
          items:
            type: object
            required:
              - uuid
              - service_family
              - status
              - quantity
              - unit_amount_minor
              - amount_minor
            properties:
              uuid:
                type: string
                format: uuid
              subscription_type_uuid:
                type: string
                format: uuid
              code:
                type: string
              name:
                type: string
              service_family:
                type: string
                example: cooling_patrol
              status:
                type: string
                enum:
                  - pending
                  - active
                  - inactive
                  - canceled
              pricing_mode:
                type: string
                enum:
                  - flat
                  - per_room
                  - per_sensor
              quantity:
                type: integer
              unit_amount_minor:
                type: integer
              amount_minor:
                type: integer
              currency_code:
                type: string
              interval:
                type: string
                enum:
                  - month
                  - year
              interval_count:
                type: integer
              odoo_sale_order_line_id:
                type:
                  - integer
                  - 'null'
        pending_upsell:
          type:
            - object
            - 'null'
          description: 'Ampliación prorrateada pendiente de cobro o sincronización.'
          properties:
            odoo_order_id:
              type: integer
            reference:
              type: string
            amount_minor:
              type: integer
            currency_code:
              type: string
            status:
              type: string
        subscription_type:
          $ref: '#/components/schemas/SubscriptionType'
        company:
          anyOf:
            -
              allOf:
                -
                  $ref: '#/components/schemas/Company'
              description: 'Compañía asociada al building. Se devuelve cuando se solicita `include=company`.'
            -
              type: 'null'
        price_summary:
          type:
            - object
            - 'null'
          properties:
            amount_minor:
              type: integer
            amount:
              type: number
              format: float
            currency_code:
              type: string
            interval:
              type: string
            interval_count:
              type: integer
        sync_state:
          type: object
          properties:
            stripe:
              type: object
              properties:
                customer_id:
                  type:
                    - string
                    - 'null'
                status:
                  type:
                    - string
                    - 'null'
            odoo:
              type: object
              properties:
                partner_id:
                  type:
                    - integer
                    - 'null'
                subscription_id:
                  type:
                    - integer
                    - 'null'
                invoice_id:
                  type:
                    - integer
                    - 'null'
        expiry:
          type: object
          properties:
            effective_end_at:
              type:
                - string
                - 'null'
              format: date-time
            days_until_effective_end:
              type:
                - integer
                - 'null'
            grace_period_days:
              type: integer
            grace_ends_at:
              type:
                - string
                - 'null'
              format: date-time
            days_until_grace_end:
              type:
                - integer
                - 'null'
            is_in_grace_period:
              type: boolean
    SubscriptionReadOnlyError:
      type: object
      additionalProperties: false
      required:
        - error
        - feature
        - message
        - payment_required
        - read_only
        - upgrade_required
      properties:
        error:
          type: string
          enum:
            - subscription_read_only
          description: 'Código estable para distinguir una restricción por impago de un permiso insuficiente.'
        feature:
          type: string
          description: 'Feature cuya operación de escritura se intentó ejecutar.'
          example: bms_rooms_write
        message:
          type: string
          example: 'La suscripción está pendiente de pago. Esta operación de escritura no está disponible hasta regularizar el pago.'
        payment_required:
          type: boolean
          enum:
            - true
        read_only:
          type: boolean
          enum:
            - true
        upgrade_required:
          type: boolean
          enum:
            - false
    SubscriptionType:
      type: object
      required:
        - uuid
        - code
        - name
        - mode
        - is_active
      properties:
        uuid:
          type: string
          format: uuid
        code:
          type: string
        billing_code:
          type: string
          description: 'Código comercial estable usado para enlazar Odoo, Winter y Stripe.'
        service_family:
          type: string
          description: 'Familia mutuamente excluyente dentro de una suscripción, por ejemplo `room_control` o `cooling_patrol`.'
        name:
          type: string
        description:
          type:
            - string
            - 'null'
        mode:
          type: string
          enum:
            - free
            - paid
        catalog_source:
          type: string
          enum:
            - winter
            - odoo
          description: 'Indica si el pricing comercial se edita en Winter o se importa desde Odoo.'
        is_active:
          type: boolean
        billing:
          type:
            - object
            - 'null'
          description: 'Precio único efectivo. Es null cuando debe elegirse una opción de billing_options.'
          properties:
            amount_minor:
              type: integer
            amount:
              type: number
              format: float
            currency_code:
              type: string
            interval:
              type: string
            interval_count:
              type: integer
            trial_days:
              type: integer
            auto_renew_default:
              type: boolean
            auto_send_expiry_reminder:
              type: boolean
        requires_price_selection:
          type: boolean
          description: 'Indica que el checkout debe enviar subscription_price_uuid.'
        billing_settings:
          type: object
          description: 'Configuración general del plan, disponible aunque haya varias opciones de precio.'
          properties:
            trial_days:
              type: integer
            auto_renew_default:
              type: boolean
        billing_options:
          type: array
          description: 'Opciones de periodicidad y precio importadas desde las reglas de suscripción de Odoo.'
          items:
            type: object
            properties:
              uuid:
                type: string
                format: uuid
              name:
                type: string
              amount_minor:
                type: integer
              amount:
                type: number
                format: float
              currency_code:
                type: string
              interval:
                type: string
                enum:
                  - month
                  - year
              interval_count:
                type: integer
        features:
          type: array
          items:
            type: object
            properties:
              code:
                type: string
              name:
                type: string
              description:
                type:
                  - string
                  - 'null'
              surface:
                type:
                  - string
                  - 'null'
        sync_state:
          type: object
          properties:
            odoo:
              type: object
              properties:
                status:
                  type:
                    - string
                    - 'null'
                product_id:
                  type:
                    - string
                    - 'null'
                synced_at:
                  type:
                    - string
                    - 'null'
                  format: date-time
            catalog:
              type: object
              properties:
                last_imported_at:
                  type:
                    - string
                    - 'null'
                  format: date-time
    ThermostatEnergyCoefficientsMetadata:
      type: object
      description: |
        Snapshot persistido de coeficientes térmicos por habitación.

        Reglas:
        - `heat` y `cool` se pueden enviar manualmente en `PUT/PATCH /rooms/{uuid}`.
        - `source` y `updated_at` los controla el backend.
        - cuando no hay override manual y sí hay suficientes inputs físicos, el backend persiste un
          snapshot `source=derived`.
      properties:
        heat:
          type:
            - number
            - 'null'
          format: float
          description: 'Coeficiente de calefacción almacenado para la habitación.'
          example: 0.93
        cool:
          type:
            - number
            - 'null'
          format: float
          description: 'Coeficiente de refrigeración almacenado para la habitación.'
          example: 0.81
        source:
          type:
            - string
            - 'null'
          enum:
            - manual
            - derived
            - mixed
            - null
          description: |
            Procedencia del snapshot persistido. `mixed` puede aparecer cuando existe override manual
            solo en un modo y el otro se resuelve por derivación o fallback.
          example: derived
        updated_at:
          type:
            - string
            - 'null'
          format: date-time
          description: 'Último momento en el que el backend recalculó o sobrescribió el snapshot persistido.'
          example: '2026-06-14T10:30:00Z'
    ThermostatRuntimeDataPoint:
      type: array
      minItems: 2
      maxItems: 2
      prefixItems:
        -
          type: string
          format: date-time
          description: 'Inicio del periodo agregado en formato ISO8601.'
        -
          $ref: '#/components/schemas/ThermostatRuntimeDataPointValue'
    ThermostatRuntimeDataPointValue:
      type: object
      properties:
        sec_heat_open:
          type:
            - number
            - 'null'
          format: float
          description: 'Segundos-equipo activos en calor; en Aidoo son demanda estimada por histéresis ±1 °C; en fan_only se deducen de ventilador y modo configurado.'
        sec_cool_open:
          type:
            - number
            - 'null'
          format: float
          description: 'Segundos-equipo activos en frío; en Aidoo son demanda estimada por histéresis ±1 °C; en fan_only se deducen de ventilador y modo configurado.'
        h_heat_open:
          type:
            - number
            - 'null'
          format: float
          description: 'Horas-equipo en calor: válvula abierta en fancoil o demanda estimada por histéresis ±1 °C en Aidoo.'
        h_cool_open:
          type:
            - number
            - 'null'
          format: float
          description: 'Horas-equipo en frío: válvula abierta en fancoil o demanda estimada por histéresis ±1 °C en Aidoo.'
        degree_hours_heat:
          type:
            - number
            - 'null'
          format: float
          description: 'Degree-hours estimados en calefacción para el periodo.'
        degree_hours_cool:
          type:
            - number
            - 'null'
          format: float
          description: 'Degree-hours estimados en refrigeración para el periodo.'
        kwh_heat_est:
          type:
            - number
            - 'null'
          format: float
          description: 'Estimación de kWh térmicos en calefacción.'
        kwh_cool_est:
          type:
            - number
            - 'null'
          format: float
          description: 'Estimación de kWh térmicos en refrigeración.'
        kwh_total_est:
          type:
            - number
            - 'null'
          format: float
          description: 'Suma estimada de kWh térmicos de calefacción y refrigeración.'
        kwh_sensible_est:
          type:
            - number
            - 'null'
          description: 'kWh sensibles estimados.'
        kwh_latent_est:
          type:
            - number
            - 'null'
          description: 'kWh latentes estimados; pueden usar humedad de respaldo.'
        temperature_assumed_seconds:
          type:
            - number
            - 'null'
          description: 'Segundos activos con temperatura supuesta.'
        humidity_assumed_seconds:
          type:
            - number
            - 'null'
          description: 'Segundos activos con humedad supuesta.'
        speed_assumed_seconds:
          type:
            - number
            - 'null'
          description: 'Segundos activos con velocidad supuesta.'
        state_covered_seconds:
          type:
            - number
            - 'null'
          description: 'Segundos-equipo cubiertos por estados dentro de su vigencia.'
        state_expected_seconds:
          type:
            - number
            - 'null'
          description: 'Segundos-equipo solicitados. Cobertura inferior no implica consumo cero en huecos.'
        h_aidoo_on:
          type:
            - number
            - 'null'
          description: 'Horas-equipo Aidoo habilitado ON.'
        aidoo_demand_covered_seconds:
          type:
            - number
            - 'null'
          description: 'Segundos-equipo Aidoo evaluables, incluyendo OFF conocido.'
        aidoo_demand_unknown_seconds:
          type:
            - number
            - 'null'
          description: 'Segundos-equipo Aidoo sin estado, temperatura o consigna suficientes.'
    ThermostatRuntimeResponse:
      type: object
      required:
        - from
        - to
      properties:
        model_version:
          type: integer
          enum:
            - 2
        energy_basis:
          type: string
          enum:
            - estimated_water_coil_total
        scope:
          type: object
          properties:
            type:
              type: string
              enum:
                - building
                - floor
                - room
                - device
            uuid:
              type: string
              description: 'UUID del scope o IEEE del dispositivo cuando `scope=device`.'
            name:
              type: string
            category:
              type:
                - string
                - 'null'
              description: 'Categoría del dispositivo cuando `scope=device`.'
        metric:
          type: string
          example: thermostat_runtime
        interval:
          type: string
          enum:
            - 15m
            - 1h
            - 4h
            - 1d
            - 1w
            - 1mo
          description: 'Periodo de agregación solicitado.'
        timezone:
          type: string
          description: 'Timezone IANA usada para definir el inicio de día/semana/mes del scope. Si el building no la define, se usa `Europe/Madrid`.'
        calculation_interval:
          type: string
          example: 15m
          description: 'Resolución fija usada internamente para el cálculo antes de agregar.'
        from:
          type: string
          format: date-time
          description: 'Inicio efectivo e inclusivo de la ventana, alineado al bucket y expresado en UTC.'
          example: '2026-06-10T00:00:00.000Z'
        to:
          type: string
          format: date-time
          description: 'Fin efectivo y exclusivo de la ventana, alineado al bucket y expresado en UTC.'
          example: '2026-06-11T00:00:00.000Z'
        units:
          type: object
          description: 'Mapa de unidades por campo del payload.'
          properties:
            sec_heat_open:
              type: string
              example: s
            sec_cool_open:
              type: string
              example: s
            h_heat_open:
              type: string
              example: h
            h_cool_open:
              type: string
              example: h
            degree_hours_heat:
              type: string
              example: 'degC*h'
            degree_hours_cool:
              type: string
              example: 'degC*h'
            kwh_heat_est:
              type: string
              example: kWh
            kwh_cool_est:
              type: string
              example: kWh
            kwh_total_est:
              type: string
              example: kWh
        data:
          type: array
          items:
            $ref: '#/components/schemas/ThermostatRuntimeDataPoint'
        aidoo_runtime_basis:
          type:
            - string
            - 'null'
          description: 'estimated_indoor_thermal_demand_hysteresis_1c cuando se incluyen dispositivos Aidoo.'
    TimeSeriesDataPoint:
      type: array
      minItems: 2
      maxItems: 2
      prefixItems:
        -
          type: string
          format: date-time
          description: 'Timestamp en formato ISO8601 que identifica el inicio del bucket devuelto.'
        -
          $ref: '#/components/schemas/TimeSeriesDataPointValue'
    TimeSeriesDataPointValue:
      anyOf:
        -
          type: object
          description: 'Valor simple del bucket. En `energy`, `value` representa consumo dentro del bucket o periodo agregado, nunca el valor bruto del contador acumulado. Cuando se solicita `enrich`, el mismo objeto puede añadir los campos de coste, CO₂ y ajustes eléctricos descritos a continuación.'
          properties:
            value:
              type: number
              format: float
            metered_value:
              type: number
              format: float
              description: 'Consumo medido original del bucket, antes de ajustes eléctricos opcionales.'
            electrical_adjustment_value:
              type: number
              format: float
              description: 'Cargas eléctricas opcionales añadidas al bucket.'
            cost_variable:
              type: number
              format: float
              description: 'Coste variable del bucket en la moneda indicada por `derived_units.cost_variable`.'
            cost_fixed:
              type: number
              format: float
              description: 'Coste fijo prorrateado del bucket en la moneda indicada por `derived_units.cost_fixed`.'
            cost_total:
              type: number
              format: float
              description: 'Coste total del bucket en la moneda indicada por `derived_units.cost_total`.'
            co2_tonnes:
              type: number
              format: float
              description: 'Emisiones del consumo real del bucket: `value` multiplicado por el factor real del periodo, en toneladas de CO₂e.'
            co2_baseline_tonnes:
              type:
                - number
                - 'null'
              format: float
              description: 'Emisiones hipotéticas del mismo consumo usando el factor baseline del periodo, en toneladas de CO₂e; `null` si falta.'
            co2_avoided_tonnes:
              type:
                - number
                - 'null'
              format: float
              description: 'Diferencia `co2_baseline_tonnes - co2_tonnes`; positiva si el factor baseline es mayor, cero si coinciden, negativa si el real es mayor. Compara factores sobre el mismo consumo, no representa el ahorro energético de Voltic y es `null` si falta baseline.'
        -
          type: object
          description: 'Valor OHLC del bucket para métricas complejas o agregadas como potencia, tensión o corriente.'
          properties:
            open:
              type: number
              format: float
            high:
              type: number
              format: float
            low:
              type: number
              format: float
            close:
              type: number
              format: float
            avg:
              type: number
              format: float
        -
          type: object
          description: 'Objeto abierto para otras respuestas agregadas o métricas con estructura específica.'
          additionalProperties: true
    TimeSeriesResponse:
      type: object
      description: |
        Respuesta de una serie temporal. Cuando `metric=energy` usa `enrich`, los totales del rango se exponen directamente en la raíz de la respuesta —no dentro de una propiedad `summary`— junto con `data` y `derived_units`.
      properties:
        scope:
          type: object
          properties:
            type:
              type: string
              enum:
                - building
                - floor
                - room
                - device
            uuid:
              type: string
              description: 'UUID interno del recurso.'
            name:
              type: string
            category:
              type:
                - string
                - 'null'
              description: 'La categoría del dispositivo (e.g., "thermostat", "switch"). Solo presente si el scope es "device".'
            ieee_address:
              type:
                - string
                - 'null'
              description: 'Identificador físico del dispositivo. Solo presente si el scope es "device".'
            connection_type:
              type:
                - string
                - 'null'
              description: 'Tipo de conexión del dispositivo. Solo presente si el scope es "device".'
        metric:
          type: string
          description: 'Métrica resuelta por la API. En `device`, nombres genéricos como `energy` o `power` pueden mapear a tablas distintas según la `category` del dispositivo.'
        unit:
          type:
            - string
            - 'null'
        interval:
          type: string
          enum:
            - 15m
            - 1h
            - 4h
            - 1d
            - 1w
            - 1mo
            - event
          description: |
            Bucket final devuelto en la respuesta. Para métricas de series temporales admite `15m`, `1h`, `4h`, `1d`, `1w` y `1mo`; para métricas de estado siempre es `event`. El cálculo interno de métricas acumulativas usa siempre una lectura semilla previa cuando hace falta obtener el primer delta del rango.
        timezone:
          type: string
          description: |
            Timezone IANA usada para alinear buckets diarios, semanales y mensuales. Los timestamps de `data` se devuelven siempre en UTC con sufijo `Z`; si el building no define timezone, se usa `Europe/Madrid` para la alineación local.
        stat:
          type:
            - array
            - 'null'
          description: 'Estadísticos aplicados al bucket devuelto. En `energy` suele ser `["sum"]`, porque la respuesta representa consumo del periodo. En métricas OHLC suele ser `["ohlc"]`.'
          items:
            type: string
        derived_units:
          type:
            - object
            - 'null'
          description: |
            Unidades derivadas opcionales que acompañan a respuestas enriquecidas. En `energy?enrich=electrical_adjustments` puede incluir `kWh` para `metered_value` y `electrical_adjustment_value`; con `cost` añade moneda para `cost_*` y con `co2`, `tCO2e` para `co2_*`.
          additionalProperties:
            type: string
        from:
          type:
            - string
            - 'null'
          format: date-time
          description: |
            Inicio real del rango cubierto por la respuesta enriquecida. Solo aparece cuando `energy` usa `enrich`.
        to:
          type:
            - string
            - 'null'
          format: date-time
          description: |
            Fin real del rango cubierto por la respuesta enriquecida. Solo aparece cuando `energy` usa `enrich`.
        kwh_total:
          type:
            - number
            - 'null'
          format: float
          description: 'Consumo total agregado del rango enriquecido, en kWh.'
        kwh_metered_total:
          type:
            - number
            - 'null'
          format: float
          description: 'Suma del consumo medido original del rango enriquecido, antes de ajustes opcionales.'
        kwh_adjustment_total:
          type:
            - number
            - 'null'
          format: float
          description: 'Suma de cargas eléctricas opcionales añadidas al rango enriquecido.'
        currency_code:
          type:
            - string
            - 'null'
          description: 'Código ISO 4217 usado para los totales de coste.'
        cost_variable_total:
          type:
            - number
            - 'null'
          format: float
          description: 'Suma del coste variable del rango enriquecido.'
        cost_fixed_total:
          type:
            - number
            - 'null'
          format: float
          description: 'Suma del coste fijo prorrateado del rango enriquecido.'
        cost_total:
          type:
            - number
            - 'null'
          format: float
          description: 'Suma total de coste del rango enriquecido.'
        co2_tonnes_total:
          type:
            - number
            - 'null'
          format: float
          description: 'Emisiones del consumo real agregado del rango; cada bucket usa su factor real vigente, en tCO2e.'
        co2_baseline_tonnes_total:
          type:
            - number
            - 'null'
          format: float
          description: 'Emisiones hipotéticas del mismo consumo agregado usando los factores baseline de cada bucket, en tCO2e.'
        co2_avoided_tonnes_total:
          type:
            - number
            - 'null'
          format: float
          description: 'Diferencia baseline menos real sobre el mismo consumo; puede ser positiva, cero o negativa y no es el ahorro de energía atribuido a Voltic, en tCO2e.'
        tariff_breakdown:
          type:
            - array
            - 'null'
          description: |
            Desglose del coste aplicado por periodo tarifario y, cuando corresponde, por banda horaria TOU.
          items:
            type: object
            properties:
              period_label:
                type: string
              period_valid_from:
                type:
                  - string
                  - 'null'
                format: date
              period_valid_to:
                type:
                  - string
                  - 'null'
                format: date
              pricing_model:
                type:
                  - string
                  - 'null'
              billing_unit:
                type:
                  - string
                  - 'null'
              flat_rate:
                type:
                  - number
                  - 'null'
                format: float
              kwh_total:
                type: number
                format: float
              cost_variable_total:
                type: number
                format: float
              cost_fixed_total:
                type: number
                format: float
              cost_total:
                type: number
                format: float
              band_breakdown:
                type:
                  - array
                  - 'null'
                items:
                  type: object
                  properties:
                    band_code:
                      type: string
                    band_label:
                      type:
                        - string
                        - 'null'
                    rate:
                      type: number
                      format: float
                    kwh_total:
                      type: number
                      format: float
                    cost_variable_total:
                      type: number
                      format: float
        data:
          type: array
          items:
            $ref: '#/components/schemas/TimeSeriesDataPoint'
    Zone:
      type: object
      required:
        - name
      properties:
        uuid:
          type: string
          format: uuid
        name:
          type: string
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      description: |
        API token Bearer emitido por Voltic para una cuenta autorizada y los edificios acordados.
        Envía `Authorization: Bearer <token>`; solicita el alta y los permisos a Voltic.
        Se mantienen las comprobaciones de pertenencia, rol, ámbito del token y features.
        El catálogo no publica el login interno ni crea un perfil nuevo de credenciales.
x-voltic-source-revision: 'sha256:ce3843d81999841c0e7b67a9cc5eee0bec0007bfca48f344585432fa5b40f867'
