milesight-powermeter-api 2024.9.23 → 2024.9.24-1
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 +89 -11
- package/jest.config.js +19 -0
- package/jest.setup.ts +104 -0
- package/npm-shrinkwrap.json +1870 -5480
- package/package.json +2 -19
- package/src/config/schema.ts +5 -0
- package/src/controller/milesight.controller.spec.ts +9 -1
- package/src/controller/three-phase-milesight.controller.spec.ts +139 -0
- package/src/controller/three-phase-milesight.controller.ts +88 -0
- package/src/modules/milesight-powermeter.module.ts +5 -3
- package/src/service/three-phase-milesight.service.spec.ts +228 -0
- package/src/service/three-phase-milesight.service.ts +97 -0
- package/timestamp.sh +1 -1
- package/tsconfig.build.json +1 -1
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "milesight-powermeter-api",
|
3
|
-
"version": "2024.9.
|
3
|
+
"version": "2024.9.24-1",
|
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,7 +22,7 @@
|
|
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": "
|
25
|
+
"@coinspect/therma-action-rule-decoder": "^2025.6.29-0",
|
26
26
|
"@nestjs/common": "^10.0.0",
|
27
27
|
"@nestjs/core": "^10.0.0",
|
28
28
|
"@nestjs/platform-express": "^10.0.0",
|
@@ -56,22 +56,5 @@
|
|
56
56
|
"ts-node": "^10.9.1",
|
57
57
|
"tsconfig-paths": "^4.2.0",
|
58
58
|
"typescript": "^5.1.3"
|
59
|
-
},
|
60
|
-
"jest": {
|
61
|
-
"moduleFileExtensions": [
|
62
|
-
"js",
|
63
|
-
"json",
|
64
|
-
"ts"
|
65
|
-
],
|
66
|
-
"rootDir": "src",
|
67
|
-
"testRegex": ".*\\.spec\\.ts$",
|
68
|
-
"transform": {
|
69
|
-
"^.+\\.(t|j)s$": "ts-jest"
|
70
|
-
},
|
71
|
-
"collectCoverageFrom": [
|
72
|
-
"**/*.(t|j)s"
|
73
|
-
],
|
74
|
-
"coverageDirectory": "../coverage",
|
75
|
-
"testEnvironment": "node"
|
76
59
|
}
|
77
60
|
}
|
package/src/config/schema.ts
CHANGED
@@ -21,6 +21,7 @@ export interface AppConfig {
|
|
21
21
|
region: string;
|
22
22
|
};
|
23
23
|
thingType: string;
|
24
|
+
threePhaseThingType: string;
|
24
25
|
elasticAPM: {
|
25
26
|
enabled: boolean;
|
26
27
|
serverURL: string;
|
@@ -94,6 +95,10 @@ const schema: convict.Schema<AppConfig> = {
|
|
94
95
|
default: 'powermeter-milesight',
|
95
96
|
env: 'MILESIGHT_POWERMETER_THING_TYPE',
|
96
97
|
},
|
98
|
+
threePhaseThingType: {
|
99
|
+
default: 'three-phase-powermeter-milesight',
|
100
|
+
env: 'MILESIGHT_THREE_PHASE_POWERMETER_THING_TYPE',
|
101
|
+
},
|
97
102
|
elasticAPM: {
|
98
103
|
enabled: {
|
99
104
|
default: false,
|
@@ -8,18 +8,26 @@ import { config } from '../config';
|
|
8
8
|
describe('MilesightController', () => {
|
9
9
|
let milesightController: MilesightController;
|
10
10
|
let milesightService: MilesightService;
|
11
|
+
let app: TestingModule;
|
11
12
|
|
12
13
|
beforeEach(async () => {
|
13
|
-
|
14
|
+
app = await Test.createTestingModule({
|
14
15
|
controllers: [MilesightController],
|
15
16
|
providers: [DownlinkService, MilesightService],
|
16
17
|
}).compile();
|
17
18
|
milesightService = app.get<MilesightService>(MilesightService);
|
18
19
|
milesightController = app.get<MilesightController>(MilesightController);
|
19
20
|
});
|
21
|
+
|
20
22
|
afterEach(() => {
|
21
23
|
jest.clearAllMocks();
|
22
24
|
});
|
25
|
+
|
26
|
+
afterAll(async () => {
|
27
|
+
if (app) {
|
28
|
+
await app.close();
|
29
|
+
}
|
30
|
+
});
|
23
31
|
describe('getDeviceList', () => {
|
24
32
|
it('should return device list details', async () => {
|
25
33
|
const queryParams = { maxResult: 1 };
|
@@ -0,0 +1,139 @@
|
|
1
|
+
import { Test, TestingModule } from '@nestjs/testing';
|
2
|
+
import { HttpException } from '@nestjs/common';
|
3
|
+
import { ThreePhaseMilesightController } from './three-phase-milesight.controller';
|
4
|
+
import { ThreePhaseMilesightService } from '../service/three-phase-milesight.service';
|
5
|
+
import { DownlinkService } from '../service/downlink.service';
|
6
|
+
import { config } from '../config';
|
7
|
+
|
8
|
+
describe('ThreePhaseMilesightController', () => {
|
9
|
+
let threePhaseMilesightController: ThreePhaseMilesightController;
|
10
|
+
let threePhaseMilesightService: ThreePhaseMilesightService;
|
11
|
+
let app: TestingModule;
|
12
|
+
|
13
|
+
beforeEach(async () => {
|
14
|
+
app = await Test.createTestingModule({
|
15
|
+
controllers: [ThreePhaseMilesightController],
|
16
|
+
providers: [DownlinkService, ThreePhaseMilesightService],
|
17
|
+
}).compile();
|
18
|
+
threePhaseMilesightService = app.get<ThreePhaseMilesightService>(ThreePhaseMilesightService);
|
19
|
+
threePhaseMilesightController = app.get<ThreePhaseMilesightController>(ThreePhaseMilesightController);
|
20
|
+
});
|
21
|
+
|
22
|
+
afterEach(() => {
|
23
|
+
jest.clearAllMocks();
|
24
|
+
});
|
25
|
+
|
26
|
+
afterAll(async () => {
|
27
|
+
if (app) {
|
28
|
+
await app.close();
|
29
|
+
}
|
30
|
+
});
|
31
|
+
describe('getDeviceList', () => {
|
32
|
+
it('should return device list details', async () => {
|
33
|
+
const queryParams = { maxResult: 1 };
|
34
|
+
const expectedResult = {
|
35
|
+
$metadata: {
|
36
|
+
httpStatusCode: 200,
|
37
|
+
requestId: 'b5f4ba6b-de9c-4c17-9879-b6e2e02d14f5',
|
38
|
+
extendedRequestId: undefined,
|
39
|
+
cfId: undefined,
|
40
|
+
attempts: 1,
|
41
|
+
totalRetryDelay: 0,
|
42
|
+
},
|
43
|
+
nextToken: 'sampleToken',
|
44
|
+
things: [
|
45
|
+
{
|
46
|
+
attributes: {},
|
47
|
+
thingArn:
|
48
|
+
'arn:aws:iot:us-east-1:224361144749:thing/aaaaaaaaa',
|
49
|
+
thingName: 'aaaaaaaaa',
|
50
|
+
thingTypeName: config.get('thingType'),
|
51
|
+
version: 1,
|
52
|
+
},
|
53
|
+
],
|
54
|
+
};
|
55
|
+
const controllerResult = {
|
56
|
+
message:
|
57
|
+
'Retrieved three-phase powermeter device list details',
|
58
|
+
result: expectedResult,
|
59
|
+
};
|
60
|
+
jest.spyOn(threePhaseMilesightService, 'getDeviceList').mockResolvedValue(
|
61
|
+
expectedResult,
|
62
|
+
);
|
63
|
+
const result = await threePhaseMilesightController.getDeviceList(queryParams);
|
64
|
+
expect(result).toEqual(controllerResult);
|
65
|
+
});
|
66
|
+
it('should handle error if device is not retrieved', async () => {
|
67
|
+
const queryParams = { maxResult: 1 };
|
68
|
+
jest.spyOn(threePhaseMilesightService, 'getDeviceList').mockResolvedValue(
|
69
|
+
null,
|
70
|
+
);
|
71
|
+
let errorResult = null;
|
72
|
+
try {
|
73
|
+
await threePhaseMilesightController.getDeviceList(queryParams);
|
74
|
+
} catch (error) {
|
75
|
+
errorResult = error;
|
76
|
+
}
|
77
|
+
expect(errorResult).toBeInstanceOf(HttpException);
|
78
|
+
});
|
79
|
+
});
|
80
|
+
describe('rebootDevice', () => {
|
81
|
+
const data = {
|
82
|
+
deviceEui: 'sampleDeviceEui',
|
83
|
+
serialNumber: 'sampleDeviceName',
|
84
|
+
};
|
85
|
+
it('should return result', async () => {
|
86
|
+
const controllerResult = {
|
87
|
+
message: `Successfully sent reboot command for three-phase device sampleDeviceEui.`,
|
88
|
+
result: true,
|
89
|
+
};
|
90
|
+
jest.spyOn(threePhaseMilesightService, 'rebootDevice').mockResolvedValue(
|
91
|
+
true,
|
92
|
+
);
|
93
|
+
const result = await threePhaseMilesightController.rebootDevice(data);
|
94
|
+
expect(result).toEqual(controllerResult);
|
95
|
+
});
|
96
|
+
it('should handle error properly', async () => {
|
97
|
+
jest.spyOn(threePhaseMilesightService, 'rebootDevice').mockResolvedValue(
|
98
|
+
null,
|
99
|
+
);
|
100
|
+
let errorResult = null;
|
101
|
+
try {
|
102
|
+
await threePhaseMilesightController.rebootDevice(data);
|
103
|
+
} catch (error) {
|
104
|
+
errorResult = error;
|
105
|
+
}
|
106
|
+
expect(errorResult).toBeInstanceOf(HttpException);
|
107
|
+
});
|
108
|
+
});
|
109
|
+
describe('setDeviceInterval', () => {
|
110
|
+
const data = {
|
111
|
+
deviceEui: 'sampleDeviceEui',
|
112
|
+
serialNumber: 'sampleDeviceName',
|
113
|
+
interval: 1440,
|
114
|
+
};
|
115
|
+
it('should return result', async () => {
|
116
|
+
const controllerResult = {
|
117
|
+
message: `Successfully sent set device interval command for three-phase device sampleDeviceEui.`,
|
118
|
+
result: true,
|
119
|
+
};
|
120
|
+
jest.spyOn(threePhaseMilesightService, 'setDeviceInterval').mockResolvedValue(
|
121
|
+
true,
|
122
|
+
);
|
123
|
+
const result = await threePhaseMilesightController.setDeviceInterval(data);
|
124
|
+
expect(result).toEqual(controllerResult);
|
125
|
+
});
|
126
|
+
it('should handle error properly', async () => {
|
127
|
+
jest.spyOn(threePhaseMilesightService, 'setDeviceInterval').mockResolvedValue(
|
128
|
+
null,
|
129
|
+
);
|
130
|
+
let errorResult = null;
|
131
|
+
try {
|
132
|
+
await threePhaseMilesightController.setDeviceInterval(data);
|
133
|
+
} catch (error) {
|
134
|
+
errorResult = error;
|
135
|
+
}
|
136
|
+
expect(errorResult).toBeInstanceOf(HttpException);
|
137
|
+
});
|
138
|
+
});
|
139
|
+
});
|
@@ -0,0 +1,88 @@
|
|
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 {
|
13
|
+
DeviceListParamsDto,
|
14
|
+
MilesightBaseDto,
|
15
|
+
MilesightSetIntervalDto,
|
16
|
+
} from '../dto/milesight.dto';
|
17
|
+
import { ThreePhaseMilesightService } from '../service/three-phase-milesight.service';
|
18
|
+
import { AWSListThings } from '@coinspect/therma-action-rule-decoder';
|
19
|
+
|
20
|
+
@Controller('powermeter/three-phase/milesight')
|
21
|
+
export class ThreePhaseMilesightController {
|
22
|
+
constructor(private readonly threePhaseMilesightService: ThreePhaseMilesightService) {}
|
23
|
+
@Get()
|
24
|
+
async getDeviceList(
|
25
|
+
@Query() queryParams: DeviceListParamsDto,
|
26
|
+
): Promise<{ message: string; result: AWSListThings }> {
|
27
|
+
const { maxResult, nextToken } = queryParams;
|
28
|
+
const result = await this.threePhaseMilesightService.getDeviceList(
|
29
|
+
maxResult,
|
30
|
+
nextToken,
|
31
|
+
);
|
32
|
+
if (!result) {
|
33
|
+
throw new HttpException(
|
34
|
+
'Error occured',
|
35
|
+
HttpStatus.INTERNAL_SERVER_ERROR,
|
36
|
+
);
|
37
|
+
}
|
38
|
+
return {
|
39
|
+
message: 'Retrieved three-phase powermeter device list details',
|
40
|
+
result,
|
41
|
+
};
|
42
|
+
}
|
43
|
+
|
44
|
+
@Post('reboot')
|
45
|
+
@UsePipes(new ValidationPipe())
|
46
|
+
async rebootDevice(
|
47
|
+
@Body() body: MilesightBaseDto,
|
48
|
+
): Promise<{ message: string; result: boolean }> {
|
49
|
+
const { deviceEui, serialNumber } = body;
|
50
|
+
const result = await this.threePhaseMilesightService.rebootDevice(
|
51
|
+
deviceEui,
|
52
|
+
serialNumber ? serialNumber : deviceEui,
|
53
|
+
);
|
54
|
+
if (!result) {
|
55
|
+
throw new HttpException(
|
56
|
+
`Error occurred rebooting three-phase device: ${deviceEui}`,
|
57
|
+
HttpStatus.INTERNAL_SERVER_ERROR,
|
58
|
+
);
|
59
|
+
}
|
60
|
+
return {
|
61
|
+
message: `Successfully sent reboot command for three-phase device ${deviceEui}.`,
|
62
|
+
result,
|
63
|
+
};
|
64
|
+
}
|
65
|
+
|
66
|
+
@Post('interval')
|
67
|
+
@UsePipes(new ValidationPipe())
|
68
|
+
async setDeviceInterval(
|
69
|
+
@Body() body: MilesightSetIntervalDto,
|
70
|
+
): Promise<{ message: string; result: boolean }> {
|
71
|
+
const { deviceEui, serialNumber, interval } = body;
|
72
|
+
const result = await this.threePhaseMilesightService.setDeviceInterval(
|
73
|
+
deviceEui,
|
74
|
+
serialNumber ? serialNumber : deviceEui,
|
75
|
+
interval,
|
76
|
+
);
|
77
|
+
if (!result) {
|
78
|
+
throw new HttpException(
|
79
|
+
`Error occurred setting interval for three-phase device: ${deviceEui}`,
|
80
|
+
HttpStatus.INTERNAL_SERVER_ERROR,
|
81
|
+
);
|
82
|
+
}
|
83
|
+
return {
|
84
|
+
message: `Successfully sent set device interval command for three-phase device ${deviceEui}.`,
|
85
|
+
result,
|
86
|
+
};
|
87
|
+
}
|
88
|
+
}
|
@@ -1,11 +1,13 @@
|
|
1
1
|
import { Module } from '@nestjs/common';
|
2
2
|
import { MilesightController } from '../controller/milesight.controller';
|
3
|
+
import { ThreePhaseMilesightController } from '../controller/three-phase-milesight.controller';
|
3
4
|
import { MilesightService } from '../service/milesight.service';
|
5
|
+
import { ThreePhaseMilesightService } from '../service/three-phase-milesight.service';
|
4
6
|
import { DownlinkModule } from './downlink.module';
|
5
7
|
|
6
8
|
@Module({
|
7
9
|
imports: [DownlinkModule],
|
8
|
-
controllers: [MilesightController],
|
9
|
-
providers: [MilesightService],
|
10
|
+
controllers: [MilesightController, ThreePhaseMilesightController],
|
11
|
+
providers: [MilesightService, ThreePhaseMilesightService],
|
10
12
|
})
|
11
|
-
export class MilesightPowermeterModule {}
|
13
|
+
export class MilesightPowermeterModule {}
|
@@ -0,0 +1,228 @@
|
|
1
|
+
import { Test, TestingModule } from '@nestjs/testing';
|
2
|
+
import { Logger } from '@nestjs/common';
|
3
|
+
import { DownlinkService } from './downlink.service';
|
4
|
+
import { ThreePhaseMilesightService } from './three-phase-milesight.service';
|
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
|
+
|
23
|
+
describe('ThreePhaseMilesightService', () => {
|
24
|
+
let threePhaseMilesightService: ThreePhaseMilesightService;
|
25
|
+
let downlinkService: DownlinkService;
|
26
|
+
let module: TestingModule;
|
27
|
+
const mockSendDownlink = jest.fn();
|
28
|
+
const mockLogger = {
|
29
|
+
error: jest.fn(),
|
30
|
+
log: jest.fn(),
|
31
|
+
warn: jest.fn(),
|
32
|
+
debug: jest.fn(),
|
33
|
+
verbose: jest.fn(),
|
34
|
+
};
|
35
|
+
|
36
|
+
beforeEach(async () => {
|
37
|
+
module = await Test.createTestingModule({
|
38
|
+
providers: [
|
39
|
+
{
|
40
|
+
provide: DownlinkService,
|
41
|
+
useValue: {
|
42
|
+
sendDownlink: mockSendDownlink,
|
43
|
+
},
|
44
|
+
},
|
45
|
+
{
|
46
|
+
provide: Logger,
|
47
|
+
useValue: mockLogger,
|
48
|
+
},
|
49
|
+
ThreePhaseMilesightService,
|
50
|
+
],
|
51
|
+
}).compile();
|
52
|
+
|
53
|
+
threePhaseMilesightService = module.get<ThreePhaseMilesightService>(ThreePhaseMilesightService);
|
54
|
+
downlinkService = module.get<DownlinkService>(DownlinkService);
|
55
|
+
});
|
56
|
+
|
57
|
+
afterEach(async () => {
|
58
|
+
jest.clearAllMocks();
|
59
|
+
mockLogger.error.mockClear();
|
60
|
+
});
|
61
|
+
|
62
|
+
afterAll(async () => {
|
63
|
+
if (module) {
|
64
|
+
await module.close();
|
65
|
+
}
|
66
|
+
});
|
67
|
+
|
68
|
+
describe('getDeviceList', () => {
|
69
|
+
it('should return three-phase device list details', async () => {
|
70
|
+
const MaxResult = 1;
|
71
|
+
const expectedResult = {
|
72
|
+
$metadata: {
|
73
|
+
httpStatusCode: 200,
|
74
|
+
requestId: 'b5f4ba6b-de9c-4c17-9879-b6e2e02d14f5',
|
75
|
+
extendedRequestId: undefined,
|
76
|
+
cfId: undefined,
|
77
|
+
attempts: 1,
|
78
|
+
totalRetryDelay: 0,
|
79
|
+
},
|
80
|
+
nextToken: 'sampleToken',
|
81
|
+
things: [
|
82
|
+
{
|
83
|
+
attributes: {},
|
84
|
+
thingArn:
|
85
|
+
'arn:aws:iot:us-east-1:224361144749:thing/three-phase-device',
|
86
|
+
thingName: 'three-phase-device',
|
87
|
+
thingTypeName: 'three-phase-powermeter-milesight',
|
88
|
+
version: 1,
|
89
|
+
},
|
90
|
+
],
|
91
|
+
};
|
92
|
+
mockGetListDevices.mockResolvedValue(expectedResult);
|
93
|
+
|
94
|
+
const result = await threePhaseMilesightService.getDeviceList(MaxResult);
|
95
|
+
|
96
|
+
expect(mockGetListDevices).toHaveBeenCalledWith(
|
97
|
+
'three-phase-powermeter-milesight',
|
98
|
+
MaxResult,
|
99
|
+
undefined
|
100
|
+
);
|
101
|
+
expect(result).toEqual(expectedResult);
|
102
|
+
});
|
103
|
+
|
104
|
+
it('should return null when getting error on device list', async () => {
|
105
|
+
const MaxResult = 1;
|
106
|
+
mockGetListDevices.mockRejectedValue(new Error('Test error'));
|
107
|
+
|
108
|
+
const response = await threePhaseMilesightService.getDeviceList(MaxResult);
|
109
|
+
expect(response).toBeNull();
|
110
|
+
mockGetListDevices.mockClear();
|
111
|
+
});
|
112
|
+
});
|
113
|
+
|
114
|
+
describe('rebootDevice', () => {
|
115
|
+
const data = {
|
116
|
+
deviceEui: 'sampleThreePhaseDeviceEui',
|
117
|
+
serialNumber: 'sampleThreePhaseDeviceName',
|
118
|
+
};
|
119
|
+
|
120
|
+
it('should return true when downlink is successful', async () => {
|
121
|
+
const expectedDownlinkCommand = 'ff10ff';
|
122
|
+
const sendDownlinkSpy = jest
|
123
|
+
.spyOn(downlinkService, 'sendDownlink')
|
124
|
+
.mockResolvedValue(true);
|
125
|
+
|
126
|
+
const result = await threePhaseMilesightService.rebootDevice(
|
127
|
+
data.deviceEui,
|
128
|
+
data.serialNumber,
|
129
|
+
);
|
130
|
+
|
131
|
+
expect(sendDownlinkSpy).toHaveBeenCalledWith(
|
132
|
+
data.deviceEui,
|
133
|
+
data.serialNumber,
|
134
|
+
expectedDownlinkCommand,
|
135
|
+
'threePhaseMilesightRebootDevice',
|
136
|
+
85,
|
137
|
+
true,
|
138
|
+
);
|
139
|
+
expect(result).toBe(true);
|
140
|
+
sendDownlinkSpy.mockClear();
|
141
|
+
});
|
142
|
+
|
143
|
+
it('should return null when downlink fails', async () => {
|
144
|
+
const sendDownlinkSpy = jest
|
145
|
+
.spyOn(downlinkService, 'sendDownlink')
|
146
|
+
.mockRejectedValue(new Error('Downlink error'));
|
147
|
+
|
148
|
+
const result = await threePhaseMilesightService.rebootDevice(
|
149
|
+
data.deviceEui,
|
150
|
+
data.serialNumber,
|
151
|
+
);
|
152
|
+
|
153
|
+
expect(result).toBeNull();
|
154
|
+
sendDownlinkSpy.mockClear();
|
155
|
+
});
|
156
|
+
});
|
157
|
+
|
158
|
+
describe('setDeviceInterval', () => {
|
159
|
+
const data = {
|
160
|
+
deviceEui: 'sampleThreePhaseDeviceEui',
|
161
|
+
serialNumber: 'sampleThreePhaseDeviceName',
|
162
|
+
interval: 1440,
|
163
|
+
};
|
164
|
+
|
165
|
+
it('should return true when downlink is successful', async () => {
|
166
|
+
const expectedDownlinkCommand = 'ff8e0005a0'; // 1440 minutes in hex
|
167
|
+
const sendDownlinkSpy = jest
|
168
|
+
.spyOn(downlinkService, 'sendDownlink')
|
169
|
+
.mockResolvedValue(true);
|
170
|
+
|
171
|
+
const result = await threePhaseMilesightService.setDeviceInterval(
|
172
|
+
data.deviceEui,
|
173
|
+
data.serialNumber,
|
174
|
+
data.interval,
|
175
|
+
);
|
176
|
+
|
177
|
+
expect(sendDownlinkSpy).toHaveBeenCalledWith(
|
178
|
+
data.deviceEui,
|
179
|
+
data.serialNumber,
|
180
|
+
expectedDownlinkCommand,
|
181
|
+
'threePhaseMilesightSetDeviceInterval',
|
182
|
+
85,
|
183
|
+
true,
|
184
|
+
);
|
185
|
+
expect(result).toBe(true);
|
186
|
+
sendDownlinkSpy.mockClear();
|
187
|
+
});
|
188
|
+
|
189
|
+
it('should return null when downlink fails', async () => {
|
190
|
+
const sendDownlinkSpy = jest
|
191
|
+
.spyOn(downlinkService, 'sendDownlink')
|
192
|
+
.mockRejectedValue(new Error('Downlink error'));
|
193
|
+
|
194
|
+
const result = await threePhaseMilesightService.setDeviceInterval(
|
195
|
+
data.deviceEui,
|
196
|
+
data.serialNumber,
|
197
|
+
data.interval,
|
198
|
+
);
|
199
|
+
|
200
|
+
expect(result).toBeNull();
|
201
|
+
sendDownlinkSpy.mockClear();
|
202
|
+
});
|
203
|
+
|
204
|
+
it('should correctly convert different intervals to hex', async () => {
|
205
|
+
const sendDownlinkSpy = jest
|
206
|
+
.spyOn(downlinkService, 'sendDownlink')
|
207
|
+
.mockResolvedValue(true);
|
208
|
+
|
209
|
+
// Test interval of 60 minutes (0x003c in hex)
|
210
|
+
await threePhaseMilesightService.setDeviceInterval(
|
211
|
+
data.deviceEui,
|
212
|
+
data.serialNumber,
|
213
|
+
60,
|
214
|
+
);
|
215
|
+
|
216
|
+
expect(sendDownlinkSpy).toHaveBeenCalledWith(
|
217
|
+
data.deviceEui,
|
218
|
+
data.serialNumber,
|
219
|
+
'ff8e00003c',
|
220
|
+
'threePhaseMilesightSetDeviceInterval',
|
221
|
+
85,
|
222
|
+
true,
|
223
|
+
);
|
224
|
+
|
225
|
+
sendDownlinkSpy.mockClear();
|
226
|
+
});
|
227
|
+
});
|
228
|
+
});
|
@@ -0,0 +1,97 @@
|
|
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
|
+
|
11
|
+
@Injectable()
|
12
|
+
export class ThreePhaseMilesightService {
|
13
|
+
private readonly logger: Logger;
|
14
|
+
private readonly awsIotAPI: AWSIotAPI;
|
15
|
+
|
16
|
+
constructor(
|
17
|
+
@Inject(DownlinkService)
|
18
|
+
private readonly downlinkService: DownlinkService,
|
19
|
+
) {
|
20
|
+
this.logger = new Logger(ThreePhaseMilesightService.name);
|
21
|
+
this.awsIotAPI = new AWSIotAPI(config.get('aws'));
|
22
|
+
}
|
23
|
+
|
24
|
+
async getDeviceList(
|
25
|
+
maxResult: number,
|
26
|
+
nextToken?: string,
|
27
|
+
): Promise<AWSListThings | null> {
|
28
|
+
try {
|
29
|
+
this.logger.log('Retrieving three-phase devices list');
|
30
|
+
const result = await this.awsIotAPI.getListThingsByType(
|
31
|
+
config.get('threePhaseThingType'),
|
32
|
+
maxResult,
|
33
|
+
nextToken,
|
34
|
+
);
|
35
|
+
return result;
|
36
|
+
} catch (error) {
|
37
|
+
this.logger.error(
|
38
|
+
`Error retrieving three-phase devices list details: ${error}`,
|
39
|
+
);
|
40
|
+
return null;
|
41
|
+
}
|
42
|
+
}
|
43
|
+
|
44
|
+
async rebootDevice(
|
45
|
+
deviceEui: string,
|
46
|
+
serialNumber: string,
|
47
|
+
): Promise<boolean | null> {
|
48
|
+
try {
|
49
|
+
this.logger.log(`Rebooting three-phase device: ${deviceEui}`);
|
50
|
+
const rebootDownlinkCommand = 'ff10ff';
|
51
|
+
const response = await this.downlinkService.sendDownlink(
|
52
|
+
deviceEui,
|
53
|
+
serialNumber,
|
54
|
+
rebootDownlinkCommand,
|
55
|
+
'threePhaseMilesightRebootDevice',
|
56
|
+
fPort,
|
57
|
+
true,
|
58
|
+
);
|
59
|
+
this.logger.log(`Successfully rebooted three-phase device ${deviceEui}`);
|
60
|
+
return response;
|
61
|
+
} catch (error) {
|
62
|
+
this.logger.error(`Error rebooting three-phase device ${deviceEui}: ${error}`);
|
63
|
+
return null;
|
64
|
+
}
|
65
|
+
}
|
66
|
+
|
67
|
+
async setDeviceInterval(
|
68
|
+
deviceEui: string,
|
69
|
+
serialNumber: string,
|
70
|
+
interval: number,
|
71
|
+
): Promise<boolean | null> {
|
72
|
+
try {
|
73
|
+
this.logger.log(
|
74
|
+
`Setting ${interval} minutes interval for three-phase device: ${deviceEui}`,
|
75
|
+
);
|
76
|
+
const hexString = interval.toString(16).padStart(4, '0');
|
77
|
+
const downlinkCommand = 'ff8e00' + hexString;
|
78
|
+
const response = await this.downlinkService.sendDownlink(
|
79
|
+
deviceEui,
|
80
|
+
serialNumber,
|
81
|
+
downlinkCommand,
|
82
|
+
'threePhaseMilesightSetDeviceInterval',
|
83
|
+
fPort,
|
84
|
+
true,
|
85
|
+
);
|
86
|
+
this.logger.log(
|
87
|
+
`Successfully set device interval for three-phase device ${deviceEui}`,
|
88
|
+
);
|
89
|
+
return response;
|
90
|
+
} catch (error) {
|
91
|
+
this.logger.error(
|
92
|
+
`Error setting device interval for three-phase device ${deviceEui}: ${error}`,
|
93
|
+
);
|
94
|
+
return null;
|
95
|
+
}
|
96
|
+
}
|
97
|
+
}
|
package/timestamp.sh
CHANGED
@@ -1 +1 @@
|
|
1
|
-
export DT=
|
1
|
+
export DT=20250702214334
|
package/tsconfig.build.json
CHANGED