@enyo-energy/sunspec-sdk 0.0.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 +1 -0
- package/dist/cjs/sunspec-devices.cjs +334 -0
- package/dist/cjs/sunspec-devices.d.cts +87 -0
- package/dist/cjs/sunspec-interfaces.cjs +52 -0
- package/dist/cjs/sunspec-interfaces.d.cts +240 -0
- package/dist/cjs/sunspec-modbus-client.cjs +715 -0
- package/dist/cjs/sunspec-modbus-client.d.cts +111 -0
- package/dist/cjs/version.cjs +19 -0
- package/dist/cjs/version.d.cts +13 -0
- package/dist/sunspec-devices.d.ts +87 -0
- package/dist/sunspec-devices.js +327 -0
- package/dist/sunspec-interfaces.d.ts +240 -0
- package/dist/sunspec-interfaces.js +49 -0
- package/dist/sunspec-modbus-client.d.ts +111 -0
- package/dist/sunspec-modbus-client.js +711 -0
- package/dist/version.d.ts +13 -0
- package/dist/version.js +15 -0
- package/package.json +45 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { type SunspecInverterControls, type SunspecInverterData, type SunspecInverterSettings, type SunspecMeterData, type SunspecModel, type SunspecMPPTData } from "./sunspec-interfaces.cjs";
|
|
2
|
+
import { IConnectionHealth } from "@enyo-energy/energy-app-sdk/dist/implementations/modbus/interfaces.js";
|
|
3
|
+
import { EnergyApp } from "@enyo-energy/energy-app-sdk";
|
|
4
|
+
export declare class SunspecModbusClient {
|
|
5
|
+
private energyApp;
|
|
6
|
+
private modbusClient;
|
|
7
|
+
private discoveredModels;
|
|
8
|
+
private scaleFactors;
|
|
9
|
+
private connected;
|
|
10
|
+
private baseAddress;
|
|
11
|
+
private connectionHealth;
|
|
12
|
+
private faultTolerantReader;
|
|
13
|
+
constructor(energyApp: EnergyApp);
|
|
14
|
+
/**
|
|
15
|
+
* Connect to Modbus device
|
|
16
|
+
*/
|
|
17
|
+
connect(host: string, port?: number, unitId?: number): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* Disconnect from Modbus device
|
|
20
|
+
*/
|
|
21
|
+
disconnect(): Promise<void>;
|
|
22
|
+
/**
|
|
23
|
+
* Discover all available Sunspec models
|
|
24
|
+
* Scans through the address space starting at 40001
|
|
25
|
+
*/
|
|
26
|
+
discoverModels(): Promise<Map<number, SunspecModel>>;
|
|
27
|
+
/**
|
|
28
|
+
* Find a specific model by ID
|
|
29
|
+
*/
|
|
30
|
+
findModel(modelId: number): SunspecModel | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* Read a register value and apply scale factor
|
|
33
|
+
*/
|
|
34
|
+
readRegisterWithScaleFactor(valueAddress: number, scaleFactorAddress?: number, quantity?: number): Promise<number>;
|
|
35
|
+
/**
|
|
36
|
+
* Convert unsigned 16-bit value to signed
|
|
37
|
+
*/
|
|
38
|
+
private convertToSigned16;
|
|
39
|
+
/**
|
|
40
|
+
* Helper to clean string values by removing null characters
|
|
41
|
+
*/
|
|
42
|
+
private cleanString;
|
|
43
|
+
/**
|
|
44
|
+
* Helper to read register value(s) using the fault-tolerant reader with data type conversion
|
|
45
|
+
*/
|
|
46
|
+
private readRegisterValue;
|
|
47
|
+
/**
|
|
48
|
+
* Read inverter data from Model 103 (Three Phase)
|
|
49
|
+
*/
|
|
50
|
+
readInverterData(): Promise<SunspecInverterData | null>;
|
|
51
|
+
/**
|
|
52
|
+
* Read single phase inverter data (Model 101)
|
|
53
|
+
*/
|
|
54
|
+
private readSinglePhaseInverterData;
|
|
55
|
+
/**
|
|
56
|
+
* Read inverter scale factors
|
|
57
|
+
*/
|
|
58
|
+
private readInverterScaleFactors;
|
|
59
|
+
/**
|
|
60
|
+
* Apply scale factor to a value
|
|
61
|
+
*/
|
|
62
|
+
private applyScaleFactor;
|
|
63
|
+
/**
|
|
64
|
+
* Read MPPT data from Model 160
|
|
65
|
+
*/
|
|
66
|
+
readMPPTData(moduleId?: number): Promise<SunspecMPPTData | null>;
|
|
67
|
+
/**
|
|
68
|
+
* Read all available MPPT strings
|
|
69
|
+
*/
|
|
70
|
+
readAllMPPTData(): Promise<SunspecMPPTData[]>;
|
|
71
|
+
/**
|
|
72
|
+
* Read meter data (Model 203 for 3-phase)
|
|
73
|
+
*/
|
|
74
|
+
readMeterData(): Promise<SunspecMeterData | null>;
|
|
75
|
+
/**
|
|
76
|
+
* Read common block data (Model 1)
|
|
77
|
+
*/
|
|
78
|
+
readCommonBlock(): Promise<any>;
|
|
79
|
+
/**
|
|
80
|
+
* Get serial number from device
|
|
81
|
+
*/
|
|
82
|
+
getSerialNumber(): Promise<string | undefined>;
|
|
83
|
+
/**
|
|
84
|
+
* Check if connected
|
|
85
|
+
*/
|
|
86
|
+
isConnected(): boolean;
|
|
87
|
+
/**
|
|
88
|
+
* Check if connection is healthy
|
|
89
|
+
*/
|
|
90
|
+
isHealthy(): boolean;
|
|
91
|
+
/**
|
|
92
|
+
* Get connection health details
|
|
93
|
+
*/
|
|
94
|
+
getConnectionHealth(): IConnectionHealth;
|
|
95
|
+
/**
|
|
96
|
+
* Read Block 121 - Inverter Basic Settings
|
|
97
|
+
*/
|
|
98
|
+
readInverterSettings(): Promise<SunspecInverterSettings | null>;
|
|
99
|
+
/**
|
|
100
|
+
* Read Block 123 - Immediate Inverter Controls
|
|
101
|
+
*/
|
|
102
|
+
readInverterControls(): Promise<SunspecInverterControls | null>;
|
|
103
|
+
/**
|
|
104
|
+
* Write Block 121 - Inverter Basic Settings
|
|
105
|
+
*/
|
|
106
|
+
writeInverterSettings(settings: Partial<SunspecInverterSettings>): Promise<boolean>;
|
|
107
|
+
/**
|
|
108
|
+
* Write Block 123 - Immediate Inverter Controls
|
|
109
|
+
*/
|
|
110
|
+
writeInverterControls(controls: Partial<SunspecInverterControls>): Promise<boolean>;
|
|
111
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Auto-generated file containing the current SDK version.
|
|
4
|
+
* This file is generated by scripts/extract-version.js and should not be edited manually.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.SDK_VERSION = void 0;
|
|
8
|
+
exports.getSdkVersion = getSdkVersion;
|
|
9
|
+
/**
|
|
10
|
+
* Current version of the enyo Energy App SDK.
|
|
11
|
+
*/
|
|
12
|
+
exports.SDK_VERSION = '0.0.1';
|
|
13
|
+
/**
|
|
14
|
+
* Gets the current SDK version.
|
|
15
|
+
* @returns The semantic version string of the SDK
|
|
16
|
+
*/
|
|
17
|
+
function getSdkVersion() {
|
|
18
|
+
return exports.SDK_VERSION;
|
|
19
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-generated file containing the current SDK version.
|
|
3
|
+
* This file is generated by scripts/extract-version.js and should not be edited manually.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Current version of the enyo Energy App SDK.
|
|
7
|
+
*/
|
|
8
|
+
export declare const SDK_VERSION = "0.0.1";
|
|
9
|
+
/**
|
|
10
|
+
* Gets the current SDK version.
|
|
11
|
+
* @returns The semantic version string of the SDK
|
|
12
|
+
*/
|
|
13
|
+
export declare function getSdkVersion(): string;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { ApplianceManager, EnergyApp } from "@enyo-energy/energy-app-sdk";
|
|
2
|
+
import { EnyoApplianceName } from "@enyo-energy/energy-app-sdk/dist/types/enyo-appliance.js";
|
|
3
|
+
import { EnyoNetworkDevice } from "@enyo-energy/energy-app-sdk/dist/types/enyo-network-device.js";
|
|
4
|
+
import { SunspecModbusClient } from "./sunspec-modbus-client.js";
|
|
5
|
+
import { EnyoDataBusMessage } from "@enyo-energy/energy-app-sdk/dist/types/enyo-data-bus-value.js";
|
|
6
|
+
/**
|
|
7
|
+
* Base abstract class for all Sunspec devices
|
|
8
|
+
*/
|
|
9
|
+
export declare abstract class BaseSunspecDevice {
|
|
10
|
+
protected readonly energyApp: EnergyApp;
|
|
11
|
+
readonly name: EnyoApplianceName[];
|
|
12
|
+
readonly networkDevice: EnyoNetworkDevice;
|
|
13
|
+
protected readonly sunspecClient: SunspecModbusClient;
|
|
14
|
+
protected readonly applianceManager: ApplianceManager;
|
|
15
|
+
protected readonly unitId: number;
|
|
16
|
+
protected applianceId?: string;
|
|
17
|
+
protected lastUpdateTime: number;
|
|
18
|
+
protected constructor(energyApp: EnergyApp, name: EnyoApplianceName[], networkDevice: EnyoNetworkDevice, sunspecClient: SunspecModbusClient, applianceManager: ApplianceManager, unitId?: number);
|
|
19
|
+
/**
|
|
20
|
+
* Connect to the device and create/update the appliance
|
|
21
|
+
*/
|
|
22
|
+
abstract connect(): Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* Disconnect from the device and update appliance state
|
|
25
|
+
*/
|
|
26
|
+
abstract disconnect(): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Update device data and return data bus messages
|
|
29
|
+
*/
|
|
30
|
+
abstract readData(clockId: string, resolution: '10s' | '30s' | '1m' | '15m'): Promise<EnyoDataBusMessage[]>;
|
|
31
|
+
/**
|
|
32
|
+
* Check if the device is connected
|
|
33
|
+
*/
|
|
34
|
+
isConnected(): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Get the appliance IDs managed by this device
|
|
37
|
+
*/
|
|
38
|
+
/**
|
|
39
|
+
* Ensure the Sunspec client is connected and models are discovered
|
|
40
|
+
*/
|
|
41
|
+
protected ensureConnected(): Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Sunspec Inverter implementation using dynamic model discovery
|
|
45
|
+
*/
|
|
46
|
+
export declare class SunspecInverter extends BaseSunspecDevice {
|
|
47
|
+
connect(): Promise<void>;
|
|
48
|
+
disconnect(): Promise<void>;
|
|
49
|
+
isConnected(): boolean;
|
|
50
|
+
readData(clockId: string, resolution: '10s' | '30s' | '1m' | '15m'): Promise<EnyoDataBusMessage[]>;
|
|
51
|
+
private mapOperatingState;
|
|
52
|
+
/**
|
|
53
|
+
* Map MPPT data to DC string structure for data bus
|
|
54
|
+
*/
|
|
55
|
+
private mapMPPTToStrings;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Sunspec Battery implementation
|
|
59
|
+
*/
|
|
60
|
+
export declare class SunspecBattery extends BaseSunspecDevice {
|
|
61
|
+
/**
|
|
62
|
+
* Connect to the battery and create/update the appliance
|
|
63
|
+
*/
|
|
64
|
+
connect(): Promise<void>;
|
|
65
|
+
disconnect(): Promise<void>;
|
|
66
|
+
/**
|
|
67
|
+
* Update battery data and return data bus messages
|
|
68
|
+
*/
|
|
69
|
+
readData(clockId: string, resolution: '10s' | '30s' | '1m' | '15m'): Promise<EnyoDataBusMessage[]>;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Sunspec Meter implementation
|
|
73
|
+
*/
|
|
74
|
+
export declare class SunspecMeter extends BaseSunspecDevice {
|
|
75
|
+
/**
|
|
76
|
+
* Connect to the meter and create/update the appliance
|
|
77
|
+
*/
|
|
78
|
+
connect(): Promise<void>;
|
|
79
|
+
/**
|
|
80
|
+
* Disconnect from the meter and update appliance state
|
|
81
|
+
*/
|
|
82
|
+
disconnect(): Promise<void>;
|
|
83
|
+
/**
|
|
84
|
+
* Update meter data and return data bus messages
|
|
85
|
+
*/
|
|
86
|
+
readData(clockId: string, resolution: '10s' | '30s' | '1m' | '15m'): Promise<EnyoDataBusMessage[]>;
|
|
87
|
+
}
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import { SunspecModelId } from "./sunspec-interfaces.js";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { EnyoApplianceConnectionType, EnyoApplianceStateEnum, EnyoApplianceTypeEnum } from "@enyo-energy/energy-app-sdk/dist/types/enyo-appliance.js";
|
|
4
|
+
import { EnyoBatteryStateEnum, EnyoDataBusMessageEnum, EnyoInverterStateEnum } from "@enyo-energy/energy-app-sdk/dist/types/enyo-data-bus-value.js";
|
|
5
|
+
import { EnyoSourceEnum } from "@enyo-energy/energy-app-sdk/dist/types/enyo-source.enum.js";
|
|
6
|
+
/**
|
|
7
|
+
* Base abstract class for all Sunspec devices
|
|
8
|
+
*/
|
|
9
|
+
export class BaseSunspecDevice {
|
|
10
|
+
energyApp;
|
|
11
|
+
name;
|
|
12
|
+
networkDevice;
|
|
13
|
+
sunspecClient;
|
|
14
|
+
applianceManager;
|
|
15
|
+
unitId;
|
|
16
|
+
applianceId;
|
|
17
|
+
lastUpdateTime = 0;
|
|
18
|
+
constructor(energyApp, name, networkDevice, sunspecClient, applianceManager, unitId = 1) {
|
|
19
|
+
this.energyApp = energyApp;
|
|
20
|
+
this.name = name;
|
|
21
|
+
this.networkDevice = networkDevice;
|
|
22
|
+
this.sunspecClient = sunspecClient;
|
|
23
|
+
this.applianceManager = applianceManager;
|
|
24
|
+
this.unitId = unitId;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Check if the device is connected
|
|
28
|
+
*/
|
|
29
|
+
isConnected() {
|
|
30
|
+
return this.sunspecClient.isConnected();
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Get the appliance IDs managed by this device
|
|
34
|
+
*/
|
|
35
|
+
/**
|
|
36
|
+
* Ensure the Sunspec client is connected and models are discovered
|
|
37
|
+
*/
|
|
38
|
+
async ensureConnected() {
|
|
39
|
+
if (!this.sunspecClient.isConnected()) {
|
|
40
|
+
await this.sunspecClient.connect(this.networkDevice.ipAddress, 502, this.unitId);
|
|
41
|
+
await this.sunspecClient.discoverModels();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Sunspec Inverter implementation using dynamic model discovery
|
|
47
|
+
*/
|
|
48
|
+
export class SunspecInverter extends BaseSunspecDevice {
|
|
49
|
+
async connect() {
|
|
50
|
+
// Ensure Sunspec client is connected
|
|
51
|
+
if (!this.sunspecClient.isConnected()) {
|
|
52
|
+
await this.sunspecClient.connect(this.networkDevice.ipAddress, 502, this.unitId);
|
|
53
|
+
await this.sunspecClient.discoverModels();
|
|
54
|
+
}
|
|
55
|
+
// Get device info from common block
|
|
56
|
+
const commonData = await this.sunspecClient.readCommonBlock();
|
|
57
|
+
// Create or update appliance
|
|
58
|
+
try {
|
|
59
|
+
this.applianceId = await this.applianceManager.createOrUpdateAppliance({
|
|
60
|
+
name: this.name,
|
|
61
|
+
type: EnyoApplianceTypeEnum.Inverter,
|
|
62
|
+
networkDevices: [this.networkDevice],
|
|
63
|
+
metadata: {
|
|
64
|
+
connectionType: EnyoApplianceConnectionType.Connector,
|
|
65
|
+
state: EnyoApplianceStateEnum.Connected,
|
|
66
|
+
serialNumber: commonData?.serialNumber,
|
|
67
|
+
modelName: commonData?.model,
|
|
68
|
+
vendorName: commonData?.manufacturer,
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
console.log(`Sunspec Inverter connected: ${this.networkDevice.hostname} (${this.applianceId})`);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
console.error(`Failed to create inverter appliance: ${error}`);
|
|
75
|
+
}
|
|
76
|
+
// Check for MPPT models
|
|
77
|
+
const mpptModel = this.sunspecClient.findModel(SunspecModelId.MPPT);
|
|
78
|
+
if (mpptModel) {
|
|
79
|
+
console.log(`MPPT model found for inverter ${this.networkDevice.hostname}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async disconnect() {
|
|
83
|
+
if (this.applianceId) {
|
|
84
|
+
await this.applianceManager.updateApplianceState(this.applianceId, EnyoApplianceConnectionType.Connector, EnyoApplianceStateEnum.Offline);
|
|
85
|
+
}
|
|
86
|
+
// Note: We don't disconnect the sunspecClient as it may be shared
|
|
87
|
+
}
|
|
88
|
+
isConnected() {
|
|
89
|
+
return this.sunspecClient.isConnected();
|
|
90
|
+
}
|
|
91
|
+
async readData(clockId, resolution) {
|
|
92
|
+
if (!this.isConnected()) {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
const messages = [];
|
|
96
|
+
const timestamp = new Date();
|
|
97
|
+
try {
|
|
98
|
+
// Read inverter data
|
|
99
|
+
const inverterData = await this.sunspecClient.readInverterData();
|
|
100
|
+
const mpptDataList = await this.sunspecClient.readAllMPPTData();
|
|
101
|
+
if (inverterData) {
|
|
102
|
+
const inverterMessage = {
|
|
103
|
+
id: randomUUID(),
|
|
104
|
+
message: EnyoDataBusMessageEnum.InverterValuesUpdateV1,
|
|
105
|
+
type: 'message',
|
|
106
|
+
source: EnyoSourceEnum.Device,
|
|
107
|
+
applianceId: this.applianceId,
|
|
108
|
+
clockId,
|
|
109
|
+
timestampIso: timestamp.toISOString(),
|
|
110
|
+
resolution,
|
|
111
|
+
data: {
|
|
112
|
+
pvPowerW: inverterData.acPower || 0,
|
|
113
|
+
activePowerLimitationW: inverterData.acPower || 0, // Using AC power as default
|
|
114
|
+
state: this.mapOperatingState(inverterData.operatingState),
|
|
115
|
+
voltageL1: inverterData.voltageAN || 0,
|
|
116
|
+
voltageL2: inverterData.voltageBN,
|
|
117
|
+
voltageL3: inverterData.voltageCN,
|
|
118
|
+
strings: this.mapMPPTToStrings(mpptDataList)
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
messages.push(inverterMessage);
|
|
122
|
+
}
|
|
123
|
+
this.lastUpdateTime = timestamp.getTime();
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
console.error(`Error updating inverter data: ${error}`);
|
|
127
|
+
}
|
|
128
|
+
return messages;
|
|
129
|
+
}
|
|
130
|
+
mapOperatingState(state) {
|
|
131
|
+
if (!state)
|
|
132
|
+
return EnyoInverterStateEnum.Off;
|
|
133
|
+
const stateMap = {
|
|
134
|
+
1: EnyoInverterStateEnum.Off,
|
|
135
|
+
2: EnyoInverterStateEnum.Sleeping,
|
|
136
|
+
3: EnyoInverterStateEnum.Starting,
|
|
137
|
+
4: EnyoInverterStateEnum.Mppt,
|
|
138
|
+
5: EnyoInverterStateEnum.Throttled,
|
|
139
|
+
6: EnyoInverterStateEnum.ShuttingDown,
|
|
140
|
+
7: EnyoInverterStateEnum.Fault,
|
|
141
|
+
8: EnyoInverterStateEnum.Standby
|
|
142
|
+
};
|
|
143
|
+
return stateMap[state] || EnyoInverterStateEnum.Off;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Map MPPT data to DC string structure for data bus
|
|
147
|
+
*/
|
|
148
|
+
mapMPPTToStrings(mpptDataList) {
|
|
149
|
+
const result = [];
|
|
150
|
+
mpptDataList.forEach((mppt, index) => {
|
|
151
|
+
result.push({
|
|
152
|
+
index: index + 1,
|
|
153
|
+
voltage: mppt.dcVoltage,
|
|
154
|
+
powerW: mppt.dcPower
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
return result;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Sunspec Battery implementation
|
|
162
|
+
*/
|
|
163
|
+
export class SunspecBattery extends BaseSunspecDevice {
|
|
164
|
+
/**
|
|
165
|
+
* Connect to the battery and create/update the appliance
|
|
166
|
+
*/
|
|
167
|
+
async connect() {
|
|
168
|
+
// Ensure Sunspec client is connected
|
|
169
|
+
await this.ensureConnected();
|
|
170
|
+
// Check if battery models exist
|
|
171
|
+
const hasBattery = this.sunspecClient.findModel(SunspecModelId.Battery) !== undefined ||
|
|
172
|
+
this.sunspecClient.findModel(SunspecModelId.BatteryBase) !== undefined;
|
|
173
|
+
if (!hasBattery) {
|
|
174
|
+
throw new Error('No battery model found in device');
|
|
175
|
+
}
|
|
176
|
+
// Get device info
|
|
177
|
+
const commonData = await this.sunspecClient.readCommonBlock();
|
|
178
|
+
// Create or update appliance
|
|
179
|
+
try {
|
|
180
|
+
this.applianceId = await this.applianceManager.createOrUpdateAppliance({
|
|
181
|
+
name: this.name,
|
|
182
|
+
type: EnyoApplianceTypeEnum.Storage,
|
|
183
|
+
networkDevices: [this.networkDevice],
|
|
184
|
+
metadata: {
|
|
185
|
+
connectionType: EnyoApplianceConnectionType.Connector,
|
|
186
|
+
state: EnyoApplianceStateEnum.Connected,
|
|
187
|
+
serialNumber: commonData?.serialNumber ? `${commonData.serialNumber}-BAT` : undefined,
|
|
188
|
+
modelName: commonData?.model ? `${commonData.model} Battery` : 'Battery',
|
|
189
|
+
vendorName: commonData?.manufacturer,
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
console.log(`Sunspec Battery connected: ${this.networkDevice.hostname} (${this.applianceId})`);
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
console.error(`Failed to create battery appliance: ${error}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
async disconnect() {
|
|
199
|
+
if (this.applianceId) {
|
|
200
|
+
await this.applianceManager.updateApplianceState(this.applianceId, EnyoApplianceConnectionType.Connector, EnyoApplianceStateEnum.Offline);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Update battery data and return data bus messages
|
|
205
|
+
*/
|
|
206
|
+
async readData(clockId, resolution) {
|
|
207
|
+
if (!this.isConnected()) {
|
|
208
|
+
return [];
|
|
209
|
+
}
|
|
210
|
+
const messages = [];
|
|
211
|
+
const timestamp = new Date();
|
|
212
|
+
try {
|
|
213
|
+
// For now, return basic battery data
|
|
214
|
+
// In a real implementation, we would read from battery models
|
|
215
|
+
const batteryMessage = {
|
|
216
|
+
id: randomUUID(),
|
|
217
|
+
message: EnyoDataBusMessageEnum.BatteryValuesUpdateV1,
|
|
218
|
+
type: 'message',
|
|
219
|
+
source: EnyoSourceEnum.Device,
|
|
220
|
+
applianceId: this.applianceId,
|
|
221
|
+
clockId,
|
|
222
|
+
timestampIso: timestamp.toISOString(),
|
|
223
|
+
resolution,
|
|
224
|
+
data: {
|
|
225
|
+
batterySoC: 50, // Placeholder - would read from battery model
|
|
226
|
+
batteryPowerW: 0,
|
|
227
|
+
state: EnyoBatteryStateEnum.Holding
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
messages.push(batteryMessage);
|
|
231
|
+
this.lastUpdateTime = timestamp.getTime();
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
console.error(`Error updating battery data: ${error}`);
|
|
235
|
+
}
|
|
236
|
+
return messages;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Sunspec Meter implementation
|
|
241
|
+
*/
|
|
242
|
+
export class SunspecMeter extends BaseSunspecDevice {
|
|
243
|
+
/**
|
|
244
|
+
* Connect to the meter and create/update the appliance
|
|
245
|
+
*/
|
|
246
|
+
async connect() {
|
|
247
|
+
// Connect with specific unit ID for meter
|
|
248
|
+
await this.ensureConnected();
|
|
249
|
+
// Check if meter models exist
|
|
250
|
+
const hasMeter = this.sunspecClient.findModel(SunspecModelId.Meter3Phase) !== undefined ||
|
|
251
|
+
this.sunspecClient.findModel(SunspecModelId.MeterWye) !== undefined ||
|
|
252
|
+
this.sunspecClient.findModel(SunspecModelId.MeterSinglePhase) !== undefined;
|
|
253
|
+
if (!hasMeter) {
|
|
254
|
+
throw new Error('No meter model found in device');
|
|
255
|
+
}
|
|
256
|
+
// Get device info
|
|
257
|
+
const commonData = await this.sunspecClient.readCommonBlock();
|
|
258
|
+
// Create or update appliance
|
|
259
|
+
try {
|
|
260
|
+
this.applianceId = await this.applianceManager.createOrUpdateAppliance({
|
|
261
|
+
name: this.name,
|
|
262
|
+
type: EnyoApplianceTypeEnum.Meter,
|
|
263
|
+
networkDevices: [this.networkDevice],
|
|
264
|
+
metadata: {
|
|
265
|
+
connectionType: EnyoApplianceConnectionType.Connector,
|
|
266
|
+
state: EnyoApplianceStateEnum.Connected,
|
|
267
|
+
serialNumber: commonData?.serialNumber ? `${commonData.serialNumber}-MTR` : undefined,
|
|
268
|
+
modelName: commonData?.model ? `${commonData.model} Meter` : 'Meter',
|
|
269
|
+
vendorName: commonData?.manufacturer,
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
console.log(`Sunspec Meter connected: ${this.networkDevice.hostname} unit ${this.unitId} (${this.applianceId})`);
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
console.error(`Failed to create meter appliance: ${error}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Disconnect from the meter and update appliance state
|
|
280
|
+
*/
|
|
281
|
+
async disconnect() {
|
|
282
|
+
if (this.applianceId) {
|
|
283
|
+
await this.applianceManager.updateApplianceState(this.applianceId, EnyoApplianceConnectionType.Connector, EnyoApplianceStateEnum.Offline);
|
|
284
|
+
}
|
|
285
|
+
// Disconnect the client since meter uses its own connection
|
|
286
|
+
await this.sunspecClient.disconnect();
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Update meter data and return data bus messages
|
|
290
|
+
*/
|
|
291
|
+
async readData(clockId, resolution) {
|
|
292
|
+
if (!this.isConnected()) {
|
|
293
|
+
return [];
|
|
294
|
+
}
|
|
295
|
+
const messages = [];
|
|
296
|
+
const timestamp = new Date();
|
|
297
|
+
try {
|
|
298
|
+
// Read meter data
|
|
299
|
+
const meterData = await this.sunspecClient.readMeterData();
|
|
300
|
+
if (meterData) {
|
|
301
|
+
const meterMessage = {
|
|
302
|
+
id: randomUUID(),
|
|
303
|
+
message: EnyoDataBusMessageEnum.MeterValuesUpdateV1,
|
|
304
|
+
type: 'message',
|
|
305
|
+
source: EnyoSourceEnum.Device,
|
|
306
|
+
applianceId: this.applianceId,
|
|
307
|
+
clockId,
|
|
308
|
+
timestampIso: timestamp.toISOString(),
|
|
309
|
+
resolution,
|
|
310
|
+
data: {
|
|
311
|
+
gridPowerW: meterData.totalPower || 0,
|
|
312
|
+
gridFeedInWh: Number(meterData.exportedEnergy || 0),
|
|
313
|
+
gridConsumptionWh: Number(meterData.importedEnergy || 0),
|
|
314
|
+
selfConsumptionW: undefined,
|
|
315
|
+
selfConsumptionWh: undefined
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
messages.push(meterMessage);
|
|
319
|
+
}
|
|
320
|
+
this.lastUpdateTime = timestamp.getTime();
|
|
321
|
+
}
|
|
322
|
+
catch (error) {
|
|
323
|
+
console.error(`Error updating meter data: ${error}`);
|
|
324
|
+
}
|
|
325
|
+
return messages;
|
|
326
|
+
}
|
|
327
|
+
}
|