浏览代码

Use OpenAPI to specify and document REST API endpoints and JSON schemas (#493)

* Advertise FUT020 support in README
* Update UI screenshot in README
* Pull REST documentation out of README, link to swagger petstore docs
* Use node v10 in Travis build
* Validate OpenAPI spec in build
Chris Mullins 6 年之前
父节点
当前提交
c332cf7023
共有 6 个文件被更改,包括 985 次插入81 次删除
  1. 3 1
      .travis.yml
  2. 8 77
      README.md
  3. 3 2
      lib/WebServer/MiLightHttpServer.cpp
  4. 950 0
      openapi.yaml
  5. 7 1
      test/remote/lib/api_client.rb
  6. 14 0
      test/remote/spec/state_spec.rb

+ 3 - 1
.travis.yml

@@ -6,14 +6,16 @@ cache:
   directories:
   - "~/.platformio"
 env:
-  - NODE_VERSION="6"
+  - NODE_VERSION="10"
 before_install:
   - nvm install $NODE_VERSION
 install:
 - pip install -U platformio
 - platformio lib install
 - cd web && npm install && cd ..
+- npm install -g swagger-cli
 script:
+- swagger-cli validate ./openapi.yaml
 - platformio run
 before_deploy:
   - ./.prepare_release

+ 8 - 77
README.md

@@ -25,8 +25,9 @@ Support has been added for the following [bulb types](http://futlight.com/produc
 Model #|Name|Compatible Bulbs
 -------|-----------|----------------
 |FUT096|RGB/W|<ol><li>FUT014</li><li>FUT016</li><li>FUT103</li>|
-|FUT005, FUT006,FUT007</li></ol>|CCT|<ol><li>FUT011</li><li>FUT017</li><li>FUT019</li></ol>|
+|FUT005<br/>FUT006<br/>FUT007</li></ol>|CCT|<ol><li>FUT011</li><li>FUT017</li><li>FUT019</li></ol>|
 |FUT098|RGB|Most RGB LED Strip Controlers|
+|FUT020|RGB|Some other RGB LED strip controllers|
 |FUT092|RGB/CCT|<ol><li>FUT012</li><li>FUT013</li><li>FUT014</li><li>FUT015</li><li>FUT103</li><li>FUT104</li><li>FUT105</li><li>Many RGB/CCT LED Strip Controllers</li></ol>|
 |FUT091|CCT v2|Most newer dual white bulbs and controllers|
 |FUT089|8-zone RGB/CCT|Most newer rgb + dual white bulbs and controllers|
@@ -105,7 +106,7 @@ Both mDNS and SSDP are supported.
 
 The HTTP endpoints (shown below) will be fully functional at this point. You should also be able to navigate to `http://<ip_of_esp>`, or `http://milight-hub.local` if your client supports mDNS. The UI should look like this:
 
-![Web UI](https://user-images.githubusercontent.com/589893/39412360-0d95ab2e-4bd0-11e8-915c-7fef7ee38761.png)
+![Web UI](https://user-images.githubusercontent.com/589893/61682228-a8151700-acc5-11e9-8b86-1e21efa6cdbe.png)
 
 
 If it does not work as expected see [Troubleshooting](https://github.com/sidoh/esp8266_milight_hub/wiki/Troubleshooting).
@@ -142,75 +143,13 @@ You can configure aliases or labels for a given _(Device Type, Device ID, Group
 * **In the REST API**: standard CRUD verbs (`GET`, `PUT`, and `DELETE`) allow you to interact with aliases via the `/gateways/:device_alias` route.
 * **MQTT**: you can configure topics to listen for commands and publish updates/state using aliases rather than IDs.
 
-## REST endpoints
-
-1. `GET /`. Opens web UI.
-1. `GET /about`. Return information about current firmware version.
-1. `POST /system`. Post commands in the form `{"comamnd": <command>}`. Currently supports the commands: `restart`.
-1. `POST /firmware`. OTA firmware update.
-1. `GET /settings`. Gets current settings as JSON.
-1. `PUT /settings`. Patches settings (e.g., doesn't overwrite keys that aren't present). Accepts a JSON blob in the body.
-1. `GET /remote_configs`. Get a list of supported remote configs (aka `device_type`s).
-1. `GET /gateway_traffic(/:device_type)?`. Starts an HTTP long poll. Returns any Milight traffic it hears. Useful if you need to know what your Milight gateway/remote ID is. Since protocols for RGBW/CCT are different, specify one of `rgbw`, `cct`, or `rgb_cct` as `:device_type.  The path `/gateway_traffic` without a `:device_type` will sniff for all protocols simultaneously.
-1. `PUT /gateways/:device_id/:device_type/:group_id`. Controls or sends commands to `:group_id` from `:device_id`. Accepts a JSON blob. The schema is documented below in the _Bulb commands_ section.
-1. `GET /gateways/:device_id/:device_type/:group_id`. Returns a JSON blob describing the state of the the provided group.
-1. `DELETE /gateways/:device_id/:device_type/:group_id`. Deletes state associated with the provided group.
-1. `(GET|PUT|DELETE) /gateways/:device_alias`.  Same as the previous three routes except acting on aliases instead of IDs.  404 is returned if the alias does not exist.
-1. `POST /raw_commands/:device_type`. Sends a raw RF packet with radio configs associated with `:device_type`. Example body:
-    ```
-    {"packet": "01 02 03 04 05 06 07 08 09", "num_repeats": 10}
-    ```
-
-#### Bulb commands
-
-Route (5) supports these commands. Note that each bulb type has support for a different subset of these commands:
-
-1. `status`. Toggles on/off. Can be "on", "off", "true", or "false".
-1. `hue`. Sets color. Should be in the range `[0, 359]`.
-1. `saturation`. Controls saturation.
-1. `level`. Controls brightness. Should be in the range `[0, 100]`.
-1. `temperature`. Controls white temperature. Should be in the range `[0, 100]`.
-1. `mode`. Sets "disco mode" setting to the specified value. Note that not all bulbs that have modes support this command. Some will only allow you to cycle through next/previous modes using commands.
-1. `command`. Sends a command to the group. Can be one of:
-   * `set_white`. Turns off RGB and enters WW/CW mode.
-   * `pair`. Emulates the pairing process. Send this command right as you connect an unpaired bulb and it will pair with the device ID being used.
-   * `unpair`. Emulates the unpairing process. Send as you connect a paired bulb to have it disassociate with the device ID being used.
-   * `next_mode`. Cycles to the next "disco mode".
-   * `previous_mode`. Cycles to the previous disco mode.
-   * `mode_speed_up`.
-   * `mode_speed_down`.
-   * `level_down`. Turns down the brightness. Not all dimmable bulbs support this command.
-   * `level_up`. Turns down the brightness. Not all dimmable bulbs support this command.
-   * `temperature_down`. Turns down the white temperature. Not all bulbs with adjustable white temperature support this command.
-   * `temperature_up`. Turns up the white temperature. Not all bulbs with adjustable white temperature support this command.
-   * `night_mode`. Enable "night mode", which is minimum brightness and bulbs only responding to on/off commands.
-   * `toggle`. Toggle on/off state.
-1. `commands`. An array containing any number of the above commands (including repeats).
-
-The following redundant commands are supported for the sake of compatibility with HomeAssistant's [`mqtt`](https://home-assistant.io/components/light.mqtt/) light platform with the `json` schema:
-
-1. `color`. Hash containing RGB color. All keys for r, g, and b should be present. For example, `{"r":255,"g":200,"b":255}`.
-1. `color_temp`. Controls white temperature. Value is in [mireds](https://en.wikipedia.org/wiki/Mired). Milight bulbs are in the range 153-370 mireds (2700K-6500K).
-1. `brightness`. Same as `level` with a range of `[0,255]`.
-1. `state`. Same as `status`.
-
-If you'd like to control bulbs in all groups paired with a particular device ID, set `:group_id` to 0.
-
-#### Examples
-
-Turn on group 2 for device ID 0xCD86, set hue to 100, and brightness to 50%:
+## REST API
 
-```
-$ curl -X PUT -H 'Content-Type: application/json' -d '{"status":"on","hue":100,"level":50}' http://esp8266/gateways/0xCD86/rgbw/2
-true%
-```
+The REST API is specified using
 
-Set color to white (disable RGB):
+[openapi.yaml](openapi.yaml) contains the raw spec, created using [OpenAPI v3](https://swagger.io/docs/specification/about/).
 
-```
-$ curl -X PUT -H 'Content-Type: application/json' -d '{"command":"set_white"}' -X PUT http://esp8266/gateways/0xCD86/rgbw/2
-true%
-```
+[You can view generated documentation here.](https://petstore.swagger.io/?url=https://raw.githubusercontent.com/sidoh/esp8266_milight_hub/blob/master/openapi.yaml#/)
 
 ## MQTT
 
@@ -284,15 +223,7 @@ irb(main):007:0> puts client.get.inspect
 
 ##### Customize fields
 
-You can select which fields should be included in state updates by configuring the `group_state_fields` parameter.  Available fields should be mostly self explanatory, with the possible exceptions of:
-
-1. `state` / `status` - same value with different keys (useful if your platform expects one or the other).
-1. `brightness` / `level` - [0, 255] and [0, 100] scales of the same value.
-1. `kelvin / color_temp` - [0, 100] and [153, 370] scales for the same value.  The later's unit is mireds.
-1. `bulb_mode` - what mode the bulb is in: white, rgb, etc.
-1. `color` / `computed_color` - behaves the same when bulb is in rgb mode.  `computed_color` will send RGB = 255,255,255 when in white mode.  This is useful for HomeAssistant where it always expects the color to be set.
-1. `oh_color` - same as `color` with a format compatible with [OpenHAB's colorRGB channel type](https://www.openhab.org/addons/bindings/mqtt.generic/#channel-type-colorrgb-colorhsb).
-1. `device_id` / `device_type` / `group_id` - this information is in the MQTT topic or REST route, but can be included in the payload in the case that processing the topic or route is more difficult.
+You can select which fields should be included in state updates by configuring the `group_state_fields` parameter.  Available fields should be mostly self explanatory, but are all documented in the REST API spec under `GroupStateField`.
 
 #### Client Status
 

+ 3 - 2
lib/WebServer/MiLightHttpServer.cpp

@@ -342,9 +342,10 @@ void MiLightHttpServer::sendGroupState(BulbId& bulbId, GroupState* state, RichHt
 
   JsonObject obj = response.json.to<JsonObject>();
 
-  if (state != NULL) {
+  if (blockOnQueue && state != NULL) {
     state->applyState(obj, bulbId, settings.groupStateFields);
-    state->debugState("test");
+  } else {
+    obj[F("success")] = true;
   }
 }
 

+ 950 - 0
openapi.yaml

@@ -0,0 +1,950 @@
+openapi: 3.0.1
+info:
+  title: ESP8266 MiLight Hub
+  description: Official documention for MiLight Hub's REST API.
+  contact:
+    email: chris@sidoh.org
+  license:
+    name: MIT
+  version: 1.0.0
+servers:
+- url: http://milight-hub
+tags:
+- name: system
+  description: Routes that return system information and allow you to control the device.
+- name: settings
+  description: Read and write settings
+- name: devices
+  description: Control lighting devices
+- name: transitions
+  description: Control transitions
+paths:
+  /about:
+    get:
+      tags:
+      - system
+      summary: Get system information
+      responses:
+        200:
+          description: success
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/About'
+  /remote_configs:
+    get:
+      tags:
+        - system
+      description: Returns list of supported remote types
+      responses:
+        200:
+          description: success
+          content:
+            applicaiton/json:
+              schema:
+                type: array
+                items:
+                  type: string
+                example:
+                  $ref: '#/components/schemas/RemoteType/enum'
+  /system:
+    post:
+      tags:
+      - system
+      summary: Control system
+      description: >
+        Send commands to the system.  Supported commands:
+
+        1. `restart`. Restart the ESP8266.
+
+        1. `clear_wifi_config`. Clears on-board wifi information. ESP8266 will reboot and enter wifi config mode.
+
+      requestBody:
+        content:
+          application/json:
+            schema:
+              type: object
+              required:
+              - command
+              properties:
+                command:
+                  type: string
+                  enum:
+                  - restart
+                  - clear_wifi_config
+      responses:
+        200:
+          description: command handled successfully
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/BooleanResponse'
+        400:
+          description: error
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/BooleanResponse'
+  /settings:
+    get:
+      tags:
+      - settings
+      description: Get existing settings
+      responses:
+        200:
+          description: success
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Settings'
+    put:
+      tags:
+      - settings
+      description: Patch existing settings with provided keys
+      requestBody:
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/Settings'
+      responses:
+        200:
+          description: success
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/BooleanResponse'
+    post:
+      tags:
+      - settings
+      description: Overwrite existing settings with the provided file
+      requestBody:
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/Settings'
+      responses:
+        200:
+          description: success
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/BooleanResponse'
+
+  /gateway_traffic/{remote-type}:
+    get:
+      tags:
+      - devices
+      summary: Read a packet
+      description:
+        Read a packet.  Does not return a response until a packet is read.
+        If `remote-type` is unspecified, will read from all remote types simultaneously.
+      parameters:
+        - $ref: '#/components/parameters/RemoteType'
+      responses:
+        200:
+          description: success
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  packet_info:
+                    type: string
+
+  /gateways/{device-id}/{remote-type}/{group-id}:
+    parameters:
+      - $ref: '#/components/parameters/DeviceId'
+      - $ref: '#/components/parameters/RemoteType'
+      - $ref: '#/components/parameters/GroupId'
+    get:
+      tags:
+        - devices
+      description:
+        Get device state
+      parameters:
+        - $ref: '#/components/parameters/BlockOnQueue'
+      responses:
+        200:
+          description: success
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/GroupState'
+    put:
+      tags:
+        - devices
+      description:
+        Patch device state with the specified keys
+      parameters:
+        - $ref: '#/components/parameters/BlockOnQueue'
+      requestBody:
+        content:
+          application/json:
+            schema:
+              allOf:
+                - $ref: '#/components/schemas/GroupState'
+                - $ref: '#/components/schemas/GroupStateCommands'
+      responses:
+        400:
+          description: error with request
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/BooleanResponse'
+        200:
+          description: success
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/GroupState'
+    delete:
+      tags:
+        - devices
+      description:
+        Delete kept state for given device
+      responses:
+        200:
+          description: success
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/BooleanResponse'
+  /gateways/{device-alias}:
+    parameters:
+      - $ref: '#/components/parameters/DeviceAlias'
+    get:
+      tags:
+        - devices
+      description: Get state for the provided alias
+      responses:
+        404:
+          description: provided device alias does not exist
+        200:
+          description: success
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/GroupState'
+    put:
+      tags:
+        - devices
+      description:
+        Patch device state with the specified keys
+      parameters:
+        - $ref: '#/components/parameters/BlockOnQueue'
+      requestBody:
+        content:
+          application/json:
+            schema:
+              allOf:
+                - $ref: '#/components/schemas/GroupState'
+                - $ref: '#/components/schemas/GroupStateCommands'
+      responses:
+        400:
+          description: error with request
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/BooleanResponse'
+        200:
+          description: success
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/GroupState'
+    delete:
+      tags:
+        - devices
+      description:
+        Delete kept state for given device
+      responses:
+        200:
+          description: success
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/BooleanResponse'
+
+  /raw_commands/{remote-type}:
+    parameters:
+      - $ref: '#/components/parameters/RemoteType'
+    post:
+      tags:
+        - devices
+      summary: Send a raw packet
+      requestBody:
+        content:
+          application/json:
+            schema:
+              type: object
+              properties:
+                packet:
+                  type: string
+                  pattern: "([A-Fa-f0-9]{2}[ ])+"
+                  description: Raw packet to send
+                  example: '01 02 03 04 05 06 07 08 09'
+                num_repeats:
+                  type: integer
+                  minimum: 1
+                  description: Number of repeated packets to send
+                  example: 50
+      responses:
+        200:
+            description: success
+            content:
+              applicaiton/json:
+                schema:
+                  $ref: '#/components/schemas/BooleanResponse'
+
+
+  /transitions:
+    get:
+      tags:
+        - transitions
+      summary: Get all active transitions
+      responses:
+        200:
+          description: success
+          content:
+            application/json:
+              schema:
+                type: array
+                items:
+                  $ref: '#/components/schemas/TransitionData'
+    post:
+      tags:
+        - transitions
+      summary: Create a new transition
+      requestBody:
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/TransitionData'
+      responses:
+        400:
+          description: error
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/BooleanResponse'
+        200:
+          description: success
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/BooleanResponse'
+  /transitions/{id}:
+    parameters:
+      - name: id
+        in: path
+        description: ID of transition
+        schema:
+          type: integer
+        required: true
+    get:
+      tags:
+        - transitions
+      summary: Get properties for provided transition ID
+      responses:
+        404:
+          description: Provided transition ID not found
+        200:
+          description: success
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/TransitionData'
+    delete:
+      tags:
+        - transitions
+      summary: Delete provided transition
+      responses:
+        404:
+          description: Provided transition ID not found
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/BooleanResponse'
+        200:
+          description: success
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/BooleanResponse'
+  /firmware:
+    post:
+      tags:
+        - system
+      summary:
+        Update firmware
+      requestBody:
+        description: Firmware file
+        content:
+          multipart/form-data:
+            schema:
+              type: object
+              properties:
+                fileName:
+                  type: string
+                  format: binary
+      responses:
+        200:
+          description: success
+        500:
+          description: server error
+components:
+  parameters:
+    DeviceAlias:
+      name: device-alias
+      in: path
+      description: Device alias saved in settings
+      schema:
+        type: string
+      required: true
+    BlockOnQueue:
+      name: blockOnQueue
+      in: query
+      description: If true, response will block on update packets being sent before returning
+      schema:
+        type: boolean
+      required: false
+    GroupId:
+      name: group-id
+      in: path
+      description: >
+        Group ID.  Should be 0-8, depending on remote type.  Group 0 is a "wildcard" group.  All bulbs paired with the same device ID will respond to commands sent to Group 0.
+      schema:
+        type: integer
+        minimum: 0
+        maximum: 8
+      required: true
+    DeviceId:
+      name: device-id
+      in: path
+      description: 2-byte device ID.  Can be decimal or hexadecimal.
+      schema:
+        oneOf:
+          - type: integer
+            minimum: 0
+            maximum: 65535
+          - type: string
+            pattern: '0x[a-fA-F0-9]+'
+      example: '0x1234'
+      required: true
+    RemoteType:
+      name: remote-type
+      in: path
+      description: Type of remote to read a packet from.  If unspecified, will read packets from all remote types.
+      schema:
+        $ref: '#/components/schemas/RemoteType'
+      required: true
+  schemas:
+    State:
+      description: "On/Off state"
+      type: string
+      enum:
+        - On
+        - Off
+      example: On
+    GroupStateCommand:
+      type: string
+      enum:
+        - unpair
+        - pair
+        - set_white
+        - night_mode
+        - level_up
+        - level_down
+        - temperature_up
+        - temperature_down
+        - next_mode
+        - previous_mode
+        - mode_speed_down
+        - mode_speed_up
+        - toggle
+      example: pair
+      description: >
+        Commands that affect a given group.  Descriptiosn follow:
+
+        * `pair`. Emulates the pairing process. Send this command right as you connect an unpaired bulb and it will pair with the device ID being used.
+        * `unpair`. Emulates the unpairing process. Send as you connect a paired bulb to have it disassociate with the device ID being used.
+        * `set_white`. Turns off RGB and enters WW/CW mode.
+        * `night_mode`.  Most devices support a "night mode," which has LEDs turned to a very dim setting -- lower than brightness 0.
+        * `level_up`. Turns down the brightness. Not all dimmable bulbs support this command.
+        * `level_down`. Turns down the brightness. Not all dimmable bulbs support this command.
+        * `temperature_up`. Turns up the white temperature. Not all bulbs with adjustable white temperature support this command.
+        * `temperature_down`. Turns down the white temperature. Not all bulbs with adjustable white temperature support this command.
+        * `next_mode`. Cycles to the next "disco mode".
+        * `previous_mode`. Cycles to the previous disco mode.
+        * `mode_speed_up`. Turn transition speed for current mode up.
+        * `mode_speed_down`. Turn transition speed for current mode down.
+        * `toggle`. Toggle on/off state.
+
+    TransitionField:
+      type: string
+      enum:
+        - hue
+        - saturation
+        - brightness
+        - level
+        - kelvin
+        - color_temp
+        - color
+      example: brightness
+    TransitionValue:
+      oneOf:
+        - type: integer
+        - type: string
+          pattern: '[0-9]{1,3},[0-9]{1,3},[0-9]{1,3}'
+      description: Either an int value or a color
+    TransitionArgs:
+      type: object
+      properties:
+        field:
+          $ref: '#/components/schemas/TransitionField'
+        start_value:
+          $ref: '#/components/schemas/TransitionValue'
+        end_value:
+          $ref: '#/components/schemas/TransitionValue'
+        duration:
+          type: number
+          format: float
+          description: Duration of transition, measured in seconds
+        period:
+          type: integer
+          description: Length of time between updates in a transition, measured in milliseconds
+        num_periods:
+          type: integer
+          description: Number of packets sent over the course of the transition
+    TransitionData:
+      allOf:
+        - $ref: '#/components/schemas/TransitionArgs'
+        - type: object
+          properties:
+            id:
+              type: integer
+            last_sent:
+              type: integer
+              description: Timestamp since last update was sent.
+            bulb:
+              $ref: '#/components/schemas/BulbId'
+            type:
+              type: string
+              enum:
+                - field
+                - color
+            current_value:
+              $ref: '#/components/schemas/TransitionValue'
+            end_value:
+              $ref: '#/components/schemas/TransitionValue'
+
+    BulbId:
+      type: object
+      properties:
+        device_id:
+          type: integer
+          minimum: 0
+          maximum: 65536
+          example: 1234
+        group_id:
+          type: integer
+          minimum: 0
+          maximum: 8
+          example: 1
+        device_type:
+          $ref: '#/components/schemas/RemoteType'
+    GroupStateCommands:
+      type: object
+      properties:
+        command:
+          oneOf:
+            - $ref: '#/components/schemas/GroupStateCommand'
+            - type: object
+              properties:
+                command:
+                  type: string
+                  enum:
+                    - transition
+                args:
+                  $ref: '#/components/schemas/TransitionArgs'
+        commands:
+          type: array
+          items:
+            $ref: '#/components/schemas/GroupStateCommand'
+          example:
+            - level_up
+            - temperature_up
+    GroupState:
+      type: object
+      properties:
+        state:
+          $ref: '#/components/schemas/State'
+        status:
+          $ref: '#/components/schemas/State'
+        hue:
+          type: integer
+          minimum: 0
+          maximum: 359
+          description: Color hue.  Will change bulb to color mode.
+        saturation:
+          type: integer
+          minimum: 0
+          maximum: 100
+          description: Color saturation.  Will normally change bulb to color mode.
+        kelvin:
+          type: integer
+          minimum: 0
+          maximum: 100
+          description: White temperature.  0 is coolest, 100 is warmest.
+        temperature:
+          type: integer
+          minimum: 0
+          maximum: 100
+          description: Alias for `kelvin`.
+        color_temp:
+          type: integer
+          minimum: 153
+          maximum: 370
+          description: White temperature measured in mireds.  Lower values are cooler.
+        mode:
+          type: integer
+          description: Party mode ID.  Actual effect depends on the bulb.
+        color:
+          oneOf:
+            - type: string
+              pattern: '[0-9]{1,3},[0-9]{1,3},[0-9]{1,3}'
+            - type: object
+              properties:
+                r:
+                  type: integer
+                g:
+                  type: integer
+                b:
+                  type: integer
+          example:
+            '255,0,255'
+        level:
+          type: integer
+          minimum: 0
+          maximum: 100
+          description: Brightness on a 0-100 scale.
+          example: 50
+        brightness:
+          type: integer
+          minimum: 0
+          maximum: 255
+          description: Brightness on a 0-255 scale.
+          example: 170
+        effect:
+          type: string
+          enum:
+            - night_mode
+            - white_mode
+
+    RemoteType:
+      type: string
+      enum:
+      - "rgbw"
+      - "cct"
+      - "rgb_cct"
+      - "rgb"
+      - "fut089"
+      - "fut091"
+      - "fut020"
+      example: rgb_cct
+    RF24Channel:
+      type: string
+      enum:
+      - LOW
+      - MID
+      - HIGH
+    LedMode:
+      type: string
+      enum:
+      - Off
+      - Slow toggle
+      - Fast toggle
+      - Slow blip
+      - Fast blip
+      - Flicker
+      - On
+    GroupStateField:
+      type: string
+      enum:
+      - state
+      - status
+      - brightness
+      - level
+      - hue
+      - saturation
+      - color
+      - mode
+      - kelvin
+      - color_temp
+      - bulb_mode
+      - computed_color
+      - effect
+      - device_id
+      - group_id
+      - device_type
+      - oh_color
+      description: >
+        Defines a field which is a part of state for a particular light device.  Most fields are self-explanatory, but documentation for each follows:
+
+        * `state` / `status` - same value with different keys (useful if your platform expects one or the other).
+
+        * `brightness` / `level` - [0, 255] and [0, 100] scales of the same value.
+
+        * `kelvin / color_temp` - [0, 100] and [153, 370] scales for the same value.  The later's unit is mireds.
+
+        * `bulb_mode` - what mode the bulb is in: white, rgb, etc.
+
+        * `color` / `computed_color` - behaves the same when bulb is in rgb mode.  `computed_color` will send RGB = 255,255,255 when in white mode.  This is useful for HomeAssistant where it always expects the color to be set.
+
+        * `oh_color` - same as `color` with a format compatible with [OpenHAB's colorRGB channel type](https://www.openhab.org/addons/bindings/mqtt.generic/#channel-type-colorrgb-colorhsb).
+
+        * `device_id` / `device_type` / `group_id` - this information is in the MQTT topic or REST route, but can be included in the payload in the case that processing the topic or route is more difficult.
+    DeviceId:
+      type: array
+      items: {}
+      example:
+        - 1234
+        - "rgb_cct"
+        - 1
+    Settings:
+      type: object
+      properties:
+        admin_username:
+          type: string
+          description: If spcified along with `admin_password`, HTTP basic auth will be enabled to access all REST endpoints.
+          default: ""
+        admin_password:
+          type: string
+          description: If spcified along with `admin_username`, HTTP basic auth will be enabled to access all REST endpoints.
+          default: ""
+        ce_pin:
+          type: integer
+          description: CE pin to use for SPI radio (nRF24, LT8900)
+          default: 4
+        csn_pin:
+          type: integer
+          description: CSN pin to use with nRF24
+          default: 15
+        reset_pin:
+          type: integer
+          description: Reset pin to use with LT8900
+          default: 0
+        led_pin:
+          type: integer
+          description: Pin to control for status LED.  Set to a negative value to invert on/off status.
+          default: -2
+        packet_repeats:
+          type: integer
+          description: Number of times to resend the same 2.4 GHz milight packet when a command is sent.
+          default: 50
+        http_repeat_factor:
+          type: integer
+          description: Packet repeats resulting from REST commands will be multiplied by this number.
+          default: 1
+        auto_restart_period:
+          type: integer
+          description: Automatically restart the device after the number of specified minutes.  Use 0 to disable.
+          default: 0
+        mqtt_server:
+          type: string
+          description: MQTT server to connect to.
+          format: hostname
+        mqtt_username:
+          type: string
+          description: If specified, use this username to authenticate with the MQTT server.
+        mqtt_password:
+          type: string
+          description: If specified, use this password to authenticate with the MQTT server.
+        mqtt_topic_pattern:
+          type: string
+          description: Topic pattern to listen on for commands.  More detail on the format in README.
+          example: milight/commands/:device_id/:device_type/:group_id
+        mqtt_update_topic_pattern:
+          type: string
+          description: Topic pattern individual intercepted commands will be sent to.  More detail on the format in README.
+          example: milight/updates/:device_id/:device_type/:group_id
+        mqtt_update_state_pattern:
+          type: string
+          description: Topic pattern device state will be sent to.  More detail on the format in README.
+          example: milight/state/:device_id/:device_type/:group_id
+        mqtt_client_status_topic:
+          type: string
+          description: Topic client status will be sent to.
+          example: milight/status
+        simple_mqtt_client_status:
+          type: boolean
+          description: If true, will use a simple enum flag (`connected` or `disconnected`) to indicate status.  If false, will send a rich JSON message including IP address, version, etc.
+          default: true
+        discovery_port:
+          type: integer
+          description: UDP discovery port
+          default: 48899
+        listen_repeats:
+          type: integer
+          description: Controls how many cycles are spent listening for packets.  Set to 0 to disable passive listening.
+          default: 3
+        state_flush_interval:
+          type: integer
+          description: Controls how many miliseconds must pass between states being flushed to persistent storage.  Set to 0 to disable throttling.
+          default: 10000
+        mqtt_state_rate_limit:
+          type: integer
+          description: Controls how many miliseconds must pass between MQTT state updates.  Set to 0 to disable throttling.
+          default: 500
+        packet_repeat_throttle_threshold:
+          type: integer
+          description:
+            Controls how packet repeats are throttled.  Packets sent with less time (measured in milliseconds) between them than this value (in milliseconds) will cause packet repeats to be throttled down.  More than this value will unthrottle up.
+          default: 200
+        packet_repeat_throttle_sensitivity:
+          type: integer
+          description:
+            Controls how packet repeats are throttled. Higher values cause packets to be throttled up and down faster.  Set to 0 to disable throttling.
+          default: 0
+          minimum: 0
+          maximum: 1000
+        packet_repeat_minimum:
+          type: integer
+          description:
+            Controls how far throttling can decrease the number of repeated packets
+          default: 3
+        enable_automatic_mode_switching:
+          type: boolean
+          description:
+            When making updates to hue or white temperature in a different bulb mode, switch back to the original bulb mode after applying the setting change.
+          default: false
+        led_mode_wifi_config:
+          $ref: '#/components/schemas/LedMode'
+        led_mode_wifi_failed:
+          $ref: '#/components/schemas/LedMode'
+        led_mode_operating:
+          $ref: '#/components/schemas/LedMode'
+        led_mode_packet:
+          $ref: '#/components/schemas/LedMode'
+        led_mode_packet_count:
+          type: integer
+          description: Number of times the LED will flash when packets are changing
+          default: 3
+        hostname:
+          type: string
+          description: Hostname that will be advertized on a DHCP request
+          pattern: "[a-zA-Z0-9-]+"
+          default: milight-hub
+        rf24_power_level:
+          type: string
+          enum:
+            - MIN
+            - LOW
+            - HIGH
+            - MAX
+          description: Power level used when packets are sent.  See nRF24 documentation for further detail.
+          default: MAX
+        rf24_listen_channel:
+          $ref: '#/components/schemas/RF24Channel'
+        wifi_static_ip:
+          type: string
+          format: ipv4
+          description: If specified, the static IP address to use
+        wifi_static_ip_gateway:
+          type: string
+          format: ipv4
+          description: If specified along with static IP, the gateway address to use
+        wifi_static_ip_netmask:
+          type: string
+          format: ipv4
+          description: If specified along with static IP, the netmask to use
+        packet_repeats_per_loop:
+          type: integer
+          default: 10
+          description: Packets are sent asynchronously.  This number controls the number of repeats sent during each iteration.  Increase this number to improve packet throughput.  Decrease to improve system multi-tasking.
+        home_assistant_discovery_prefix:
+          type: string
+          description: If specified along with MQTT settings, will enable HomeAssistant MQTT discovery using the specified discovery prefix.  HomeAssistant's default is `homeassistant/`.
+        wifi_mode:
+          type: string
+          enum:
+            - B
+            - G
+            - N
+          description: Forces WiFi into the spcified mode.  Try using B or G mode if you are having stability issues.
+          default: N
+        rf24_channels:
+          type: array
+          items:
+            $ref: '#/components/schemas/RF24Channel'
+          description: Defines which channels we send on.  Each remote type has three channels.  We can send on any subset of these.
+        device_ids:
+          type: array
+          items:
+            $ref: '#/components/schemas/DeviceId'
+          description:
+            "List of saved device IDs, stored as 3-long arrays.  Elements are: 1) remote ID, 2) remote type, 3) group ID"
+          example:
+            - [1234, 'rgb_cct', 1]
+            - [5678, 'fut089', 5]
+        gateway_configs:
+          type: array
+          items:
+            type: integer
+          description: "List of UDP servers, stored as 3-long arrays.  Elements are 1) remote ID to bind to, 2) UDP port to listen on, 3) protocol version (5 or 6)"
+          example:
+            - [1234, 5555, 6]
+        group_state_fields:
+          type: array
+          items:
+            $ref: '#/components/schemas/GroupStateField'
+        group_id_aliases:
+          type: object
+          description: Keys are aliases, values are 3-long arrays with same schema as items in `device_ids`.
+          example:
+            alias1: [1234, 'rgb_cct', 1]
+            alias2: [1234, 'rgb_cct', 2]
+
+    BooleanResponse:
+      type: object
+      required:
+      - success
+      properties:
+        success:
+          type: boolean
+        error:
+          type: string
+          description: If an error occurred, message specifying what went wrong
+    About:
+      type: object
+      properties:
+        firmware:
+          type: string
+          description: Always set to "milight-hub"
+        version:
+          type: string
+          description: Semver version string
+        ip_address:
+          type: string
+        reset_reason:
+          type: string
+          description: Reason the system was last rebooted
+        variant:
+          type: string
+          description: Firmware variant (e.g., d1_mini, nodemcuv2)
+        free_heap:
+          type: integer
+          format: int64
+          description: Amount of free heap remaining (measured in bytes)
+        arduino_version:
+          type: string
+          description: Version of Arduino SDK firmware was built with
+        queue_stats:
+          type: object
+          properties:
+            length:
+              type: integer
+              description: Number of enqueued packets to be sent
+            dropped_packets:
+              type: integer
+              description: Number of packets that have been dropped since last reboot

+ 7 - 1
test/remote/lib/api_client.rb

@@ -96,7 +96,13 @@ class ApiClient
   end
 
   def state_path(params = {})
-    "/gateways/#{params[:id]}/#{params[:type]}/#{params[:group_id]}?blockOnQueue=true"
+    query = if params[:blockOnQueue].nil? || params[:blockOnQueue]
+      "?blockOnQueue=true"
+    else
+      ""
+    end
+
+    "/gateways/#{params[:id]}/#{params[:type]}/#{params[:group_id]}#{query}"
   end
 
   def delete_state(params = {})

+ 14 - 0
test/remote/spec/state_spec.rb

@@ -15,6 +15,20 @@ RSpec.describe 'State' do
     @client.delete_state(@id_params)
   end
 
+  context 'blockOnQueue parameter' do
+    it 'should not receive state if we don\'t block on the packet queue' do
+      response = @client.patch_state({status: 'ON'}, @id_params.merge(blockOnQueue: false))
+
+      expect(response).to eq({'success' => true})
+    end
+
+    it 'should receive state if we do block on the packet queue' do
+      response = @client.patch_state({status: 'ON'}, @id_params.merge(blockOnQueue: true))
+
+      expect(response).to eq({'status' => 'ON'})
+    end
+  end
+
   context 'initial state' do
     it 'should assume white mode for device types that are white-only' do
       %w(cct fut091).each do |type|