milesight-powermeter-api 0.0.2-0 → 2024.9.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -46
- package/npm-shrinkwrap.json +19165 -27
- package/package.json +7 -1
- package/src/config/configurations/.gitkeep +0 -0
- package/src/config/index.ts +15 -0
- package/src/config/schema.ts +117 -0
- package/src/controller/milesight.controller.spec.ts +101 -0
- package/src/controller/milesight.controller.ts +61 -0
- package/src/dto/index.ts +1 -0
- package/src/dto/milesight.dto.ts +31 -0
- package/src/helper/initapm.ts +11 -0
- package/src/main.ts +6 -3
- package/src/modules/downlink.module.ts +8 -0
- package/src/modules/milesight-powermeter.module.ts +11 -0
- package/src/service/downlink.service.ts +66 -0
- package/src/service/milesight.service.spec.ts +113 -0
- package/src/service/milesight.service.ts +64 -0
- package/src/types.ts +0 -0
- package/timestamp.sh +1 -1
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "milesight-powermeter-api",
|
3
|
-
"version": "
|
3
|
+
"version": "2024.9.12",
|
4
4
|
"description": "Milesight Powermeter API is an internal tool accessible on backend. The API allows detail retrieval and command sending to its powermeters.",
|
5
5
|
"author": "Lester Vitor",
|
6
6
|
"license": "UNLICENSED",
|
@@ -22,9 +22,14 @@
|
|
22
22
|
"build:kube:ci": "scripts/build-app.sh mewe/lorawan-milesight-powermeter-api-service"
|
23
23
|
},
|
24
24
|
"dependencies": {
|
25
|
+
"@coinspect/therma-action-rule-decoder": "^2024.9.12",
|
25
26
|
"@nestjs/common": "^10.0.0",
|
26
27
|
"@nestjs/core": "^10.0.0",
|
27
28
|
"@nestjs/platform-express": "^10.0.0",
|
29
|
+
"class-transformer": "^0.5.1",
|
30
|
+
"class-validator": "^0.14.1",
|
31
|
+
"convict": "^6.2.4",
|
32
|
+
"elastic-apm-node": "^4.7.0",
|
28
33
|
"reflect-metadata": "^0.2.0",
|
29
34
|
"rxjs": "^7.8.1"
|
30
35
|
},
|
@@ -32,6 +37,7 @@
|
|
32
37
|
"@nestjs/cli": "^10.0.0",
|
33
38
|
"@nestjs/schematics": "^10.0.0",
|
34
39
|
"@nestjs/testing": "^10.0.0",
|
40
|
+
"@types/convict": "^6.1.6",
|
35
41
|
"@types/express": "^4.17.17",
|
36
42
|
"@types/jest": "^29.5.2",
|
37
43
|
"@types/node": "^20.3.1",
|
File without changes
|
@@ -0,0 +1,15 @@
|
|
1
|
+
import * as convict from 'convict';
|
2
|
+
import * as fs from 'fs';
|
3
|
+
import path from 'path';
|
4
|
+
|
5
|
+
import { AppConfig, schema } from './schema';
|
6
|
+
|
7
|
+
const config: convict.Config<AppConfig> = convict(schema);
|
8
|
+
|
9
|
+
const configPath = `${__dirname}/configurations/${config.get('stage')}.json`;
|
10
|
+
if (fs.existsSync(configPath)) {
|
11
|
+
config.loadFile(path.resolve(configPath));
|
12
|
+
}
|
13
|
+
config.validate({ allowed: 'strict' });
|
14
|
+
|
15
|
+
export { config };
|
@@ -0,0 +1,117 @@
|
|
1
|
+
import convict from 'convict';
|
2
|
+
|
3
|
+
export interface AppConfig {
|
4
|
+
env: string;
|
5
|
+
stage: string;
|
6
|
+
port: number;
|
7
|
+
fport: number;
|
8
|
+
amqp: {
|
9
|
+
hardware: {
|
10
|
+
host: string;
|
11
|
+
vhost: string;
|
12
|
+
username: string;
|
13
|
+
password: string;
|
14
|
+
downlinkExchange: string;
|
15
|
+
port: number;
|
16
|
+
prefetch: number;
|
17
|
+
server: string;
|
18
|
+
};
|
19
|
+
};
|
20
|
+
aws: {
|
21
|
+
region: string;
|
22
|
+
};
|
23
|
+
thingType: string;
|
24
|
+
elasticAPM: {
|
25
|
+
enabled: boolean;
|
26
|
+
serverURL: string;
|
27
|
+
transactionSampleRate: string;
|
28
|
+
secretToken: string;
|
29
|
+
};
|
30
|
+
}
|
31
|
+
|
32
|
+
const schema: convict.Schema<AppConfig> = {
|
33
|
+
env: {
|
34
|
+
default: 'development',
|
35
|
+
env: 'NODE_ENV',
|
36
|
+
format: ['production', 'development', 'test'],
|
37
|
+
},
|
38
|
+
stage: {
|
39
|
+
default: 'development',
|
40
|
+
env: 'STAGE',
|
41
|
+
format: ['production', 'development', 'staging'],
|
42
|
+
},
|
43
|
+
port: {
|
44
|
+
default: 9060,
|
45
|
+
env: 'MILESIGHT_POWERMETER_API_PORT',
|
46
|
+
},
|
47
|
+
fport: {
|
48
|
+
default: 85,
|
49
|
+
env: 'MILESIGHT_POWERMETER_DOWNLINK_FPORT',
|
50
|
+
},
|
51
|
+
amqp: {
|
52
|
+
hardware: {
|
53
|
+
host: {
|
54
|
+
default: 'localhost',
|
55
|
+
env: 'AMQP_HOST',
|
56
|
+
},
|
57
|
+
prefetch: {
|
58
|
+
default: 200,
|
59
|
+
env: 'AMQP_PREFETCH',
|
60
|
+
},
|
61
|
+
vhost: {
|
62
|
+
default: '/',
|
63
|
+
env: 'AMQP_VHOST',
|
64
|
+
},
|
65
|
+
username: {
|
66
|
+
default: 'guest',
|
67
|
+
env: 'AMQP_USERNAME',
|
68
|
+
},
|
69
|
+
password: {
|
70
|
+
default: 'guest',
|
71
|
+
env: 'AMQP_PASSWORD',
|
72
|
+
},
|
73
|
+
port: {
|
74
|
+
default: 5672,
|
75
|
+
env: 'AMQP_PORT',
|
76
|
+
},
|
77
|
+
downlinkExchange: {
|
78
|
+
default: 'hardware.downlinkexchange',
|
79
|
+
env: 'AMQP_DOWNLINK_EXCHANGE',
|
80
|
+
},
|
81
|
+
server: {
|
82
|
+
default: '',
|
83
|
+
env: 'AMQP_HARDWARE_SERVER',
|
84
|
+
},
|
85
|
+
},
|
86
|
+
},
|
87
|
+
aws: {
|
88
|
+
region: {
|
89
|
+
default: 'us-east-1',
|
90
|
+
env: 'AWS_REGION',
|
91
|
+
},
|
92
|
+
},
|
93
|
+
thingType: {
|
94
|
+
default: 'powermeter-milesight',
|
95
|
+
env: 'MILESIGHT_POWERMETER_THING_TYPE',
|
96
|
+
},
|
97
|
+
elasticAPM: {
|
98
|
+
enabled: {
|
99
|
+
default: false,
|
100
|
+
env: 'ELASTIC_APM_ENABLED',
|
101
|
+
},
|
102
|
+
serverURL: {
|
103
|
+
default: 'http://localhost:8200',
|
104
|
+
env: 'ELASTIC_APM_SERVER_URL',
|
105
|
+
},
|
106
|
+
transactionSampleRate: {
|
107
|
+
default: '1.0',
|
108
|
+
env: 'TRANSACTION_SAMPLE_RATE',
|
109
|
+
},
|
110
|
+
secretToken: {
|
111
|
+
default: '',
|
112
|
+
env: 'ELASTIC_APM_SECRET_TOKEN',
|
113
|
+
},
|
114
|
+
},
|
115
|
+
};
|
116
|
+
|
117
|
+
export { schema };
|
@@ -0,0 +1,101 @@
|
|
1
|
+
import { Test, TestingModule } from '@nestjs/testing';
|
2
|
+
import { HttpException } from '@nestjs/common';
|
3
|
+
import { MilesightController } from './milesight.controller';
|
4
|
+
import { MilesightService } from '../service/milesight.service';
|
5
|
+
import { DownlinkService } from '../service/downlink.service';
|
6
|
+
import { config } from '../config';
|
7
|
+
|
8
|
+
describe('MilesightController', () => {
|
9
|
+
let milesightController: MilesightController;
|
10
|
+
let milesightService: MilesightService;
|
11
|
+
|
12
|
+
beforeEach(async () => {
|
13
|
+
const app: TestingModule = await Test.createTestingModule({
|
14
|
+
controllers: [MilesightController],
|
15
|
+
providers: [DownlinkService, MilesightService],
|
16
|
+
}).compile();
|
17
|
+
milesightService = app.get<MilesightService>(MilesightService);
|
18
|
+
milesightController = app.get<MilesightController>(MilesightController);
|
19
|
+
});
|
20
|
+
afterEach(() => {
|
21
|
+
jest.clearAllMocks();
|
22
|
+
});
|
23
|
+
describe('getDeviceList', () => {
|
24
|
+
it('should return device list details', async () => {
|
25
|
+
const queryParams = { maxResult: 1 };
|
26
|
+
const expectedResult = {
|
27
|
+
$metadata: {
|
28
|
+
httpStatusCode: 200,
|
29
|
+
requestId: 'b5f4ba6b-de9c-4c17-9879-b6e2e02d14f5',
|
30
|
+
extendedRequestId: undefined,
|
31
|
+
cfId: undefined,
|
32
|
+
attempts: 1,
|
33
|
+
totalRetryDelay: 0,
|
34
|
+
},
|
35
|
+
nextToken: 'sampleToken',
|
36
|
+
things: [
|
37
|
+
{
|
38
|
+
attributes: {},
|
39
|
+
thingArn:
|
40
|
+
'arn:aws:iot:us-east-1:224361144749:thing/aaaaaaaaa',
|
41
|
+
thingName: 'aaaaaaaaa',
|
42
|
+
thingTypeName: config.get('thingType'),
|
43
|
+
version: 1,
|
44
|
+
},
|
45
|
+
],
|
46
|
+
};
|
47
|
+
const controllerResult = {
|
48
|
+
message:
|
49
|
+
'Retrieved single phase powermeter device list details',
|
50
|
+
result: expectedResult,
|
51
|
+
};
|
52
|
+
jest.spyOn(milesightService, 'getDeviceList').mockResolvedValue(
|
53
|
+
expectedResult,
|
54
|
+
);
|
55
|
+
const result = await milesightController.getDeviceList(queryParams);
|
56
|
+
expect(result).toEqual(controllerResult);
|
57
|
+
});
|
58
|
+
it('should handle error if device is not retrieved', async () => {
|
59
|
+
const queryParams = { maxResult: 1 };
|
60
|
+
jest.spyOn(milesightService, 'getDeviceList').mockResolvedValue(
|
61
|
+
null,
|
62
|
+
);
|
63
|
+
let errorResult = null;
|
64
|
+
try {
|
65
|
+
await milesightController.getDeviceList(queryParams);
|
66
|
+
} catch (error) {
|
67
|
+
errorResult = error;
|
68
|
+
}
|
69
|
+
expect(errorResult).toBeInstanceOf(HttpException);
|
70
|
+
});
|
71
|
+
});
|
72
|
+
describe('rebootDevice', () => {
|
73
|
+
const data = {
|
74
|
+
deviceEui: 'sampleDeviceEui',
|
75
|
+
serialNumber: 'sampleDeviceName',
|
76
|
+
};
|
77
|
+
it('should return result', async () => {
|
78
|
+
const controllerResult = {
|
79
|
+
message: `Successfully sent reboot command for sampleDeviceEui.`,
|
80
|
+
result: true,
|
81
|
+
};
|
82
|
+
jest.spyOn(milesightService, 'rebootDevice').mockResolvedValue(
|
83
|
+
true,
|
84
|
+
);
|
85
|
+
const result = await milesightController.rebootDevice(data);
|
86
|
+
expect(result).toEqual(controllerResult);
|
87
|
+
});
|
88
|
+
it('should handle error properly', async () => {
|
89
|
+
jest.spyOn(milesightService, 'rebootDevice').mockResolvedValue(
|
90
|
+
null,
|
91
|
+
);
|
92
|
+
let errorResult = null;
|
93
|
+
try {
|
94
|
+
await milesightController.rebootDevice(data);
|
95
|
+
} catch (error) {
|
96
|
+
errorResult = error;
|
97
|
+
}
|
98
|
+
expect(errorResult).toBeInstanceOf(HttpException);
|
99
|
+
});
|
100
|
+
});
|
101
|
+
});
|
@@ -0,0 +1,61 @@
|
|
1
|
+
import {
|
2
|
+
Body,
|
3
|
+
Controller,
|
4
|
+
HttpException,
|
5
|
+
HttpStatus,
|
6
|
+
Get,
|
7
|
+
Query,
|
8
|
+
Post,
|
9
|
+
UsePipes,
|
10
|
+
ValidationPipe,
|
11
|
+
} from '@nestjs/common';
|
12
|
+
import { DeviceListParamsDto, MilesightBaseDto } from '../dto/milesight.dto';
|
13
|
+
import { MilesightService } from '../service/milesight.service';
|
14
|
+
import { AWSListThings } from '@coinspect/therma-action-rule-decoder';
|
15
|
+
|
16
|
+
@Controller('powermeter/milesight')
|
17
|
+
export class MilesightController {
|
18
|
+
constructor(private readonly milesightService: MilesightService) {}
|
19
|
+
@Get()
|
20
|
+
async getDeviceList(
|
21
|
+
@Query() queryParams: DeviceListParamsDto,
|
22
|
+
): Promise<{ message: string; result: AWSListThings }> {
|
23
|
+
const { maxResult, nextToken } = queryParams;
|
24
|
+
const result = await this.milesightService.getDeviceList(
|
25
|
+
maxResult,
|
26
|
+
nextToken,
|
27
|
+
);
|
28
|
+
if (!result) {
|
29
|
+
throw new HttpException(
|
30
|
+
'Error occured',
|
31
|
+
HttpStatus.INTERNAL_SERVER_ERROR,
|
32
|
+
);
|
33
|
+
}
|
34
|
+
return {
|
35
|
+
message: 'Retrieved single phase powermeter device list details',
|
36
|
+
result,
|
37
|
+
};
|
38
|
+
}
|
39
|
+
|
40
|
+
@Post('reboot')
|
41
|
+
@UsePipes(new ValidationPipe())
|
42
|
+
async rebootDevice(
|
43
|
+
@Body() body: MilesightBaseDto,
|
44
|
+
): Promise<{ message: string; result: boolean }> {
|
45
|
+
const { deviceEui, serialNumber } = body;
|
46
|
+
const result = await this.milesightService.rebootDevice(
|
47
|
+
deviceEui,
|
48
|
+
serialNumber ? serialNumber : deviceEui,
|
49
|
+
);
|
50
|
+
if (!result) {
|
51
|
+
throw new HttpException(
|
52
|
+
`Error occured rebooting device: ${deviceEui}`,
|
53
|
+
HttpStatus.INTERNAL_SERVER_ERROR,
|
54
|
+
);
|
55
|
+
}
|
56
|
+
return {
|
57
|
+
message: `Successfully sent reboot command for ${deviceEui}.`,
|
58
|
+
result,
|
59
|
+
};
|
60
|
+
}
|
61
|
+
}
|
package/src/dto/index.ts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
export * from './milesight.dto';
|
@@ -0,0 +1,31 @@
|
|
1
|
+
import {
|
2
|
+
IsNotEmpty,
|
3
|
+
IsOptional,
|
4
|
+
IsString,
|
5
|
+
Min,
|
6
|
+
Max,
|
7
|
+
IsInt,
|
8
|
+
} from 'class-validator';
|
9
|
+
import { Type } from 'class-transformer';
|
10
|
+
|
11
|
+
export class MilesightBaseDto {
|
12
|
+
@IsNotEmpty()
|
13
|
+
@IsString()
|
14
|
+
deviceEui: string;
|
15
|
+
|
16
|
+
@IsOptional()
|
17
|
+
@IsString()
|
18
|
+
serialNumber: string;
|
19
|
+
}
|
20
|
+
|
21
|
+
export class DeviceListParamsDto {
|
22
|
+
@Min(1)
|
23
|
+
@Max(250)
|
24
|
+
@IsInt()
|
25
|
+
@Type(() => Number)
|
26
|
+
maxResult: number;
|
27
|
+
|
28
|
+
@IsOptional()
|
29
|
+
@IsString()
|
30
|
+
nextToken?: string;
|
31
|
+
}
|
@@ -0,0 +1,11 @@
|
|
1
|
+
import * as apm from 'elastic-apm-node';
|
2
|
+
import { config } from '../config';
|
3
|
+
apm.start({
|
4
|
+
active: Boolean(config.get('elasticAPM.enabled') || false),
|
5
|
+
serviceName: `lorawan-milesight-powermeter-api-${config.get('stage')}`,
|
6
|
+
serverUrl: config.get('elasticAPM.serverURL'),
|
7
|
+
transactionSampleRate: parseFloat(
|
8
|
+
config.get('elasticAPM.transactionSampleRate'),
|
9
|
+
),
|
10
|
+
secretToken: config.get('elasticAPM.secretToken'),
|
11
|
+
});
|
package/src/main.ts
CHANGED
@@ -1,8 +1,11 @@
|
|
1
|
+
import './helper/initapm';
|
1
2
|
import { NestFactory } from '@nestjs/core';
|
2
|
-
import {
|
3
|
+
import { config } from './config';
|
4
|
+
|
5
|
+
import { MilesightPowermeterModule } from './modules/milesight-powermeter.module';
|
3
6
|
|
4
7
|
async function bootstrap() {
|
5
|
-
|
6
|
-
|
8
|
+
const app = await NestFactory.create(MilesightPowermeterModule);
|
9
|
+
await app.listen(config.get('port'));
|
7
10
|
}
|
8
11
|
bootstrap();
|
@@ -0,0 +1,11 @@
|
|
1
|
+
import { Module } from '@nestjs/common';
|
2
|
+
import { MilesightController } from '../controller/milesight.controller';
|
3
|
+
import { MilesightService } from '../service/milesight.service';
|
4
|
+
import { DownlinkModule } from './downlink.module';
|
5
|
+
|
6
|
+
@Module({
|
7
|
+
imports: [DownlinkModule],
|
8
|
+
controllers: [MilesightController],
|
9
|
+
providers: [MilesightService],
|
10
|
+
})
|
11
|
+
export class MilesightPowermeterModule {}
|
@@ -0,0 +1,66 @@
|
|
1
|
+
import {
|
2
|
+
AmqpService,
|
3
|
+
AWSDownlinkService,
|
4
|
+
} from '@coinspect/therma-action-rule-decoder';
|
5
|
+
import { Injectable } from '@nestjs/common';
|
6
|
+
|
7
|
+
import { config } from '../config';
|
8
|
+
|
9
|
+
@Injectable()
|
10
|
+
export class DownlinkService {
|
11
|
+
private amqpService: AmqpService;
|
12
|
+
private downlinkExchange: string;
|
13
|
+
constructor() {
|
14
|
+
const {
|
15
|
+
host: hostname,
|
16
|
+
port,
|
17
|
+
username,
|
18
|
+
password,
|
19
|
+
vhost,
|
20
|
+
prefetch,
|
21
|
+
downlinkExchange,
|
22
|
+
server,
|
23
|
+
} = config.get('amqp.hardware');
|
24
|
+
const amqpConfig = server
|
25
|
+
? server
|
26
|
+
: {
|
27
|
+
hostname,
|
28
|
+
port,
|
29
|
+
username,
|
30
|
+
password,
|
31
|
+
vhost,
|
32
|
+
};
|
33
|
+
this.downlinkExchange = downlinkExchange;
|
34
|
+
this.amqpService = new AmqpService(
|
35
|
+
amqpConfig,
|
36
|
+
'milesight-powermeter-api',
|
37
|
+
prefetch,
|
38
|
+
'AmqpService',
|
39
|
+
true,
|
40
|
+
);
|
41
|
+
}
|
42
|
+
async sendDownlink(
|
43
|
+
deviceEui: string,
|
44
|
+
deviceName: string,
|
45
|
+
payload: string,
|
46
|
+
type: string,
|
47
|
+
fport: number,
|
48
|
+
confirmed: boolean,
|
49
|
+
) {
|
50
|
+
const channel = await this.amqpService.createChannel();
|
51
|
+
const downlinkService = new AWSDownlinkService(
|
52
|
+
channel,
|
53
|
+
this.downlinkExchange,
|
54
|
+
);
|
55
|
+
const response = await downlinkService.sendDownlink(
|
56
|
+
deviceEui,
|
57
|
+
deviceName,
|
58
|
+
payload,
|
59
|
+
type,
|
60
|
+
fport,
|
61
|
+
confirmed,
|
62
|
+
);
|
63
|
+
await channel.channel.close();
|
64
|
+
return response;
|
65
|
+
}
|
66
|
+
}
|
@@ -0,0 +1,113 @@
|
|
1
|
+
import { Test, TestingModule } from '@nestjs/testing';
|
2
|
+
import { DownlinkService } from './downlink.service';
|
3
|
+
import { MilesightService } from './milesight.service';
|
4
|
+
import { config } from '../config';
|
5
|
+
|
6
|
+
const mockGetListDevices = jest.fn();
|
7
|
+
const mockGetDeviceWithTags = jest.fn();
|
8
|
+
jest.mock('@coinspect/therma-action-rule-decoder', () => {
|
9
|
+
const actualPackage = jest.requireActual(
|
10
|
+
'@coinspect/therma-action-rule-decoder',
|
11
|
+
);
|
12
|
+
return {
|
13
|
+
...actualPackage,
|
14
|
+
AWSIotAPI: jest.fn().mockImplementation(() => ({
|
15
|
+
getListThingsByType: mockGetListDevices,
|
16
|
+
})),
|
17
|
+
AWSWirelessIotAPI: jest.fn().mockImplementation(() => ({
|
18
|
+
getDeviceWithTags: mockGetDeviceWithTags,
|
19
|
+
})),
|
20
|
+
};
|
21
|
+
});
|
22
|
+
describe('MilesightService', () => {
|
23
|
+
let milesightService: MilesightService;
|
24
|
+
let downlinkService: DownlinkService;
|
25
|
+
const mockSendDownlink = jest.fn();
|
26
|
+
beforeEach(async () => {
|
27
|
+
const module: TestingModule = await Test.createTestingModule({
|
28
|
+
providers: [
|
29
|
+
{
|
30
|
+
provide: DownlinkService,
|
31
|
+
useValue: {
|
32
|
+
sendDownlink: mockSendDownlink,
|
33
|
+
},
|
34
|
+
},
|
35
|
+
MilesightService,
|
36
|
+
],
|
37
|
+
}).compile();
|
38
|
+
|
39
|
+
milesightService = module.get<MilesightService>(MilesightService);
|
40
|
+
downlinkService = module.get<DownlinkService>(DownlinkService);
|
41
|
+
});
|
42
|
+
afterEach(() => {
|
43
|
+
jest.clearAllMocks();
|
44
|
+
});
|
45
|
+
describe('getDeviceList', () => {
|
46
|
+
it('should return device list details', async () => {
|
47
|
+
const MaxResult = 1;
|
48
|
+
const expectedResult = {
|
49
|
+
$metadata: {
|
50
|
+
httpStatusCode: 200,
|
51
|
+
requestId: 'b5f4ba6b-de9c-4c17-9879-b6e2e02d14f5',
|
52
|
+
extendedRequestId: undefined,
|
53
|
+
cfId: undefined,
|
54
|
+
attempts: 1,
|
55
|
+
totalRetryDelay: 0,
|
56
|
+
},
|
57
|
+
nextToken: 'sampleToken',
|
58
|
+
things: [
|
59
|
+
{
|
60
|
+
attributes: {},
|
61
|
+
thingArn:
|
62
|
+
'arn:aws:iot:us-east-1:224361144749:thing/aaaaaaaaa',
|
63
|
+
thingName: 'aaaaaaaaa',
|
64
|
+
thingTypeName: config.get('thingType'),
|
65
|
+
version: 1,
|
66
|
+
},
|
67
|
+
],
|
68
|
+
};
|
69
|
+
mockGetListDevices.mockResolvedValue(expectedResult);
|
70
|
+
|
71
|
+
const result = await milesightService.getDeviceList(MaxResult);
|
72
|
+
|
73
|
+
expect(mockGetListDevices).toBeCalled();
|
74
|
+
expect(result).toEqual(expectedResult);
|
75
|
+
});
|
76
|
+
|
77
|
+
it('should return null when getting error on device list', async () => {
|
78
|
+
const MaxResult = 1;
|
79
|
+
mockGetListDevices.mockRejectedValue(new Error('Test error'));
|
80
|
+
|
81
|
+
const response = await milesightService.getDeviceList(MaxResult);
|
82
|
+
expect(response).toBeNull();
|
83
|
+
mockGetListDevices.mockClear();
|
84
|
+
});
|
85
|
+
});
|
86
|
+
|
87
|
+
describe('rebootDevice', () => {
|
88
|
+
const data = {
|
89
|
+
deviceEui: 'sampleDeviceEui',
|
90
|
+
serialNumber: 'sampleDeviceName',
|
91
|
+
};
|
92
|
+
|
93
|
+
it('should return true when downlink is successful', async () => {
|
94
|
+
const expectedDownlinkCommand = 'ff10ff';
|
95
|
+
const sendDownlinkSpy = jest
|
96
|
+
.spyOn(downlinkService, 'sendDownlink')
|
97
|
+
.mockResolvedValue(true);
|
98
|
+
await milesightService.rebootDevice(
|
99
|
+
data.deviceEui,
|
100
|
+
data.serialNumber,
|
101
|
+
);
|
102
|
+
expect(sendDownlinkSpy).toHaveBeenCalledWith(
|
103
|
+
data.deviceEui,
|
104
|
+
data.serialNumber,
|
105
|
+
expectedDownlinkCommand,
|
106
|
+
'milesightPowermeterRebootDevice',
|
107
|
+
85,
|
108
|
+
true,
|
109
|
+
);
|
110
|
+
sendDownlinkSpy.mockClear();
|
111
|
+
});
|
112
|
+
});
|
113
|
+
});
|
@@ -0,0 +1,64 @@
|
|
1
|
+
import {
|
2
|
+
AWSIotAPI,
|
3
|
+
AWSListThings,
|
4
|
+
} from '@coinspect/therma-action-rule-decoder';
|
5
|
+
import { Inject, Injectable, Logger } from '@nestjs/common';
|
6
|
+
import { DownlinkService } from './downlink.service';
|
7
|
+
import { config } from '../config';
|
8
|
+
|
9
|
+
const fPort = config.get('fport');
|
10
|
+
@Injectable()
|
11
|
+
export class MilesightService {
|
12
|
+
private readonly logger: Logger;
|
13
|
+
private readonly awsIotAPI: AWSIotAPI;
|
14
|
+
constructor(
|
15
|
+
@Inject(DownlinkService)
|
16
|
+
private readonly downlinkService: DownlinkService,
|
17
|
+
) {
|
18
|
+
this.logger = new Logger(MilesightService.name);
|
19
|
+
this.awsIotAPI = new AWSIotAPI(config.get('aws'));
|
20
|
+
}
|
21
|
+
|
22
|
+
async getDeviceList(
|
23
|
+
maxResult: number,
|
24
|
+
nextToken?: string,
|
25
|
+
): Promise<AWSListThings | null> {
|
26
|
+
try {
|
27
|
+
this.logger.log('Retrieving devices list');
|
28
|
+
const result = await this.awsIotAPI.getListThingsByType(
|
29
|
+
config.get('thingType'),
|
30
|
+
maxResult,
|
31
|
+
nextToken,
|
32
|
+
);
|
33
|
+
return result;
|
34
|
+
} catch (error) {
|
35
|
+
this.logger.error(
|
36
|
+
`Error retrieving devices list details: ${error}`,
|
37
|
+
);
|
38
|
+
return null;
|
39
|
+
}
|
40
|
+
}
|
41
|
+
|
42
|
+
async rebootDevice(
|
43
|
+
deviceEui: string,
|
44
|
+
serialNumber: string,
|
45
|
+
): Promise<boolean | null> {
|
46
|
+
try {
|
47
|
+
this.logger.log(`Rebooting device: ${deviceEui}`);
|
48
|
+
const rebootDownlinkCommand = 'ff10ff';
|
49
|
+
const response = await this.downlinkService.sendDownlink(
|
50
|
+
deviceEui,
|
51
|
+
serialNumber,
|
52
|
+
rebootDownlinkCommand,
|
53
|
+
'milesightPowermeterRebootDevice',
|
54
|
+
fPort,
|
55
|
+
true,
|
56
|
+
);
|
57
|
+
this.logger.log(`Successfully rebooted ${deviceEui}`);
|
58
|
+
return response;
|
59
|
+
} catch (error) {
|
60
|
+
this.logger.error(`Error rebooting ${deviceEui}: ${error}`);
|
61
|
+
return null;
|
62
|
+
}
|
63
|
+
}
|
64
|
+
}
|
package/src/types.ts
ADDED
File without changes
|
package/timestamp.sh
CHANGED
@@ -1 +1 @@
|
|
1
|
-
export DT=
|
1
|
+
export DT=20240918072210
|