--- /dev/null
+# Code Style and Conventions
+
+## TypeScript/Node.js Conventions
+
+- **Naming**: camelCase for variables/functions/methods, PascalCase for classes/types/enums/interfaces
+- **Async operations**: Prefer async/await over raw Promises; handle rejections explicitly with try/catch
+- **Error handling**: Use typed errors (BaseError, OCPPError) with structured properties; avoid generic Error
+- **Null safety**: Avoid non-null assertions (!); use optional chaining (?.) and nullish coalescing (??)
+- **Type safety**: Prefer explicit types over any; use type guards and discriminated unions where appropriate
+
+## OCPP-specific Conventions
+
+- **Command naming**: Follow OCPP standard naming exactly (e.g., RemoteStartTransaction, BootNotification, StatusNotification)
+- **Enumeration naming**: Use standard OCPP specifications enumeration names and values exactly
+- **Version handling**: Clearly distinguish between OCPP 1.6 and 2.0.x implementations in separate namespaces/files
+- **Payload validation**: Validate against OCPP JSON schemas when ocppStrictCompliance is enabled
+- **Message format**: Use standard SRPC format: [messageTypeId, messageId, action, payload]
+
+## Testing Conventions
+
+- Use `describe()` and `it()` functions from Node.js test runner
+- Test files should be named `*.test.ts`
+- Use `@std/expect` for assertions
+- Mock charging stations with `createChargingStation()` or `createChargingStationWithEvses()`
+- Use `/* eslint-disable */` comments for specific test requirements
+- Async tests should use `await` in describe/it callbacks
+
+## File Organization
+
+- OCPP 1.6 code in `src/charging-station/ocpp/1.6/`
+- OCPP 2.0 code in `src/charging-station/ocpp/2.0/`
+- Types in `src/types/` with proper exports through index files
+- Tests mirror source structure in `tests/`
--- /dev/null
+# OCPP Implementation Architecture
+
+## Overview
+
+The project implements both OCPP 1.6 and OCPP 2.0.x protocols with clear separation:
+
+## Key Components
+
+### OCPP 2.0 Request Service
+
+- **Location**: `src/charging-station/ocpp/2.0/OCPP20RequestService.ts`
+- **Purpose**: Handles outgoing OCPP 2.0 requests
+- **Key Methods**:
+ - `buildRequestPayload()`: Constructs request payloads
+ - `requestHandler()`: Handles request processing
+ - Constructor sets up payload validation functions
+
+### Supported OCPP 2.0 Commands
+
+- `BOOT_NOTIFICATION`: Station startup notification
+- `HEARTBEAT`: Keep-alive messages
+- `NOTIFY_REPORT`: Configuration reports
+- `STATUS_NOTIFICATION`: Status updates
+
+### Request/Response Flow
+
+1. Request constructed via `buildRequestPayload()`
+2. Validation performed using JSON schemas
+3. Request sent via WebSocket
+4. Response handled by `OCPP20ResponseService`
+
+### Testing Patterns
+
+- Tests located in `tests/charging-station/ocpp/2.0/`
+- Use `OCPP20IncomingRequestService` for testing incoming requests
+- Use `OCPP20RequestService` for testing outgoing requests
+- Mock charging stations created with factory functions
+- Follow integration testing approach with real service instances
--- /dev/null
+# E-Mobility Charging Stations Simulator - Project Overview
+
+## Purpose
+
+Simple Node.js software to simulate and scale a set of charging stations based on the OCPP-J protocol as part of SAP e-Mobility solution.
+
+## Tech Stack
+
+- **Runtime**: Node.js (>=20.11.0)
+- **Language**: TypeScript
+- **Package Manager**: pnpm (>=9.0.0)
+- **Testing Framework**: Node.js native test runner with @std/expect
+- **Build Tool**: esbuild
+- **Code Quality**: ESLint, Prettier, neostandard
+- **OCPP Versions**: 1.6 and 2.0.x support
+
+## Project Structure
+
+```
+src/
+├── charging-station/ # Core charging station implementation
+│ ├── ocpp/ # OCPP protocol implementations
+│ │ ├── 1.6/ # OCPP 1.6 specific code
+│ │ └── 2.0/ # OCPP 2.0 specific code
+│ └── ui-server/ # UI server implementation
+├── types/ # TypeScript type definitions
+├── utils/ # Utility functions
+tests/
+├── charging-station/ # Charging station tests
+│ └── ocpp/ # OCPP-specific tests
+ui/web/ # Web UI application
+```
+
+## Key Features
+
+- OCPP 1.6 and 2.0.x protocol support
+- Charging stations simulation and scaling
+- Web UI for monitoring and control
+- WebSocket and HTTP protocols
+- Docker deployment support
--- /dev/null
+# Development Commands and Workflow
+
+## Essential Development Commands
+
+### Package Management
+
+```bash
+pnpm install # Install dependencies
+pnpm clean:node_modules # Clean node_modules
+```
+
+### Building
+
+```bash
+pnpm build # Production build
+pnpm build:dev # Development build with source maps
+pnpm clean:dist # Clean build output
+```
+
+### Running
+
+```bash
+pnpm start # Start production version
+pnpm start:dev # Start development version
+pnpm start:dev:debug # Start with debugging enabled
+```
+
+### Testing
+
+```bash
+pnpm build:dev # Development build with source maps
+pnpm test # Run all tests
+pnpm test:debug # Run tests with debugging
+pnpm coverage # Generate coverage report
+pnpm coverage:html # Generate HTML coverage report
+```
+
+### Code Quality
+
+```bash
+pnpm lint # Run linter
+pnpm lint:fix # Fix linting issues
+pnpm format # Format code with Prettier and fix ESLint issues
+```
+
+### UI Development
+
+```bash
+cd ui/web
+pnpm dev # Start web UI development server
+pnpm build # Build web UI for production
+```
--- /dev/null
+# Task Completion Checklist
+
+## After Completing Any Task
+
+### 1. Code Quality Checks
+
+- [ ] Run `pnpm lint` to check for linting issues
+- [ ] Run `pnpm format` to format code and fix auto-fixable issues
+- [ ] Ensure TypeScript compilation passes (part of build process)
+
+### 2. Testing
+
+- [ ] Run `pnpm test` to ensure all tests pass
+- [ ] If new functionality added, ensure appropriate tests are included
+- [ ] Check test coverage if relevant: `pnpm coverage`
+
+### 3. Build Verification
+
+- [ ] Run `pnpm build` to ensure production build succeeds
+- [ ] For development changes, verify `pnpm build:dev` works
+
+### 4. Documentation
+
+- [ ] Update relevant documentation if public API changed
+- [ ] Ensure commit messages follow Conventional Commits format
+- [ ] Update CHANGELOG.md if needed for user-facing changes
+
+### 5. OCPP Compliance (if applicable)
+
+- [ ] Verify OCPP standard compliance
+- [ ] Check that new OCPP commands/responses follow specification exactly
+- [ ] Validate against JSON schemas when `ocppStrictCompliance` is enabled
+
+## Git Workflow
+
+- Use Conventional Commits format for commit messages
+- Branch from `main` for new features
+- Ensure all quality gates pass before merging
+
+## Pre-commit Hooks
+
+The project uses husky for pre-commit hooks that automatically:
+
+- Run linting
+- Run formatting
+- Validate commit messages
--- /dev/null
+# language of the project (csharp, python, rust, java, typescript, go, cpp, or ruby)
+# * For C, use cpp
+# * For JavaScript, use typescript
+# Special requirements:
+# * csharp: Requires the presence of a .sln file in the project folder.
+language: typescript
+
+# the encoding used by text files in the project
+# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
+encoding: 'utf-8'
+
+# whether to use the project's gitignore file to ignore files
+# Added on 2025-04-07
+ignore_all_files_in_gitignore: true
+# list of additional paths to ignore
+# same syntax as gitignore, so you can use * and **
+# Was previously called `ignored_dirs`, please update your config if you are using that.
+# Added (renamed) on 2025-04-07
+ignored_paths: []
+
+# whether the project is in read-only mode
+# If set to true, all editing tools will be disabled and attempts to use them will result in an error
+# Added on 2025-04-18
+read_only: false
+
+# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
+# Below is the complete list of tools for convenience.
+# To make sure you have the latest list of tools, and to view their descriptions,
+# execute `uv run scripts/print_tool_overview.py`.
+#
+# * `activate_project`: Activates a project by name.
+# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
+# * `create_text_file`: Creates/overwrites a file in the project directory.
+# * `delete_lines`: Deletes a range of lines within a file.
+# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
+# * `execute_shell_command`: Executes a shell command.
+# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
+# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
+# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
+# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
+# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
+# * `initial_instructions`: Gets the initial instructions for the current project.
+# Should only be used in settings where the system prompt cannot be set,
+# e.g. in clients you have no control over, like Claude Desktop.
+# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
+# * `insert_at_line`: Inserts content at a given line in a file.
+# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
+# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
+# * `list_memories`: Lists memories in Serena's project-specific memory store.
+# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
+# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
+# * `read_file`: Reads a file within the project directory.
+# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
+# * `remove_project`: Removes a project from the Serena configuration.
+# * `replace_lines`: Replaces a range of lines within a file with new content.
+# * `replace_symbol_body`: Replaces the full definition of a symbol.
+# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
+# * `search_for_pattern`: Performs a search for a pattern in the project.
+# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
+# * `switch_modes`: Activates modes by providing a list of their names
+# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
+# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
+# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
+# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
+excluded_tools: []
+
+# initial prompt for the project. It will always be given to the LLM upon activating the project
+# (contrary to the memories, which are loaded on demand).
+initial_prompt: 'You are working on a free and open source software project that simulates charging stations. Refer to the memories for more details and `.github/copilot-instructions.md` for development guidelines.'
+
+project_name: 'e-mobility-charging-stations-simulator'
- :white_check_mark: BootNotification
- :white_check_mark: GetBaseReport (partial)
+- :white_check_mark: GetVariables
- :white_check_mark: NotifyReport
#### Authorization
return this.getConfigurationFromFile()?.automaticTransactionGeneratorStatuses
}
+ public getConnectionTimeout (): number {
+ if (getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut) != null) {
+ return convertToInt(
+ getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut)?.value ??
+ Constants.DEFAULT_CONNECTION_TIMEOUT
+ )
+ }
+ return Constants.DEFAULT_CONNECTION_TIMEOUT
+ }
+
public getConnectorIdByTransactionId (transactionId: number | undefined): number | undefined {
if (transactionId == null) {
return undefined
}
}
+ public getWebSocketPingInterval (): number {
+ return getConfigurationKey(this, StandardParametersKey.WebSocketPingInterval) != null
+ ? convertToInt(getConfigurationKey(this, StandardParametersKey.WebSocketPingInterval)?.value)
+ : Constants.DEFAULT_WEBSOCKET_PING_INTERVAL
+ }
+
public hasConnector (connectorId: number): boolean {
if (this.hasEvses) {
for (const evseStatus of this.evses.values()) {
throw new BaseError(errorMsg)
}
- // 0 for disabling
- private getConnectionTimeout (): number {
- if (getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut) != null) {
- return convertToInt(
- getConfigurationKey(this, StandardParametersKey.ConnectionTimeOut)?.value ??
- Constants.DEFAULT_CONNECTION_TIMEOUT
- )
- }
- return Constants.DEFAULT_CONNECTION_TIMEOUT
- }
-
private getCurrentOutType (stationInfo?: ChargingStationInfo): CurrentType {
return (
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
)
}
- private getWebSocketPingInterval (): number {
- return getConfigurationKey(this, StandardParametersKey.WebSocketPingInterval) != null
- ? convertToInt(getConfigurationKey(this, StandardParametersKey.WebSocketPingInterval)?.value)
- : 0
- }
-
private handleErrorMessage (errorResponse: ErrorResponse): void {
const [messageType, messageId, errorType, errorMessage, errorDetails] = errorResponse
if (!this.requests.has(messageId)) {
import { OCPPError } from '../../../exception/index.js'
import {
+ AttributeEnumType,
ConnectorEnumType,
ConnectorStatusEnum,
+ DataEnumType,
ErrorType,
GenericDeviceModelStatusEnumType,
type IncomingRequestHandler,
type JsonType,
type OCPP20ClearCacheRequest,
OCPP20ComponentName,
+ OCPP20ConnectorStatusEnumType,
+ OCPP20DeviceInfoVariableName,
type OCPP20GetBaseReportRequest,
type OCPP20GetBaseReportResponse,
+ type OCPP20GetVariablesRequest,
+ type OCPP20GetVariablesResponse,
OCPP20IncomingRequestCommand,
type OCPP20NotifyReportRequest,
type OCPP20NotifyReportResponse,
import { isAsyncFunction, logger } from '../../../utils/index.js'
import { OCPPIncomingRequestService } from '../OCPPIncomingRequestService.js'
import { OCPP20ServiceUtils } from './OCPP20ServiceUtils.js'
+import { OCPP20VariableManager } from './OCPP20VariableManager.js'
const moduleName = 'OCPP20IncomingRequestService'
this.incomingRequestHandlers = new Map<OCPP20IncomingRequestCommand, IncomingRequestHandler>([
[OCPP20IncomingRequestCommand.CLEAR_CACHE, super.handleRequestClearCache.bind(this)],
[OCPP20IncomingRequestCommand.GET_BASE_REPORT, this.handleRequestGetBaseReport.bind(this)],
+ [OCPP20IncomingRequestCommand.GET_VARIABLES, this.handleRequestGetVariables.bind(this)],
])
this.payloadValidateFunctions = new Map<
OCPP20IncomingRequestCommand,
)
),
],
+ [
+ OCPP20IncomingRequestCommand.GET_VARIABLES,
+ this.ajv.compile(
+ OCPP20ServiceUtils.parseJsonSchemaFile<OCPP20GetVariablesRequest>(
+ 'assets/json-schemas/ocpp/2.0/GetVariablesRequest.json',
+ moduleName,
+ 'constructor'
+ )
+ ),
+ ],
])
// Handle incoming request events
this.on(
this.validatePayload = this.validatePayload.bind(this)
}
+ public handleRequestGetVariables (
+ chargingStation: ChargingStation,
+ commandPayload: OCPP20GetVariablesRequest
+ ): OCPP20GetVariablesResponse {
+ const getVariablesResponse: OCPP20GetVariablesResponse = {
+ getVariableResult: [],
+ }
+
+ // Use VariableManager to get variables
+ const variableManager = OCPP20VariableManager.getInstance()
+
+ // Get variables using VariableManager
+ const results = variableManager.getVariables(chargingStation, commandPayload.getVariableData)
+ getVariablesResponse.getVariableResult = results
+
+ logger.debug(
+ `${chargingStation.logPrefix()} ${moduleName}.handleRequestGetVariables: Processed ${String(commandPayload.getVariableData.length)} variable requests, returning ${String(results.length)} results`
+ )
+
+ return getVariablesResponse
+ }
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters
public async incomingRequestHandler<ReqType extends JsonType, ResType extends JsonType>(
chargingStation: ChargingStation,
},
variableAttribute: [
{
- type: 'Actual',
+ type: AttributeEnumType.Actual as string,
value: configKey.value,
},
],
variableCharacteristics: {
- dataType: 'string',
+ dataType: DataEnumType.string,
supportsMonitoring: false,
},
})
if (stationInfo.chargePointModel) {
reportData.push({
component: { name: OCPP20ComponentName.ChargingStation },
- variable: { name: 'Model' },
- variableAttribute: [{ type: 'Actual', value: stationInfo.chargePointModel }],
- variableCharacteristics: { dataType: 'string', supportsMonitoring: false },
+ variable: { name: OCPP20DeviceInfoVariableName.Model },
+ variableAttribute: [
+ { type: AttributeEnumType.Actual as string, value: stationInfo.chargePointModel },
+ ],
+ variableCharacteristics: { dataType: DataEnumType.string, supportsMonitoring: false },
})
}
if (stationInfo.chargePointVendor) {
reportData.push({
component: { name: OCPP20ComponentName.ChargingStation },
- variable: { name: 'VendorName' },
- variableAttribute: [{ type: 'Actual', value: stationInfo.chargePointVendor }],
- variableCharacteristics: { dataType: 'string', supportsMonitoring: false },
+ variable: { name: OCPP20DeviceInfoVariableName.VendorName },
+ variableAttribute: [
+ { type: AttributeEnumType.Actual as string, value: stationInfo.chargePointVendor },
+ ],
+ variableCharacteristics: { dataType: DataEnumType.string, supportsMonitoring: false },
})
}
if (stationInfo.chargePointSerialNumber) {
reportData.push({
component: { name: OCPP20ComponentName.ChargingStation },
- variable: { name: 'SerialNumber' },
- variableAttribute: [{ type: 'Actual', value: stationInfo.chargePointSerialNumber }],
- variableCharacteristics: { dataType: 'string', supportsMonitoring: false },
+ variable: { name: OCPP20DeviceInfoVariableName.SerialNumber },
+ variableAttribute: [
+ {
+ type: AttributeEnumType.Actual as string,
+ value: stationInfo.chargePointSerialNumber,
+ },
+ ],
+ variableCharacteristics: { dataType: DataEnumType.string, supportsMonitoring: false },
})
}
if (stationInfo.firmwareVersion) {
reportData.push({
component: { name: OCPP20ComponentName.ChargingStation },
- variable: { name: 'FirmwareVersion' },
- variableAttribute: [{ type: 'Actual', value: stationInfo.firmwareVersion }],
- variableCharacteristics: { dataType: 'string', supportsMonitoring: false },
+ variable: { name: OCPP20DeviceInfoVariableName.FirmwareVersion },
+ variableAttribute: [
+ { type: AttributeEnumType.Actual as string, value: stationInfo.firmwareVersion },
+ ],
+ variableCharacteristics: { dataType: DataEnumType.string, supportsMonitoring: false },
})
}
}
if (chargingStation.ocppConfiguration?.configurationKey) {
for (const configKey of chargingStation.ocppConfiguration.configurationKey) {
const variableAttributes = []
- variableAttributes.push({ type: 'Actual', value: configKey.value })
+ variableAttributes.push({
+ type: AttributeEnumType.Actual as string,
+ value: configKey.value,
+ })
if (!configKey.readonly) {
- variableAttributes.push({ type: 'Target', value: undefined })
+ variableAttributes.push({
+ type: AttributeEnumType.Target as string,
+ value: undefined,
+ })
}
reportData.push({
variable: { name: configKey.key },
variableAttribute: variableAttributes,
variableCharacteristics: {
- dataType: 'string',
+ dataType: DataEnumType.string,
supportsMonitoring: false,
},
})
evse: { id: evseId },
name: OCPP20ComponentName.EVSE,
},
- variable: { name: 'AvailabilityState' },
- variableAttribute: [{ type: 'Actual', value: evse.availability }],
- variableCharacteristics: { dataType: 'string', supportsMonitoring: true },
+ variable: { name: OCPP20DeviceInfoVariableName.AvailabilityState },
+ variableAttribute: [
+ { type: AttributeEnumType.Actual as string, value: evse.availability },
+ ],
+ variableCharacteristics: { dataType: DataEnumType.string, supportsMonitoring: true },
})
if (evse.connectors.size > 0) {
for (const [connectorId, connector] of evse.connectors) {
evse: { connectorId, id: evseId },
name: OCPP20ComponentName.EVSE,
},
- variable: { name: 'ConnectorType' },
+ variable: { name: OCPP20DeviceInfoVariableName.ConnectorType },
variableAttribute: [
- { type: 'Actual', value: connector.type ?? ConnectorEnumType.Unknown },
+ {
+ type: AttributeEnumType.Actual as string,
+ value: connector.type ?? ConnectorEnumType.Unknown,
+ },
],
- variableCharacteristics: { dataType: 'string', supportsMonitoring: false },
+ variableCharacteristics: {
+ dataType: DataEnumType.string,
+ supportsMonitoring: false,
+ },
})
}
}
evse: { connectorId, id: 1 },
name: OCPP20ComponentName.Connector,
},
- variable: { name: 'ConnectorType' },
+ variable: { name: OCPP20DeviceInfoVariableName.ConnectorType },
variableAttribute: [
- { type: 'Actual', value: connector.type ?? ConnectorEnumType.Unknown },
+ {
+ type: AttributeEnumType.Actual as string,
+ value: connector.type ?? ConnectorEnumType.Unknown,
+ },
],
- variableCharacteristics: { dataType: 'string', supportsMonitoring: false },
+ variableCharacteristics: {
+ dataType: DataEnumType.string,
+ supportsMonitoring: false,
+ },
})
}
}
if (stationInfo.chargePointModel) {
reportData.push({
component: { name: OCPP20ComponentName.ChargingStation },
- variable: { name: 'Model' },
- variableAttribute: [{ type: 'Actual', value: stationInfo.chargePointModel }],
- variableCharacteristics: { dataType: 'string', supportsMonitoring: false },
+ variable: { name: OCPP20DeviceInfoVariableName.Model },
+ variableAttribute: [
+ { type: AttributeEnumType.Actual as string, value: stationInfo.chargePointModel },
+ ],
+ variableCharacteristics: { dataType: DataEnumType.string, supportsMonitoring: false },
})
}
if (stationInfo.chargePointVendor) {
reportData.push({
component: { name: OCPP20ComponentName.ChargingStation },
- variable: { name: 'VendorName' },
- variableAttribute: [{ type: 'Actual', value: stationInfo.chargePointVendor }],
- variableCharacteristics: { dataType: 'string', supportsMonitoring: false },
+ variable: { name: OCPP20DeviceInfoVariableName.VendorName },
+ variableAttribute: [
+ { type: AttributeEnumType.Actual as string, value: stationInfo.chargePointVendor },
+ ],
+ variableCharacteristics: { dataType: DataEnumType.string, supportsMonitoring: false },
})
}
if (stationInfo.firmwareVersion) {
reportData.push({
component: { name: OCPP20ComponentName.ChargingStation },
- variable: { name: 'FirmwareVersion' },
- variableAttribute: [{ type: 'Actual', value: stationInfo.firmwareVersion }],
- variableCharacteristics: { dataType: 'string', supportsMonitoring: false },
+ variable: { name: OCPP20DeviceInfoVariableName.FirmwareVersion },
+ variableAttribute: [
+ { type: AttributeEnumType.Actual as string, value: stationInfo.firmwareVersion },
+ ],
+ variableCharacteristics: { dataType: DataEnumType.string, supportsMonitoring: false },
})
}
}
reportData.push({
component: { name: OCPP20ComponentName.ChargingStation },
- variable: { name: 'AvailabilityState' },
+ variable: { name: OCPP20DeviceInfoVariableName.AvailabilityState },
variableAttribute: [
{
- type: 'Actual',
- value: chargingStation.inAcceptedState() ? 'Available' : 'Unavailable',
+ type: AttributeEnumType.Actual as string,
+ value: chargingStation.inAcceptedState()
+ ? OCPP20ConnectorStatusEnumType.Available
+ : OCPP20ConnectorStatusEnumType.Unavailable,
},
],
- variableCharacteristics: { dataType: 'string', supportsMonitoring: true },
+ variableCharacteristics: { dataType: DataEnumType.string, supportsMonitoring: true },
})
if (chargingStation.evses.size > 0) {
evse: { id: evseId },
name: OCPP20ComponentName.EVSE,
},
- variable: { name: 'AvailabilityState' },
- variableAttribute: [{ type: 'Actual', value: evse.availability }],
- variableCharacteristics: { dataType: 'string', supportsMonitoring: true },
+ variable: { name: OCPP20DeviceInfoVariableName.AvailabilityState },
+ variableAttribute: [
+ { type: AttributeEnumType.Actual as string, value: evse.availability },
+ ],
+ variableCharacteristics: { dataType: DataEnumType.string, supportsMonitoring: true },
})
}
} else if (chargingStation.connectors.size > 0) {
evse: { connectorId, id: 1 },
name: OCPP20ComponentName.Connector,
},
- variable: { name: 'AvailabilityState' },
+ variable: { name: OCPP20DeviceInfoVariableName.AvailabilityState },
variableAttribute: [
- { type: 'Actual', value: connector.status ?? ConnectorStatusEnum.Unavailable },
+ {
+ type: AttributeEnumType.Actual as string,
+ value: connector.status ?? ConnectorStatusEnum.Unavailable,
+ },
],
- variableCharacteristics: { dataType: 'string', supportsMonitoring: true },
+ variableCharacteristics: {
+ dataType: DataEnumType.string,
+ supportsMonitoring: true,
+ },
})
}
}
`${chargingStation.logPrefix()} ${moduleName}.handleRequestGetBaseReport: GetBaseReport request received with requestId ${commandPayload.requestId} and reportBase ${commandPayload.reportBase}`
)
- const supportedReportBases = [
- ReportBaseEnumType.ConfigurationInventory,
- ReportBaseEnumType.FullInventory,
- ReportBaseEnumType.SummaryInventory,
- ]
-
- if (!supportedReportBases.includes(commandPayload.reportBase)) {
+ if (!Object.values(ReportBaseEnumType).includes(commandPayload.reportBase)) {
logger.warn(
`${chargingStation.logPrefix()} ${moduleName}.handleRequestGetBaseReport: Unsupported reportBase ${commandPayload.reportBase}`
)
--- /dev/null
+// Partial Copyright Jerome Benoit. 2021-2025. All Rights Reserved.
+
+import { millisecondsToSeconds } from 'date-fns'
+
+import {
+ AttributeEnumType,
+ type ComponentType,
+ GenericDeviceModelStatusEnumType,
+ GetVariableStatusEnumType,
+ MutabilityEnumType,
+ OCPP20ComponentName,
+ type OCPP20GetVariableDataType,
+ type OCPP20GetVariableResultType,
+ OCPP20OptionalVariableName,
+ OCPP20RequiredVariableName,
+ type VariableType,
+} from '../../../types/index.js'
+import { Constants, logger } from '../../../utils/index.js'
+import { type ChargingStation } from '../../ChargingStation.js'
+
+/**
+ * Configuration for a standard OCPP 2.0 variable
+ */
+interface StandardVariableConfig {
+ attributeTypes: AttributeEnumType[]
+ defaultValue?: string
+ mutability: MutabilityEnumType
+ persistent: boolean
+}
+
+/**
+ * Centralized manager for OCPP 2.0 variables handling.
+ * Manages standard variables and provides unified access to variable data.
+ */
+export class OCPP20VariableManager {
+ private static instance: null | OCPP20VariableManager = null
+
+ private readonly standardVariables = new Map<string, StandardVariableConfig>()
+
+ private constructor () {
+ this.initializeStandardVariables()
+ }
+
+ public static getInstance (): OCPP20VariableManager {
+ OCPP20VariableManager.instance ??= new OCPP20VariableManager()
+ return OCPP20VariableManager.instance
+ }
+
+ /**
+ * Get variable data for a charging station
+ * @param chargingStation - The charging station instance
+ * @param getVariableData - Array of variable data to retrieve
+ * @returns Array of variable results
+ */
+ public getVariables (
+ chargingStation: ChargingStation,
+ getVariableData: OCPP20GetVariableDataType[]
+ ): OCPP20GetVariableResultType[] {
+ const results: OCPP20GetVariableResultType[] = []
+
+ for (const variableData of getVariableData) {
+ try {
+ const result = this.getVariable(chargingStation, variableData)
+ results.push(result)
+ } catch (error) {
+ logger.error(
+ `${chargingStation.logPrefix()} Error getting variable ${variableData.variable.name}:`,
+ error
+ )
+ results.push({
+ attributeStatus: GetVariableStatusEnumType.Rejected,
+ attributeType: variableData.attributeType,
+ component: variableData.component,
+ statusInfo: {
+ additionalInfo: 'Internal error occurred while retrieving variable',
+ reasonCode: GenericDeviceModelStatusEnumType.Rejected,
+ },
+ variable: variableData.variable,
+ })
+ }
+ }
+
+ return results
+ }
+
+ /**
+ * Get a single variable
+ * @param chargingStation - The charging station instance
+ * @param variableData - Variable data to retrieve
+ * @returns Variable result
+ */
+ private getVariable (
+ chargingStation: ChargingStation,
+ variableData: OCPP20GetVariableDataType
+ ): OCPP20GetVariableResultType {
+ const { attributeType, component, variable } = variableData
+
+ // Check if component is valid for this charging station
+ if (!this.isComponentValid(chargingStation, component)) {
+ return {
+ attributeStatus: GetVariableStatusEnumType.UnknownComponent,
+ attributeStatusInfo: {
+ additionalInfo: `Component ${component.name} is not supported by this charging station`,
+ reasonCode: GenericDeviceModelStatusEnumType.NotSupported,
+ },
+ attributeType,
+ component,
+ variable,
+ }
+ }
+
+ // Check if variable exists
+ if (!this.isVariableSupported(chargingStation, component, variable)) {
+ return {
+ attributeStatus: GetVariableStatusEnumType.UnknownVariable,
+ attributeStatusInfo: {
+ additionalInfo: `Variable ${variable.name} is not supported for component ${component.name}`,
+ reasonCode: GenericDeviceModelStatusEnumType.NotSupported,
+ },
+ attributeType,
+ component,
+ variable,
+ }
+ }
+
+ // Check if attribute type is supported
+ if (attributeType && !this.isAttributeTypeSupported(variable, attributeType)) {
+ return {
+ attributeStatus: GetVariableStatusEnumType.NotSupportedAttributeType,
+ attributeStatusInfo: {
+ additionalInfo: `Attribute type ${attributeType} is not supported for variable ${variable.name}`,
+ reasonCode: GenericDeviceModelStatusEnumType.NotSupported,
+ },
+ attributeType,
+ component,
+ variable,
+ }
+ }
+
+ // Get the variable value
+ const variableValue = this.getVariableValue(chargingStation, component, variable, attributeType)
+
+ return {
+ attributeStatus: GetVariableStatusEnumType.Accepted,
+ attributeType,
+ attributeValue: variableValue,
+ component,
+ variable,
+ }
+ }
+
+ /**
+ * Get the actual variable value from the charging station
+ * @param chargingStation - The charging station instance
+ * @param component - The component containing the variable
+ * @param variable - The variable to get the value for
+ * @param attributeType - The type of attribute (Actual, Target, etc.)
+ * @returns The variable value as string
+ */
+ private getVariableValue (
+ chargingStation: ChargingStation,
+ component: ComponentType,
+ variable: VariableType,
+ attributeType?: AttributeEnumType
+ ): string {
+ const variableName = variable.name
+ const componentName = component.name
+
+ // Handle standard ChargingStation variables
+ if (componentName === (OCPP20ComponentName.ChargingStation as string)) {
+ if (variableName === (OCPP20OptionalVariableName.HeartbeatInterval as string)) {
+ return millisecondsToSeconds(chargingStation.getHeartbeatInterval()).toString()
+ }
+
+ if (variableName === (OCPP20OptionalVariableName.WebSocketPingInterval as string)) {
+ return chargingStation.getWebSocketPingInterval().toString()
+ }
+
+ if (variableName === (OCPP20RequiredVariableName.EVConnectionTimeOut as string)) {
+ return Constants.DEFAULT_EV_CONNECTION_TIMEOUT.toString()
+ }
+
+ if (variableName === (OCPP20RequiredVariableName.MessageTimeout as string)) {
+ return chargingStation.getConnectionTimeout().toString()
+ }
+
+ // Try to get from OCPP configuration
+ const configKey = chargingStation.ocppConfiguration?.configurationKey?.find(
+ key => key.key === variableName
+ )
+ return configKey?.value ?? ''
+ }
+
+ // Handle Connector variables
+ if (componentName === (OCPP20ComponentName.Connector as string)) {
+ const connectorId = component.instance ? parseInt(component.instance, 10) : 1
+ const connector = chargingStation.connectors.get(connectorId)
+
+ if (connector) {
+ // Add connector-specific variable handling here
+ switch (variableName) {
+ // Add connector variables as needed
+ default:
+ return ''
+ }
+ }
+ }
+
+ // Handle EVSE variables
+ if (componentName === (OCPP20ComponentName.EVSE as string)) {
+ const evseId = component.instance ? parseInt(component.instance, 10) : 1
+ const evse = chargingStation.evses.get(evseId)
+
+ if (evse) {
+ // Add EVSE-specific variable handling here
+ switch (variableName) {
+ // Add EVSE variables as needed
+ default:
+ return ''
+ }
+ }
+ }
+
+ return ''
+ }
+
+ /**
+ * Initialize standard OCPP 2.0 variables configuration
+ */
+ private initializeStandardVariables (): void {
+ // ChargingStation component variables
+ this.standardVariables.set(
+ `${OCPP20ComponentName.ChargingStation}.${OCPP20OptionalVariableName.HeartbeatInterval}`,
+ {
+ attributeTypes: [AttributeEnumType.Actual, AttributeEnumType.Target],
+ defaultValue: millisecondsToSeconds(Constants.DEFAULT_HEARTBEAT_INTERVAL).toString(),
+ mutability: MutabilityEnumType.ReadWrite,
+ persistent: true,
+ }
+ )
+
+ this.standardVariables.set(
+ `${OCPP20ComponentName.ChargingStation}.${OCPP20OptionalVariableName.WebSocketPingInterval}`,
+ {
+ attributeTypes: [AttributeEnumType.Actual, AttributeEnumType.Target],
+ defaultValue: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL.toString(),
+ mutability: MutabilityEnumType.ReadWrite,
+ persistent: true,
+ }
+ )
+
+ this.standardVariables.set(
+ `${OCPP20ComponentName.ChargingStation}.${OCPP20RequiredVariableName.EVConnectionTimeOut}`,
+ {
+ attributeTypes: [AttributeEnumType.Actual, AttributeEnumType.Target],
+ defaultValue: Constants.DEFAULT_EV_CONNECTION_TIMEOUT.toString(),
+ mutability: MutabilityEnumType.ReadWrite,
+ persistent: true,
+ }
+ )
+
+ this.standardVariables.set(
+ `${OCPP20ComponentName.ChargingStation}.${OCPP20RequiredVariableName.MessageTimeout}`,
+ {
+ attributeTypes: [AttributeEnumType.Actual, AttributeEnumType.Target],
+ defaultValue: Constants.DEFAULT_CONNECTION_TIMEOUT.toString(),
+ mutability: MutabilityEnumType.ReadWrite,
+ persistent: true,
+ }
+ )
+
+ // Add more standard variables as needed
+ }
+
+ /**
+ * Check if attribute type is supported for the variable
+ * @param variable - The variable to check attribute support for
+ * @param attributeType - The attribute type to validate
+ * @returns True if the attribute type is supported by the variable
+ */
+ private isAttributeTypeSupported (
+ variable: VariableType,
+ attributeType: AttributeEnumType
+ ): boolean {
+ // Most variables support only Actual attribute by default
+ // Only certain variables support other attribute types like Target, MinSet, MaxSet
+ if (attributeType === AttributeEnumType.Actual) {
+ return true
+ }
+
+ // For other attribute types, check if variable supports them
+ // This is a simplified implementation - in production you'd have a configuration map
+ const variablesWithConfigurableAttributes: string[] = [
+ OCPP20OptionalVariableName.WebSocketPingInterval,
+ // Add other variables that support configuration
+ ]
+
+ return variablesWithConfigurableAttributes.includes(variable.name)
+ }
+
+ /**
+ * Check if a component is valid for the charging station
+ * @param chargingStation - The charging station instance to validate against
+ * @param component - The component to check validity for
+ * @returns True if the component is valid for the charging station
+ */
+ private isComponentValid (chargingStation: ChargingStation, component: ComponentType): boolean {
+ const componentName = component.name
+
+ // Always support ChargingStation component
+ if (componentName === (OCPP20ComponentName.ChargingStation as string)) {
+ return true
+ }
+
+ // Support Connector components if station has connectors
+ if (
+ componentName === (OCPP20ComponentName.Connector as string) &&
+ chargingStation.connectors.size > 0
+ ) {
+ // Check if specific connector instance exists
+ if (component.instance != null) {
+ const connectorId = parseInt(component.instance, 10)
+ return chargingStation.connectors.has(connectorId)
+ }
+ return true
+ }
+
+ // Support EVSE components if station has EVSEs
+ if (componentName === (OCPP20ComponentName.EVSE as string) && chargingStation.hasEvses) {
+ // Check if specific EVSE instance exists
+ if (component.instance != null) {
+ const evseId = parseInt(component.instance, 10)
+ return chargingStation.evses.has(evseId)
+ }
+ return true
+ }
+
+ // Other components can be added here as needed
+ return false
+ }
+
+ /**
+ * Check if a variable is supported by the component
+ * @param chargingStation - The charging station instance
+ * @param component - The component to check
+ * @param variable - The variable to validate
+ * @returns True if the variable is supported by the component
+ */
+ private isVariableSupported (
+ chargingStation: ChargingStation,
+ component: ComponentType,
+ variable: VariableType
+ ): boolean {
+ const variableKey = `${component.name}.${variable.name}`
+
+ // Check standard variables
+ if (this.standardVariables.has(variableKey)) {
+ return true
+ }
+
+ // Check known optional and required variables
+ const knownVariables = [
+ ...Object.values(OCPP20OptionalVariableName),
+ ...Object.values(OCPP20RequiredVariableName),
+ ]
+
+ return knownVariables.includes(
+ variable.name as OCPP20OptionalVariableName | OCPP20RequiredVariableName
+ )
+ }
+}
export { OCPP20IncomingRequestService } from './2.0/OCPP20IncomingRequestService.js'
export { OCPP20RequestService } from './2.0/OCPP20RequestService.js'
export { OCPP20ResponseService } from './2.0/OCPP20ResponseService.js'
+export { OCPP20VariableManager } from './2.0/OCPP20VariableManager.js'
export { OCPPIncomingRequestService } from './OCPPIncomingRequestService.js'
export { OCPPRequestService } from './OCPPRequestService.js'
export {
} from './ocpp/1.6/Transaction.js'
export {
BootReasonEnumType,
+ type ComponentType,
type CustomDataType,
+ DataEnumType,
GenericDeviceModelStatusEnumType,
OCPP20ComponentName,
OCPP20ConnectorStatusEnumType,
type OCPP20BootNotificationRequest,
type OCPP20ClearCacheRequest,
type OCPP20GetBaseReportRequest,
+ type OCPP20GetVariablesRequest,
type OCPP20HeartbeatRequest,
OCPP20IncomingRequestCommand,
type OCPP20NotifyReportRequest,
OCPP20BootNotificationResponse,
OCPP20ClearCacheResponse,
OCPP20GetBaseReportResponse,
+ OCPP20GetVariablesResponse,
OCPP20HeartbeatResponse,
OCPP20NotifyReportResponse,
OCPP20StatusNotificationResponse,
} from './ocpp/2.0/Responses.js'
-export { OCPP20OptionalVariableName } from './ocpp/2.0/Variables.js'
+export {
+ AttributeEnumType,
+ GetVariableStatusEnumType,
+ MutabilityEnumType,
+ OCPP20DeviceInfoVariableName,
+ type OCPP20GetVariableDataType,
+ type OCPP20GetVariableResultType,
+ OCPP20OptionalVariableName,
+ OCPP20RequiredVariableName,
+ type VariableType,
+} from './ocpp/2.0/Variables.js'
export { ChargePointErrorCode } from './ocpp/ChargePointErrorCode.js'
export {
type ChargingProfile,
}
interface VariableCharacteristicsType extends JsonObject {
- dataType: string
+ dataType: DataEnumType
supportsMonitoring: boolean
}
ReportBaseEnumType,
ReportDataType,
} from './Common.js'
-import type { OCPP20SetVariableDataType } from './Variables.js'
+import type { OCPP20GetVariableDataType, OCPP20SetVariableDataType } from './Variables.js'
export enum OCPP20IncomingRequestCommand {
CLEAR_CACHE = 'ClearCache',
GET_BASE_REPORT = 'GetBaseReport',
+ GET_VARIABLES = 'GetVariables',
REQUEST_START_TRANSACTION = 'RequestStartTransaction',
REQUEST_STOP_TRANSACTION = 'RequestStopTransaction',
}
requestId: number
}
+export interface OCPP20GetVariablesRequest extends JsonObject {
+ getVariableData: OCPP20GetVariableDataType[]
+}
+
export type OCPP20HeartbeatRequest = EmptyObject
export interface OCPP20InstallCertificateRequest extends JsonObject {
InstallCertificateStatusEnumType,
StatusInfoType,
} from './Common.js'
-import type { OCPP20SetVariableResultType } from './Variables.js'
+import type { OCPP20GetVariableResultType, OCPP20SetVariableResultType } from './Variables.js'
export interface OCPP20BootNotificationResponse extends JsonObject {
currentTime: Date
statusInfo?: StatusInfoType
}
+export interface OCPP20GetVariablesResponse extends JsonObject {
+ getVariableResult: OCPP20GetVariableResultType[]
+}
+
export interface OCPP20HeartbeatResponse extends JsonObject {
currentTime: Date
}
import type { JsonObject } from '../../JsonType.js'
import type { ComponentType, StatusInfoType } from './Common.js'
+export enum AttributeEnumType {
+ Actual = 'Actual',
+ MaxSet = 'MaxSet',
+ MinSet = 'MinSet',
+ Target = 'Target',
+}
+
+export enum GetVariableStatusEnumType {
+ Accepted = 'Accepted',
+ NotSupportedAttributeType = 'NotSupportedAttributeType',
+ Rejected = 'Rejected',
+ UnknownComponent = 'UnknownComponent',
+ UnknownVariable = 'UnknownVariable',
+}
+
+export enum MutabilityEnumType {
+ ReadOnly = 'ReadOnly',
+ ReadWrite = 'ReadWrite',
+ WriteOnly = 'WriteOnly',
+}
+
+export enum OCPP20DeviceInfoVariableName {
+ AvailabilityState = 'AvailabilityState',
+ ConnectorType = 'ConnectorType',
+ FirmwareVersion = 'FirmwareVersion',
+ Model = 'Model',
+ SerialNumber = 'SerialNumber',
+ VendorName = 'VendorName',
+}
+
export enum OCPP20OptionalVariableName {
HeartbeatInterval = 'HeartbeatInterval',
WebSocketPingInterval = 'WebSocketPingInterval',
ConnectionUrl = 'ConnectionUrl',
}
-enum AttributeEnumType {
- Actual = 'Actual',
- MaxSet = 'MaxSet',
- MinSet = 'MinSet',
- Target = 'Target',
-}
-
enum SetVariableStatusEnumType {
Accepted = 'Accepted',
NotSupportedAttributeType = 'NotSupportedAttributeType',
variable?: VariableType
}
+export interface OCPP20GetVariableDataType extends JsonObject {
+ attributeType?: AttributeEnumType
+ component: ComponentType
+ variable: VariableType
+}
+
+export interface OCPP20GetVariableResultType extends JsonObject {
+ attributeStatus: GetVariableStatusEnumType
+ attributeStatusInfo?: StatusInfoType
+ attributeType?: AttributeEnumType
+ attributeValue?: string
+ component: ComponentType
+ variable: VariableType
+}
+
export interface OCPP20SetVariableDataType extends JsonObject {
attributeType?: AttributeEnumType
attributeValue: string
}
type VariableName =
+ | OCPP20DeviceInfoVariableName
| OCPP20OptionalVariableName
| OCPP20RequiredVariableName
| OCPP20VendorVariableName
static readonly DEFAULT_CIRCULAR_BUFFER_CAPACITY = 386
static readonly DEFAULT_CONNECTION_TIMEOUT = 30 // Seconds
+ static readonly DEFAULT_EV_CONNECTION_TIMEOUT = 180 // Seconds
static readonly DEFAULT_FLUCTUATION_PERCENT = 5
static readonly DEFAULT_HASH_ALGORITHM = 'sha384'
static readonly DEFAULT_METER_VALUES_INTERVAL = 60000 // Ms
static readonly DEFAULT_PERFORMANCE_DIRECTORY = 'performance'
-
static readonly DEFAULT_PERFORMANCE_RECORDS_DB_NAME = 'e-mobility-charging-stations-simulator'
static readonly DEFAULT_PERFORMANCE_RECORDS_FILENAME = 'performanceRecords.json'
+
static readonly DEFAULT_STATION_INFO: Readonly<Partial<ChargingStationInfo>> = Object.freeze({
automaticTransactionGeneratorPersistentConfiguration: true,
autoReconnectMaxRetries: -1,
})
static readonly DEFAULT_UI_SERVER_HOST = 'localhost'
-
static readonly DEFAULT_UI_SERVER_PORT = 8080
+
+ static readonly DEFAULT_WEBSOCKET_PING_INTERVAL = 30 // Seconds
+
static readonly EMPTY_FROZEN_OBJECT = Object.freeze({})
static readonly EMPTY_FUNCTION: () => void = Object.freeze(() => {
--- /dev/null
+import type { ChargingStation } from '../src/charging-station/index.js'
+import type {
+ ChargingStationConfiguration,
+ ChargingStationInfo,
+ ChargingStationTemplate,
+} from '../src/types/index.js'
+
+import {
+ OCPP20ConnectorStatusEnumType,
+ OCPP20OptionalVariableName,
+ OCPPVersion,
+} from '../src/types/index.js'
+import { Constants } from '../src/utils/index.js'
+
+/**
+ * Options to customize the construction of a ChargingStation test instance
+ */
+export interface ChargingStationOptions {
+ baseName?: string
+ connectionTimeout?: number
+ hasEvses?: boolean
+ heartbeatInterval?: number
+ ocppConfiguration?: ChargingStationConfiguration
+ started?: boolean
+ starting?: boolean
+ stationInfo?: Partial<ChargingStationInfo>
+ websocketPingInterval?: number
+}
+
+const CHARGING_STATION_BASE_NAME = 'CS-TEST'
+
+/**
+ * Creates a ChargingStation instance for tests
+ * @param options - Options to customize the ChargingStation instance
+ * @returns A mock ChargingStation instance
+ */
+export function createChargingStation (options: ChargingStationOptions = {}): ChargingStation {
+ const baseName = options.baseName ?? CHARGING_STATION_BASE_NAME
+ const templateIndex = 1
+ const connectionTimeout = options.connectionTimeout ?? Constants.DEFAULT_CONNECTION_TIMEOUT
+ const heartbeatInterval = options.heartbeatInterval ?? Constants.DEFAULT_HEARTBEAT_INTERVAL
+ const websocketPingInterval =
+ options.websocketPingInterval ?? Constants.DEFAULT_WEBSOCKET_PING_INTERVAL
+
+ return {
+ connectors: new Map(),
+ evses: new Map(),
+ getConnectionTimeout: () => connectionTimeout,
+ getHeartbeatInterval: () => heartbeatInterval,
+ getWebSocketPingInterval: () => websocketPingInterval,
+ hasEvses: options.hasEvses ?? false,
+ inAcceptedState: () => true,
+ logPrefix: () => `${baseName} |`,
+ ocppConfiguration: options.ocppConfiguration ?? {
+ configurationKey: [
+ {
+ key: OCPP20OptionalVariableName.WebSocketPingInterval,
+ value: websocketPingInterval.toString(),
+ },
+ { key: OCPP20OptionalVariableName.HeartbeatInterval, value: heartbeatInterval.toString() },
+ ],
+ },
+ started: options.started ?? false,
+ starting: options.starting,
+ stationInfo: {
+ baseName,
+ chargingStationId: `${baseName}-00001`,
+ hashId: 'test-hash-id',
+ maximumAmperage: 16,
+ maximumPower: 12000,
+ templateIndex,
+ templateName: 'test-template.json',
+ ...options.stationInfo,
+ },
+ wsConnection: {
+ pingInterval: websocketPingInterval,
+ },
+ } as unknown as ChargingStation
+}
+
+/**
+ * Creates a ChargingStation template for tests
+ * @param baseName - Base name for the template
+ * @returns A ChargingStationTemplate instance
+ */
+export function createChargingStationTemplate (
+ baseName = CHARGING_STATION_BASE_NAME
+): ChargingStationTemplate {
+ return {
+ baseName,
+ } as ChargingStationTemplate
+}
+
+/**
+ * Creates a ChargingStation instance with connectors and EVSEs configured for OCPP 2.0
+ * @param options - Options to customize the ChargingStation instance
+ * @returns A mock ChargingStation instance with EVSEs
+ */
+export function createChargingStationWithEvses (
+ options: ChargingStationOptions = {}
+): ChargingStation {
+ const chargingStation = createChargingStation({
+ hasEvses: true,
+ stationInfo: {
+ ocppVersion: OCPPVersion.VERSION_201,
+ ...options.stationInfo,
+ },
+ ...options,
+ })
+
+ // Add default connectors and EVSEs
+ Object.assign(chargingStation, {
+ connectors: new Map([
+ [1, { status: OCPP20ConnectorStatusEnumType.Available }],
+ [2, { status: OCPP20ConnectorStatusEnumType.Available }],
+ ]),
+ evses: new Map([
+ [1, { connectors: new Map([[1, {}]]) }],
+ [2, { connectors: new Map([[1, {}]]) }],
+ ]),
+ })
+
+ return chargingStation
+}
import { expect } from '@std/expect'
import { describe, it } from 'node:test'
-import type { ChargingStation } from '../../src/charging-station/index.js'
-
import {
checkChargingStationState,
checkConfiguration,
type ChargingStationTemplate,
type ConnectorStatus,
ConnectorStatusEnum,
- type EvseStatus,
OCPPVersion,
} from '../../src/types/index.js'
import { logger } from '../../src/utils/Logger.js'
+import { createChargingStation, createChargingStationTemplate } from '../ChargingStationFactory.js'
await describe('Helpers test suite', async () => {
const baseName = 'CS-TEST'
- const chargingStationTemplate = {
- baseName,
- } as ChargingStationTemplate
- const chargingStation = {
- connectors: new Map<number, ConnectorStatus>(),
- evses: new Map<number, EvseStatus>(),
- logPrefix: () => `${baseName} |`,
- started: false,
- } as ChargingStation
+ const chargingStationTemplate = createChargingStationTemplate(baseName)
+ const chargingStation = createChargingStation({ baseName })
await it('Verify getChargingStationId()', () => {
expect(getChargingStationId(1, chargingStationTemplate)).toBe(`${baseName}-00001`)
})
await it('Verify validateStationInfo()', () => {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ delete (chargingStation as any).stationInfo
expect(() => {
validateStationInfo(chargingStation)
}).toThrow(new BaseError('Missing charging station information'))
--- /dev/null
+/* eslint-disable @typescript-eslint/no-unsafe-member-access */
+/* eslint-disable @typescript-eslint/no-unsafe-assignment */
+/* eslint-disable @typescript-eslint/no-unsafe-call */
+/* eslint-disable @typescript-eslint/no-explicit-any */
+
+import { expect } from '@std/expect'
+import { describe, it } from 'node:test'
+
+import { IdTagsCache } from '../../../../src/charging-station/IdTagsCache.js'
+import { OCPP20IncomingRequestService } from '../../../../src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.js'
+import { Constants } from '../../../../src/utils/index.js'
+import { createChargingStationWithEvses } from '../../../ChargingStationFactory.js'
+import { TEST_CHARGING_STATION_NAME } from './OCPP20TestConstants.js'
+
+await describe('OCPP20IncomingRequestService ClearCache integration tests', async () => {
+ const mockChargingStation = createChargingStationWithEvses({
+ baseName: TEST_CHARGING_STATION_NAME,
+ heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+ stationInfo: {
+ ocppStrictCompliance: false,
+ },
+ websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+ })
+
+ // Initialize idTagsCache to avoid undefined errors
+ mockChargingStation.idTagsCache = IdTagsCache.getInstance()
+
+ const incomingRequestService = new OCPP20IncomingRequestService()
+
+ await it('Should handle ClearCache request successfully', async () => {
+ const response = await (incomingRequestService as any).handleRequestClearCache(
+ mockChargingStation
+ )
+
+ expect(response).toBeDefined()
+ expect(typeof response).toBe('object')
+ expect(response.status).toBeDefined()
+ expect(typeof response.status).toBe('string')
+ expect(['Accepted', 'Rejected']).toContain(response.status)
+ })
+
+ await it('Should return correct status based on cache clearing result', async () => {
+ // Test the actual behavior - ClearCache should work with ID tags cache
+
+ const response = await (incomingRequestService as any).handleRequestClearCache(
+ mockChargingStation
+ )
+
+ expect(response).toBeDefined()
+ expect(response.status).toBeDefined()
+ // Should be either Accepted or Rejected based on cache state
+ expect(['Accepted', 'Rejected']).toContain(response.status)
+ })
+})
--- /dev/null
+/* eslint-disable @typescript-eslint/no-unsafe-member-access */
+/* eslint-disable @typescript-eslint/no-unsafe-assignment */
+/* eslint-disable @typescript-eslint/no-unsafe-call */
+/* eslint-disable @typescript-eslint/no-explicit-any */
+
+import { expect } from '@std/expect'
+import { describe, it } from 'node:test'
+
+import { OCPP20IncomingRequestService } from '../../../../src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.js'
+import {
+ GenericDeviceModelStatusEnumType,
+ OCPP20ComponentName,
+ OCPP20DeviceInfoVariableName,
+ type OCPP20GetBaseReportRequest,
+ ReportBaseEnumType,
+} from '../../../../src/types/index.js'
+import { Constants } from '../../../../src/utils/index.js'
+import { createChargingStationWithEvses } from '../../../ChargingStationFactory.js'
+import {
+ TEST_CHARGE_POINT_MODEL,
+ TEST_CHARGE_POINT_SERIAL_NUMBER,
+ TEST_CHARGE_POINT_VENDOR,
+ TEST_CHARGING_STATION_NAME,
+ TEST_FIRMWARE_VERSION,
+} from './OCPP20TestConstants.js'
+
+await describe('OCPP20IncomingRequestService GetBaseReport integration tests', async () => {
+ const mockChargingStation = createChargingStationWithEvses({
+ baseName: TEST_CHARGING_STATION_NAME,
+ heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+ stationInfo: {
+ chargePointModel: TEST_CHARGE_POINT_MODEL,
+ chargePointSerialNumber: TEST_CHARGE_POINT_SERIAL_NUMBER,
+ chargePointVendor: TEST_CHARGE_POINT_VENDOR,
+ firmwareVersion: TEST_FIRMWARE_VERSION,
+ ocppStrictCompliance: false,
+ },
+ websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+ })
+
+ const incomingRequestService = new OCPP20IncomingRequestService()
+
+ await it('Should handle GetBaseReport request with ConfigurationInventory', () => {
+ const request: OCPP20GetBaseReportRequest = {
+ reportBase: ReportBaseEnumType.ConfigurationInventory,
+ requestId: 1,
+ }
+
+ const response = (incomingRequestService as any).handleRequestGetBaseReport(
+ mockChargingStation,
+ request
+ )
+
+ expect(response).toBeDefined()
+ expect(response.status).toBe(GenericDeviceModelStatusEnumType.Accepted)
+ })
+
+ await it('Should handle GetBaseReport request with FullInventory', () => {
+ const request: OCPP20GetBaseReportRequest = {
+ reportBase: ReportBaseEnumType.FullInventory,
+ requestId: 2,
+ }
+
+ const response = (incomingRequestService as any).handleRequestGetBaseReport(
+ mockChargingStation,
+ request
+ )
+
+ expect(response).toBeDefined()
+ expect(response.status).toBe(GenericDeviceModelStatusEnumType.Accepted)
+ })
+
+ await it('Should handle GetBaseReport request with SummaryInventory', () => {
+ const request: OCPP20GetBaseReportRequest = {
+ reportBase: ReportBaseEnumType.SummaryInventory,
+ requestId: 3,
+ }
+
+ const response = (incomingRequestService as any).handleRequestGetBaseReport(
+ mockChargingStation,
+ request
+ )
+
+ expect(response).toBeDefined()
+ expect(response.status).toBe(GenericDeviceModelStatusEnumType.Accepted)
+ })
+
+ await it('Should return NotSupported for unsupported reportBase', () => {
+ const request: OCPP20GetBaseReportRequest = {
+ reportBase: 'UnsupportedReportBase' as any,
+ requestId: 4,
+ }
+
+ const response = (incomingRequestService as any).handleRequestGetBaseReport(
+ mockChargingStation,
+ request
+ )
+
+ expect(response).toBeDefined()
+ expect(response.status).toBe(GenericDeviceModelStatusEnumType.NotSupported)
+ })
+
+ await it('Should return EmptyResultSet when no data is available', () => {
+ // Create a charging station with minimal configuration
+ const minimalChargingStation = createChargingStationWithEvses({
+ baseName: 'CS-MINIMAL',
+ ocppConfiguration: {
+ configurationKey: [],
+ },
+ stationInfo: {
+ ocppStrictCompliance: false,
+ },
+ })
+
+ const request: OCPP20GetBaseReportRequest = {
+ reportBase: ReportBaseEnumType.ConfigurationInventory,
+ requestId: 5,
+ }
+
+ const response = (incomingRequestService as any).handleRequestGetBaseReport(
+ minimalChargingStation,
+ request
+ )
+
+ expect(response).toBeDefined()
+ expect(response.status).toBe(GenericDeviceModelStatusEnumType.EmptyResultSet)
+ })
+
+ await it('Should build correct report data for ConfigurationInventory', () => {
+ const request: OCPP20GetBaseReportRequest = {
+ reportBase: ReportBaseEnumType.ConfigurationInventory,
+ requestId: 6,
+ }
+
+ // Test the buildReportData method indirectly by calling handleRequestGetBaseReport
+ // and checking if it returns Accepted status (which means data was built successfully)
+ const response = (incomingRequestService as any).handleRequestGetBaseReport(
+ mockChargingStation,
+ request
+ )
+
+ expect(response).toBeDefined()
+ expect(response.status).toBe(GenericDeviceModelStatusEnumType.Accepted)
+
+ // We can also test the buildReportData method directly if needed
+ const reportData = (incomingRequestService as any).buildReportData(
+ mockChargingStation,
+ ReportBaseEnumType.ConfigurationInventory
+ )
+
+ expect(Array.isArray(reportData)).toBe(true)
+ expect(reportData.length).toBeGreaterThan(0)
+
+ // Check that each report data item has the expected structure
+ for (const item of reportData) {
+ expect(item.component).toBeDefined()
+ expect(item.component.name).toBeDefined()
+ expect(item.variable).toBeDefined()
+ expect(item.variable.name).toBeDefined()
+ expect(item.variableAttribute).toBeDefined()
+ expect(Array.isArray(item.variableAttribute)).toBe(true)
+ expect(item.variableCharacteristics).toBeDefined()
+ }
+ })
+
+ await it('Should build correct report data for FullInventory with station info', () => {
+ const reportData = (incomingRequestService as any).buildReportData(
+ mockChargingStation,
+ ReportBaseEnumType.FullInventory
+ )
+
+ expect(Array.isArray(reportData)).toBe(true)
+ expect(reportData.length).toBeGreaterThan(0)
+
+ // Check for station info variables
+ const modelVariable = reportData.find(
+ (item: any) =>
+ item.variable.name === OCPP20DeviceInfoVariableName.Model &&
+ item.component.name === OCPP20ComponentName.ChargingStation
+ )
+ expect(modelVariable).toBeDefined()
+ expect(modelVariable.variableAttribute[0].value).toBe(TEST_CHARGE_POINT_MODEL)
+
+ const vendorVariable = reportData.find(
+ (item: any) =>
+ item.variable.name === OCPP20DeviceInfoVariableName.VendorName &&
+ item.component.name === OCPP20ComponentName.ChargingStation
+ )
+ expect(vendorVariable).toBeDefined()
+ expect(vendorVariable.variableAttribute[0].value).toBe(TEST_CHARGE_POINT_VENDOR)
+ })
+
+ await it('Should build correct report data for SummaryInventory', () => {
+ const reportData = (incomingRequestService as any).buildReportData(
+ mockChargingStation,
+ ReportBaseEnumType.SummaryInventory
+ )
+
+ expect(Array.isArray(reportData)).toBe(true)
+ expect(reportData.length).toBeGreaterThan(0)
+
+ // Check for availability state variable
+ const availabilityVariable = reportData.find(
+ (item: any) =>
+ item.variable.name === OCPP20DeviceInfoVariableName.AvailabilityState &&
+ item.component.name === OCPP20ComponentName.ChargingStation
+ )
+ expect(availabilityVariable).toBeDefined()
+ expect(availabilityVariable.variableCharacteristics.supportsMonitoring).toBe(true)
+ })
+
+ await it('Should handle GetBaseReport with EVSE structure', () => {
+ // The createChargingStationWithEvses should create a station with EVSEs
+ const stationWithEvses = createChargingStationWithEvses({
+ baseName: 'CS-EVSE-001',
+ hasEvses: true,
+ stationInfo: {
+ chargePointModel: 'EVSE Test Model',
+ chargePointVendor: 'EVSE Test Vendor',
+ ocppStrictCompliance: false,
+ },
+ })
+
+ const reportData = (incomingRequestService as any).buildReportData(
+ stationWithEvses,
+ ReportBaseEnumType.FullInventory
+ )
+
+ expect(Array.isArray(reportData)).toBe(true)
+ expect(reportData.length).toBeGreaterThan(0)
+
+ // Check if EVSE components are included when EVSEs exist
+ const evseComponents = reportData.filter(
+ (item: any) => item.component.name === OCPP20ComponentName.EVSE
+ )
+ if (stationWithEvses.evses.size > 0) {
+ expect(evseComponents.length).toBeGreaterThan(0)
+ }
+ })
+
+ await it('Should validate unsupported reportBase correctly', () => {
+ const reportData = (incomingRequestService as any).buildReportData(
+ mockChargingStation,
+ 'InvalidReportBase' as any
+ )
+
+ expect(Array.isArray(reportData)).toBe(true)
+ expect(reportData.length).toBe(0)
+ })
+})
--- /dev/null
+/* eslint-disable @typescript-eslint/no-unsafe-member-access */
+
+import { expect } from '@std/expect'
+import { millisecondsToSeconds } from 'date-fns'
+import { describe, it } from 'node:test'
+
+import { OCPP20IncomingRequestService } from '../../../../src/charging-station/ocpp/2.0/OCPP20IncomingRequestService.js'
+import {
+ AttributeEnumType,
+ GetVariableStatusEnumType,
+ OCPP20ComponentName,
+ type OCPP20GetVariablesRequest,
+ OCPP20OptionalVariableName,
+ OCPP20RequiredVariableName,
+} from '../../../../src/types/index.js'
+import { Constants } from '../../../../src/utils/index.js'
+import { createChargingStationWithEvses } from '../../../ChargingStationFactory.js'
+import {
+ TEST_CHARGING_STATION_NAME,
+ TEST_CONNECTOR_INVALID_INSTANCE,
+ TEST_CONNECTOR_VALID_INSTANCE,
+} from './OCPP20TestConstants.js'
+
+await describe('OCPP20IncomingRequestService GetVariables integration tests', async () => {
+ const mockChargingStation = createChargingStationWithEvses({
+ baseName: TEST_CHARGING_STATION_NAME,
+ heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+ stationInfo: {
+ ocppStrictCompliance: false,
+ },
+ websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+ })
+
+ const incomingRequestService = new OCPP20IncomingRequestService()
+
+ await it('Should handle GetVariables request with valid variables', async () => {
+ const request: OCPP20GetVariablesRequest = {
+ getVariableData: [
+ {
+ attributeType: AttributeEnumType.Actual,
+ component: { name: OCPP20ComponentName.ChargingStation },
+ variable: { name: OCPP20OptionalVariableName.HeartbeatInterval },
+ },
+ {
+ component: { name: OCPP20ComponentName.ChargingStation },
+ variable: { name: OCPP20OptionalVariableName.WebSocketPingInterval },
+ },
+ ],
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any
+ const response = await (incomingRequestService as any).handleRequestGetVariables(
+ mockChargingStation,
+ request
+ )
+
+ expect(response).toBeDefined()
+ expect(response.getVariableResult).toBeDefined()
+ expect(Array.isArray(response.getVariableResult)).toBe(true)
+ expect(response.getVariableResult).toHaveLength(2)
+
+ // Check first variable (HeartbeatInterval)
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+ const firstResult = response.getVariableResult[0]
+ expect(firstResult.attributeStatus).toBe(GetVariableStatusEnumType.Accepted)
+ expect(firstResult.attributeType).toBe(AttributeEnumType.Actual)
+ expect(firstResult.attributeValue).toBe(
+ millisecondsToSeconds(Constants.DEFAULT_HEARTBEAT_INTERVAL).toString()
+ )
+ expect(firstResult.component.name).toBe(OCPP20ComponentName.ChargingStation)
+ expect(firstResult.variable.name).toBe(OCPP20OptionalVariableName.HeartbeatInterval)
+ expect(firstResult.attributeStatusInfo).toBeUndefined()
+
+ // Check second variable (WebSocketPingInterval)
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+ const secondResult = response.getVariableResult[1]
+ expect(secondResult.attributeStatus).toBe(GetVariableStatusEnumType.Accepted)
+ expect(secondResult.attributeType).toBeUndefined()
+ expect(secondResult.attributeValue).toBe(Constants.DEFAULT_WEBSOCKET_PING_INTERVAL.toString())
+ expect(secondResult.component.name).toBe(OCPP20ComponentName.ChargingStation)
+ expect(secondResult.variable.name).toBe(OCPP20OptionalVariableName.WebSocketPingInterval)
+ expect(secondResult.attributeStatusInfo).toBeUndefined()
+ })
+
+ await it('Should handle GetVariables request with invalid variables', async () => {
+ const request: OCPP20GetVariablesRequest = {
+ getVariableData: [
+ {
+ component: { name: OCPP20ComponentName.ChargingStation },
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
+ variable: { name: 'InvalidVariable' as any },
+ },
+ {
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
+ component: { name: 'InvalidComponent' as any },
+ variable: { name: OCPP20OptionalVariableName.HeartbeatInterval },
+ },
+ ],
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any
+ const response = await (incomingRequestService as any).handleRequestGetVariables(
+ mockChargingStation,
+ request
+ )
+
+ expect(response).toBeDefined()
+ expect(response.getVariableResult).toBeDefined()
+ expect(Array.isArray(response.getVariableResult)).toBe(true)
+ expect(response.getVariableResult).toHaveLength(2)
+
+ // Check first variable (should be UnknownVariable)
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+ const firstResult = response.getVariableResult[0]
+ expect(firstResult.attributeStatus).toBe(GetVariableStatusEnumType.UnknownVariable)
+ expect(firstResult.attributeType).toBeUndefined()
+ expect(firstResult.attributeValue).toBeUndefined()
+ expect(firstResult.component.name).toBe(OCPP20ComponentName.ChargingStation)
+ expect(firstResult.variable.name).toBe('InvalidVariable')
+ expect(firstResult.attributeStatusInfo).toBeDefined()
+
+ // Check second variable (should be UnknownComponent)
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+ const secondResult = response.getVariableResult[1]
+ expect(secondResult.attributeStatus).toBe(GetVariableStatusEnumType.UnknownComponent)
+ expect(secondResult.attributeType).toBeUndefined()
+ expect(secondResult.attributeValue).toBeUndefined()
+ expect(secondResult.component.name).toBe('InvalidComponent')
+ expect(secondResult.variable.name).toBe(OCPP20OptionalVariableName.HeartbeatInterval)
+ expect(secondResult.attributeStatusInfo).toBeDefined()
+ })
+
+ await it('Should handle GetVariables request with unsupported attribute types', async () => {
+ const request: OCPP20GetVariablesRequest = {
+ getVariableData: [
+ {
+ attributeType: AttributeEnumType.Target, // Not supported for HeartbeatInterval
+ component: { name: OCPP20ComponentName.ChargingStation },
+ variable: { name: OCPP20OptionalVariableName.HeartbeatInterval },
+ },
+ ],
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any
+ const response = await (incomingRequestService as any).handleRequestGetVariables(
+ mockChargingStation,
+ request
+ )
+
+ expect(response).toBeDefined()
+ expect(response.getVariableResult).toBeDefined()
+ expect(Array.isArray(response.getVariableResult)).toBe(true)
+ expect(response.getVariableResult).toHaveLength(1)
+
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+ const result = response.getVariableResult[0]
+ expect(result.attributeStatus).toBe(GetVariableStatusEnumType.NotSupportedAttributeType)
+ })
+
+ await it('Should handle GetVariables request with Connector components', async () => {
+ const request: OCPP20GetVariablesRequest = {
+ getVariableData: [
+ {
+ component: {
+ instance: TEST_CONNECTOR_VALID_INSTANCE,
+ name: OCPP20ComponentName.Connector,
+ },
+ variable: { name: OCPP20RequiredVariableName.AuthorizeRemoteStart },
+ },
+ {
+ component: {
+ instance: TEST_CONNECTOR_INVALID_INSTANCE, // Non-existent connector
+ name: OCPP20ComponentName.Connector,
+ },
+ variable: { name: OCPP20RequiredVariableName.AuthorizeRemoteStart },
+ },
+ ],
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any
+ const response = await (incomingRequestService as any).handleRequestGetVariables(
+ mockChargingStation,
+ request
+ )
+
+ expect(response).toBeDefined()
+ expect(response.getVariableResult).toBeDefined()
+ expect(Array.isArray(response.getVariableResult)).toBe(true)
+ expect(response.getVariableResult).toHaveLength(2)
+
+ // Check valid connector
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+ const firstResult = response.getVariableResult[0]
+ expect(firstResult.attributeStatus).toBe(GetVariableStatusEnumType.Accepted)
+ expect(firstResult.component.name).toBe(OCPP20ComponentName.Connector)
+ expect(firstResult.component.instance).toBe(TEST_CONNECTOR_VALID_INSTANCE)
+
+ // Check invalid connector
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+ const secondResult = response.getVariableResult[1]
+ expect(secondResult.attributeStatus).toBe(GetVariableStatusEnumType.UnknownComponent)
+ expect(secondResult.component.instance).toBe(TEST_CONNECTOR_INVALID_INSTANCE)
+ })
+})
--- /dev/null
+/* eslint-disable @typescript-eslint/no-unsafe-member-access */
+/* eslint-disable @typescript-eslint/no-unsafe-assignment */
+/* eslint-disable @typescript-eslint/no-unsafe-call */
+/* eslint-disable @typescript-eslint/no-explicit-any */
+
+import { expect } from '@std/expect'
+import { describe, it } from 'node:test'
+
+import { OCPP20RequestService } from '../../../../src/charging-station/ocpp/2.0/OCPP20RequestService.js'
+import { OCPP20ResponseService } from '../../../../src/charging-station/ocpp/2.0/OCPP20ResponseService.js'
+import {
+ BootReasonEnumType,
+ type OCPP20BootNotificationRequest,
+ OCPP20RequestCommand,
+} from '../../../../src/types/index.js'
+import { type ChargingStationType } from '../../../../src/types/ocpp/2.0/Common.js'
+import { Constants } from '../../../../src/utils/index.js'
+import { createChargingStation } from '../../../ChargingStationFactory.js'
+import {
+ TEST_CHARGE_POINT_MODEL,
+ TEST_CHARGE_POINT_SERIAL_NUMBER,
+ TEST_CHARGE_POINT_VENDOR,
+ TEST_CHARGING_STATION_NAME,
+ TEST_FIRMWARE_VERSION,
+} from './OCPP20TestConstants.js'
+
+await describe('OCPP20RequestService BootNotification integration tests', async () => {
+ const mockResponseService = new OCPP20ResponseService()
+ const requestService = new OCPP20RequestService(mockResponseService)
+
+ const mockChargingStation = createChargingStation({
+ baseName: TEST_CHARGING_STATION_NAME,
+ heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+ stationInfo: {
+ chargePointModel: TEST_CHARGE_POINT_MODEL,
+ chargePointSerialNumber: TEST_CHARGE_POINT_SERIAL_NUMBER,
+ chargePointVendor: TEST_CHARGE_POINT_VENDOR,
+ firmwareVersion: TEST_FIRMWARE_VERSION,
+ ocppStrictCompliance: false,
+ },
+ websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+ })
+
+ await it('Should build BootNotification request payload correctly with PowerUp reason', () => {
+ const chargingStationInfo: ChargingStationType = {
+ firmwareVersion: TEST_FIRMWARE_VERSION,
+ model: TEST_CHARGE_POINT_MODEL,
+ serialNumber: TEST_CHARGE_POINT_SERIAL_NUMBER,
+ vendorName: TEST_CHARGE_POINT_VENDOR,
+ }
+
+ const requestParams: OCPP20BootNotificationRequest = {
+ chargingStation: chargingStationInfo,
+ reason: BootReasonEnumType.PowerUp,
+ }
+
+ // Access the private buildRequestPayload method via type assertion
+ const payload = (requestService as any).buildRequestPayload(
+ mockChargingStation,
+ OCPP20RequestCommand.BOOT_NOTIFICATION,
+ requestParams
+ )
+
+ expect(payload).toBeDefined()
+ expect(payload.chargingStation).toBeDefined()
+ expect(payload.chargingStation.model).toBe(TEST_CHARGE_POINT_MODEL)
+ expect(payload.chargingStation.vendorName).toBe(TEST_CHARGE_POINT_VENDOR)
+ expect(payload.chargingStation.firmwareVersion).toBe(TEST_FIRMWARE_VERSION)
+ expect(payload.chargingStation.serialNumber).toBe(TEST_CHARGE_POINT_SERIAL_NUMBER)
+ expect(payload.reason).toBe(BootReasonEnumType.PowerUp)
+ })
+
+ await it('Should build BootNotification request payload correctly with ApplicationReset reason', () => {
+ const chargingStationInfo: ChargingStationType = {
+ firmwareVersion: '2.1.3',
+ model: 'Advanced Model X1',
+ serialNumber: 'ADV-SN-002',
+ vendorName: 'Advanced Vendor',
+ }
+
+ const requestParams: OCPP20BootNotificationRequest = {
+ chargingStation: chargingStationInfo,
+ reason: BootReasonEnumType.ApplicationReset,
+ }
+
+ const payload = (requestService as any).buildRequestPayload(
+ mockChargingStation,
+ OCPP20RequestCommand.BOOT_NOTIFICATION,
+ requestParams
+ )
+
+ expect(payload).toBeDefined()
+ expect(payload.chargingStation).toBeDefined()
+ expect(payload.chargingStation.model).toBe('Advanced Model X1')
+ expect(payload.chargingStation.vendorName).toBe('Advanced Vendor')
+ expect(payload.chargingStation.firmwareVersion).toBe('2.1.3')
+ expect(payload.chargingStation.serialNumber).toBe('ADV-SN-002')
+ expect(payload.reason).toBe(BootReasonEnumType.ApplicationReset)
+ })
+
+ await it('Should build BootNotification request payload correctly with minimal required fields', () => {
+ const chargingStationInfo: ChargingStationType = {
+ model: 'Basic Model',
+ vendorName: 'Basic Vendor',
+ // Optional fields omitted: firmwareVersion, serialNumber, customData, modem
+ }
+
+ const requestParams: OCPP20BootNotificationRequest = {
+ chargingStation: chargingStationInfo,
+ reason: BootReasonEnumType.FirmwareUpdate,
+ }
+
+ const payload = (requestService as any).buildRequestPayload(
+ mockChargingStation,
+ OCPP20RequestCommand.BOOT_NOTIFICATION,
+ requestParams
+ )
+
+ expect(payload).toBeDefined()
+ expect(payload.chargingStation).toBeDefined()
+ expect(payload.chargingStation.model).toBe('Basic Model')
+ expect(payload.chargingStation.vendorName).toBe('Basic Vendor')
+ expect(payload.chargingStation.firmwareVersion).toBeUndefined()
+ expect(payload.chargingStation.serialNumber).toBeUndefined()
+ expect(payload.reason).toBe(BootReasonEnumType.FirmwareUpdate)
+ })
+
+ await it('Should handle all BootReasonEnumType values correctly', () => {
+ const chargingStationInfo: ChargingStationType = {
+ model: TEST_CHARGE_POINT_MODEL,
+ vendorName: TEST_CHARGE_POINT_VENDOR,
+ }
+
+ const testReasons = [
+ BootReasonEnumType.ApplicationReset,
+ BootReasonEnumType.FirmwareUpdate,
+ BootReasonEnumType.LocalReset,
+ BootReasonEnumType.PowerUp,
+ BootReasonEnumType.RemoteReset,
+ BootReasonEnumType.ScheduledReset,
+ BootReasonEnumType.Triggered,
+ BootReasonEnumType.Unknown,
+ BootReasonEnumType.Watchdog,
+ ]
+
+ testReasons.forEach(reason => {
+ const requestParams: OCPP20BootNotificationRequest = {
+ chargingStation: chargingStationInfo,
+ reason,
+ }
+
+ const payload = (requestService as any).buildRequestPayload(
+ mockChargingStation,
+ OCPP20RequestCommand.BOOT_NOTIFICATION,
+ requestParams
+ )
+
+ expect(payload).toBeDefined()
+ expect(payload.reason).toBe(reason)
+ expect(payload.chargingStation).toBeDefined()
+ })
+ })
+
+ await it('Should validate payload structure matches OCPP20BootNotificationRequest interface', () => {
+ const chargingStationInfo: ChargingStationType = {
+ customData: {
+ vendorId: 'TEST_VENDOR',
+ },
+ firmwareVersion: '3.0.0',
+ model: 'Validation Test Model',
+ serialNumber: 'VAL-001',
+ vendorName: 'Validation Vendor',
+ }
+
+ const requestParams: OCPP20BootNotificationRequest = {
+ chargingStation: chargingStationInfo,
+ reason: BootReasonEnumType.PowerUp,
+ }
+
+ const payload = (requestService as any).buildRequestPayload(
+ mockChargingStation,
+ OCPP20RequestCommand.BOOT_NOTIFICATION,
+ requestParams
+ )
+
+ // Validate that the payload has the exact structure of OCPP20BootNotificationRequest
+ expect(typeof payload).toBe('object')
+ expect(payload).toHaveProperty('chargingStation')
+ expect(payload).toHaveProperty('reason')
+ expect(Object.keys(payload as object)).toHaveLength(2)
+
+ // Validate chargingStation structure
+ expect(typeof payload.chargingStation).toBe('object')
+ expect(payload.chargingStation).toHaveProperty('model')
+ expect(payload.chargingStation).toHaveProperty('vendorName')
+ expect(typeof payload.chargingStation.model).toBe('string')
+ expect(typeof payload.chargingStation.vendorName).toBe('string')
+
+ // Validate optional fields
+ if (payload.chargingStation.firmwareVersion !== undefined) {
+ expect(typeof payload.chargingStation.firmwareVersion).toBe('string')
+ }
+ if (payload.chargingStation.serialNumber !== undefined) {
+ expect(typeof payload.chargingStation.serialNumber).toBe('string')
+ }
+ if (payload.chargingStation.customData !== undefined) {
+ expect(typeof payload.chargingStation.customData).toBe('object')
+ }
+ })
+})
--- /dev/null
+/* eslint-disable @typescript-eslint/no-unsafe-member-access */
+/* eslint-disable @typescript-eslint/no-unsafe-assignment */
+/* eslint-disable @typescript-eslint/no-unsafe-call */
+/* eslint-disable @typescript-eslint/no-explicit-any */
+
+import { expect } from '@std/expect'
+import { describe, it } from 'node:test'
+
+import { OCPP20RequestService } from '../../../../src/charging-station/ocpp/2.0/OCPP20RequestService.js'
+import { OCPP20ResponseService } from '../../../../src/charging-station/ocpp/2.0/OCPP20ResponseService.js'
+import { type OCPP20HeartbeatRequest, OCPP20RequestCommand } from '../../../../src/types/index.js'
+import { Constants } from '../../../../src/utils/index.js'
+import { createChargingStation } from '../../../ChargingStationFactory.js'
+import {
+ TEST_CHARGE_POINT_MODEL,
+ TEST_CHARGE_POINT_SERIAL_NUMBER,
+ TEST_CHARGE_POINT_VENDOR,
+ TEST_CHARGING_STATION_NAME,
+ TEST_FIRMWARE_VERSION,
+} from './OCPP20TestConstants.js'
+
+await describe('OCPP20RequestService HeartBeat integration tests', async () => {
+ const mockResponseService = new OCPP20ResponseService()
+ const requestService = new OCPP20RequestService(mockResponseService)
+
+ const mockChargingStation = createChargingStation({
+ baseName: TEST_CHARGING_STATION_NAME,
+ heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+ stationInfo: {
+ chargePointModel: TEST_CHARGE_POINT_MODEL,
+ chargePointSerialNumber: TEST_CHARGE_POINT_SERIAL_NUMBER,
+ chargePointVendor: TEST_CHARGE_POINT_VENDOR,
+ firmwareVersion: TEST_FIRMWARE_VERSION,
+ ocppStrictCompliance: false,
+ },
+ websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+ })
+
+ await it('Should build HeartBeat request payload correctly with empty object', () => {
+ const requestParams: OCPP20HeartbeatRequest = {}
+
+ // Access the private buildRequestPayload method via type assertion
+ const payload = (requestService as any).buildRequestPayload(
+ mockChargingStation,
+ OCPP20RequestCommand.HEARTBEAT,
+ requestParams
+ )
+
+ expect(payload).toBeDefined()
+ expect(typeof payload).toBe('object')
+ expect(Object.keys(payload as object)).toHaveLength(0)
+ })
+
+ await it('Should build HeartBeat request payload correctly without parameters', () => {
+ // Test without passing any request parameters
+ const payload = (requestService as any).buildRequestPayload(
+ mockChargingStation,
+ OCPP20RequestCommand.HEARTBEAT
+ )
+
+ expect(payload).toBeDefined()
+ expect(typeof payload).toBe('object')
+ expect(Object.keys(payload as object)).toHaveLength(0)
+ })
+
+ await it('Should validate payload structure matches OCPP20HeartbeatRequest interface', () => {
+ const requestParams: OCPP20HeartbeatRequest = {}
+
+ const payload = (requestService as any).buildRequestPayload(
+ mockChargingStation,
+ OCPP20RequestCommand.HEARTBEAT,
+ requestParams
+ )
+
+ // Validate that the payload is an empty object as required by OCPP 2.0 spec
+ expect(typeof payload).toBe('object')
+ expect(payload).not.toBeNull()
+ expect(Array.isArray(payload)).toBe(false)
+ expect(Object.keys(payload as object)).toHaveLength(0)
+ expect(JSON.stringify(payload)).toBe('{}')
+ })
+
+ await it('Should handle HeartBeat request consistently across multiple calls', () => {
+ const requestParams: OCPP20HeartbeatRequest = {}
+
+ // Call buildRequestPayload multiple times to ensure consistency
+ const payload1 = (requestService as any).buildRequestPayload(
+ mockChargingStation,
+ OCPP20RequestCommand.HEARTBEAT,
+ requestParams
+ )
+
+ const payload2 = (requestService as any).buildRequestPayload(
+ mockChargingStation,
+ OCPP20RequestCommand.HEARTBEAT,
+ requestParams
+ )
+
+ const payload3 = (requestService as any).buildRequestPayload(
+ mockChargingStation,
+ OCPP20RequestCommand.HEARTBEAT
+ )
+
+ // All payloads should be identical empty objects
+ expect(payload1).toEqual(payload2)
+ expect(payload2).toEqual(payload3)
+ expect(JSON.stringify(payload1)).toBe('{}')
+ expect(JSON.stringify(payload2)).toBe('{}')
+ expect(JSON.stringify(payload3)).toBe('{}')
+ })
+
+ await it('Should handle HeartBeat request with different charging station configurations', () => {
+ const alternativeChargingStation = createChargingStation({
+ baseName: 'CS-ALTERNATIVE-002',
+ heartbeatInterval: 120,
+ stationInfo: {
+ chargePointModel: 'Alternative Model',
+ chargePointSerialNumber: 'ALT-SN-002',
+ chargePointVendor: 'Alternative Vendor',
+ firmwareVersion: '2.5.1',
+ ocppStrictCompliance: true,
+ },
+ websocketPingInterval: 45,
+ })
+
+ const requestParams: OCPP20HeartbeatRequest = {}
+
+ const payload = (requestService as any).buildRequestPayload(
+ alternativeChargingStation,
+ OCPP20RequestCommand.HEARTBEAT,
+ requestParams
+ )
+
+ // HeartBeat payload should remain empty regardless of charging station configuration
+ expect(payload).toBeDefined()
+ expect(typeof payload).toBe('object')
+ expect(Object.keys(payload as object)).toHaveLength(0)
+ expect(JSON.stringify(payload)).toBe('{}')
+ })
+
+ await it('Should verify HeartBeat request conforms to OCPP 2.0 specification', () => {
+ const requestParams: OCPP20HeartbeatRequest = {}
+
+ const payload = (requestService as any).buildRequestPayload(
+ mockChargingStation,
+ OCPP20RequestCommand.HEARTBEAT,
+ requestParams
+ )
+
+ // According to OCPP 2.0 specification, HeartBeat request should be an empty object
+ // This validates compliance with the official OCPP 2.0 standard
+ expect(payload).toBeDefined()
+ expect(payload).toEqual({})
+ expect(Object.prototype.hasOwnProperty.call(payload, 'constructor')).toBe(false)
+
+ // Ensure it's a plain object and not an instance of another type
+ expect(Object.getPrototypeOf(payload)).toBe(Object.prototype)
+ })
+})
--- /dev/null
+/* eslint-disable @typescript-eslint/no-unsafe-member-access */
+/* eslint-disable @typescript-eslint/no-unsafe-assignment */
+/* eslint-disable @typescript-eslint/no-unsafe-call */
+/* eslint-disable @typescript-eslint/no-explicit-any */
+
+import { expect } from '@std/expect'
+import { describe, it } from 'node:test'
+
+import { OCPP20RequestService } from '../../../../src/charging-station/ocpp/2.0/OCPP20RequestService.js'
+import { OCPP20ResponseService } from '../../../../src/charging-station/ocpp/2.0/OCPP20ResponseService.js'
+import {
+ AttributeEnumType,
+ DataEnumType,
+ OCPP20ComponentName,
+ OCPP20DeviceInfoVariableName,
+ type OCPP20NotifyReportRequest,
+ OCPP20OptionalVariableName,
+ OCPP20RequestCommand,
+ type ReportDataType,
+} from '../../../../src/types/index.js'
+import { Constants } from '../../../../src/utils/index.js'
+import { createChargingStation } from '../../../ChargingStationFactory.js'
+import {
+ TEST_CHARGE_POINT_MODEL,
+ TEST_CHARGE_POINT_SERIAL_NUMBER,
+ TEST_CHARGE_POINT_VENDOR,
+ TEST_CHARGING_STATION_NAME,
+ TEST_FIRMWARE_VERSION,
+} from './OCPP20TestConstants.js'
+
+await describe('OCPP20RequestService NotifyReport integration tests', async () => {
+ const mockResponseService = new OCPP20ResponseService()
+ const requestService = new OCPP20RequestService(mockResponseService)
+
+ const mockChargingStation = createChargingStation({
+ baseName: TEST_CHARGING_STATION_NAME,
+ heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+ stationInfo: {
+ chargePointModel: TEST_CHARGE_POINT_MODEL,
+ chargePointSerialNumber: TEST_CHARGE_POINT_SERIAL_NUMBER,
+ chargePointVendor: TEST_CHARGE_POINT_VENDOR,
+ firmwareVersion: TEST_FIRMWARE_VERSION,
+ ocppStrictCompliance: false,
+ },
+ websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+ })
+
+ await it('Should build NotifyReport request payload correctly with minimal required fields', () => {
+ const requestParams: OCPP20NotifyReportRequest = {
+ generatedAt: new Date('2023-10-22T10:30:00.000Z'),
+ requestId: 123,
+ seqNo: 0,
+ }
+
+ // Access the private buildRequestPayload method via type assertion
+ const payload = (requestService as any).buildRequestPayload(
+ mockChargingStation,
+ OCPP20RequestCommand.NOTIFY_REPORT,
+ requestParams
+ )
+
+ expect(payload).toBeDefined()
+ expect(payload.generatedAt).toBeInstanceOf(Date)
+ expect(payload.requestId).toBe(123)
+ expect(payload.seqNo).toBe(0)
+ expect(payload.tbc).toBeUndefined()
+ expect(payload.reportData).toBeUndefined()
+ })
+
+ await it('Should build NotifyReport request payload correctly with reportData', () => {
+ const reportData: ReportDataType[] = [
+ {
+ component: {
+ name: OCPP20ComponentName.ChargingStation,
+ },
+ variable: {
+ name: OCPP20DeviceInfoVariableName.Model,
+ },
+ variableAttribute: [
+ {
+ type: AttributeEnumType.Actual,
+ value: 'Test Model X1',
+ },
+ ],
+ variableCharacteristics: {
+ dataType: DataEnumType.string,
+ supportsMonitoring: false,
+ },
+ },
+ ]
+
+ const requestParams: OCPP20NotifyReportRequest = {
+ generatedAt: new Date('2023-10-22T14:15:30.000Z'),
+ reportData,
+ requestId: 456,
+ seqNo: 1,
+ tbc: false,
+ }
+
+ const payload = (requestService as any).buildRequestPayload(
+ mockChargingStation,
+ OCPP20RequestCommand.NOTIFY_REPORT,
+ requestParams
+ )
+
+ expect(payload).toBeDefined()
+ expect(payload.generatedAt).toBeInstanceOf(Date)
+ expect(payload.requestId).toBe(456)
+ expect(payload.seqNo).toBe(1)
+ expect(payload.tbc).toBe(false)
+ expect(payload.reportData).toEqual(reportData)
+ expect(Array.isArray(payload.reportData)).toBe(true)
+ expect(payload.reportData).toHaveLength(1)
+ })
+
+ await it('Should build NotifyReport request payload correctly with multiple reportData items', () => {
+ const reportData: ReportDataType[] = [
+ {
+ component: {
+ name: OCPP20ComponentName.ChargingStation,
+ },
+ variable: {
+ name: OCPP20DeviceInfoVariableName.Model,
+ },
+ variableAttribute: [
+ {
+ type: AttributeEnumType.Actual,
+ value: 'Advanced Model',
+ },
+ ],
+ variableCharacteristics: {
+ dataType: DataEnumType.string,
+ supportsMonitoring: false,
+ },
+ },
+ {
+ component: {
+ name: OCPP20ComponentName.ChargingStation,
+ },
+ variable: {
+ name: OCPP20DeviceInfoVariableName.VendorName,
+ },
+ variableAttribute: [
+ {
+ type: AttributeEnumType.Actual,
+ value: 'Advanced Vendor',
+ },
+ ],
+ variableCharacteristics: {
+ dataType: DataEnumType.string,
+ supportsMonitoring: false,
+ },
+ },
+ {
+ component: {
+ name: OCPP20ComponentName.OCPPCommCtrlr,
+ },
+ variable: {
+ name: OCPP20OptionalVariableName.HeartbeatInterval,
+ },
+ variableAttribute: [
+ {
+ type: AttributeEnumType.Actual,
+ value: '60',
+ },
+ {
+ type: AttributeEnumType.Target,
+ value: '60',
+ },
+ ],
+ variableCharacteristics: {
+ dataType: DataEnumType.integer,
+ supportsMonitoring: true,
+ },
+ },
+ ]
+
+ const requestParams: OCPP20NotifyReportRequest = {
+ generatedAt: new Date('2023-10-22T16:45:00.000Z'),
+ reportData,
+ requestId: 789,
+ seqNo: 2,
+ tbc: true,
+ }
+
+ const payload = (requestService as any).buildRequestPayload(
+ mockChargingStation,
+ OCPP20RequestCommand.NOTIFY_REPORT,
+ requestParams
+ )
+
+ expect(payload).toBeDefined()
+ expect(payload.generatedAt).toBeInstanceOf(Date)
+ expect(payload.requestId).toBe(789)
+ expect(payload.seqNo).toBe(2)
+ expect(payload.tbc).toBe(true)
+ expect(payload.reportData).toEqual(reportData)
+ expect(Array.isArray(payload.reportData)).toBe(true)
+ expect(payload.reportData).toHaveLength(3)
+ })
+
+ await it('Should build NotifyReport request payload correctly with fragmented report (tbc=true)', () => {
+ const reportData: ReportDataType[] = [
+ {
+ component: {
+ name: OCPP20ComponentName.ChargingStation,
+ },
+ variable: {
+ name: OCPP20DeviceInfoVariableName.SerialNumber,
+ },
+ variableAttribute: [
+ {
+ type: AttributeEnumType.Actual,
+ value: 'SN-FRAGMENT-001',
+ },
+ ],
+ variableCharacteristics: {
+ dataType: DataEnumType.string,
+ supportsMonitoring: false,
+ },
+ },
+ ]
+
+ const requestParams: OCPP20NotifyReportRequest = {
+ generatedAt: new Date('2023-10-22T18:20:15.000Z'),
+ reportData,
+ requestId: 999,
+ seqNo: 0,
+ tbc: true, // Indicates more fragments to follow
+ }
+
+ const payload = (requestService as any).buildRequestPayload(
+ mockChargingStation,
+ OCPP20RequestCommand.NOTIFY_REPORT,
+ requestParams
+ )
+
+ expect(payload).toBeDefined()
+ expect(payload.generatedAt).toBeInstanceOf(Date)
+ expect(payload.requestId).toBe(999)
+ expect(payload.seqNo).toBe(0)
+ expect(payload.tbc).toBe(true)
+ expect(payload.reportData).toEqual(reportData)
+ expect(Array.isArray(payload.reportData)).toBe(true)
+ expect(payload.reportData).toHaveLength(1)
+ })
+
+ await it('Should build NotifyReport request payload correctly with empty reportData array', () => {
+ const requestParams: OCPP20NotifyReportRequest = {
+ generatedAt: new Date('2023-10-22T09:00:00.000Z'),
+ reportData: [], // Empty array
+ requestId: 100,
+ seqNo: 0,
+ tbc: false,
+ }
+
+ const payload = (requestService as any).buildRequestPayload(
+ mockChargingStation,
+ OCPP20RequestCommand.NOTIFY_REPORT,
+ requestParams
+ )
+
+ expect(payload).toBeDefined()
+ expect(payload.generatedAt).toBeInstanceOf(Date)
+ expect(payload.requestId).toBe(100)
+ expect(payload.seqNo).toBe(0)
+ expect(payload.tbc).toBe(false)
+ expect(payload.reportData).toEqual([])
+ expect(Array.isArray(payload.reportData)).toBe(true)
+ expect(payload.reportData).toHaveLength(0)
+ })
+
+ await it('Should handle different AttributeEnumType values correctly', () => {
+ const testAttributes = [
+ AttributeEnumType.Actual,
+ AttributeEnumType.Target,
+ AttributeEnumType.MinSet,
+ AttributeEnumType.MaxSet,
+ ]
+
+ testAttributes.forEach((attributeType, index) => {
+ const reportData: ReportDataType[] = [
+ {
+ component: {
+ name: 'TestComponent',
+ },
+ variable: {
+ name: 'TestVariable',
+ },
+ variableAttribute: [
+ {
+ type: attributeType,
+ value: `Test Value ${index.toString()}`,
+ },
+ ],
+ variableCharacteristics: {
+ dataType: DataEnumType.string,
+ supportsMonitoring: true,
+ },
+ },
+ ]
+
+ const requestParams: OCPP20NotifyReportRequest = {
+ generatedAt: new Date(),
+ reportData,
+ requestId: 200 + index,
+ seqNo: index,
+ tbc: false,
+ }
+
+ const payload = (requestService as any).buildRequestPayload(
+ mockChargingStation,
+ OCPP20RequestCommand.NOTIFY_REPORT,
+ requestParams
+ )
+
+ expect(payload).toBeDefined()
+ expect(payload.reportData[0].variableAttribute[0].type).toBe(attributeType)
+ expect(payload.reportData[0].variableAttribute[0].value).toBe(
+ `Test Value ${index.toString()}`
+ )
+ })
+ })
+
+ await it('Should handle different DataEnumType values correctly', () => {
+ const testDataTypes = [
+ { dataType: DataEnumType.string, value: 'test string' },
+ { dataType: DataEnumType.integer, value: '42' },
+ { dataType: DataEnumType.decimal, value: '3.14' },
+ { dataType: DataEnumType.boolean, value: 'true' },
+ { dataType: DataEnumType.dateTime, value: '2023-10-22T12:00:00Z' },
+ ]
+
+ testDataTypes.forEach((testCase, index) => {
+ const reportData: ReportDataType[] = [
+ {
+ component: {
+ name: 'DataTypeTestComponent',
+ },
+ variable: {
+ name: `${testCase.dataType}Variable`,
+ },
+ variableAttribute: [
+ {
+ type: AttributeEnumType.Actual,
+ value: testCase.value,
+ },
+ ],
+ variableCharacteristics: {
+ dataType: testCase.dataType,
+ supportsMonitoring: false,
+ },
+ },
+ ]
+
+ const requestParams: OCPP20NotifyReportRequest = {
+ generatedAt: new Date(),
+ reportData,
+ requestId: 300 + index,
+ seqNo: index,
+ tbc: false,
+ }
+
+ const payload = (requestService as any).buildRequestPayload(
+ mockChargingStation,
+ OCPP20RequestCommand.NOTIFY_REPORT,
+ requestParams
+ )
+
+ expect(payload).toBeDefined()
+ expect(payload.reportData[0].variableCharacteristics.dataType).toBe(testCase.dataType)
+ expect(payload.reportData[0].variableAttribute[0].value).toBe(testCase.value)
+ })
+ })
+
+ await it('Should validate payload structure matches OCPP20NotifyReportRequest interface', () => {
+ const reportData: ReportDataType[] = [
+ {
+ component: {
+ name: 'ValidationTest',
+ },
+ variable: {
+ name: 'ValidationVariable',
+ },
+ variableAttribute: [
+ {
+ type: AttributeEnumType.Actual,
+ value: 'validation value',
+ },
+ ],
+ variableCharacteristics: {
+ dataType: DataEnumType.string,
+ supportsMonitoring: true,
+ },
+ },
+ ]
+
+ const requestParams: OCPP20NotifyReportRequest = {
+ generatedAt: new Date('2023-10-22T13:30:45.000Z'),
+ reportData,
+ requestId: 1001,
+ seqNo: 5,
+ tbc: false,
+ }
+
+ const payload = (requestService as any).buildRequestPayload(
+ mockChargingStation,
+ OCPP20RequestCommand.NOTIFY_REPORT,
+ requestParams
+ )
+
+ // Validate that the payload has the exact structure of OCPP20NotifyReportRequest
+ expect(typeof payload).toBe('object')
+ expect(payload).toHaveProperty('generatedAt')
+ expect(payload).toHaveProperty('requestId')
+ expect(payload).toHaveProperty('seqNo')
+ expect(payload).toHaveProperty('reportData')
+ expect(payload).toHaveProperty('tbc')
+
+ // Validate required fields
+ expect(payload.generatedAt).toBeInstanceOf(Date)
+ expect(typeof payload.requestId).toBe('number')
+ expect(typeof payload.seqNo).toBe('number')
+
+ // Validate optional fields
+ if (payload.reportData !== undefined) {
+ expect(Array.isArray(payload.reportData)).toBe(true)
+ if (payload.reportData.length > 0) {
+ expect(typeof payload.reportData[0]).toBe('object')
+ expect(payload.reportData[0]).toHaveProperty('component')
+ expect(payload.reportData[0]).toHaveProperty('variable')
+ expect(payload.reportData[0]).toHaveProperty('variableAttribute')
+ }
+ }
+
+ if (payload.tbc !== undefined) {
+ expect(typeof payload.tbc).toBe('boolean')
+ }
+ })
+
+ await it('Should handle complex reportData with multiple variable attributes', () => {
+ const reportData: ReportDataType[] = [
+ {
+ component: {
+ name: 'ComplexComponent',
+ },
+ variable: {
+ name: 'ComplexVariable',
+ },
+ variableAttribute: [
+ {
+ type: AttributeEnumType.Actual,
+ value: 'actual value',
+ },
+ {
+ type: AttributeEnumType.Target,
+ value: 'target value',
+ },
+ {
+ type: AttributeEnumType.MinSet,
+ value: '0',
+ },
+ {
+ type: AttributeEnumType.MaxSet,
+ value: '100',
+ },
+ ],
+ variableCharacteristics: {
+ dataType: DataEnumType.integer,
+ supportsMonitoring: true,
+ },
+ },
+ ]
+
+ const requestParams: OCPP20NotifyReportRequest = {
+ generatedAt: new Date('2023-10-22T20:15:30.000Z'),
+ reportData,
+ requestId: 2001,
+ seqNo: 10,
+ tbc: false,
+ }
+
+ const payload = (requestService as any).buildRequestPayload(
+ mockChargingStation,
+ OCPP20RequestCommand.NOTIFY_REPORT,
+ requestParams
+ )
+
+ expect(payload).toBeDefined()
+ expect(payload.reportData[0].variableAttribute).toHaveLength(4)
+ expect(payload.reportData[0].variableAttribute[0].type).toBe(AttributeEnumType.Actual)
+ expect(payload.reportData[0].variableAttribute[1].type).toBe(AttributeEnumType.Target)
+ expect(payload.reportData[0].variableAttribute[2].type).toBe(AttributeEnumType.MinSet)
+ expect(payload.reportData[0].variableAttribute[3].type).toBe(AttributeEnumType.MaxSet)
+ })
+
+ await it('Should preserve all payload properties correctly', () => {
+ const testDate = new Date('2023-10-22T11:22:33.444Z')
+ const reportData: ReportDataType[] = [
+ {
+ component: {
+ name: 'PreserveTestComponent',
+ },
+ variable: {
+ name: 'PreserveTestVariable',
+ },
+ variableAttribute: [
+ {
+ type: AttributeEnumType.Actual,
+ value: 'preserve test value',
+ },
+ ],
+ },
+ ]
+
+ const requestParams: OCPP20NotifyReportRequest = {
+ generatedAt: testDate,
+ reportData,
+ requestId: 3001,
+ seqNo: 15,
+ tbc: true,
+ }
+
+ const payload = (requestService as any).buildRequestPayload(
+ mockChargingStation,
+ OCPP20RequestCommand.NOTIFY_REPORT,
+ requestParams
+ )
+
+ // Verify all input properties are preserved exactly
+ expect(payload.generatedAt).toBe(testDate)
+ expect(payload.requestId).toBe(3001)
+ expect(payload.seqNo).toBe(15)
+ expect(payload.tbc).toBe(true)
+ expect(payload.reportData).toBe(reportData)
+
+ // Verify no additional properties are added
+ const expectedKeys = ['generatedAt', 'reportData', 'requestId', 'seqNo', 'tbc']
+ const actualKeys = Object.keys(payload as object).sort()
+ expectedKeys.sort()
+ expect(actualKeys).toEqual(expectedKeys)
+ })
+})
--- /dev/null
+/* eslint-disable @typescript-eslint/no-unsafe-member-access */
+/* eslint-disable @typescript-eslint/no-unsafe-assignment */
+/* eslint-disable @typescript-eslint/no-unsafe-call */
+/* eslint-disable @typescript-eslint/no-explicit-any */
+
+import { expect } from '@std/expect'
+import { describe, it } from 'node:test'
+
+import { OCPP20RequestService } from '../../../../src/charging-station/ocpp/2.0/OCPP20RequestService.js'
+import { OCPP20ResponseService } from '../../../../src/charging-station/ocpp/2.0/OCPP20ResponseService.js'
+import {
+ OCPP20ConnectorStatusEnumType,
+ OCPP20RequestCommand,
+ type OCPP20StatusNotificationRequest,
+} from '../../../../src/types/index.js'
+import { Constants } from '../../../../src/utils/index.js'
+import { createChargingStationWithEvses } from '../../../ChargingStationFactory.js'
+import {
+ TEST_FIRMWARE_VERSION,
+ TEST_STATUS_CHARGE_POINT_MODEL,
+ TEST_STATUS_CHARGE_POINT_SERIAL_NUMBER,
+ TEST_STATUS_CHARGE_POINT_VENDOR,
+ TEST_STATUS_CHARGING_STATION_NAME,
+} from './OCPP20TestConstants.js'
+
+await describe('OCPP20RequestService StatusNotification integration tests', async () => {
+ const mockResponseService = new OCPP20ResponseService()
+ const requestService = new OCPP20RequestService(mockResponseService)
+
+ const mockChargingStation = createChargingStationWithEvses({
+ baseName: TEST_STATUS_CHARGING_STATION_NAME,
+ heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+ stationInfo: {
+ chargePointModel: TEST_STATUS_CHARGE_POINT_MODEL,
+ chargePointSerialNumber: TEST_STATUS_CHARGE_POINT_SERIAL_NUMBER,
+ chargePointVendor: TEST_STATUS_CHARGE_POINT_VENDOR,
+ firmwareVersion: TEST_FIRMWARE_VERSION,
+ ocppStrictCompliance: false,
+ },
+ websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+ })
+
+ await it('Should build StatusNotification request payload correctly with Available status', () => {
+ const testTimestamp = new Date('2024-01-15T10:30:00.000Z')
+
+ const requestParams: OCPP20StatusNotificationRequest = {
+ connectorId: 1,
+ connectorStatus: OCPP20ConnectorStatusEnumType.Available,
+ evseId: 1,
+ timestamp: testTimestamp,
+ }
+
+ // Access the private buildRequestPayload method via type assertion
+ const payload = (requestService as any).buildRequestPayload(
+ mockChargingStation,
+ OCPP20RequestCommand.STATUS_NOTIFICATION,
+ requestParams
+ )
+
+ expect(payload).toBeDefined()
+ expect(payload.connectorId).toBe(1)
+ expect(payload.connectorStatus).toBe(OCPP20ConnectorStatusEnumType.Available)
+ expect(payload.evseId).toBe(1)
+ expect(payload.timestamp).toBe(testTimestamp)
+ })
+
+ await it('Should build StatusNotification request payload correctly with Occupied status', () => {
+ const testTimestamp = new Date('2024-01-15T11:45:30.000Z')
+
+ const requestParams: OCPP20StatusNotificationRequest = {
+ connectorId: 2,
+ connectorStatus: OCPP20ConnectorStatusEnumType.Occupied,
+ evseId: 2,
+ timestamp: testTimestamp,
+ }
+
+ const payload = (requestService as any).buildRequestPayload(
+ mockChargingStation,
+ OCPP20RequestCommand.STATUS_NOTIFICATION,
+ requestParams
+ )
+
+ expect(payload).toBeDefined()
+ expect(payload.connectorId).toBe(2)
+ expect(payload.connectorStatus).toBe(OCPP20ConnectorStatusEnumType.Occupied)
+ expect(payload.evseId).toBe(2)
+ expect(payload.timestamp).toBe(testTimestamp)
+ })
+
+ await it('Should build StatusNotification request payload correctly with Faulted status', () => {
+ const testTimestamp = new Date('2024-01-15T12:15:45.500Z')
+
+ const requestParams: OCPP20StatusNotificationRequest = {
+ connectorId: 1,
+ connectorStatus: OCPP20ConnectorStatusEnumType.Faulted,
+ evseId: 1,
+ timestamp: testTimestamp,
+ }
+
+ const payload = (requestService as any).buildRequestPayload(
+ mockChargingStation,
+ OCPP20RequestCommand.STATUS_NOTIFICATION,
+ requestParams
+ )
+
+ expect(payload).toBeDefined()
+ expect(payload.connectorId).toBe(1)
+ expect(payload.connectorStatus).toBe(OCPP20ConnectorStatusEnumType.Faulted)
+ expect(payload.evseId).toBe(1)
+ expect(payload.timestamp).toBe(testTimestamp)
+ })
+
+ await it('Should handle all OCPP20ConnectorStatusEnumType values correctly', () => {
+ const testTimestamp = new Date('2024-01-15T13:00:00.000Z')
+
+ const statusValues = [
+ OCPP20ConnectorStatusEnumType.Available,
+ OCPP20ConnectorStatusEnumType.Faulted,
+ OCPP20ConnectorStatusEnumType.Occupied,
+ OCPP20ConnectorStatusEnumType.Reserved,
+ OCPP20ConnectorStatusEnumType.Unavailable,
+ ]
+
+ statusValues.forEach((status, index) => {
+ const requestParams: OCPP20StatusNotificationRequest = {
+ connectorId: index + 1,
+ connectorStatus: status,
+ evseId: index + 1,
+ timestamp: testTimestamp,
+ }
+
+ const payload = (requestService as any).buildRequestPayload(
+ mockChargingStation,
+ OCPP20RequestCommand.STATUS_NOTIFICATION,
+ requestParams
+ )
+
+ expect(payload).toBeDefined()
+ expect(payload.connectorStatus).toBe(status)
+ expect(payload.connectorId).toBe(index + 1)
+ expect(payload.evseId).toBe(index + 1)
+ expect(payload.timestamp).toBe(testTimestamp)
+ })
+ })
+
+ await it('Should validate payload structure matches OCPP20StatusNotificationRequest interface', () => {
+ const testTimestamp = new Date('2024-01-15T14:30:15.123Z')
+
+ const requestParams: OCPP20StatusNotificationRequest = {
+ connectorId: 3,
+ connectorStatus: OCPP20ConnectorStatusEnumType.Reserved,
+ evseId: 2,
+ timestamp: testTimestamp,
+ }
+
+ const payload = (requestService as any).buildRequestPayload(
+ mockChargingStation,
+ OCPP20RequestCommand.STATUS_NOTIFICATION,
+ requestParams
+ )
+
+ // Validate that the payload has the exact structure of OCPP20StatusNotificationRequest
+ expect(typeof payload).toBe('object')
+ expect(payload).toHaveProperty('connectorId')
+ expect(payload).toHaveProperty('connectorStatus')
+ expect(payload).toHaveProperty('evseId')
+ expect(payload).toHaveProperty('timestamp')
+ expect(Object.keys(payload as object)).toHaveLength(4)
+
+ // Validate field types
+ expect(typeof payload.connectorId).toBe('number')
+ expect(typeof payload.connectorStatus).toBe('string')
+ expect(typeof payload.evseId).toBe('number')
+ expect(payload.timestamp).toBeInstanceOf(Date)
+
+ // Validate field values
+ expect(payload.connectorId).toBe(3)
+ expect(payload.connectorStatus).toBe(OCPP20ConnectorStatusEnumType.Reserved)
+ expect(payload.evseId).toBe(2)
+ expect(payload.timestamp).toBe(testTimestamp)
+ })
+
+ await it('Should handle edge case connector and EVSE IDs correctly', () => {
+ const testTimestamp = new Date('2024-01-15T15:45:00.000Z')
+
+ // Test with connector ID 0 (valid in OCPP 2.0 for the charging station itself)
+ const requestParamsConnector0: OCPP20StatusNotificationRequest = {
+ connectorId: 0,
+ connectorStatus: OCPP20ConnectorStatusEnumType.Available,
+ evseId: 1,
+ timestamp: testTimestamp,
+ }
+
+ const payloadConnector0 = (requestService as any).buildRequestPayload(
+ mockChargingStation,
+ OCPP20RequestCommand.STATUS_NOTIFICATION,
+ requestParamsConnector0
+ )
+
+ expect(payloadConnector0).toBeDefined()
+ expect(payloadConnector0.connectorId).toBe(0)
+ expect(payloadConnector0.connectorStatus).toBe(OCPP20ConnectorStatusEnumType.Available)
+ expect(payloadConnector0.evseId).toBe(1)
+ expect(payloadConnector0.timestamp).toBe(testTimestamp)
+
+ // Test with EVSE ID 0 (valid in OCPP 2.0 for the charging station itself)
+ const requestParamsEvse0: OCPP20StatusNotificationRequest = {
+ connectorId: 1,
+ connectorStatus: OCPP20ConnectorStatusEnumType.Unavailable,
+ evseId: 0,
+ timestamp: testTimestamp,
+ }
+
+ const payloadEvse0 = (requestService as any).buildRequestPayload(
+ mockChargingStation,
+ OCPP20RequestCommand.STATUS_NOTIFICATION,
+ requestParamsEvse0
+ )
+
+ expect(payloadEvse0).toBeDefined()
+ expect(payloadEvse0.connectorId).toBe(1)
+ expect(payloadEvse0.connectorStatus).toBe(OCPP20ConnectorStatusEnumType.Unavailable)
+ expect(payloadEvse0.evseId).toBe(0)
+ expect(payloadEvse0.timestamp).toBe(testTimestamp)
+ })
+
+ await it('Should handle different timestamp formats correctly', () => {
+ const testCases = [
+ new Date('2024-01-01T00:00:00.000Z'), // Start of year
+ new Date('2024-12-31T23:59:59.999Z'), // End of year
+ new Date(), // Current time
+ new Date('2024-06-15T12:30:45.678Z'), // Mid-year with milliseconds
+ ]
+
+ testCases.forEach((timestamp, index) => {
+ const requestParams: OCPP20StatusNotificationRequest = {
+ connectorId: 1,
+ connectorStatus: OCPP20ConnectorStatusEnumType.Available,
+ evseId: 1,
+ timestamp,
+ }
+
+ const payload = (requestService as any).buildRequestPayload(
+ mockChargingStation,
+ OCPP20RequestCommand.STATUS_NOTIFICATION,
+ requestParams
+ )
+
+ expect(payload).toBeDefined()
+ expect(payload.timestamp).toBe(timestamp)
+ expect(payload.timestamp).toBeInstanceOf(Date)
+ })
+ })
+})
--- /dev/null
+/**
+ * Common test constants for OCPP 2.0 tests
+ */
+
+// Test charging station information
+export const TEST_CHARGING_STATION_NAME = 'CS-TEST-001'
+export const TEST_CHARGE_POINT_MODEL = 'Test Model'
+export const TEST_CHARGE_POINT_SERIAL_NUMBER = 'TEST-SN-001'
+export const TEST_CHARGE_POINT_VENDOR = 'Test Vendor'
+export const TEST_FIRMWARE_VERSION = '1.0.0'
+
+// Test connector instances
+export const TEST_CONNECTOR_VALID_INSTANCE = '1'
+export const TEST_CONNECTOR_INVALID_INSTANCE = '999'
+
+// Test charging station information for status notification tests
+export const TEST_STATUS_CHARGING_STATION_NAME = 'CS-TEST-STATUS-001'
+export const TEST_STATUS_CHARGE_POINT_MODEL = 'Test Status Model'
+export const TEST_STATUS_CHARGE_POINT_SERIAL_NUMBER = 'TEST-STATUS-SN-001'
+export const TEST_STATUS_CHARGE_POINT_VENDOR = 'Test Status Vendor'
--- /dev/null
+/* eslint-disable @typescript-eslint/no-unsafe-member-access */
+
+import { expect } from '@std/expect'
+import { millisecondsToSeconds } from 'date-fns'
+import { describe, it } from 'node:test'
+
+import { OCPP20VariableManager } from '../../../../src/charging-station/ocpp/2.0/OCPP20VariableManager.js'
+import {
+ AttributeEnumType,
+ type ComponentType,
+ GetVariableStatusEnumType,
+ OCPP20ComponentName,
+ type OCPP20GetVariableDataType,
+ OCPP20OptionalVariableName,
+ OCPP20RequiredVariableName,
+ type VariableType,
+} from '../../../../src/types/index.js'
+import { Constants } from '../../../../src/utils/index.js'
+import { createChargingStationWithEvses } from '../../../ChargingStationFactory.js'
+import { TEST_CHARGING_STATION_NAME } from './OCPP20TestConstants.js'
+
+await describe('OCPP20VariableManager test suite', async () => {
+ // Create mock ChargingStation with EVSEs for OCPP 2.0 testing
+ const mockChargingStation = createChargingStationWithEvses({
+ baseName: TEST_CHARGING_STATION_NAME,
+ heartbeatInterval: Constants.DEFAULT_HEARTBEAT_INTERVAL,
+ websocketPingInterval: Constants.DEFAULT_WEBSOCKET_PING_INTERVAL,
+ })
+
+ await it('Verify that OCPP20VariableManager can be instantiated as singleton', () => {
+ const manager1 = OCPP20VariableManager.getInstance()
+ const manager2 = OCPP20VariableManager.getInstance()
+
+ expect(manager1).toBeDefined()
+ expect(manager1).toBe(manager2) // Same instance (singleton)
+ })
+
+ await describe('getVariables method tests', async () => {
+ const manager = OCPP20VariableManager.getInstance()
+
+ await it('Should handle valid ChargingStation component requests', () => {
+ const request: OCPP20GetVariableDataType[] = [
+ {
+ attributeType: AttributeEnumType.Actual,
+ component: { name: OCPP20ComponentName.ChargingStation },
+ variable: { name: OCPP20OptionalVariableName.HeartbeatInterval },
+ },
+ {
+ attributeType: AttributeEnumType.Actual,
+ component: { name: OCPP20ComponentName.ChargingStation },
+ variable: { name: OCPP20RequiredVariableName.EVConnectionTimeOut },
+ },
+ ]
+
+ const result = manager.getVariables(mockChargingStation, request)
+
+ expect(Array.isArray(result)).toBe(true)
+ expect(result).toHaveLength(2)
+ // First variable: HeartbeatInterval
+ expect(result[0].attributeStatus).toBe(GetVariableStatusEnumType.Accepted)
+ expect(result[0].attributeType).toBe(AttributeEnumType.Actual)
+ expect(result[0].attributeValue).toBe(
+ millisecondsToSeconds(Constants.DEFAULT_HEARTBEAT_INTERVAL).toString()
+ )
+ expect(result[0].component.name).toBe(OCPP20ComponentName.ChargingStation)
+ expect(result[0].variable.name).toBe(OCPP20OptionalVariableName.HeartbeatInterval)
+ expect(result[0].attributeStatusInfo).toBeUndefined()
+ // Second variable: EVConnectionTimeOut
+ expect(result[1].attributeStatus).toBe(GetVariableStatusEnumType.Accepted)
+ expect(result[1].attributeType).toBe(AttributeEnumType.Actual)
+ expect(result[1].attributeValue).toBe(Constants.DEFAULT_EV_CONNECTION_TIMEOUT.toString())
+ expect(result[1].component.name).toBe(OCPP20ComponentName.ChargingStation)
+ expect(result[1].variable.name).toBe(OCPP20RequiredVariableName.EVConnectionTimeOut)
+ expect(result[1].attributeStatusInfo).toBeUndefined()
+ })
+
+ await it('Should handle valid Connector component requests', () => {
+ const request: OCPP20GetVariableDataType[] = [
+ {
+ component: {
+ instance: '1',
+ name: OCPP20ComponentName.Connector,
+ },
+ variable: { name: OCPP20RequiredVariableName.AuthorizeRemoteStart },
+ },
+ ]
+
+ const result = manager.getVariables(mockChargingStation, request)
+
+ expect(Array.isArray(result)).toBe(true)
+ expect(result).toHaveLength(1)
+ expect(result[0].attributeStatus).toBe(GetVariableStatusEnumType.Accepted)
+ expect(result[0].attributeType).toBeUndefined()
+ expect(result[0].attributeValue).toBe('')
+ expect(result[0].component.name).toBe(OCPP20ComponentName.Connector)
+ expect(result[0].component.instance).toBe('1')
+ expect(result[0].variable.name).toBe(OCPP20RequiredVariableName.AuthorizeRemoteStart)
+ expect(result[0].attributeStatusInfo).toBeUndefined()
+ })
+
+ await it('Should handle invalid component gracefully', () => {
+ const request: OCPP20GetVariableDataType[] = [
+ {
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
+ component: { name: 'InvalidComponent' as any },
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
+ variable: { name: 'SomeVariable' as any },
+ },
+ ]
+
+ const result = manager.getVariables(mockChargingStation, request)
+
+ expect(Array.isArray(result)).toBe(true)
+ expect(result).toHaveLength(1)
+ expect(result[0].attributeStatus).toBe(GetVariableStatusEnumType.UnknownComponent)
+ expect(result[0].attributeType).toBeUndefined()
+ expect(result[0].attributeValue).toBeUndefined()
+ expect(result[0].component.name).toBe('InvalidComponent')
+ expect(result[0].variable.name).toBe('SomeVariable')
+ expect(result[0].attributeStatusInfo).toBeDefined()
+ expect(result[0].attributeStatusInfo?.reasonCode).toBe('NotSupported')
+ expect(result[0].attributeStatusInfo?.additionalInfo).toContain(
+ 'Component InvalidComponent is not supported'
+ )
+ })
+
+ await it('Should handle invalid variable gracefully', () => {
+ const request: OCPP20GetVariableDataType[] = [
+ {
+ component: { name: OCPP20ComponentName.ChargingStation },
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
+ variable: { name: 'InvalidVariable' as any },
+ },
+ ]
+
+ const result = manager.getVariables(mockChargingStation, request)
+
+ expect(Array.isArray(result)).toBe(true)
+ expect(result).toHaveLength(1)
+ expect(result[0].attributeStatus).toBe(GetVariableStatusEnumType.UnknownVariable)
+ expect(result[0].attributeType).toBeUndefined()
+ expect(result[0].attributeValue).toBeUndefined()
+ expect(result[0].component.name).toBe(OCPP20ComponentName.ChargingStation)
+ expect(result[0].variable.name).toBe('InvalidVariable')
+ expect(result[0].attributeStatusInfo).toBeDefined()
+ expect(result[0].attributeStatusInfo?.reasonCode).toBe('NotSupported')
+ expect(result[0].attributeStatusInfo?.additionalInfo).toContain(
+ 'Variable InvalidVariable is not supported'
+ )
+ })
+
+ await it('Should handle unsupported attribute type gracefully', () => {
+ const request: OCPP20GetVariableDataType[] = [
+ {
+ attributeType: AttributeEnumType.Target, // Not supported for this variable
+ component: { name: OCPP20ComponentName.ChargingStation },
+ variable: { name: OCPP20OptionalVariableName.HeartbeatInterval },
+ },
+ ]
+
+ const result = manager.getVariables(mockChargingStation, request)
+
+ expect(Array.isArray(result)).toBe(true)
+ expect(result).toHaveLength(1)
+ expect(result[0].attributeStatus).toBe(GetVariableStatusEnumType.NotSupportedAttributeType)
+ expect(result[0].attributeType).toBe(AttributeEnumType.Target)
+ expect(result[0].attributeValue).toBeUndefined()
+ expect(result[0].component.name).toBe(OCPP20ComponentName.ChargingStation)
+ expect(result[0].variable.name).toBe(OCPP20OptionalVariableName.HeartbeatInterval)
+ expect(result[0].attributeStatusInfo).toBeDefined()
+ expect(result[0].attributeStatusInfo?.reasonCode).toBe('NotSupported')
+ expect(result[0].attributeStatusInfo?.additionalInfo).toContain(
+ 'Attribute type Target is not supported'
+ )
+ })
+
+ await it('Should handle non-existent connector instance', () => {
+ const request: OCPP20GetVariableDataType[] = [
+ {
+ component: {
+ instance: '999', // Non-existent connector
+ name: OCPP20ComponentName.Connector,
+ },
+ variable: { name: OCPP20RequiredVariableName.AuthorizeRemoteStart },
+ },
+ ]
+
+ const result = manager.getVariables(mockChargingStation, request)
+
+ expect(Array.isArray(result)).toBe(true)
+ expect(result).toHaveLength(1)
+ expect(result[0].attributeStatus).toBe(GetVariableStatusEnumType.UnknownComponent)
+ expect(result[0].attributeType).toBeUndefined()
+ expect(result[0].attributeValue).toBeUndefined()
+ expect(result[0].component.name).toBe(OCPP20ComponentName.Connector)
+ expect(result[0].component.instance).toBe('999')
+ expect(result[0].variable.name).toBe(OCPP20RequiredVariableName.AuthorizeRemoteStart)
+ expect(result[0].attributeStatusInfo).toBeDefined()
+ expect(result[0].attributeStatusInfo?.reasonCode).toBe('NotSupported')
+ expect(result[0].attributeStatusInfo?.additionalInfo).toContain(
+ 'Component Connector is not supported'
+ )
+ })
+
+ await it('Should handle multiple variables in single request', () => {
+ const request: OCPP20GetVariableDataType[] = [
+ {
+ component: { name: OCPP20ComponentName.ChargingStation },
+ variable: { name: OCPP20OptionalVariableName.HeartbeatInterval },
+ },
+ {
+ component: { name: OCPP20ComponentName.ChargingStation },
+ variable: { name: OCPP20OptionalVariableName.WebSocketPingInterval },
+ },
+ {
+ attributeType: AttributeEnumType.Actual,
+ component: { name: OCPP20ComponentName.ChargingStation },
+ variable: { name: OCPP20RequiredVariableName.MessageTimeout },
+ },
+ ]
+
+ const result = manager.getVariables(mockChargingStation, request)
+
+ expect(Array.isArray(result)).toBe(true)
+ expect(result).toHaveLength(3)
+ // First variable: HeartbeatInterval
+ expect(result[0].attributeStatus).toBe(GetVariableStatusEnumType.Accepted)
+ expect(result[0].attributeType).toBeUndefined()
+ expect(result[0].attributeValue).toBe(
+ millisecondsToSeconds(Constants.DEFAULT_HEARTBEAT_INTERVAL).toString()
+ )
+ expect(result[0].component.name).toBe(OCPP20ComponentName.ChargingStation)
+ expect(result[0].variable.name).toBe(OCPP20OptionalVariableName.HeartbeatInterval)
+ expect(result[0].attributeStatusInfo).toBeUndefined()
+ // Second variable: WebSocketPingInterval
+ expect(result[1].attributeStatus).toBe(GetVariableStatusEnumType.Accepted)
+ expect(result[1].attributeType).toBeUndefined()
+ expect(result[1].attributeValue).toBe(Constants.DEFAULT_WEBSOCKET_PING_INTERVAL.toString())
+ expect(result[1].component.name).toBe(OCPP20ComponentName.ChargingStation)
+ expect(result[1].variable.name).toBe(OCPP20OptionalVariableName.WebSocketPingInterval)
+ expect(result[1].attributeStatusInfo).toBeUndefined()
+ // Third variable: MessageTimeout
+ expect(result[2].attributeStatus).toBe(GetVariableStatusEnumType.Accepted)
+ expect(result[2].attributeType).toBe(AttributeEnumType.Actual)
+ expect(result[2].attributeValue).toBe(mockChargingStation.getConnectionTimeout().toString())
+ expect(result[2].component.name).toBe(OCPP20ComponentName.ChargingStation)
+ expect(result[2].variable.name).toBe(OCPP20RequiredVariableName.MessageTimeout)
+ expect(result[2].attributeStatusInfo).toBeUndefined()
+ })
+
+ await it('Should handle EVSE component when supported', () => {
+ const request: OCPP20GetVariableDataType[] = [
+ {
+ component: {
+ instance: '1',
+ name: OCPP20ComponentName.EVSE,
+ },
+ variable: { name: OCPP20RequiredVariableName.AuthorizeRemoteStart },
+ },
+ ]
+
+ const result = manager.getVariables(mockChargingStation, request)
+
+ expect(Array.isArray(result)).toBe(true)
+ expect(result).toHaveLength(1)
+ // Should be accepted since mockChargingStation has EVSEs
+ expect(result[0].attributeStatus).toBe(GetVariableStatusEnumType.Accepted)
+ expect(result[0].attributeType).toBeUndefined()
+ expect(result[0].attributeValue).toBe('')
+ expect(result[0].component.name).toBe(OCPP20ComponentName.EVSE)
+ expect(result[0].component.instance).toBe('1')
+ expect(result[0].variable.name).toBe(OCPP20RequiredVariableName.AuthorizeRemoteStart)
+ expect(result[0].attributeStatusInfo).toBeUndefined()
+ })
+ })
+
+ await describe('Component validation tests', async () => {
+ const manager = OCPP20VariableManager.getInstance()
+
+ await it('Should validate ChargingStation component as always valid', () => {
+ const component: ComponentType = { name: OCPP20ComponentName.ChargingStation }
+
+ // Access private method through any casting for testing
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any
+ const isValid = (manager as any).isComponentValid(mockChargingStation, component)
+ expect(isValid).toBe(true)
+ })
+
+ await it('Should validate Connector component when connectors exist', () => {
+ const component: ComponentType = { instance: '1', name: OCPP20ComponentName.Connector }
+
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any
+ const isValid = (manager as any).isComponentValid(mockChargingStation, component)
+ expect(isValid).toBe(true)
+ })
+
+ await it('Should reject invalid connector instance', () => {
+ const component: ComponentType = { instance: '999', name: OCPP20ComponentName.Connector }
+
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any
+ const isValid = (manager as any).isComponentValid(mockChargingStation, component)
+ expect(isValid).toBe(false)
+ })
+ })
+
+ await describe('Variable support validation tests', async () => {
+ const manager = OCPP20VariableManager.getInstance()
+
+ await it('Should support standard HeartbeatInterval variable', () => {
+ const component: ComponentType = { name: OCPP20ComponentName.ChargingStation }
+ const variable: VariableType = { name: OCPP20OptionalVariableName.HeartbeatInterval }
+
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any
+ const isSupported = (manager as any).isVariableSupported(
+ mockChargingStation,
+ component,
+ variable
+ )
+ expect(isSupported).toBe(true)
+ })
+
+ await it('Should support known OCPP variables', () => {
+ const component: ComponentType = { name: OCPP20ComponentName.ChargingStation }
+ const variable: VariableType = { name: OCPP20OptionalVariableName.WebSocketPingInterval }
+
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any
+ const isSupported = (manager as any).isVariableSupported(
+ mockChargingStation,
+ component,
+ variable
+ )
+ expect(isSupported).toBe(true)
+ })
+
+ await it('Should reject unknown variables', () => {
+ const component: ComponentType = { name: OCPP20ComponentName.ChargingStation }
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
+ const variable: VariableType = { name: 'UnknownVariable' as any }
+
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any
+ const isSupported = (manager as any).isVariableSupported(
+ mockChargingStation,
+ component,
+ variable
+ )
+ expect(isSupported).toBe(false)
+ })
+ })
+
+ await describe('Attribute type validation tests', async () => {
+ const manager = OCPP20VariableManager.getInstance()
+
+ await it('Should support Actual attribute by default', () => {
+ const variable: VariableType = { name: OCPP20OptionalVariableName.HeartbeatInterval }
+
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any
+ const isSupported = (manager as any).isAttributeTypeSupported(
+ variable,
+ AttributeEnumType.Actual
+ )
+ expect(isSupported).toBe(true)
+ })
+
+ await it('Should reject unsupported attribute types for most variables', () => {
+ const variable: VariableType = { name: OCPP20OptionalVariableName.HeartbeatInterval }
+
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any
+ const isSupported = (manager as any).isAttributeTypeSupported(
+ variable,
+ AttributeEnumType.Target
+ )
+ expect(isSupported).toBe(false)
+ })
+ })
+})
## Running the server with OCPP command sending
-You can also specify a command and a period duration with the --command and --period options respectively when running the server. The server will then send your chosen command to the connected client(s) every period seconds.
+### Command Line Interface
-### GetBaseReport Command
+```shell
+poetry run task server --command <COMMAND_NAME> --period <SECONDS>
+```
+
+**Options:**
+
+- `--command <COMMAND_NAME>`: The OCPP command to send (see available commands below)
+- `--period <SECONDS>`: Interval in seconds between command sends
-To run the server and send a GetBaseReport command every 5 seconds, use:
+**Example:**
```shell
poetry run task server --command GetBaseReport --period 5
```
-### ClearCache Command
+### Available Outgoing Commands
-To run the server and send a ClearCache command every 5 seconds, use:
+- `ClearCache` - Clear the charging station cache
+- `GetBaseReport` - Request a base configuration report
+- `GetVariables` - Get variable values from the charging station
+- `SetVariables` - Set variable values on the charging station
+- `RequestStartTransaction` - Request to start a transaction
+- `RequestStopTransaction` - Request to stop a transaction
+- `Reset` - Reset the charging station
+- `UnlockConnector` - Unlock a specific connector
+- `ChangeAvailability` - Change connector availability
+- `TriggerMessage` - Trigger a specific message
+- `DataTransfer` - Send custom data
+
+### Testing the Server
+
+To run the test suite and validate all implemented commands:
```shell
-poetry run task server --command ClearCache --period 5
+poetry run task test
```
-Please be mindful that these commands were examples according to the provided scenario, the available commands and their syntax might vary depending on the ocpp version and the implemented functionalities on your client.
-
## Overview of the Server Scripts
### Server.py
[tool.taskipy.tasks]
server = "poetry run python server.py"
+test = "poetry run python test_server.py"
format = "ruff check --fix . && ruff format ."
lint = "ruff check --diff . && ruff format --check --diff ."
from ocpp.v201.enums import (
Action,
AuthorizationStatusEnumType,
+ ChangeAvailabilityStatusEnumType,
ClearCacheStatusEnumType,
+ DataTransferStatusEnumType,
GenericDeviceModelStatusEnumType,
+ MessageTriggerEnumType,
+ OperationalStatusEnumType,
RegistrationStatusEnumType,
ReportBaseEnumType,
+ ResetEnumType,
+ ResetStatusEnumType,
TransactionEventEnumType,
+ TriggerMessageStatusEnumType,
+ UnlockStatusEnumType,
)
from websockets import ConnectionClosed
logging.info("Received %s", Action.notify_report)
return ocpp.v201.call_result.NotifyReport()
+ @on(Action.data_transfer)
+ async def on_data_transfer(self, vendor_id: str, **kwargs):
+ logging.info("Received %s", Action.data_transfer)
+ return ocpp.v201.call_result.DataTransfer(
+ status=DataTransferStatusEnumType.accepted
+ )
+
+ @on(Action.firmware_status_notification)
+ async def on_firmware_status_notification(self, status, **kwargs):
+ logging.info("Received %s", Action.firmware_status_notification)
+ return ocpp.v201.call_result.FirmwareStatusNotification()
+
+ @on(Action.log_status_notification)
+ async def on_log_status_notification(self, status, request_id: int, **kwargs):
+ logging.info("Received %s", Action.log_status_notification)
+ return ocpp.v201.call_result.LogStatusNotification()
+
+ @on(Action.security_event_notification)
+ async def on_security_event_notification(self, event_type, timestamp, **kwargs):
+ logging.info("Received %s", Action.security_event_notification)
+ return ocpp.v201.call_result.SecurityEventNotification()
+
# Request handlers to emit OCPP messages.
async def _send_clear_cache(self):
request = ocpp.v201.call.ClearCache()
else:
logging.info("%s failed", Action.get_base_report)
+ async def _send_get_variables(self):
+ request = ocpp.v201.call.GetVariables(
+ get_variable_data=[
+ {
+ "component": {"name": "ChargingStation"},
+ "variable": {"name": "AvailabilityState"},
+ }
+ ]
+ )
+ await self.call(request)
+ logging.info("%s response received", Action.get_variables)
+
+ async def _send_set_variables(self):
+ request = ocpp.v201.call.SetVariables(
+ set_variable_data=[
+ {
+ "component": {"name": "ChargingStation"},
+ "variable": {"name": "HeartbeatInterval"},
+ "attribute_value": "30",
+ }
+ ]
+ )
+ await self.call(request)
+ logging.info("%s response received", Action.set_variables)
+
+ async def _send_request_start_transaction(self):
+ request = ocpp.v201.call.RequestStartTransaction(
+ id_token={"id_token": "test_token", "type": "ISO14443"},
+ evse_id=1,
+ remote_start_id=randint(1, 1000), # noqa: S311
+ )
+ await self.call(request)
+ logging.info("%s response received", Action.request_start_transaction)
+
+ async def _send_request_stop_transaction(self):
+ request = ocpp.v201.call.RequestStopTransaction(
+ transaction_id="test_transaction_123"
+ )
+ await self.call(request)
+ logging.info("%s response received", Action.request_stop_transaction)
+
+ async def _send_reset(self):
+ request = ocpp.v201.call.Reset(type=ResetEnumType.immediate)
+ response = await self.call(request)
+
+ if (
+ hasattr(response, "status")
+ and response.status == ResetStatusEnumType.accepted
+ ):
+ logging.info("%s successful", Action.reset)
+ else:
+ logging.info("%s failed", Action.reset)
+
+ async def _send_unlock_connector(self):
+ request = ocpp.v201.call.UnlockConnector(evse_id=1, connector_id=1)
+ response = await self.call(request)
+
+ if response.status == UnlockStatusEnumType.unlocked:
+ logging.info("%s successful", Action.unlock_connector)
+ else:
+ logging.info("%s failed", Action.unlock_connector)
+
+ async def _send_change_availability(self):
+ request = ocpp.v201.call.ChangeAvailability(
+ operational_status=OperationalStatusEnumType.operative
+ )
+ response = await self.call(request)
+
+ if (
+ hasattr(response, "status")
+ and response.status == ChangeAvailabilityStatusEnumType.accepted
+ ):
+ logging.info("%s successful", Action.change_availability)
+ else:
+ logging.info("%s failed", Action.change_availability)
+
+ async def _send_trigger_message(self):
+ request = ocpp.v201.call.TriggerMessage(
+ requested_message=MessageTriggerEnumType.status_notification
+ )
+ response = await self.call(request)
+
+ if (
+ hasattr(response, "status")
+ and response.status == TriggerMessageStatusEnumType.accepted
+ ):
+ logging.info("%s successful", Action.trigger_message)
+ else:
+ logging.info("%s failed", Action.trigger_message)
+
+ async def _send_data_transfer(self):
+ request = ocpp.v201.call.DataTransfer(
+ vendor_id="TestVendor", message_id="TestMessage", data="test_data"
+ )
+ response = await self.call(request)
+
+ if response.status == DataTransferStatusEnumType.accepted:
+ logging.info("%s successful", Action.data_transfer)
+ else:
+ logging.info("%s failed", Action.data_transfer)
+
async def _send_command(self, command_name: Action):
logging.debug("Sending OCPP command %s", command_name)
match command_name:
await self._send_clear_cache()
case Action.get_base_report:
await self._send_get_base_report()
+ case Action.get_variables:
+ await self._send_get_variables()
+ case Action.set_variables:
+ await self._send_set_variables()
+ case Action.request_start_transaction:
+ await self._send_request_start_transaction()
+ case Action.request_stop_transaction:
+ await self._send_request_stop_transaction()
+ case Action.reset:
+ await self._send_reset()
+ case Action.unlock_connector:
+ await self._send_unlock_connector()
+ case Action.change_availability:
+ await self._send_change_availability()
+ case Action.trigger_message:
+ await self._send_trigger_message()
+ case Action.data_transfer:
+ await self._send_data_transfer()
case _:
logging.info(f"Not supported command {command_name}")
--- /dev/null
+#!/usr/bin/env python3
+"""
+Test script to verify OCPP 2.0 commands supported by the mock server
+"""
+
+import logging
+
+# Configure logging
+logging.basicConfig(level=logging.INFO)
+
+
+def test_outgoing_commands():
+ """Test the presence of outgoing command methods"""
+ try:
+ from server import ChargePoint
+ except ImportError as e:
+ logging.error(f"Failed to import ChargePoint: {e}")
+ return [("import_error", "❌ FAIL: Cannot import server module")]
+
+ expected_methods = [
+ "_send_clear_cache",
+ "_send_get_base_report",
+ "_send_get_variables",
+ "_send_set_variables",
+ "_send_request_start_transaction",
+ "_send_request_stop_transaction",
+ "_send_reset",
+ "_send_unlock_connector",
+ "_send_change_availability",
+ "_send_trigger_message",
+ "_send_data_transfer",
+ ]
+
+ results = []
+
+ for method_name in expected_methods:
+ command_name = method_name.replace("_send_", "")
+ if hasattr(ChargePoint, method_name):
+ results.append((command_name, "✅ PASS"))
+ logging.info(f"Method {method_name}: PASS")
+ else:
+ results.append((command_name, "❌ FAIL: Missing method"))
+ logging.error(f"Method {method_name}: FAIL - Missing method")
+
+ return results
+
+
+def check_incoming_handlers():
+ """Verify that all incoming handlers are present"""
+ try:
+ from server import ChargePoint
+ except ImportError as e:
+ logging.error(f"Failed to import ChargePoint: {e}")
+ return [("import_error", "❌ FAIL: Cannot import server module")]
+
+ expected_handlers = [
+ "boot_notification",
+ "heartbeat",
+ "status_notification",
+ "authorize",
+ "transaction_event",
+ "meter_values",
+ "notify_report",
+ "data_transfer",
+ "firmware_status_notification",
+ "log_status_notification",
+ "security_event_notification",
+ ]
+
+ results = []
+
+ for action_name in expected_handlers:
+ handler_name = f"on_{action_name}"
+ if hasattr(ChargePoint, handler_name):
+ results.append((action_name, "✅ PASS"))
+ logging.info(f"Handler {handler_name}: PASS")
+ else:
+ results.append((action_name, "❌ FAIL: Missing handler"))
+ logging.error(f"Handler {handler_name}: FAIL - Missing handler")
+
+ return results
+
+
+def main():
+ """Main test function"""
+ print("=" * 60)
+ print("OCPP 2.0 Mock Server Test")
+ print("=" * 60)
+
+ print("\n🔄 Testing outgoing commands...")
+ outgoing_results = test_outgoing_commands()
+
+ print("\n🔄 Testing incoming handlers...")
+ incoming_results = check_incoming_handlers()
+
+ print("\n📊 Test results:")
+ print("\n--- Outgoing commands ---")
+ for command, status in outgoing_results:
+ print(f"{status} {command}")
+
+ print("\n--- Incoming handlers ---")
+ for handler, status in incoming_results:
+ print(f"{status} {handler}")
+
+ # Statistics
+ outgoing_pass = len([r for r in outgoing_results if "PASS" in r[1]])
+ incoming_pass = len([r for r in incoming_results if "PASS" in r[1]])
+ total_pass = outgoing_pass + incoming_pass
+ total_tests = len(outgoing_results) + len(incoming_results)
+
+ print("\n📈 Statistics:")
+ print(f" Outgoing commands: {outgoing_pass}/{len(outgoing_results)}")
+ print(f" Incoming handlers: {incoming_pass}/{len(incoming_results)}")
+ print(
+ f" Total: {total_pass}/{total_tests} ({total_pass / total_tests * 100:.1f}%)"
+ )
+
+ if total_pass == total_tests:
+ print("\n🎉 All tests passed!")
+ return 0
+ else:
+ print(f"\n⚠️ {total_tests - total_pass} test(s) failed")
+ return 1
+
+
+if __name__ == "__main__":
+ exit_code = main()
+ exit(exit_code)
import { expect } from '@std/expect'
import { describe, it } from 'node:test'
-import type { ChargingStation } from '../../src/charging-station/index.js'
-
import {
FileType,
GenericStatus,
handleSendMessageError,
} from '../../src/utils/ErrorUtils.js'
import { logger } from '../../src/utils/Logger.js'
+import { createChargingStation } from '../ChargingStationFactory.js'
await describe('ErrorUtils test suite', async () => {
- const chargingStation = {
- logPrefix: () => 'CS-TEST |',
- } as ChargingStation
+ const chargingStation = createChargingStation({ baseName: 'CS-TEST' })
await it('Verify handleFileException()', t => {
t.mock.method(console, 'warn')