node.jsgrpcgoogle-smart-homegrpc-node

I called the ReportStateAndNotification rpc function and kept getting the error "Request contains an invalid argument."


I want to report the device status to Google Home Graph via RPC communication. In the program, I use @grpc/grpc-js library and refer to the data structure agreed in Google official document (https://developers.home.google.com/reference/home-graph/rpc/google.home.graph.v1#google.home.graph.v1.ReportStateAndNotificationRequest) to call reportStateAndNotification function, but it always prompts me that the parameters are wrong. The error message is "Error: 3 INVALID_ARGUMENT: Request contains an invalid argument."

The code I use is the following :

const grpc = require('@grpc/grpc-js');
const { GoogleAuth } = require('google-auth-library');
const protoLoader = require('@grpc/proto-loader');
const protos = require('google-proto-files');
const uuid = require('uuid');

async function getCredentials() {
  const sslCredentials = grpc.credentials.createSsl();
  const googleAuth = new GoogleAuth({
    scopes: 'https://www.googleapis.com/auth/homegraph'
  });
  const authClient = await googleAuth.getClient();
  const callCredentials = grpc.credentials.createFromGoogleCredential(
    authClient
  );
  const credentials = grpc.credentials.combineChannelCredentials(
    sslCredentials,
    callCredentials
  );
  return credentials;
}

async function getHomegraph() {
  const homegraphProto = await protoLoader.load(
    protos.getProtoPath('home/graph', 'v1', 'homegraph.proto'), {
    includeDirs: [protos.getProtoPath('..')]
  }
  );
  const homegraph = grpc.loadPackageDefinition(
    homegraphProto
  ).google.home.graph.v1;
  return homegraph;
}

(async function () {
  const credentials = await getCredentials();
  const homegraph = await getHomegraph();

  const homegraphService = new homegraph.HomeGraphApiService(
    'homegraph.googleapis.com', credentials
  );

  const changeReportData = {
    "requestId": uuid.v4(),
    "agentUserId": "51f82f14-aba6-417e-8ca8-5827dc5b3de6",
    "payload": {
      "devices": {
        "states": {
          "10002b10b4": {
            "on": true,
            "online": true
          }
        }
      }
    }
  };

  homegraphService.reportStateAndNotification(changeReportData, function (err, result) {
    if (err) {
      console.error(err);
    } else {
      console.log(result);
    }
  });
})();

Data sent

test1

{
    "requestId": "43c81c50-7628-44e7-9668-14ef3ea5e759",
    "agentUserId": "51f82f14-aba6-417e-8ca8-5827dc5b3de6",
    "payload": {
        "devices": {
            "states": {
                "10002b10b4": {
                    "on": true,
                    "online": true
                }
            }
        }
    }
}

test2

{
    "request_id": "d887f9ee-81eb-480b-9bf3-412132ec7cec",
    "agent_user_id": "51f82f14-aba6-417e-8ca8-5827dc5b3de6",
    "payload": {
        "devices": {
            "states": {
                "10002b10b4": {
                    "on": true,
                    "online": true
                }
            }
        }
    }
}

This error show all the time no matter data I sended with name agent_user_id or agentUserId like above. But I was written according to the official example ReportStateAndNotificationRequest.


Solution

  • In the request, payload.devices.states has the type Struct. The representation of a Struct in protobuf is not a simple JSON object, but a map of field names to Value objects. For example, in your test, the states object

    {
      "10002b10b4": {
        "on": true,
        "online": true
      }
    }
    

    would be represented like this:

    {
      "10002b10b4": {
        "struct_value": {
          "on": {
            "bool_value": true
          },
          "online": {
            "bool_value": true
          }
        }
      }
    }
    

    It is possible to perform this transformation automatically, using a function like this one in @grpc/grpc-js-xds:

    function validateValue(obj: any): Value {
      if (Array.isArray(obj)) {
        return {
          kind: 'listValue',
          listValue: {
            values: obj.map((value) => validateValue(value)),
          },
        };
      } else {
        switch (typeof obj) {
          case 'boolean':
            return {
              kind: 'boolValue',
              boolValue: obj,
            };
          case 'number':
            return {
              kind: 'numberValue',
              numberValue: obj,
            };
          case 'string':
            return {
              kind: 'stringValue',
              stringValue: obj,
            };
          case 'object':
            if (obj === null) {
              return {
                kind: 'nullValue',
                nullValue: 'NULL_VALUE',
              };
            } else {
              return {
                kind: 'structValue',
                structValue: getStructFromJson(obj),
              };
            }
          default:
            throw new Error(`Could not handle struct value of type ${typeof obj}`);
        }
      }
    }
    
    function getStructFromJson(obj: any): Struct {
      if (typeof obj !== 'object' || obj === null) {
        throw new Error('Invalid JSON object for Struct field');
      }
      const fields: { [key: string]: Value } = {};
      for (const [fieldName, value] of Object.entries(obj)) {
        fields[fieldName] = validateValue(value);
      }
      return {
        fields,
      };
    }