1.3.2 • Published 2 years ago

hos-connect-c2c-helper v1.3.2

Weekly downloads
-
License
Apache-2.0
Repository
-
Last release
2 years ago

HOS Connect C2C Integration Helper usage

Overview

The purpose of the helper library is to simplify the integration process by handling the HOS Connect C2C protocol for you, which includes elements like early validation of responses before sending them back to HOS Connect cloud, and general simplification of the API offered.

Requirements

The following features of the system to be integrated with HOS Connect C2C need to be met in order to succeed:

  • an OAuth 2.0 service supporting authorization_code flow.
  • a way to tell userId given an access token issued for that user.
  • access to API, of any sort, allowing your integration code to read and modify state of the system to be integrated. In particular allowing to obtain device state, set device state and obtain a list of devices owned by a given user (by userID or by user's access token)

Walk-through

This section is provided as a step-by-step guidance. You can follow along as you code.

In order to use the helper, you need to first instantiate HiLinkHelper object. Please examine the below code sample:

import HelperModule from 'hos-connect-c2c-helper';

import CentralAccessTokenCheck from './hilink/centralAccessTokenCheckImpl.js';
import AccountAssociation from './hilink/accountAssociationImpl.js';
import DeviceConversion from './hilink/deviceConversionImpl.js';
import ConfigurationInfo from './hilink/configurationInfoImpl.js';
import HttpRequestMaker from './hilink/httpRequestMakerImpl.js';

const HelperApi = HelperModule.com.huawei.hilink.c2c.integration.helper.api;

/**
 * Constructs a HiLinkHelper instance.
 * HiLinkHelper is a library simplifying HOS Connect C2C integrations.
 */
const Helper = new HelperApi.HiLinkHelper(
    CentralAccessTokenCheck,
    AccountAssociation,
    DeviceConversion,
    ConfigurationInfo,
    HttpRequestMaker,
);

export default Helper;

As you can see, there are several dependencies that are needed to instantiate the helper. Those are meant to translate various parts of the protocol to and from the HOS Connect C2C standard.

Once they are implemented, the majority of work is done. Below we will address each of them in greater detail.

If you choose so, you can omit the CentralAccessTokenCheck dependency and pass a null instead. More on that in the Central Access Token Check section below.

Central Access Token Check

The first decision to make is whether you want to implement the central AT check or instead pass each request from HOS Connect to your system with AT and only validate the AT there.

When the central check is implemented, helper will use it to get user id based on the AT, and in all other dependencies of HiLinkHelper object, authorizedUserIdOrNull will be set to that user id. When central check is not implemented, the authorizedUserIdOrNull will be null. The property is part of userInfo object provided to most methods across dependencies.

Below is an example of CentralAccessToken implementation:

import UserInfoClient from '../api/userInfoClient.js';

/**
 * Handles converting access token to userID.
 * It is an optional thing to implement and pass
 * to the HiLinkHelper's constructor.
 *
 * When present, in each of the methods in deviceConversionImpl.js
 * and accountAssociationImpl.js, userInfo param will contain a
 * non-null authorizedUserIdOrNull property. Otherwise, the property
 * will be null.
 */
const CentralAccessTokenCheck = {
    getUserIdByToken: async function (accessToken, resultConsumer) {
        const response = await UserInfoClient.getUserInfo(accessToken);
        if (response && response.id) {
            resultConsumer.onSuccess(response.id.toString());
        } else {
            resultConsumer.onUnauthorized();
        }
    },
};

export default CentralAccessTokenCheck;

Here you can see a single dependency, which provides userInfo in a way specific to the partner system. You can use a similar approach, if your OAuth provides /userInfo endpoint, but in general - any way to obtain user id based on the AT will do, as long as it's performant enough.

Please also note that the userId passed to the onSuccess consumer needs to be a String, so if you use numeric ids, like in the example above, please make sure to convert them.

Another thing, which will be a common theme across all dependencies of HiLinkHelper, is the presence of some variation of a "consumer" object, which is basically a scaffolding for callbacks. They will mostly provide methods like onSuccess, onResult and onError, while also providing onUnauthorized and onIllegalAccess.

Important: please make sure to always call one, and only one, of the callbacks in a given execution path.

Account Association, aka linking accounts

The first step, from the end-user's perspective, is account linking. In AiLife application, they will choose your brand from a list, and they will be redirected to your OAuth 2.0 /authorize endpoint.

Once they authorize HOS Connect to access their account in partner system, HOS Connect cloud will obtain AT from your OAuth 2.0 and afterwards the AccountAssociation's associate() function will be called with your implementation of it.

The main purpose of the associate() function is to obtain and persist account association information. Particularly the userId and HuaweiId association, for later look-up. Please make sure to save the huaweiID of the user (along with the userId it corresponds to) to a database in associate().

The association can be deleted from database in dropAssociation() function implementation, which is called when end-user un-links the accounts.

Below is a sample implementation of the AccountAssociation object:

import AccountAssociationsRepository from '../repository/associationsRepository.js';
import WebhooksManager from '../webhooksManager.js';

/**
 * When user clicks on "link accounts" in Ai Life app
 * and finishes the OAuth 2.0 flow granting the
 * necessary authority, the associate() method
 * will be called.
 *
 * associate() and dropAssociation() can be used
 * to invoke any actions partner system requires
 * in terms of setup and cleanup respectively.
 */
const AccountAssociation = {
    associate: async function (userInfo, consumer) {
        const userId = parseInt(userInfo.authorizedUserIdOrNull);
        await AccountAssociationsRepository.save(
            userId,
            userInfo.huaweiId,
        );
        await WebhooksManager.register(
            userInfo.accessToken,
            userId,
        );
        consumer.onSuccess(null);
    },
    dropAssociation: async function (userInfo, consumer) {
        const userId = parseInt(userInfo.authorizedUserIdOrNull);
        await AccountAssociationsRepository.deleteByUserId(
            userId,
        );
        await WebhooksManager.unregister(userInfo.accessToken);
        consumer.onSuccess();
    },
};

export default AccountAssociation;

Please note that, as always, a proper consumer callback is called by the implementation.

Note 2: In associate function, the consumer.onSuccess(null) in called in particular, which is possible when CentralAccessTokenCheck implementation is provided. Otherwise, userId (in a string form) should be passed as a param there.

Note 3: In the example, association and unlinking accounts also involves additional, partner-specific setup and cleanup actions, in this case webhooks registration and un-registration respectively. You can analogically trigger other custom setup/cleanup operations there.

Device Conversion

That implementation takes care of incoming requests from HOS Connect. It needs to provide existing devices owned by a given user (by userId or access token), change state of devices (like turning a smart-light on and off) and read their states (obtaining a list of device properties and their states).

Below is an example of implementation of DeviceConversion object:

import helper from 'hos-connect-c2c-helper';

import DeviceAccessClient from '../api/deviceAccessClient.js';
import DiscoveryConversion from './conversion/discoveryConversion.js';
import CommandConversion from './conversion/commandConversion.js';
import SnapshotConversion from './conversion/snapshotConversion.js';

const HelperApi = helper.com.huawei.hilink.c2c.integration.helper.api;
const CommandResult = HelperApi.CommandResult;

/**
 * Here HOS Connect's requests are handled with use of Partner API and
 * /hilink/conversion/* implementations in order to convert properties
 * and values to and from Partner format.
 */
const DeviceConversion = {
    discover: async function (userInfo, consumer) {
        const devicesInfo =
            await DeviceAccessClient.getDevices(userInfo.accessToken);

        if (devicesInfo.error) {
            switch (devicesInfo.error) {
                case 'unauthorized':
                    return consumer.onUnauthorized();
                case 'invalid params':
                    return consumer.onError('failed to obtain devices list, invalid params');
                default:
                    return consumer.onError('unknown error');
            }
        }

        const devicesConverted =
            DiscoveryConversion.mapAllToHiLinkFormat(devicesInfo);

        // convert data to helper's format
        const helperFormat = devicesConverted.map((it) => {
            const servicesInfo = it.deviceServices.map((info) => {
                return new HelperApi.HiLinkDeviceService(
                    info.serviceType,
                    info.serviceId,
                );
            });

            return new HelperApi.DeviceInformation(
                it.deviceId,
                it.deviceName,
                it.prodId,
                it.roomName,
                it.model,
                it.devType,
                it.manu,
                helper.convertArrayToArrayList(servicesInfo),
            );
        });

        // convert the array to a type required by helper
        const deviceInfoArrayList = helper.convertArrayToArrayList(helperFormat);
        consumer.onSuccess(deviceInfoArrayList);
    },
    executeCommand: async function (
        userInfo,
        deviceId,
        serviceId,
        dataJson,
        prodId,
        consumer,
    ) {
        const serviceData = JSON.parse(dataJson);
        const partnerCommand = CommandConversion.toPartnerFormat(
            serviceId,
            serviceData,
        );

        const apiResponse = await DeviceAccessClient.setProperty(
            userInfo.accessToken,
            deviceId,
            partnerCommand.propName,
            partnerCommand.propValue,
        );

        if (apiResponse.error) {
            switch (apiResponse.error) {
                case 'unauthorized':
                    return consumer.onResult(CommandResult.UNAUTHORIZED);
                case 'device not found':
                    return consumer.onResult(CommandResult.DEVICE_DOES_NOT_EXIST);
                default:
                    return consumer.onResult(CommandResult.FAILURE);
            }
        }

        return consumer.onResult(CommandResult.SUCCESS);
    },
    snapshot: async function (userInfo, deviceId, consumer) {
        // fetching information on user's devices
        const devices = await DeviceAccessClient
            .getDevices(userInfo.accessToken);

        if (devices.error) {
            switch (devices.error) {
                case 'unauthorized':
                    return consumer.onUnauthorized();
                case 'invalid params':
                    return consumer.onError('failed to obtain devices list, invalid params');
                default:
                    return consumer.onError('unknown error');
            }
        }

        // taking just the requested one
        const device = devices
            .filter((device) => device.deviceId.toString() === deviceId)[0];

        if (!device) {
            return consumer.onError('no device with requested ID found');
        }

        // obtaining values via partner api client
        const promisedPropertiesData = device.properties
            .map((property) => {
                return DeviceAccessClient.getProperty(
                    userInfo.accessToken,
                    deviceId,
                    property,
                );
            });

        const propertiesData =
            await Promise.all(promisedPropertiesData);

        if (propertiesData.some((value) => value === undefined)) {
            return consumer.onError(
                'failed to obtain all properties\' data from partner system',
            );
        }

        // protocol conversion to HiLink format
        const convertedData =
            SnapshotConversion.mapAllToHiLinkFormat(propertiesData)
                .map((service) => {
                    return {
                        // eslint-disable-next-line new-cap
                        timestamp: helper.convertNumberToLong(service.timestamp),
                        serviceId: service.serviceId,
                        serviceDataJson: JSON.stringify(service.serviceData),
                    };
                });

        const snapshot = new HelperApi.DeviceSnapshot(
            deviceId,
            device.isOnline,
            helper.convertArrayToArrayList(convertedData),
        );

        consumer.onSuccess(snapshot);
    },
};

export default DeviceConversion;

consumer.onError('some error's message text'); can be used to signalize a failure to respond to a request. One exception is executeCommand() function, where consumer.onResult() has to be used, with one of predefined possible values. Notice the use of the:

import helper from 'hos-connect-c2c-helper';
// (...)
const HelperApi = helper.com.huawei.hilink.c2c.integration.helper.api;
const CommandResult = HelperApi.CommandResult;
// (...)
return consumer.onResult(CommandResult.SUCCESS);

It is advisable to use return statement when calling consumer's callbacks, as this helps avoid calling more than one callback or calling the same callback more than once, which would lead to a crash.

For compatibility reasons, type converter util-functions are offered by the helper library. This is because the library is not a JS project itself, but a transpiled Kotlin Multiplatform code. Please notice the following example:

import helper from 'hos-connect-c2c-helper';
//(...)
const snapshot = new HelperApi.DeviceSnapshot(
        deviceId,
        device.isOnline,
        // a converter util function use:
        helper.convertArrayToArrayList(convertedData),
);

Other than the util methods, please notice the final formats of data sent to HOS Connect. For discovery the formats are provided by a dependency "DeviceAccessClient" which is one way to implement it, the file can look like the following sample:

/**
 * Device discovery of HOS Connect needs some params, which are also
 * statically defined in the HOS Connect console for each device model.
 *
 * The parameters required from the partner API side are:
 * {model, id, name};
 */
const conversions = {
          'K8': ((params) => {
            return {
                deviceId: params.id,
                deviceName: params.name,
                roomName: null,
                model: '123',
                devType: '046',
                manu: '001',
                prodId: '98G4',
                deviceServices: [
                    {serviceType: 'switch', serviceId: 'switch'},
                ],
            };
        }),
        'Z3': ((params) => {
            return {
                deviceId: params.id,
                deviceName: params.name,
                roomName: null,
                model: '1.0.0.2',
                devType: '01B',
                manu: '001',
                prodId: '9A4J',
                deviceServices: [
                    {serviceType: 'switch', serviceId: 'switch'},
                    {serviceType: 'brightness', serviceId: 'brightness'},
                    {serviceType: 'colour', serviceId: 'colour'},
                ],
            };
        }),
    };

const DiscoveryConversion = {
    toHiLinkFormat: function (model, id, name) {
        const params = {model, id, name};
        const convertedObject = conversions[model](params);
        return convertedObject;
    },
    mapAllToHiLinkFormat(devices) {
        return devices.map((info) => {
            return this.toHiLinkFormat(
                info.model,
                info.deviceId.toString(),
                info.deviceName,
            );
        });
    },
};

export default DiscoveryConversion;

Where K8 and Z3 are device models, and the properties of each object are required fields for HOS Connect device discovery.

For executeCommand function, a second dependency is used, as a reference implementation is provided below:

/**
 * Converts from HOS Connect param names and value formats,
 * to partner property names and formats of data.
 * The key in the mapping is the HOS Connect property name,
 * like 'switch' for example.
 */
const conversions = {
          'switch': ((params) => {
            const convertedValue = params.on === 1 ? 'on' : 'off';
            return {
                propName: 'on_off',
                propValue: convertedValue,
            };
        }),
        'brightness': ((params) => {
            const convertedValue = params.brightness;
            return {
                propName: 'brightness',
                propValue: convertedValue,
            };
        }),
        'colour': ((params) => {
            const channels = [params.red, params.green, params.blue];
            const strings = channels.map((int) => int.toString(16));
            const padded = strings.map((str) => str.length == 1 ? '0' + str : str);
            const convertedValue = '#' + padded.join('');
            return {
                propName: 'colour',
                propValue: convertedValue,
            };
        }),
    };

const CommandConversion = {
    toPartnerFormat: function (serviceId, serviceData) {
        const partnerCommand = conversions[serviceId](serviceData);
        return partnerCommand;
    },
};

export default CommandConversion;

The conversions object contains keys (HOS Connect DeviceServices (property names)) and conversion methods appropriate for each of those services. For example "switch" is converted into a partner "on_off" property. The above code is just an example of how the conversion logic might be structured.

Finally, SnapshotConversion dependency is doing the same as the CommandConversion, but in the opposite direction. That is from partner format to HOS Connect format. Example implementation can be:

/**
 * Converts property names and values from partner format
 * to HOS Connect format.
 * The key in the mapping is the property name in the partner system.
 * The corresponding HOS Connect property name and value format can
 * be found in HOS Connect console, or obtained directly from Huawei.
 */
const conversions = {
          'on_off': ((value) => {
            return {
                serviceId: 'switch',
                serviceData: {
                    on: value === 'on' ? 1 : 0,
                },
            };
        }),
        'brightness': ((value) => {
            return {
                serviceId: 'brightness',
                serviceData: {
                    brightness: value,
                },
            };
        }),
        'colour': ((value) => {
            const hex = value[0] === '#' ? value.substring(1) : value;
            const parts = hex.match(/.{1,2}/g)
                .map((value) => parseInt(value, 16));
            return {
                serviceId: 'colour',
                serviceData: {
                    red: parts[0],
                    green: parts[1],
                    blue: parts[2],
                    white: 255,
                },
            };
        }),
    };

const SnapshotConversion = {
    toHiLinkFormat: function (propName, propValue, timestamp) {
        const converted = conversions[propName](propValue);
        const snapshotData = {
            timestamp: timestamp,
            serviceId: converted.serviceId,
            serviceData: converted.serviceData,
        };
        return snapshotData;
    },
    mapAllToHiLinkFormat(propertiesData) {
        return propertiesData
            .map((propertyData) => {
                console.log(propertyData);
                return this.toHiLinkFormat(
                    propertyData.propName,
                        propertyData.propValue,
                        propertyData.timestamp,
                );
            });
    },
};

export default SnapshotConversion;

Configuration info

The ConfigurationInfo object contains urls, appId and appSecret needed by HOS Connect C2C integration. It might look like in the example below:

import HelperModule from 'hos-connect-c2c-helper';
import Resources from '../cfg/resources.js';

const HelperApi = HelperModule.com.huawei.hilink.c2c.integration.helper.api;

const ConfigurationInfo = new HelperApi.ConfigurationInfo(
        Resources.cfgInfoParams.partnerAppId,
        Resources.cfgInfoParams.partnerAppSecret,
        Resources.cfgInfoParams.huaweiOAuthUrl,
        Resources.cfgInfoParams.hiLinkNotificationsUrl,
);

export default ConfigurationInfo;

Please note that all of the parameters of the constructor are Strings.

Http Request Maker

The last dependency of the HiLinkHelper is the HttpRequestMaker. The helper library will use it in order to make requests related to events, including handling of authorization. The appId and appSecret you've provided in the ConfigurationInfo will be used as credentials here.

Example implementation is the following:

import fetch from 'node-fetch';
import helper from 'hos-connect-c2c-helper';

/**
 * Provides HiLinkHelper with a way to access the outer world.
 * It is used to authorize with Huawei's OAuth 2.0 service
 * and then send events to HOS Connect cloud.
 */
const HttpRequestMaker = {
  postApplicationJsonRequest: async function (
          address,
          authorizationHeader,
          bodyJson,
          consumer,
  ) {
    try {
      const response = await fetch(address, {
        method: 'POST',
        headers: {
          'Authorization': 'Bearer ' + authorizationHeader,
          'Content-Type': 'application/json',
          'accept': ['application/json'],
        },
        body: bodyJson,
      });

      const resBody = await response.json();
      const rawJson = JSON.stringify(resBody);

      console.log(`successfully sent app json request initiated by helper`);
      console.log(`sent body:${bodyJson}`);
      console.log(`got response:${rawJson}`);

      consumer.onSuccess({body: rawJson, httpCode: response.status});
    } catch (error) {
      console.log(error);
      return consumer.onError();
    }
  },
  postUrlEncodedRequest: async function (address, params, consumer) {
    const paramsArray = helper.convertMapToArray(params);
    try {
      const formBody = new URLSearchParams();

      paramsArray.forEach((element) => {
        formBody.append(element.key, element.value);
      });

      const response = await fetch(address, {
        method: 'POST',
        headers: {'Content-Type': 'application/x-www-form-urlencoded'},
        body: formBody,
      });

      const resBody = await response.json();
      const rawJson = JSON.stringify(resBody);

      console.log(`successfully sent url-encoded request initiated by helper`);
      console.log(`got response:${rawJson}`);

      consumer.onSuccess({body: rawJson, httpCode: response.status});
    } catch (error) {
      console.log(error);
      return consumer.onError();
    }
  },
};

export default HttpRequestMaker;

HiLinkHelper usage

With the above implementations in place, and a HiLinkHelper object instantiated, the next step is to provide a controller to handle incoming requests from HOS Connect cloud, and optionally also setup events sending, in order to notify HOS Connect about changes in user's IOT devices (addition, removal, statusChange).

Receiving inbound communication from HOS Connect cloud

A sample of code receiving the requests is below:

import express from 'express';
import Helper from '../helperConfiguration.js';

// eslint-disable-next-line new-cap
const router = express.Router();

router.post('/', (req, res) => {
  Helper.processRequest(
          req.headers.authorization,
          JSON.stringify(req.body),
          {
            onResponse: function (response) {
              console.log(response.body);
              console.log(response.httpCode);
              res.status(response.httpCode).send(response.body);
            },
          },
  );
});

export default router;

Please make sure to log at least the responses prepared by the helper. They will contain error messages and might prove helpful in diagnostics of issues.

Sending outbound communication to HOS Connect cloud (events)

HiLinkHelper has several functions for sending 3 types of events (device addition, device removal, device status change) all of which come in two varieties: regular and "auto". The automatic variants of the functions differ in just the fact they will use your implementation of DeviceConversion, previously passed as a param to HiLinkHelper's constructor, while the regular variants of the functions will require you to provide data manually.

You can reuse the below util for events sending or implement something along the lines on your own.

import helper from 'hos-connect-c2c-helper';

import Helper from './helperConfiguration.js';
import SnapshotConversion from './hilink/conversion/snapshotConversion.js';
import DiscoveryConversion from './hilink/conversion/discoveryConversion.js';
import AccountAssociationsRepository
  from './repository/associationsRepository.js';

const HelperApi = helper.com.huawei.hilink.c2c.integration.helper.api;

async function getHuaweiIdByUserId(userId) {
  return AccountAssociationsRepository.getHuaweiIdByUserId(userId);
}

const eventResponseLogger = {
  onResponse: function (response) {
    console.log('event sending resulted in: ' + response.status +
            ', with description: ' + response.description);
  },
};

/**
 * This object wraps around the HiLinkHelper's notify* functions
 * and provides conversion based on logic defined in
 * ./hilink/conversion/snapshotConversion.js
 *  and
 * ./hilink/conversion/discoveryConversion.js
 * as well as requires to be provided a repository of
 * partnerUserId to huaweiId mapping, which is supposed to be
 * obtained during account linking. The repository is expected to
 * live at ./repository/associationsRepository.js
 *
 * Please make sure you've modified the aforementioned files to
 * suit your IOT devices system and performance/scalability requirements.
 *
 * Sample of expected format from the snapshot conversion:
 * ```js
 * {
 *  timestamp: 1641301846685,
 *  serviceId: 'switch',
 *  serviceData: { on: 0 }
 * }
 * ```
 * Sample of expected format from the discovery conversion:
 * ```js
 * {
 *   deviceId: 'hilm789',
 *   deviceName: 'Valve',
 *   roomName: null,
 *   model: '123',
 *   devType: '046',
 *   manu: '001',
 *   prodId: '98G4',
 *   deviceServices: [ { serviceType: 'switch', serviceId: 'switch' } ]
 * }
 * ```
 * The repository is expected to have getHuaweiIdByUserId(userId) function,
 * returning String.
 */
const EventsSender = {
  /**
   * Lets HOS Connect cloud know about a change in the state of a device.
   * Could be a change of one or more properties, or a change in isOnline status.
   * @param {String} userId Id of the user in the partner system.
   * Will be used both as is and to query
   * for HuaweiId of the user.
   * @param {String} deviceId
   * @param {Array<*>} deviceProperties Properties supported by the device.
   * All readable ones.
   * Example value:
   * ```js
   * [ { propName: 'on_off', propValue: 'off', timestamp: 1641301203109 } ]
   * ```
   * The timestamp is milliseconds since 01.01.1970 00:00:00 UTC.
   * Prop name and value come from your system and need to be handled
   * by your implementation of snapshot conversion.
   * @param {Boolean} isOnline Whether or not the device is online
   */
  notifyDeviceStatusChange: async function (
          userId,
          deviceId,
          deviceProperties,
          isOnline,
  ) {
    const huaweiId = await getHuaweiIdByUserId(userId);
    // protocol conversion to HiLink format
    const convertedData =
            SnapshotConversion.mapAllToHiLinkFormat(deviceProperties)
                    .map((service) => {
                      return {
                        // eslint-disable-next-line new-cap
                        timestamp: helper.convertNumberToLong(service.timestamp),
                        serviceId: service.serviceId,
                        serviceDataJson: JSON.stringify(service.serviceData),
                      };
                    });

    const snapshot = new HelperApi.DeviceSnapshot(
            deviceId,
            isOnline,
            helper.convertArrayToArrayList(convertedData),
    );

    Helper.notifyDeviceStatusChange(
            userId,
            huaweiId,
            snapshot,
            eventResponseLogger,
    );
  },
  /**
   * Uses your implementation in DeviceConversion.snapshot() for creation
   * of the object describing the state
   * of a given device.
   * @param {String?} accessToken an access token that will be presented
   * in your snapshot conversion implementation.
   * You can pass null here, in which case an empty string will be
   * presented in the DeviceConversion.snapshot impl.
   * @param {String} userId
   * @param {String} deviceId
   */
  notifyDeviceStatusChangeAuto: async function (accessToken, userId, deviceId) {
    const huaweiId = await getHuaweiIdByUserId(userId);
    Helper.notifyDeviceStatusChangeAuto(
            accessToken,
            userId,
            huaweiId,
            deviceId,
            eventResponseLogger,
    );
  },
  /**
   * Lets HiLink know about a new device being added/paired by end user.
   * @param {String} userId
   * @param {String} deviceModel device model, recognized by your
   * implementation of deviceConversion
   * @param {String} deviceId
   * @param {String} deviceName displayed name of the device
   */
  notifyDeviceAddition: async function (
          userId,
          deviceModel,
          deviceId,
          deviceName,
  ) {
    const huaweiId = await getHuaweiIdByUserId(userId);

    // obtain full device info for HiLink
    const deviceConverted = DiscoveryConversion.toHiLinkFormat(
            deviceModel,
            deviceId,
            deviceName,
    );

    // convert data to helper's format
    const servicesInfo = deviceConverted.deviceServices.map((info) => {
      return new HelperApi.HiLinkDeviceService(
              info.serviceType,
              info.serviceId,
      );
    });

    const hiLinkDeviceInfo = new HelperApi.DeviceInformation(
            deviceConverted.deviceId,
            deviceConverted.deviceName,
            deviceConverted.prodId,
            deviceConverted.roomName,
            deviceConverted.model,
            deviceConverted.devType,
            deviceConverted.manu,
            helper.convertArrayToArrayList(servicesInfo),
    );

    Helper.notifyDeviceAddition(
            userId,
            huaweiId,
            hiLinkDeviceInfo,
            eventResponseLogger,
    );
  },
  /**
   * Lets HiLink know about a new device, uses your implementation
   * in DeviceConversion.discover in order to obtain information
   * about the device.
   * @param {String?} accessToken an access token that will be presented
   * in your discovery conversion implementation.
   * You can pass null here, in which case an empty string will be presented
   * in the DeviceConversion.discover impl.
   * @param {String} userId
   * @param {String} deviceId
   */
  notifyDeviceAdditionAuto: async function (accessToken, userId, deviceId) {
    const huaweiId = await getHuaweiIdByUserId(userId);
    Helper.notifyDeviceAdditionAuto(
            accessToken,
            userId,
            huaweiId,
            deviceId,
            eventResponseLogger,
    );
  },
  /**
   * Lets HiLink know about a device being removed/unpaired by the user.
   * @param {String} userId
   * @param {String} deviceId
   */
  notifyDeviceRemoval: async function (userId, deviceId) {
    const huaweiId = await getHuaweiIdByUserId(userId);
    Helper.notifyDeviceRemoval(
            userId,
            huaweiId,
            deviceId,
            eventResponseLogger,
    );
  },
};

export default EventsSender;