Added Endpoint for pushing consumptions
MODIFY
src/assets/server/rest/v1/docs/e-mobility-oas.json
src/assets/server/rest/v1/docs/e-mobility-oas.json
}
}
},
+ "/api/assets/{id}/consumption": {
+ "post": {
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "description": "Push latest asset consumption",
+ "parameters": [
+ {
+ "$ref": "#/components/parameters/assetID"
+ }
+ ],
+ "tags": [
+ "Assets"
+ ],
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/AssetConsumption"
+ }
+ }
+ }
+ },
+ "responses": {
+ "201": {
+ "description": "Asset consumption created successfully",
+ "content": {
+ "application/json": {
+ "example": {
+ "consumption": "###",
+ "status": "Success"
+ }
+ }
+ }
+ },
+ "401": {
+ "$ref": "#/components/responses/UnauthorizedError"
+ },
+ "403": {
+ "$ref": "#/components/responses/ForbiddenError"
+ },
+ "500": {
+ "$ref": "#/components/responses/BackendError"
+ },
+ "550": {
+ "description": "Asset does not exist"
+ }
+ }
+ }
+ },
"/api/charging-profiles": {
"get": {
"security": [
}
}
},
+ "AssetConsumption": {
+ "type": "object",
+ "required": [
+ "assetID",
+ "startedAt",
+ "endedAt",
+ "instantWatts"
+ ],
+ "properties": {
+ "assetID": {
+ "$ref": "#/components/parameters/assetID"
+ },
+ "startedAt": {
+ "type": "string",
+ "format": "date-time",
+ "example": "2020-12-31T23:00:00.000Z"
+ },
+ "endedAt": {
+ "type": "string",
+ "format": "date-time",
+ "example": "2020-12-31T23:01:00.000Z"
+ },
+ "instantWatts": {
+ "type": "number"
+ },
+ "instantWattsL1": {
+ "type": "number"
+ },
+ "instantWattsL2": {
+ "type": "number"
+ },
+ "instantWattsL3": {
+ "type": "number"
+ },
+ "instantAmps": {
+ "type": "number"
+ },
+ "instantAmpsL1": {
+ "type": "number"
+ },
+ "instantAmpsL2": {
+ "type": "number"
+ },
+ "instantAmpsL3": {
+ "type": "number"
+ },
+ "instantVolts": {
+ "type": "number"
+ },
+ "instantVoltsL1": {
+ "type": "number"
+ },
+ "instantVoltsL2": {
+ "type": "number"
+ },
+ "instantVoltsL3": {
+ "type": "number"
+ },
+ "consumptionWh": {
+ "type": "number"
+ },
+ "consumptionAmps": {
+ "type": "number"
+ },
+ "stateOfCharge": {
+ "type": "number"
+ }
+ }
+ },
"Notifications": {
"type": "object",
"properties": {
"type": "string"
}
},
+ "assetID": {
+ "in": "path",
+ "name": "id",
+ "description": "Asset ID",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
"connectorID": {
"in": "path",
"name": "connectorId",

ADD
src/assets/server/rest/v1/schemas/asset/create-asset-consumption.json
+{
+ "title": "Create Asset Consumptions",
+ "type": "object",
+ "properties": {
+ "assetID": {
+ "$ref": "common.json#/definitions/id"
+ },
+ "startedAt": {
+ "type": "string",
+ "format":"date-time",
+ "sanitize": "mongo"
+ },
+ "endedAt": {
+ "type": "string",
+ "format":"date-time",
+ "sanitize": "mongo"
+ },
+ "instantWatts": {
+ "type": "number",
+ "sanitize": "mongo"
+ },
+ "instantWattsL1": {
+ "type": "number",
+ "sanitize": "mongo"
+ },
+ "instantWattsL2": {
+ "type": "number",
+ "sanitize": "mongo"
+ },
+ "instantWattsL3": {
+ "type": "number",
+ "sanitize": "mongo"
+ },
+ "instantAmps": {
+ "type": "number",
+ "sanitize": "mongo"
+ },
+ "instantAmpsL1": {
+ "type": "number",
+ "sanitize": "mongo"
+ },
+ "instantAmpsL2": {
+ "type": "number",
+ "sanitize": "mongo"
+ },
+ "instantAmpsL3": {
+ "type": "number",
+ "sanitize": "mongo"
+ },
+ "instantVolts": {
+ "type": "number",
+ "sanitize": "mongo"
+ },
+ "instantVoltsL1": {
+ "type": "number",
+ "sanitize": "mongo"
+ },
+ "instantVoltsL2": {
+ "type": "number",
+ "sanitize": "mongo"
+ },
+ "instantVoltsL3": {
+ "type": "number",
+ "sanitize": "mongo"
+ },
+ "consumptionWh": {
+ "type": "number",
+ "sanitize": "mongo"
+ },
+ "consumptionAmps": {
+ "type": "number",
+ "sanitize": "mongo"
+ },
+ "stateOfCharge": {
+ "type": "number",
+ "sanitize": "mongo"
+ }
+ },
+ "required": [
+ "assetID",
+ "startedAt",
+ "endedAt",
+ "instantWatts"
+ ]
+}

MODIFY
src/authorization/Authorizations.ts
src/authorization/Authorizations.ts
@@ -743,6 +743,10 @@ export default class Authorizations {
return Authorizations.canPerformAction(loggedUser, Entity.ASSET, Action.RETRIEVE_CONSUMPTION);
}
+ public static async canCreateAssetConsumption(loggedUser: UserToken): Promise<boolean> {
+ return Authorizations.canPerformAction(loggedUser, Entity.ASSET, Action.CREATE_CONSUMPTION);
+ }
+
public static async canEndUserReportError(loggedUser: UserToken): Promise<boolean> {
return Authorizations.canPerformAction(loggedUser, Entity.NOTIFICATION, Action.CREATE);
}

MODIFY
src/authorization/AuthorizationsDefinition.ts
src/authorization/AuthorizationsDefinition.ts
@@ -169,13 +169,13 @@ const AUTHORIZATION_DEFINITION: AuthorizationDefinition = {
{ resource: Entity.INVOICE, action: [Action.DOWNLOAD, Action.READ], attributes: ['*'] },
{
resource: Entity.ASSET, action: [Action.CREATE, Action.READ, Action.UPDATE, Action.DELETE,
- Action.CHECK_CONNECTION, Action.RETRIEVE_CONSUMPTION], attributes: ['*']
+ Action.CHECK_CONNECTION, Action.RETRIEVE_CONSUMPTION, Action.CREATE_CONSUMPTION], attributes: ['*']
},
{
resource: Entity.ASSETS, action: [Action.LIST, Action.IN_ERROR],
attributes: [
'id', 'name', 'siteAreaID', 'siteArea.id', 'siteArea.name', 'siteArea.siteID', 'siteID', 'assetType', 'coordinates',
- 'dynamicAsset', 'connectionID', 'meterID', 'currentInstantWatts', 'currentStateOfCharge', 'issuer'
+ 'dynamicAsset', 'usesPushAPI', 'connectionID', 'meterID', 'currentInstantWatts', 'currentStateOfCharge', 'issuer'
]
},
{ resource: Entity.SETTINGS, action: Action.LIST, attributes: ['*'] },

MODIFY
src/scheduler/tasks/AssetGetConsumptionTask.ts
src/scheduler/tasks/AssetGetConsumptionTask.ts
@@ -39,6 +39,9 @@ export default class AssetGetConsumptionTask extends SchedulerTask {
);
// Process them
for (const asset of dynamicAssets.result) {
+ if (asset.usesPushAPI) {
+ continue;
+ }
const assetLock = await LockingHelper.createAssetRetrieveConsumptionsLock(tenant.id, asset);
if (assetLock) {
try {

MODIFY
src/server/rest/v1/router/GlobalRouter.ts
src/server/rest/v1/router/GlobalRouter.ts
import express, { NextFunction, Request, Response } from 'express';
+import AssetRouter from './api/AssetRouter';
import AuthRouter from './auth/AuthRouter';
import AuthService from '../service/AuthService';
import BillingRouter from './api/BillingRouter';
@@ -38,6 +39,7 @@ export default class GlobalRouter {
AuthService.checkSessionHash.bind(this),
[
new ChargingStationRouter().buildRoutes(),
+ new AssetRouter().buildRoutes(),
new TagRouter().buildRoutes(),
new TenantRouter().buildRoutes(),
new TransactionRouter().buildRoutes(),

ADD
src/server/rest/v1/router/api/AssetRouter.ts
+/* eslint-disable @typescript-eslint/no-misused-promises */
+import { ServerAction, ServerRoute } from '../../../../../types/Server';
+import express, { NextFunction, Request, Response } from 'express';
+
+import AssetService from '../../service/AssetService';
+import RouterUtils from '../RouterUtils';
+
+export default class AssetRouter {
+ private router: express.Router;
+
+ public constructor() {
+ this.router = express.Router();
+ }
+
+ public buildRoutes(): express.Router {
+ this.buildRouteCreateAssetConsumption();
+ return this.router;
+ }
+
+ protected buildRouteCreateAssetConsumption(): void {
+ this.router.post(`/${ServerRoute.REST_ASSET_CONSUMPTION}`, async (req: Request, res: Response, next: NextFunction) => {
+ await RouterUtils.handleServerAction(AssetService.handleCreateAssetConsumption.bind(this), ServerAction.ASSET_CONSUMPTION, req, res, next);
+ });
+ }
+}

MODIFY
src/server/rest/v1/service/AssetService.ts
src/server/rest/v1/service/AssetService.ts
@@ -9,14 +9,18 @@ import AssetFactory from '../../../../integration/asset/AssetFactory';
import { AssetInErrorType } from '../../../../types/InError';
import AssetSecurity from './security/AssetSecurity';
import AssetStorage from '../../../../storage/mongodb/AssetStorage';
+import AssetValidator from '../validator/AssetValidator';
import AuthorizationService from './AuthorizationService';
import Authorizations from '../../../../authorization/Authorizations';
import Constants from '../../../../utils/Constants';
+import Consumption from '../../../../types/Consumption';
import ConsumptionStorage from '../../../../storage/mongodb/ConsumptionStorage';
import Logging from '../../../../utils/Logging';
+import OCPPUtils from '../../../../server/ocpp/utils/OCPPUtils';
import { ServerAction } from '../../../../types/Server';
import SiteArea from '../../../../types/SiteArea';
import SiteAreaStorage from '../../../../storage/mongodb/SiteAreaStorage';
+import { StatusCodes } from 'http-status-codes';
import TenantComponents from '../../../../types/TenantComponents';
import Utils from '../../../../utils/Utils';
import UtilsService from './UtilsService';
@@ -86,6 +90,100 @@ export default class AssetService {
next();
}
+ public static async handleCreateAssetConsumption(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise<void> {
+ // Check if component is active
+ UtilsService.assertComponentIsActiveFromToken(req.user, TenantComponents.ASSET,
+ Action.CREATE_CONSUMPTION, Entity.ASSETS, MODULE_NAME, 'handleCreateAssetConsumption');
+ // Filter
+ const filteredRequest = AssetValidator.getInstance().validateCreateAssetConsumption({ ...req.params, ...req.body });
+ UtilsService.assertIdIsProvided(action, filteredRequest.assetID, MODULE_NAME,
+ 'handleCreateAssetConsumption', req.user);
+ // Check auth
+ if (!await Authorizations.canCreateAssetConsumption(req.user)) {
+ throw new AppAuthError({
+ errorCode: HTTPAuthError.FORBIDDEN,
+ user: req.user,
+ action: Action.CREATE_CONSUMPTION, entity: Entity.ASSET,
+ module: MODULE_NAME, method: 'handleCreateAssetConsumption',
+ value: filteredRequest.assetID
+ });
+ }
+ // Get Asset
+ const asset = await AssetStorage.getAsset(req.user.tenantID, filteredRequest.assetID, { withSiteArea: true });
+ UtilsService.assertObjectExists(action, asset, `Asset ID '${filteredRequest.assetID}' does not exist`,
+ MODULE_NAME, 'handleCreateAssetConsumption', req.user);
+ // Check if connection ID exists
+ if (!Utils.isNullOrUndefined(asset.connectionID)) {
+ throw new AppError({
+ source: Constants.CENTRAL_SERVER,
+ errorCode: HTTPError.GENERAL_ERROR,
+ message: `The asset '${asset.name}' has a defined connection. The push API can not be used`,
+ module: MODULE_NAME, method: 'handleCreateAssetConsumption',
+ user: req.user,
+ action: action
+ });
+ }
+ // Check dates order
+ if (filteredRequest.startedAt && filteredRequest.endedAt &&
+ !moment(filteredRequest.endedAt).isAfter(moment(filteredRequest.startedAt))) {
+ throw new AppError({
+ source: Constants.CENTRAL_SERVER,
+ errorCode: HTTPError.GENERAL_ERROR,
+ message: `The requested start date '${moment(filteredRequest.startedAt).toISOString()}' is after the end date '${moment(filteredRequest.endedAt).toISOString()}' `,
+ module: MODULE_NAME, method: 'handleCreateAssetConsumption',
+ user: req.user,
+ action: action
+ });
+ }
+ // Check if the latest consumption is pushed
+ const lastConsumption = await ConsumptionStorage.getLastAssetConsumption(req.user.tenantID, { assetID: filteredRequest.assetID });
+ if (moment(filteredRequest.startedAt).isBefore(moment(lastConsumption.endedAt))) {
+ throw new AppError({
+ source: Constants.CENTRAL_SERVER,
+ errorCode: HTTPError.GENERAL_ERROR,
+ message: `The start date '${moment(filteredRequest.startedAt).toISOString()}' of the pushed consumption is before the end date '${moment(lastConsumption.endedAt).toISOString()}' of the latest asset consumption`,
+ module: MODULE_NAME, method: 'handleCreateAssetConsumption',
+ user: req.user,
+ action: action
+ });
+ }
+ // Add site area
+ const consumptionToSave: Consumption = {
+ ...filteredRequest,
+ siteAreaID: asset.siteAreaID,
+ siteID: asset.siteArea.siteID,
+ };
+ if (Utils.isNullOrUndefined(consumptionToSave.instantAmps)) {
+ consumptionToSave.instantAmps = Utils.createDecimal(consumptionToSave.instantWatts).div(asset.siteArea.voltage).toNumber();
+ }
+ if (!Utils.isNullOrUndefined(consumptionToSave.consumptionWh) && Utils.isNullOrUndefined(consumptionToSave.consumptionAmps)) {
+ consumptionToSave.consumptionAmps = Utils.createDecimal(consumptionToSave.consumptionWh).div(asset.siteArea.voltage).toNumber();
+ }
+ await OCPPUtils.addSiteLimitationToConsumption(req.tenant, asset.siteArea, consumptionToSave);
+ // Get the ConsumptionValues
+ await ConsumptionStorage.saveConsumption(req.user.tenantID, consumptionToSave);
+ asset.currentConsumptionWh = filteredRequest.consumptionWh;
+ asset.currentInstantAmps = filteredRequest.instantAmps;
+ asset.currentInstantAmpsL1 = filteredRequest.instantAmpsL1;
+ asset.currentInstantAmpsL2 = filteredRequest.instantAmpsL2;
+ asset.currentInstantAmpsL3 = filteredRequest.instantAmpsL3;
+ asset.currentInstantVolts = filteredRequest.instantVolts;
+ asset.currentInstantVoltsL1 = filteredRequest.instantVoltsL1;
+ asset.currentInstantVoltsL2 = filteredRequest.instantVoltsL2;
+ asset.currentInstantVoltsL3 = filteredRequest.instantVoltsL3;
+ asset.currentInstantWatts = filteredRequest.instantWatts;
+ asset.currentInstantWattsL1 = filteredRequest.instantWattsL1;
+ asset.currentInstantWattsL2 = filteredRequest.instantWattsL2;
+ asset.currentInstantWattsL3 = filteredRequest.instantWattsL3;
+ asset.currentStateOfCharge = filteredRequest.stateOfCharge;
+ // Save Asset
+ await AssetStorage.saveAsset(req.user.tenantID, asset);
+ // Return
+ res.status(StatusCodes.CREATED).json(Object.assign({ consumption: consumptionToSave }, Constants.REST_RESPONSE_SUCCESS));
+ next();
+ }
+
+
public static async handleCheckAssetConnection(action: ServerAction, req: Request, res: Response, next: NextFunction): Promise<void> {
// Check if component is active
UtilsService.assertComponentIsActiveFromToken(req.user, TenantComponents.ASSET,
@@ -167,6 +265,18 @@ export default class AssetService {
detailedMessages: { asset }
});
}
+ // Uses Push API
+ if (asset.usesPushAPI) {
+ throw new AppError({
+ source: Constants.CENTRAL_SERVER,
+ errorCode: HTTPError.GENERAL_ERROR,
+ module: MODULE_NAME, method: 'handleRetrieveConsumption',
+ action: action,
+ user: req.user,
+ message: 'This Asset is using the push API, no consumption can be retrieved',
+ detailedMessages: { asset }
+ });
+ }
// Get asset factory
const assetImpl = await AssetFactory.getAssetImpl(req.tenant, asset.connectionID);
if (!assetImpl) {
@@ -426,6 +536,7 @@ export default class AssetService {
coordinates: filteredRequest.coordinates,
image: filteredRequest.image,
dynamicAsset: filteredRequest.dynamicAsset,
+ usesPushAPI: filteredRequest.usesPushAPI,
connectionID: filteredRequest.connectionID,
meterID: filteredRequest.meterID,
createdBy: { id: req.user.id },
@@ -499,6 +610,7 @@ export default class AssetService {
asset.coordinates = filteredRequest.coordinates;
asset.image = filteredRequest.image;
asset.dynamicAsset = filteredRequest.dynamicAsset;
+ asset.usesPushAPI = filteredRequest.usesPushAPI;
asset.connectionID = filteredRequest.connectionID;
asset.meterID = filteredRequest.meterID;
asset.lastChangedBy = { 'id': req.user.id };

MODIFY
src/server/rest/v1/service/UtilsService.ts
src/server/rest/v1/service/UtilsService.ts
@@ -1341,20 +1341,20 @@ export default class UtilsService {
}
}
if (asset.dynamicAsset) {
- if (!asset.connectionID) {
+ if (!asset.connectionID && !asset.usesPushAPI) {
throw new AppError({
source: Constants.CENTRAL_SERVER,
errorCode: HTTPError.GENERAL_ERROR,
- message: 'Asset connection is mandatory',
+ message: 'Asset connection is mandatory, if it is not using push API',
module: MODULE_NAME, method: 'checkIfAssetValid',
user: req.user.id
});
}
- if (!asset.meterID) {
+ if (!asset.meterID && !asset.usesPushAPI) {
throw new AppError({
source: Constants.CENTRAL_SERVER,
errorCode: HTTPError.GENERAL_ERROR,
- message: 'Asset meter ID is mandatory',
+ message: 'Asset meter ID is mandatory, if it is not using push API',
module: MODULE_NAME, method: 'checkIfAssetValid',
user: req.user.id
});

MODIFY
src/server/rest/v1/service/security/AssetSecurity.ts
src/server/rest/v1/service/security/AssetSecurity.ts
@@ -74,6 +74,7 @@ export default class AssetSecurity {
filteredRequest.staticValueWatt = sanitize(request.staticValueWatt),
filteredRequest.image = request.image;
filteredRequest.dynamicAsset = UtilsSecurity.filterBoolean(request.dynamicAsset);
+ filteredRequest.usesPushAPI = UtilsSecurity.filterBoolean(request.usesPushAPI);
if (Utils.objectHasProperty(request, 'coordinates') && !Utils.isEmptyArray(request.coordinates) && request.coordinates.length === 2) {
filteredRequest.coordinates = [
sanitize(request.coordinates[0]),

ADD
src/server/rest/v1/validator/AssetValidator.ts
+import Consumption from '../../../../types/Consumption';
+import Schema from '../../../../types/validator/Schema';
+import SchemaValidator from './SchemaValidator';
+import fs from 'fs';
+import global from '../../../../types/GlobalType';
+
+export default class AssetValidator extends SchemaValidator {
+ private static instance: AssetValidator|null = null;
+ private createAssetConsumption: Schema;
+
+ private constructor() {
+ super('AssetValidator');
+ this.createAssetConsumption = JSON.parse(fs.readFileSync(`${global.appRoot}/assets/server/rest/v1/schemas/asset/create-asset-consumption.json`, 'utf8'));
+ }
+
+ public static getInstance(): AssetValidator {
+ if (!AssetValidator.instance) {
+ AssetValidator.instance = new AssetValidator();
+ }
+ return AssetValidator.instance;
+ }
+
+ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+ public validateCreateAssetConsumption(data: any): Consumption {
+ // Validate schema
+ this.validate(this.createAssetConsumption, data);
+ return data;
+ }
+}

MODIFY
src/storage/mongodb/AssetStorage.ts
src/storage/mongodb/AssetStorage.ts
@@ -57,7 +57,8 @@ export default class AssetStorage {
powerWattsLastSmartChargingRun: Utils.convertToFloat(assetToSave.powerWattsLastSmartChargingRun),
fluctuationPercent: Utils.convertToFloat(assetToSave.fluctuationPercent),
staticValueWatt: Utils.convertToFloat(assetToSave.staticValueWatt),
- dynamicAsset: assetToSave.dynamicAsset,
+ dynamicAsset: Utils.convertToBoolean(assetToSave.dynamicAsset),
+ usesPushAPI: Utils.convertToBoolean(assetToSave.usesPushAPI),
issuer: Utils.convertToBoolean(assetToSave.issuer),
connectionID: assetToSave.connectionID,
meterID: assetToSave.meterID,

MODIFY
src/storage/mongodb/ConsumptionStorage.ts
src/storage/mongodb/ConsumptionStorage.ts
@@ -142,6 +142,45 @@ export default class ConsumptionStorage {
return consumptionsMDB;
}
+ static async getLastAssetConsumption(tenantID: string, params: { assetID: string }): Promise<Consumption> {
+ // Debug
+ const uniqueTimerID = Logging.traceStart(tenantID, MODULE_NAME, 'getLastAssetConsumption');
+ // Check
+ await DatabaseUtils.checkTenant(tenantID);
+ // Create Aggregation
+ const aggregation = [];
+ // Filters
+ aggregation.push({
+ $match: {
+ assetID: Utils.convertToObjectID(params.assetID)
+ }
+ });
+ // Convert Object ID to string
+ DatabaseUtils.pushConvertObjectIDToString(aggregation, 'siteAreaID');
+ DatabaseUtils.pushConvertObjectIDToString(aggregation, 'siteID');
+ DatabaseUtils.pushConvertObjectIDToString(aggregation, 'userID');
+ DatabaseUtils.pushRenameDatabaseID(aggregation);
+ // Sort
+ aggregation.push({
+ $sort: { startedAt: -1 }
+ });
+ // Limit
+ aggregation.push({
+ $limit: 1
+ });
+ let consumption: Consumption = null;
+ // Read DB
+ const consumptionsMDB = await global.database.getCollection<Consumption>(tenantID, 'consumptions')
+ .aggregate(aggregation, { allowDiskUse: true })
+ .toArray();
+ if (consumptionsMDB && consumptionsMDB.length > 0) {
+ consumption = consumptionsMDB[0];
+ }
+ // Debug
+ await Logging.traceEnd(tenantID, MODULE_NAME, 'getLastAssetConsumption', uniqueTimerID, consumptionsMDB);
+ return consumption;
+ }
+
static async getSiteAreaConsumptions(tenantID: string,
params: { siteAreaID: string; startDate: Date; endDate: Date },
projectFields?: string[]): Promise<Consumption[]> {

MODIFY
src/types/Asset.ts
src/types/Asset.ts
@@ -16,6 +16,7 @@ export default interface Asset extends CreatedUpdatedProps, AbstractCurrentConsu
issuer: boolean;
image?: string;
dynamicAsset: boolean;
+ usesPushAPI:boolean;
connectionID?: string;
meterID?: string;
values: Consumption[],

MODIFY
src/types/Authorization.ts
src/types/Authorization.ts
@@ -144,6 +144,7 @@ export enum Action {
CHECK_CONNECTION = 'CheckConnection',
CLEAR_BILLING_TEST_DATA = 'ClearBillingTestData',
RETRIEVE_CONSUMPTION = 'RetrieveConsumption',
+ CREATE_CONSUMPTION = 'CreateConsumption',
PING = 'Ping',
GENERATE_LOCAL_TOKEN = 'GenerateLocalToken',
REGISTER = 'Register',

MODIFY
src/types/Server.ts
src/types/Server.ts
@@ -504,6 +504,8 @@ export enum ServerRoute {
REST_TAGS = 'tags',
REST_TAG = 'tags/:id',
+ REST_ASSET_CONSUMPTION = 'assets/:assetID/consumption',
+
REST_PING = 'ping',
REST_TENANTS = 'tenants',

Added Endpoint for pushing consumptions