Controlling Philips Hue with HTTP2

Cover Image for Controlling Philips Hue with HTTP2

Introduction

The previous post was about connecting to a Philips Hue bridge over HTTPS. This will go a step further and show how to connect over HTTPS with HTTP/2 and how you can use this connection to send requests and listen for state change events.

Why use HTTP/2?

HTTP/2 is a more efficient way to communicate between a client/server because it allows multiplexing multiple HTTP requests over a single TCP connection. Request multiplexing may not matter as much if the client only needs to send 1 request at a time because then a single TCP socket could be reused for each HTTP/1 request. However, Philips Hue supports Server-Sent Events, as a way of notifying clients when state changes happen on the Hue bridge. By setting up an HTTP/2 connection with the Hue bridge, we are able to receive these events and send control requests over a single TCP connection.

For further information about HTTP/2, here are a couple good resources:

HTTP/2 Client

For HTTPS, we had used Axios as the client. However, Axios does not currently support HTTP/2, so we will use the http2 Node.js library instead. Here is an example of a thin client wrapped around http2.

import http2, { ClientHttp2Session, IncomingHttpHeaders, IncomingHttpStatusHeader } from "http2";

export interface Response {
  headers: IncomingHttpHeaders & IncomingHttpStatusHeader
  data: string
}

export class Http2Client{
  private http2Connection: ClientHttp2Session;

  constructor(url: string, options?: http2.ClientSessionOptions | http2.SecureClientSessionOptions) {
    // Create a single HTTP2 connection for the client
    this.http2Connection = http2.connect(url, options);
    this.http2Connection.on('error', (err) => {
      console.error(err);
      this.http2Connection.close();
    });
  }

  public async get(path: string, headers: http2.OutgoingHttpHeaders): Promise<void> {
    await this.executeRequest({ ":method": "GET", ':path': path, ...headers });
  }

  public async put(path: string, headers: http2.OutgoingHttpHeaders, body: any): Promise<void> {
    await this.executeRequest({ ":method": "PUT", ':path': path, ...headers }, body);
  }

  public async post(path: string, headers: http2.OutgoingHttpHeaders, body: any): Promise<void> {
    await this.executeRequest({ ":method": "POST", ':path': path, ...headers }, body);
  }

  public async delete(path: string, headers: http2.OutgoingHttpHeaders): Promise<void> {
    await this.executeRequest({ ":method": "DELETE", ':path': path, ...headers });
  }

  private executeRequest(headers: http2.OutgoingHttpHeaders, body?: any): Promise<Response> {
    return new Promise((resolve, reject) => {
      const stream = this.http2Connection.request(headers);
      if (body !== undefined) {
        // This example is only setup to work with JSON formatted request data.
        stream.write(JSON.stringify(body), 'utf8');
      }

      // This example is only setup to work with text response data.
      stream.setEncoding('utf8');
      let response: Response = {
        headers: {},
        data: ""
      }
      stream.on('response', (responseHeaders: IncomingHttpHeaders & IncomingHttpStatusHeader) => {
        response.headers = responseHeaders;
      });

      // Listens for response data
      stream.on('data', (chunk) => {
          response.data += chunk;
      });

      // The server will end its side of the stream after sending the response.
      stream.on('end', () => {
          stream.close();
          resolve(response);
      });

      stream.on('error', (e) => {
          reject(e);
      });

      // End this side of the stream after the 
      stream.end();
    });
  }
}

With the client in place, we can create a connection with the Hue bridge, like so:

const http2Client: Http2Client = new Http2Client(
  `https://${bridgeIp}`,
  {
    ca: ca,
    checkServerIdentity: (hostname: string, cert: PeerCertificate) => {
      if (cert.subject.CN === this.bridge.getId().toLowerCase()) {
        console.log("Successful server identity check!");
        return undefined;
      } else {
        return new Error("Server identity check failed. CN does not match bridgeId.");
      }
    },
  });

Server-Sent Events

The above example client supports sending requests to a Hue bridge, but we also want to be able to receive events from it. To do that, we just need to add one additional method to the client. The event connection is started by sending a get request with a "text/event-stream" accept header. The server then holds open the stream on its end and a data event is triggered for each server-side event.

public createEventSource(
  path: string,
  headers: http2.OutgoingHttpHeaders,
  onData: (data: string) => void,
  onClose: (error?: any) => void): void {

  const stream = this.http2Connection.request({ ":method": "GET", ':path': path, "Accept": "text/event-stream", ...headers });
  stream.setEncoding('utf8');

  // Each data event will contain a single event from the Hue bridge.
  stream.on('data', (data) => {
      onData(data);
  });
  stream.on('end', () => {
      stream.close();
      onClose();
  });
  stream.on('error', (error) => {
    console.error(error);
    stream.close();
    onClose(error);
  });
  stream.end();
}

The event source needs to be created with the /eventstream/clip/v2 path, and a hue-application-key header must be set to authorize the call.

await http2Client.createEventSource(
  "/eventstream/clip/v2",
  { "hue-application-key": `${bridgeUsername}` },
  (data: string) => { console.log(data) },
  (error?: any) => { console.log(`Event source closed! ${error}`)});

Example Event

If you turn on a Philips light through the Hue app after making the event source connection then you will see a data response, like so:

id: 1634576695:0
data: [{&quot;creationtime&quot;:&quot;2021-10-18T17:04:55Z&quot;,&quot;data&quot;:[{&quot;id&quot;:&quot;e706416a-8c92-46ef-8589-3453f3235b13&quot;,&quot;on&quot;:{&quot;on&quot;:true},&quot;owner&quot;:{&quot;rid&quot;:&quot;3f4ac4e9-d67a-4dbd-8a16-5ea7e373f281&quot;,&quot;rtype&quot;:&quot;device&quot;},&quot;type&quot;:&quot;light&quot;}],&quot;id&quot;:&quot;9de116fc-5fd2-4b74-8414-0f30cb2cbe04&quot;,&quot;type&quot;:&quot;update&quot;}]

(This example response is from the Hue developer docs. It doesn't contain any of my personal Hue data.

Wrapping Up

This post covered how to make an HTTPS connection with a Philips Hue bridge with HTTP/2. It also showed how to listen for server-sent events from the Hue bridge, which is a cool new feature of the v2 API. In the next post, I will build a home automation tool that uses these server-sent events.