Skip to content

Commit

Permalink
Tests
Browse files Browse the repository at this point in the history
  • Loading branch information
rpochet committed Sep 23, 2023
1 parent 91691e5 commit ad7efe4
Show file tree
Hide file tree
Showing 9 changed files with 244 additions and 78 deletions.
53 changes: 3 additions & 50 deletions server/services/sunspec/lib/sunspec.connect.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
const os = require('os');
const { CONFIGURATION, DEFAULT, SCAN_OPTIONS } = require('./sunspec.constants');
const { DEFAULT } = require('./sunspec.constants');
const { WEBSOCKET_MESSAGE_TYPES, EVENTS } = require('../../../utils/constants');
const { ModbusClient } = require('./utils/sunspec.ModbusClient');
const logger = require('../../../utils/logger');
const { ServiceNotConfiguredError } = require('../../../utils/coreErrors');

/**
* @description Initialize service with dependencies and connect to devices.
Expand All @@ -13,54 +11,9 @@ const { ServiceNotConfiguredError } = require('../../../utils/coreErrors');
async function connect() {
logger.debug(`SunSpec: Connecting...`);

const rawIPMasks = await this.gladys.variable.getValue(CONFIGURATION.SUNSPEC_IP_MASKS, this.serviceId);
if (rawIPMasks !== null) {
const loadedIPMasks = JSON.parse(rawIPMasks);
this.ipMasks = [];
loadedIPMasks.forEach((option) => {
const mask = { ...option, networkInterface: false };
this.ipMasks.push(mask);
});
} else {
throw new ServiceNotConfiguredError();
}
this.sunspecIps = await this.scan();

// Complete masks with network interfaces
const networkInterfaces = os.networkInterfaces();
Object.keys(networkInterfaces).forEach((interfaceName) => {
const interfaces = networkInterfaces[interfaceName];

interfaces.forEach((interfaceDetails) => {
const { family, cidr: mask, internal } = interfaceDetails;

// Filter on IP family
if (SCAN_OPTIONS.IP_FAMILY.includes(family) && !internal) {
const boundMask = this.ipMasks.find((currentMask) => currentMask.mask === mask);
// Add not already bound masks
if (!boundMask) {
// Check subnet mask
const subnetMask = mask.split('/')[1];
// Default disable for large IP ranges (minimum value /24 to enable interface)
const enabled = Number.parseInt(subnetMask, 10) >= 24;
const networkInterfaceMask = {
mask,
name: interfaceName,
networkInterface: true,
enabled,
};
this.ipMasks.push(networkInterfaceMask);
} else {
// Force override with real information
boundMask.name = interfaceName;
boundMask.networkInterface = true;
}
}
});
});

const sunspecIps = await this.scan();

const promises = [...sunspecIps].map(async (ip) => {
const promises = [...this.sunspecIps].map(async (ip) => {
const modbus = new ModbusClient(this.modbusClient);
try {
await modbus.connect(ip, DEFAULT.MODBUS_PORT);
Expand Down
1 change: 1 addition & 0 deletions server/services/sunspec/lib/sunspec.getStatus.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ function getStatus() {

return {
connected: this.connected,
sunspecIps: this.sunspecIps,
};
}

Expand Down
56 changes: 53 additions & 3 deletions server/services/sunspec/lib/sunspec.scan.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
const os = require('os');
const { EVENTS, WEBSOCKET_MESSAGE_TYPES } = require('../../../utils/constants');
const logger = require('../../../utils/logger');
const { SCAN_OPTIONS } = require('./sunspec.constants');
const { CONFIGURATION, SCAN_OPTIONS, DEFAULT } = require('./sunspec.constants');
const { ServiceNotConfiguredError } = require('../../../utils/coreErrors');

/**
* @description Scan for network devices.
Expand All @@ -15,6 +17,51 @@ async function scan() {
}
logger.info(`Sunspec starts scanning devices...`);

const rawIPMasks = await this.gladys.variable.getValue(CONFIGURATION.SUNSPEC_IP_MASKS, this.serviceId);
if (rawIPMasks !== null) {
const loadedIPMasks = JSON.parse(rawIPMasks);
this.ipMasks = [];
loadedIPMasks.forEach((option) => {
const mask = { ...option, networkInterface: false };
this.ipMasks.push(mask);
});
} else {
throw new ServiceNotConfiguredError();
}

// Complete masks with network interfaces
const networkInterfaces = os.networkInterfaces();
Object.keys(networkInterfaces).forEach((interfaceName) => {
const interfaces = networkInterfaces[interfaceName];

interfaces.forEach((interfaceDetails) => {
const { family, cidr: mask, internal } = interfaceDetails;

// Filter on IP family
if (SCAN_OPTIONS.IP_FAMILY.includes(family) && !internal) {
const boundMask = this.ipMasks.find((currentMask) => currentMask.mask === mask);
// Add not already bound masks
if (!boundMask) {
// Check subnet mask
const subnetMask = mask.split('/')[1];
// Default disable for large IP ranges (minimum value /24 to enable interface)
const enabled = Number.parseInt(subnetMask, 10) >= 24;
const networkInterfaceMask = {
mask,
name: interfaceName,
networkInterface: true,
enabled,
};
this.ipMasks.push(networkInterfaceMask);
} else {
// Force override with real information
boundMask.name = interfaceName;
boundMask.networkInterface = true;
}
}
});
});

this.scanning = true;
this.eventManager.emit(EVENTS.WEBSOCKET.SEND_ALL, {
type: WEBSOCKET_MESSAGE_TYPES.SUNSPEC.SCANNING,
Expand All @@ -24,14 +71,17 @@ async function scan() {
});

const enabledMasks = this.ipMasks.filter((mask) => mask.enabled).map(({ mask }) => mask);
this.scanner = new this.ScannerClass(enabledMasks, '-p502');
this.scanner = new this.ScannerClass(enabledMasks, `-p${DEFAULT.MODBUS_PORT}`);
this.scanner.scanTimeout = SCAN_OPTIONS.SCAN_TIMEOUT;

const scanDone = (discoveredDevices, success) => {
this.scanning = false;
this.eventManager.emit(EVENTS.WEBSOCKET.SEND_ALL, {
type: WEBSOCKET_MESSAGE_TYPES.SUNSPEC.SCANNING,
payload: { scanning: false, success },
payload: {
scanning: false,
success,
},
});
};

Expand Down
23 changes: 12 additions & 11 deletions server/services/sunspec/lib/utils/sunspec.ModbusClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@ const { REGISTER, DEFAULT, MODEL } = require('../sunspec.constants');
const { trimString } = require('./sunspec.utils');

class ModbusClient {
constructor(modbusClient) {
this.modbusClient = modbusClient;
constructor(modbusClientApi) {
this.modbusClientApi = modbusClientApi;
this.models = {};
}

async connect(sunspecHost, sunspecPort) {
try {
await this.modbusClient.connectTCP(sunspecHost, { port: sunspecPort, timeout: 10000 });
await this.modbusClientApi.connectTCP(sunspecHost, { port: sunspecPort, timeout: 10000 });
logger.info(`SunSpec service connected`);
// this.modbusClient.setID(UNIT_ID.SID);
// this.modbusClientApi.setID(UNIT_ID.SID);
const sid = await this.readRegisterAsInt32(REGISTER.SID);
if (sid !== DEFAULT.SUNSPEC_MODBUS_MAP) {
logger.error(`Invalid SID received. Expected ${DEFAULT.SUNSPEC_MODBUS_MAP} but got ${sid}`);
Expand All @@ -31,6 +31,7 @@ class ModbusClient {
let nextModelLength;
let registerId = REGISTER.MODEL_ID + 1;
registerId += (await this.readRegisterAsInt16(registerId)) + 1;
// eslint-disable-next-line no-constant-condition
while (true) {
// eslint-disable-next-line no-await-in-loop
nextModel = await this.readRegisterAsInt16(registerId);
Expand All @@ -53,18 +54,18 @@ class ModbusClient {

close() {
return new Promise((resolve, reject) => {
this.modbusClient.close(() => {
this.modbusClientApi.close(() => {
logger.info('SunSpec service disconnected');
resolve();
});
});
}

getValueModel() {
if (this.models[MODEL.INVERTER_1_PHASE] !== null) {
if (this.models[MODEL.INVERTER_1_PHASE]) {
return MODEL.INVERTER_1_PHASE;
}
if (this.models[MODEL.INVERTER_3_PHASE] !== null) {
if (this.models[MODEL.INVERTER_3_PHASE]) {
return MODEL.INVERTER_3_PHASE;
}
return MODEL.INVERTER_SPLIT_PHASE;
Expand All @@ -77,7 +78,7 @@ class ModbusClient {

async readRegister(registerId, registerLength) {
return new Promise((resolve, reject) => {
this.modbusClient.readHoldingRegisters(registerId - 1, registerLength, (err, data) => {
this.modbusClientApi.readHoldingRegisters(registerId - 1, registerLength, (err, data) => {
if (err) {
reject(err);
} else {
Expand All @@ -89,7 +90,7 @@ class ModbusClient {

async readRegisterAsString(registerId, registerLength) {
return new Promise((resolve, reject) => {
this.modbusClient.readHoldingRegisters(registerId - 1, registerLength, (err, data) => {
this.modbusClientApi.readHoldingRegisters(registerId - 1, registerLength, (err, data) => {
if (err) {
reject(err);
} else {
Expand All @@ -102,7 +103,7 @@ class ModbusClient {

async readRegisterAsInt16(registerId) {
return new Promise((resolve, reject) => {
this.modbusClient.readHoldingRegisters(registerId - 1, 1, (err, data) => {
this.modbusClientApi.readHoldingRegisters(registerId - 1, 1, (err, data) => {
if (err) {
reject(err);
} else {
Expand All @@ -115,7 +116,7 @@ class ModbusClient {

async readRegisterAsInt32(registerId) {
return new Promise((resolve, reject) => {
this.modbusClient.readHoldingRegisters(registerId - 1, 2, (err, data) => {
this.modbusClientApi.readHoldingRegisters(registerId - 1, 2, (err, data) => {
if (err) {
reject(err);
} else {
Expand Down
14 changes: 0 additions & 14 deletions server/test/services/sunspec/lib/sunspec.connect.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ const SunSpecManager = proxyquire('../../../../services/sunspec/lib', {
'./sunspec.scanDevices': { scanDevices: scanDevicesMock },
});

const { ServiceNotConfiguredError } = require('../../../../utils/coreErrors');
const { EVENTS, WEBSOCKET_MESSAGE_TYPES } = require('../../../../utils/constants');

const SERVICE_ID = 'faea9c35-759a-44d5-bcc9-2af1de37b8b4';
Expand All @@ -44,19 +43,6 @@ describe('SunSpec connect', () => {
sinon.reset();
});

it('should not connect - not configured', async () => {
gladys.variable.getValue = fake.resolves(null);

try {
await sunSpecManager.connect();
assert.fail();
} catch (error) {
expect(error).to.be.an.instanceof(ServiceNotConfiguredError);
}

expect(sunSpecManager.connected).eql(false);
});

it('should connect', async () => {
gladys.variable.getValue = fake.resolves('[{"ip":"192.168.1.0/24"}]');

Expand Down
4 changes: 4 additions & 0 deletions server/test/services/sunspec/lib/sunspec.getStatus.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,21 @@ describe('SunSpec getStatus', () => {

it('should connected', async () => {
sunSpecManager.connected = true;
sunSpecManager.sunspecIps = ['192.168.1.xx'];
const status = await sunSpecManager.getStatus();
expect(status).to.deep.equals({
connected: true,
sunspecIps: ['192.168.1.xx'],
});
});

it('should not connected', async () => {
sunSpecManager.connected = false;
sunSpecManager.sunspecIps = ['192.168.1.xx'];
const status = await sunSpecManager.getStatus();
expect(status).to.deep.equals({
connected: false,
sunspecIps: ['192.168.1.xx'],
});
});
});
72 changes: 72 additions & 0 deletions server/test/services/sunspec/lib/sunspec.scan.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
const { expect } = require('chai');
const sinon = require('sinon');

const proxyquire = require('proxyquire');
const { EVENTS, WEBSOCKET_MESSAGE_TYPES } = require('../../../../utils/constants');
const { ServiceNotConfiguredError } = require('../../../../utils/coreErrors');

const { fake, assert } = sinon;
const ModbusTCPMock = require('./utils/ModbusTCPMock.test');
const ScannerClassMock = require('./utils/ScannerClassMock.test');

const SERVICE_ID = 'faea9c35-759a-44d5-bcc9-2af1de37b8b4';

const SunSpecManager = proxyquire('../../../../services/sunspec/lib', {
ModbusTCP: { ModbusTCP: ModbusTCPMock },
ScannerClass: { ScannerClass: ScannerClassMock },
});

describe('SunSpec scan', () => {
// PREPARE
let gladys;
let sunSpecManager;

beforeEach(() => {
gladys = {
event: {
emit: fake.resolves(null),
},
variable: {
getValue: fake.resolves('[{"ip":"192.168.1.0/24"}]'),
},
};
sunSpecManager = new SunSpecManager(gladys, ModbusTCPMock, ScannerClassMock, SERVICE_ID);
});

afterEach(() => {
sinon.reset();
});

it('should not scan - not configured', async () => {
try {
await sunSpecManager.scan();
assert.fail();
} catch (error) {
expect(error).to.be.an.instanceof(ServiceNotConfiguredError);
}
});

it('should not scan - already scanning', async () => {
sunSpecManager.scanning = true;
await sunSpecManager.scan();
assert.notCalled(gladys.variable.getValue);
});

it('should find Sunspec device', async () => {
await sunSpecManager.scan();
assert.callCount(sunSpecManager.eventManager.emit, 3);
assert.calledWithExactly(sunSpecManager.eventManager.emit, EVENTS.WEBSOCKET.SEND_ALL, {
type: WEBSOCKET_MESSAGE_TYPES.SUNSPEC.SCANNING,
payload: {
scanning: true,
},
});
assert.calledWithExactly(sunSpecManager.eventManager.emit, EVENTS.WEBSOCKET.SEND_ALL, {
type: WEBSOCKET_MESSAGE_TYPES.SUNSPEC.SCANNING,
payload: {
scanning: false,
success: true,
},
});
});
});
13 changes: 13 additions & 0 deletions server/test/services/sunspec/lib/utils/ScannerClassMock.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class ScannerClassMock {
// eslint-disable-next-line class-methods-use-this
on(step, cb) {
cb([
{
ip: '192.168.1.xx',
openPorts: [502],
},
]);
}
}

module.exports = ScannerClassMock;
Loading

0 comments on commit ad7efe4

Please sign in to comment.