asyncapi: 3.0.0

# Source-of-truth Zod schemas (KEEP IN SYNC):
#   - packages/shared/src/schemas/vcp.ts
#       VcpMessageEnvelopeSchema   — common envelope (version, messageId, correlationId,
#                                    timestamp, source, siteId, payload)
#       CanonicalTelemetrySchema   — realtime telemetry payload (10 s cadence)
#       DeviceTelemetrySchema      — per-device sub-object inside CanonicalTelemetry
#       MeterReadingPayloadSchema  — 1-min meter-register payload
#       MeterEntrySchema           — per-meter sub-object inside MeterReading
#       AlarmPayloadSchema         — alarm event payload
#       CommandAckPayloadSchema    — command ACK/NACK payload (status queue)
#       CommandResultSchema        — per-device result sub-object inside CommandAck
#       (CommandStatusPayload has no Zod schema; types/vcp.ts interface is SoT)
#   - packages/shared/src/types/vcp.ts
#       VcpMessage<T>              — TypeScript interface for the envelope (mirrors Zod schema)
#       CommandStatusPayload       — execution status payload (no Zod schema — interface only)
#   - packages/shared/src/types/enums.ts
#       VcpAlarmSeverity, VcpAlarmCode, RejectionCode, OperatingMode
#   - packages/shared/src/types/vcp.ts (enums section)
#       AssetType
#
# When any of the above schemas/interfaces change, update this YAML in lockstep.

info:
  title: Voke Partner AMQP Stream
  version: '1.1'
  description: |
    Partner-facing event stream over AMQPS. Each organisation has its own
    queue prefix (`vcp.{orgSlug}.event.*`). Connect to:

      amqps://<orgSlug>:<apiKey>@amqp.voke.turena.cz:5671/partner-<apiKeyId>

    Every partner API key is provisioned with its own dedicated vhost named
    `partner-{apiKeyId}` — the connection URI from your bundle already embeds it.
    The broker is TLS 1.3 only.

    **Auth:** username = org slug, password = plaintext API key secret
    (must carry the `vcp:connect` scope). Auth is delegated by RabbitMQ to
    the Voke API via the `rabbitmq_auth_backend_http` plugin.

    **Durability:** all queues are `durable: true`; all messages are
    published `persistent: true` (delivery mode 2).

    **ACK discipline (outbound queues):** `ack` after successful processing;
    `nack(requeue=false)` on schema validation failures so malformed messages
    route to the dead-letter queue rather than cycling.

servers:
  production:
    host: amqp.voke.turena.cz:5671
    protocol: amqps
    description: |
      Production AMQPS endpoint (TLS terminated, port 5671).
      Use the fully-assembled URI from the Connect Partner wizard in the
      Voke admin UI rather than reconstructing it manually.

defaultContentType: application/json

channels:
  telemetry:
    address: 'vcp.{orgSlug}.event.telemetry'
    description: |
      Outbound channel — Voke publishes, partner consumes.

      Carries two message types on distinct routing keys that both bind to
      the default queue. Each key ends with the site UUID so a partner can
      subscribe per PLC:
        - `{orgSlug}.event.telemetry.realtime.{siteId}` — 10-second canonical
          snapshot of grid/FVE/BESS/consumption power and state-of-charge.
        - `{orgSlug}.event.telemetry.meter.{siteId}` — 1-minute absolute
          register reading from physical meters (replaces the v1.0 15-minute
          IntervalTelemetry path).

      Choosing granularity at bind time (in your own per-key vhost, on the
      `vcp` topic exchange):
        - `…telemetry.realtime.#` — all sites on one queue (the default
          queue is bound this way, so doing nothing keeps today's firehose).
        - `…telemetry.realtime.{siteId}` — exactly one PLC.
        - bind `…realtime.{A}` + `…realtime.{B}` on one queue — a chosen
          subset.

      `siteId` is the routing discriminator because it is stable and present
      on every message; the EAN is NOT in the routing key (it is nullable,
      mutable, and absent on realtime). Build the EAN → siteId map once from
      `GET /vcp/sites` and bind siteIds. Partners that only need accounting
      data consume the `.meter.` subkey; those needing control feedback also
      handle `.realtime.`.
    parameters:
      orgSlug:
        description: The organisation slug (matches the API-key username).
    messages:
      realtimeTelemetry:
        $ref: '#/components/messages/RealtimeTelemetry'
      meterReading:
        $ref: '#/components/messages/MeterReading'

  alarm:
    address: 'vcp.{orgSlug}.event.alarm'
    description: |
      Outbound channel — Voke publishes, partner consumes.

      Carries alarm events from PLC/EMS devices. Routing key:
      `{orgSlug}.event.alarm` (multi-word wildcard `#` binding, so future
      subtypes land in the same queue without a rebind).

      Both active alarms (`clearedAt` absent) and cleared alarms
      (`clearedAt` set) flow through this channel.
    parameters:
      orgSlug:
        description: The organisation slug.
    messages:
      alarmEvent:
        $ref: '#/components/messages/AlarmEvent'

  execution:
    address: 'vcp.{orgSlug}.event.execution'
    description: |
      Outbound channel — Voke publishes, partner consumes.

      Carries mid-flight execution status updates for dispatched commands
      (setpoints, device commands, operating-mode changes). Routing key:
      `{orgSlug}.event.execution` (multi-word wildcard `#` binding).

      This channel reports *what the site is actually doing* (EXECUTING,
      COMPLETED, DEVIATED, FAILED). The initial synchronous ACK/NACK
      lives on the `event.status` channel instead.
    parameters:
      orgSlug:
        description: The organisation slug.
    messages:
      commandExecution:
        $ref: '#/components/messages/CommandExecution'

  status:
    address: 'vcp.{orgSlug}.event.status'
    description: |
      Outbound channel — Voke publishes, partner consumes.

      Carries command acknowledgements (ACK / NACK / PARTIAL / QUEUED).
      Currently the active routing key is:
        - `{orgSlug}.event.command.ack` — command ACK/NACK

      The binding uses single-word wildcard subtypes (`event.command.*`,
      `event.mode.*`, `event.schedule.*`) so future mode-change and
      schedule-status events land here automatically.
    parameters:
      orgSlug:
        description: The organisation slug.
    messages:
      commandAck:
        $ref: '#/components/messages/CommandAck'

operations:
  receiveRealtimeTelemetry:
    action: receive
    channel: { $ref: '#/channels/telemetry' }
    messages:
      - $ref: '#/channels/telemetry/messages/realtimeTelemetry'
    description: >-
      Subscribe to 10-second real-time telemetry snapshots. Routing key
      `{orgSlug}.event.telemetry.realtime.{siteId}` — bind `…realtime.#`
      for all sites or `…realtime.{siteId}` for one PLC.

  receiveMeterReading:
    action: receive
    channel: { $ref: '#/channels/telemetry' }
    messages:
      - $ref: '#/channels/telemetry/messages/meterReading'
    description: >-
      Subscribe to 1-minute absolute meter-register readings. Routing key
      `{orgSlug}.event.telemetry.meter.{siteId}` — bind `…meter.#` for all
      sites or `…meter.{siteId}` for one PLC.

  receiveAlarm:
    action: receive
    channel: { $ref: '#/channels/alarm' }
    description: Subscribe to alarm events (active and cleared).

  receiveExecution:
    action: receive
    channel: { $ref: '#/channels/execution' }
    description: Subscribe to mid-flight command execution status updates.

  receiveStatus:
    action: receive
    channel: { $ref: '#/channels/status' }
    description: Subscribe to command ACK/NACK and future status events.

components:
  messages:
    RealtimeTelemetry:
      name: RealtimeTelemetry
      title: Realtime Telemetry (10s)
      summary: 10-second canonical power snapshot for a site.
      contentType: application/json
      headers:
        type: object
        required: [vcp-version]
        properties:
          vcp-version:
            type: string
            enum: ['1.1']
            description: VCP protocol version. Always '1.1'.
          adapter-type:
            type: string
            description: Optional adapter type tag set by the publishing adapter.
          org-id:
            type: string
            description: Optional organisation UUID set by the publisher.
      payload:
        $ref: '#/components/schemas/RealtimeTelemetryEnvelope'

    MeterReading:
      name: MeterReading
      title: Meter Reading (1 min)
      summary: 1-minute absolute meter-register snapshot for a site.
      contentType: application/json
      headers:
        type: object
        required: [vcp-version]
        properties:
          vcp-version:
            type: string
            enum: ['1.1']
            description: VCP protocol version. Always '1.1'.
      payload:
        $ref: '#/components/schemas/MeterReadingEnvelope'

    AlarmEvent:
      name: AlarmEvent
      title: Alarm Event
      summary: Device or control-loop alarm (active or cleared).
      contentType: application/json
      headers:
        type: object
        required: [vcp-version]
        properties:
          vcp-version:
            type: string
            enum: ['1.1']
            description: VCP protocol version. Always '1.1'.
      payload:
        $ref: '#/components/schemas/AlarmEventEnvelope'

    CommandExecution:
      name: CommandExecution
      title: Command Execution Status
      summary: Mid-flight execution status for a dispatched command.
      contentType: application/json
      headers:
        type: object
        required: [vcp-version]
        properties:
          vcp-version:
            type: string
            enum: ['1.1']
            description: VCP protocol version. Always '1.1'.
      payload:
        $ref: '#/components/schemas/CommandExecutionEnvelope'

    CommandAck:
      name: CommandAck
      title: Command ACK / NACK
      summary: Synchronous acknowledgement or rejection for a partner-submitted command.
      contentType: application/json
      headers:
        type: object
        required: [vcp-version]
        properties:
          vcp-version:
            type: string
            enum: ['1.1']
            description: VCP protocol version. Always '1.1'.
      payload:
        $ref: '#/components/schemas/CommandAckEnvelope'

  schemas:
    # ─── Common envelope ──────────────────────────────────────────────────────
    #
    # Source: VcpMessageEnvelopeSchema in packages/shared/src/schemas/vcp.ts
    #         and VcpMessage<T> interface in packages/shared/src/types/vcp.ts
    #
    # Note: the plan draft listed `orgId` and `signature` as envelope fields.
    # The actual Zod schema and TypeScript interface do NOT include either.
    # `orgId` is encoded in the routing key / queue prefix, not in the envelope.
    # `signature` lives in the AMQP message properties layer (HMAC over the
    # serialised payload), not as a JSON field — see the VCP signing docs.
    # The `schemaVersion` field from the plan draft is named `version` in the
    # real schema (z.literal('1.1')).
    VcpEnvelope:
      type: object
      description: |
        Common wrapper around every VCP message. All fields except
        `correlationId` are required.
      required:
        - version
        - messageId
        - timestamp
        - source
        - siteId
        - payload
      properties:
        version:
          type: string
          enum: ['1.1']
          description: VCP protocol version. Currently pinned to '1.1'.
          example: '1.1'
        messageId:
          type: string
          description: >
            UUID v4 generated by the publisher for this specific message.
            Use for idempotency / deduplication.
          example: '550e8400-e29b-41d4-a716-446655440000'
        correlationId:
          type: string
          description: >
            Optional. Echoes the `correlationId` from the originating inbound
            command message, enabling request-response correlation. Present on
            command ACK and execution status messages; absent on telemetry and
            alarm events.
        timestamp:
          type: string
          format: date-time
          description: >
            ISO-8601 UTC timestamp of when the message was created by Voke.
          example: '2024-01-15T10:30:00.000Z'
        source:
          type: string
          description: >
            Publisher identifier. Always 'voke' for messages originating from
            the Voke platform.
          example: 'voke'
        siteId:
          type: string
          description: >
            Voke site (plant) UUID. For command ACK and execution status
            messages that are not site-scoped, this is '_system'.
          example: 'd290f1ee-6c54-4b01-90e6-d701748f0851'
        payload:
          description: Message-type-specific payload. See the concrete envelope schemas.

    # ─── Realtime Telemetry ───────────────────────────────────────────────────
    #
    # Source: CanonicalTelemetrySchema + DeviceTelemetrySchema
    #         in packages/shared/src/schemas/vcp.ts
    #         (mirrors CanonicalTelemetry + DeviceTelemetry in types/vcp.ts)
    #
    # Published every ~10 seconds per site. Routing key:
    #   {orgSlug}.event.telemetry.realtime.{siteId}
    # Bind `…realtime.#` for all sites or `…realtime.{siteId}` for one PLC.
    RealtimeTelemetryEnvelope:
      allOf:
        - $ref: '#/components/schemas/VcpEnvelope'
        - type: object
          properties:
            payload:
              $ref: '#/components/schemas/CanonicalTelemetryPayload'

    CanonicalTelemetryPayload:
      type: object
      description: |
        Canonical real-time power snapshot. All top-level power/energy
        fields are nullable — a null means the value is unavailable for
        this site at this moment (e.g. meter offline).
      required:
        - gridPowerKw
        - fvePowerKw
        - batteryPowerKw
        - consumptionPowerKw
        - socPercent
        - availableBatteryEnergyKwh
        - batteryTemperatureCelsius
        - currentOperatingMode
        - dataQuality
      properties:
        gridPowerKw:
          type: number
          nullable: true
          description: >
            Grid power in kW. Positive = import (consuming from grid),
            negative = export (injecting to grid).
        fvePowerKw:
          type: number
          nullable: true
          description: PV/FVE generation power in kW (always ≥ 0 when not null).
        batteryPowerKw:
          type: number
          nullable: true
          description: >
            Battery power in kW. Positive = charging, negative = discharging.
        consumptionPowerKw:
          type: number
          nullable: true
          description: Total site consumption power in kW.
        socPercent:
          type: number
          nullable: true
          description: Battery state-of-charge as a percentage (0–100).
        availableBatteryEnergyKwh:
          type: number
          nullable: true
          description: Available battery energy in kWh at current SOC.
        batteryTemperatureCelsius:
          type: number
          nullable: true
          description: Battery temperature in degrees Celsius.
        currentOperatingMode:
          type: string
          enum:
            - STANDARD
            - ZERO_EXPORT
            - MAX_EXPORT
            - PEAK_SHAVING
            - LOCAL_OPTIMIZATION
            - GRID_TARGET
            - LDS_SUPPORT
          description: >
            The operating mode currently active at the site.
            Source: OperatingMode enum in packages/shared/src/types/enums.ts.
        dataQuality:
          type: string
          enum:
            - GOOD
            - INTERPOLATED
            - STALE
            - MISSING
          description: >
            Quality indicator for this snapshot. GOOD = fresh measurement;
            INTERPOLATED = gap-filled; STALE = last known value repeated;
            MISSING = no data available.
        devices:
          type: array
          description: >
            Optional per-device breakdown. May be omitted when the adapter
            does not expose per-device granularity.
          items:
            $ref: '#/components/schemas/DeviceTelemetry'

    DeviceTelemetry:
      type: object
      description: Per-asset telemetry sub-object within a realtime snapshot.
      required:
        - deviceId
        - assetType
        - powerKw
      properties:
        deviceId:
          type: string
          minLength: 1
          description: Opaque device identifier assigned by the adapter.
        assetType:
          type: string
          enum:
            - BESS
            - FVE
            - METER
            - HEAT_PUMP
            - EV_CHARGER
            - THERMOSTAT
            - INVERTER
            - GENERIC
          description: >
            Asset category. Source: AssetType enum in
            packages/shared/src/types/vcp.ts.
        powerKw:
          type: number
          description: >
            Asset power in kW. Sign convention matches the top-level fields
            (positive = import/charge, negative = export/discharge) for the
            relevant asset type.
        socPercent:
          type: number
          description: State-of-charge percentage. Present for BESS assets.
        temperatureCelsius:
          type: number
          description: Asset temperature in degrees Celsius. Present when reported.
        availableCapacityKwh:
          type: number
          description: Available energy capacity in kWh. Present for BESS assets.
        regulationPercent:
          type: number
          description: >
            Regulation headroom as a percentage. Present when the adapter
            exposes curtailment state.

    # ─── Meter Reading ────────────────────────────────────────────────────────
    #
    # Source: MeterReadingPayloadSchema + MeterEntrySchema
    #         in packages/shared/src/schemas/vcp.ts
    #         (mirrors MeterReadingPayload + MeterEntry in types/vcp.ts)
    #
    # Published every ~1 minute per site. Routing key:
    #   {orgSlug}.event.telemetry.meter.{siteId}
    # Bind `…meter.#` for all sites or `…meter.{siteId}` for one PLC.
    MeterReadingEnvelope:
      allOf:
        - $ref: '#/components/schemas/VcpEnvelope'
        - type: object
          properties:
            payload:
              $ref: '#/components/schemas/MeterReadingPayload'

    MeterReadingPayload:
      type: object
      description: |
        1-minute absolute meter-register snapshot. Carries cumulative
        energy register values (not differences). Consumers derive delta
        energy by subtracting the previous reading's register value.

        Replaces the v1.0 15-minute IntervalTelemetry path.
      required:
        - readingAt
        - meters
        - dataQuality
      properties:
        readingAt:
          type: string
          format: date-time
          description: >
            UTC timestamp of the meter reading (top-of-minute or as reported
            by the meter).
          example: '2024-01-15T10:30:00.000Z'
        eanCons:
          type: string
          nullable: true
          pattern: '^\d{18}$'
          description: >
            Consumption metering-point EAN (odběrný EAN, 18 digits) of the
            site. Null when not configured. Matches `eanCons` on the
            `GET /vcp/sites` catalog — lets consumers route readings to
            their own EAN-keyed model without a mapping table.
          example: '859182400123456789'
        eanProd:
          type: string
          nullable: true
          pattern: '^\d{18}$'
          description: >
            Production metering-point EAN (výrobní EAN, 18 digits) of the
            site. Null when not configured.
          example: '859182400987654321'
        meters:
          type: array
          minItems: 1
          description: >
            Array of per-meter readings. Must contain at least one entry.
          items:
            $ref: '#/components/schemas/MeterEntry'
        dataQuality:
          type: string
          enum:
            - GOOD
            - DEGRADED
            - ESTIMATED
          description: >
            Quality of this reading. GOOD = direct from meter;
            DEGRADED = partial loss (some meters offline);
            ESTIMATED = calculated/interpolated.

    MeterEntry:
      type: object
      description: >
        Absolute register values from a single physical meter. All register
        fields are optional — only registers that the meter physically exposes
        are included. Registers are monotonically increasing kWh counters.
      required:
        - deviceId
        - role
      properties:
        deviceId:
          type: string
          minLength: 1
          description: >
            Sub-device externalId (e.g. "GRID", "PV1") — the SAME identifier
            as `DeviceTelemetry.deviceId` and `subDevices[].deviceId` on
            `GET /vcp/sites`. One wire rule: deviceId = sub-device externalId,
            everywhere.
        role:
          type: string
          enum:
            - GRID
            - FVE
            - BESS
            - CONSUMPTION
          description: >
            Functional role of this meter.
            Source: MeterRoleSchema in packages/shared/src/schemas/vcp.ts.
        importRegisterKwh:
          type: number
          description: >
            Cumulative energy imported (consumed from grid / charged into BESS)
            in kWh. Monotonically increasing.
        exportRegisterKwh:
          type: number
          description: >
            Cumulative energy exported (injected to grid / discharged from BESS)
            in kWh. Monotonically increasing.
        productionRegisterKwh:
          type: number
          description: >
            Cumulative FVE production energy in kWh. Only present on FVE meters.
        chargeRegisterKwh:
          type: number
          description: Cumulative BESS charge energy in kWh. Only present on BESS meters.
        dischargeRegisterKwh:
          type: number
          description: >
            Cumulative BESS discharge energy in kWh. Only present on BESS meters.
        consumptionRegisterKwh:
          type: number
          description: >
            Cumulative site consumption energy in kWh.
            Only present on CONSUMPTION meters.

    # ─── Alarm Event ─────────────────────────────────────────────────────────
    #
    # Source: AlarmPayloadSchema in packages/shared/src/schemas/vcp.ts
    #         (mirrors AlarmPayload in types/vcp.ts)
    #
    # Routing key: {orgSlug}.event.alarm
    #
    # Note: AlarmPayload.actualValue and AlarmPayload.expectedValue are
    # z.union([z.string(), z.number()]).optional() in the Zod schema.
    # JSON Schema does not support untagged union for primitives cleanly;
    # the closest representation is oneOf: [string, number]. Represented
    # below as type: [string, number] which is valid JSON Schema draft-07+.
    # AsyncAPI 3.0 uses JSON Schema draft-07.
    AlarmEventEnvelope:
      allOf:
        - $ref: '#/components/schemas/VcpEnvelope'
        - type: object
          properties:
            payload:
              $ref: '#/components/schemas/AlarmPayload'

    AlarmPayload:
      type: object
      description: >
        Alarm event payload. Carries both active alarms (clearedAt absent)
        and clear notifications (clearedAt set). Partners should use
        `alarmId` for deduplication and upsert.
      required:
        - alarmId
        - severity
        - code
        - message
        - raisedAt
      properties:
        alarmId:
          type: string
          minLength: 1
          description: >
            Unique alarm identifier, stable across raise/clear events for
            the same alarm instance.
        severity:
          type: string
          enum:
            - P1_CRITICAL
            - P2_MAJOR
            - P3_MINOR
            - P4_INFO
          description: >
            Alarm severity. Source: VcpAlarmSeverity enum in
            packages/shared/src/types/enums.ts.
        code:
          type: string
          enum:
            - COMM_LOSS
            - COMM_DEGRADED
            - BESS_SOC_LOW
            - BESS_SOC_HIGH
            - BESS_TEMP_HIGH
            - BESS_FAULT
            - FVE_CURTAILED
            - FVE_INVERTER_FAULT
            - GRID_EXPORT_LIMIT_REACHED
            - GRID_IMPORT_LIMIT_REACHED
            - SETPOINT_DEVIATION
            - SETPOINT_UNACHIEVABLE
            - FALLBACK_ACTIVATED
            - OPERATING_MODE_NOT_HONORED
            - SCHEDULE_SLOT_MISSED
            - CONSTRAINT_VIOLATION
          description: >
            Structured alarm code. Source: VcpAlarmCode enum in
            packages/shared/src/types/enums.ts. Codes with an `_`-prefixed
            comment in the source (OPERATING_MODE_NOT_HONORED,
            SCHEDULE_SLOT_MISSED, CONSTRAINT_VIOLATION) were added in v1.1.
        message:
          type: string
          minLength: 1
          description: Human-readable description of the alarm condition.
        deviceId:
          type: string
          minLength: 1
          description: >
            Optional. Identifies the specific device that triggered the alarm.
            Omitted for site-level or control-loop alarms.
        raisedAt:
          type: string
          format: date-time
          description: UTC timestamp when the alarm was first raised.
          example: '2024-01-15T10:30:00.000Z'
        clearedAt:
          type: string
          format: date-time
          description: >
            UTC timestamp when the alarm was cleared. Absent while the alarm
            is still active; present in the clear notification message.
        actualValue:
          description: >
            The observed value that triggered the alarm. May be a string or
            number depending on the alarm code.
            Zod source: z.union([z.string(), z.number()]).optional()
          oneOf:
            - type: string
            - type: number
        expectedValue:
          description: >
            The value that was expected / the configured threshold. May be a
            string or number.
            Zod source: z.union([z.string(), z.number()]).optional()
          oneOf:
            - type: string
            - type: number
        breachedField:
          type: string
          description: >
            Name of the configuration or setpoint field whose threshold was
            breached (e.g. 'maxExportKw').
        tolerancePercent:
          type: number
          description: >
            Deviation tolerance that was in effect when the alarm was raised,
            expressed as a percentage.
        metadata:
          type: object
          description: >
            Arbitrary key-value metadata provided by the adapter or PLC.
            Values may be of any type.
          additionalProperties: true

    # ─── Command Execution Status ─────────────────────────────────────────────
    #
    # Source: CommandStatusPayload interface in packages/shared/src/types/vcp.ts
    #
    # NOTE: Unlike the other three payload types, CommandStatusPayload has NO
    # Zod schema — it is a plain TypeScript interface. The interface is the
    # single source of truth. Any future Zod schema addition should be reflected
    # here.
    #
    # Routing key: {orgSlug}.event.execution
    #
    # Published mid-flight (EXECUTING) and on completion (COMPLETED, DEVIATED,
    # FAILED). The initial synchronous ACK/NACK lives on the status channel.
    CommandExecutionEnvelope:
      allOf:
        - $ref: '#/components/schemas/VcpEnvelope'
        - type: object
          properties:
            payload:
              $ref: '#/components/schemas/CommandExecutionPayload'

    CommandExecutionPayload:
      type: object
      description: >
        Mid-flight and final execution status for a dispatched command.
        Correlate with the originating command using `envelope.correlationId`.
      required:
        - commandType
        - status
      properties:
        commandType:
          type: string
          description: >
            Identifies the command type that is being executed (e.g.
            'site-setpoint', 'device', 'emergency', 'operating-mode').
        status:
          type: string
          enum:
            - EXECUTING
            - COMPLETED
            - DEVIATED
            - FAILED
          description: >
            Current execution state. EXECUTING = command accepted and in
            progress; COMPLETED = target achieved; DEVIATED = command
            executed but actual value differs from target beyond tolerance;
            FAILED = execution error.
        actualValueKw:
          type: number
          description: >
            Actual power achieved at the site in kW at the time of this
            status update. Present when measurable.
        targetValueKw:
          type: number
          description: Target power setpoint in kW from the original command.
        deviationKw:
          type: number
          description: >
            Difference between actual and target power in kW.
            Positive = over-performing, negative = under-performing.
            Present when status is DEVIATED.
        reason:
          type: string
          description: >
            Optional free-text explanation, typically present on FAILED or
            DEVIATED status.

    # ─── Command ACK / NACK ───────────────────────────────────────────────────
    #
    # Source: CommandAckPayloadSchema + CommandResultSchema
    #         in packages/shared/src/schemas/vcp.ts
    #         (mirrors CommandAckPayload + CommandResult in types/vcp.ts)
    #
    # Routing key: {orgSlug}.event.command.ack
    #
    # Published synchronously after Voke processes an inbound command message.
    # Correlate with the originating command using `envelope.correlationId`.
    #
    # Note: the Zod schema has a superRefine constraint: `results` is required
    # and non-empty when `status = PARTIAL`. This invariant cannot be expressed
    # in JSON Schema and is enforced at runtime only.
    CommandAckEnvelope:
      allOf:
        - $ref: '#/components/schemas/VcpEnvelope'
        - type: object
          properties:
            payload:
              $ref: '#/components/schemas/CommandAckPayload'

    CommandAckPayload:
      type: object
      description: >
        Command acknowledgement payload. Carries the overall disposition of
        the command plus optional per-device results for batch commands.

        Runtime constraint (not expressible in JSON Schema): when
        `status = PARTIAL`, `results` must be present and non-empty.
        Source: superRefine in CommandAckPayloadSchema (vcp.ts).
      required:
        - status
        - commandType
      properties:
        status:
          type: string
          enum:
            - ACCEPTED
            - REJECTED
            - PARTIAL
            - QUEUED
          description: >
            Overall disposition of the command. ACCEPTED = fully accepted;
            REJECTED = refused (see rejectionCode); PARTIAL = accepted for
            some devices, rejected for others (batch commands only — see
            results); QUEUED = accepted and queued for deferred execution.
        commandType:
          type: string
          minLength: 1
          description: >
            Identifies the command type being acknowledged (e.g.
            'site-setpoint', 'device', 'emergency', 'operating-mode').
        message:
          type: string
          description: >
            Optional human-readable explanation, typically present on
            REJECTED or PARTIAL status.
        rejectionCode:
          type: string
          enum:
            - CONSTRAINT_VIOLATION
            - INVALID_COMMAND
            - INVALID_PAYLOAD
            - DEVICE_OFFLINE
            - DEVICE_FAULT
            - UNSUPPORTED_FOR_TOPOLOGY
          description: >
            Machine-readable rejection reason. Present when status = REJECTED.
            Source: RejectionCode enum in packages/shared/src/types/enums.ts.
        results:
          type: array
          description: >
            Per-device results for batch device commands. Present when
            status = PARTIAL (required by runtime constraint) or optionally
            when status = ACCEPTED/REJECTED for per-device granularity.
          items:
            $ref: '#/components/schemas/CommandResult'

    CommandResult:
      type: object
      description: Per-device result within a batch command ACK.
      required:
        - status
      properties:
        deviceId:
          type: string
          minLength: 1
          description: >
            Identifier of the device this result applies to. May be omitted
            for non-device-scoped sub-results.
        status:
          type: string
          enum:
            - ACCEPTED
            - REJECTED
          description: Disposition for this specific device.
        message:
          type: string
          description: Optional human-readable explanation for this device's result.
        rejectionCode:
          type: string
          enum:
            - CONSTRAINT_VIOLATION
            - INVALID_COMMAND
            - INVALID_PAYLOAD
            - DEVICE_OFFLINE
            - DEVICE_FAULT
            - UNSUPPORTED_FOR_TOPOLOGY
          description: >
            Machine-readable rejection reason for this device. Present when
            this device's status = REJECTED.
            Source: RejectionCode enum in packages/shared/src/types/enums.ts.
