Contents

3a4oT/solarman-swift

Solarman V5 protocol client in pure Swift — communicate with WiFi data loggers on port 8899

Overview

SolarmanV5 enables communication with Solarman (IGEN-Tech) WiFi data logging sticks that use the proprietary V5 protocol. These loggers connect solar inverters to the Solarman Cloud and expose a local TCP interface on port 8899.

Key Insight: The V5 protocol wraps standard Modbus RTU frames, allowing direct communication with inverters without disrupting cloud operations.

Features

  • Pure Swift — No C dependencies
  • SwiftNIO — High-performance async TCP networking
  • Swift 6.2 — Typed throws, Span<UInt8> parsing, Mutex request serialization
  • Full Modbus Support — All 9 function codes supported by pysolarmanv5
  • Observability — swift-log, swift-metrics, ServiceLifecycle integration

Compatibility

This library implements the Solarman V5 protocol used by IGEN-Tech WiFi data logging sticks. Compatibility depends on your logger using the V5 protocol on TCP port 8899.

Note: Solis S3-WIFI-ST uses a different protocol and is not supported.

Installation

Swift Package Manager

dependencies: [
    .package(url: "https://github.com/3a4oT/solarman-swift.git", from: "1.0.0")
]

Then add to your target:

.target(
    name: "YourApp",
    dependencies: [
        .product(name: "SolarmanV5", package: "solarman-swift"),
    ]
)

Quick Start

Scoped Client (CLI / Scripts / Tests)

Auto-closes connection when scope exits. Best for one-off operations:

import SolarmanV5

let registers = try await withSolarmanV5Client(
    host: "192.168.1.100",
    serial: 1712345678
) { client in
    try await client.readHoldingRegisters(address: 0, count: 10).registers
}

Long-Lived Client (Services / Daemons)

For persistent connections with logging, metrics, and graceful shutdown:

import Logging
import SolarmanV5
import ServiceLifecycle

let logger = Logger(label: "solar")
let metrics = SolarmanMetrics()

let client = SolarmanV5Client(
    host: "192.168.1.100",
    serial: 1712345678,
    logger: logger,
    metrics: metrics
)

try await client.connect()
let response = try await client.readHoldingRegisters(address: 0, count: 10)
print(response.registers)

// Graceful shutdown with ServiceLifecycle
let group = ServiceGroup(
    services: [client],
    gracefulShutdownSignals: [.sigterm, .sigint],
    logger: logger
)
try await group.run()

Configuration Options

let config = SolarmanClientConfiguration(
    host: "192.168.1.100",
    serial: 1712345678,
    port: 8899,                              // Default V5 port
    unitId: 1,                               // Modbus slave ID
    timeout: .seconds(60),                   // Per pysolarmanv5 default
    retries: 3,                              // Retry attempts
    idleTimeout: .seconds(60),               // Auto-disconnect on inactivity
    reconnectionStrategy: .exponentialBackoff(
        initialDelay: .milliseconds(100),
        maxDelay: .seconds(30)
    ),
    v5ErrorCorrection: false                 // Naive frame recovery (rare)
)

let client = SolarmanV5Client(
    configuration: config,
    logger: logger,
    metrics: metrics
)

Supported Function Codes

| Code | Function | Method | |:----:|----------|--------| | 0x01 | Read Coils | readCoils(address:count:) | | 0x02 | Read Discrete Inputs | readDiscreteInputs(address:count:) | | 0x03 | Read Holding Registers | readHoldingRegisters(address:count:) | | 0x04 | Read Input Registers | readInputRegisters(address:count:) | | 0x05 | Write Single Coil | writeSingleCoil(address:value:) | | 0x06 | Write Single Register | writeSingleRegister(address:value:) | | 0x0F | Write Multiple Coils | writeMultipleCoils(address:values:) | | 0x10 | Write Multiple Registers | writeMultipleRegisters(address:values:) | | 0x16 | Mask Write Register | maskWriteRegister(address:andMask:orMask:) |

Raw Frame Access

For custom function codes or debugging:

// Without CRC (auto-appended)
let response = try await client.sendRawModbusFrame([0x01, 0x03, 0x00, 0x00, 0x00, 0x0A])

// With CRC (sent as-is)
let response = try await client.sendRawModbusFrameWithCRC(frameWithCRC)

Reconnection Strategies

| Strategy | Description | |----------|-------------| | .disabled | No auto-reconnect; call connect() manually | | .immediate | Reconnect immediately on disconnect (goburrow/modbus style) | | .exponentialBackoff(initialDelay:maxDelay:) | Reconnect with increasing delays (pymodbus style) |

Error Handling

All client methods throw SolarmanClientError with typed throws:

do {
    let response = try await client.readHoldingRegisters(address: 0, count: 10)
} catch .timeout {
    // Connection or read timed out
} catch .modbusException(let exception) {
    // Device returned Modbus exception (e.g., illegal address)
} catch .v5FrameError(let message) {
    // V5 protocol error (checksum, markers, etc.)
} catch .notConnected {
    // Client not connected
}

Retryable vs Non-Retryable Errors

| Error | Retryable | Notes | |-------|:---------:|-------| | timeout | Yes | Network delay | | ioError | Yes | Connection reset | | channelClosed | Yes | Unexpected disconnect | | connectionFailed | Yes | Initial connect failed | | modbusException | No | Device rejected request | | v5FrameError | No | Protocol violation | | invalidParameter | No | Invalid input |

Metrics

When SolarmanMetrics is provided, the following Prometheus-compatible metrics are recorded:

| Metric | Type | Labels | |--------|------|--------| | solarman_connection_active | Gauge | serial | | solarman_requests_total | Counter | serial, function_code, status | | solarman_request_duration_seconds | Timer | serial, function_code | | solarman_retries_total | Counter | serial, function_code | | solarman_reconnections_total | Counter | serial |

Protocol Details

V5 Frame Structure

┌─────────────────────────────────────────────────────────────────┐
                        V5 Frame                                 
├────────┬────────┬──────────────────────────────────┬────────────┤
 Header  Payload         Modbus RTU Frame           Trailer   
 11 B    14-15 B      (Big Endian, with CRC)          2 B     
└────────┴────────┴──────────────────────────────────┴────────────┘

| Field | Size | Encoding | Notes | |-------|:----:|:--------:|-------| | Start | 1 | — | 0xA5 | | Length | 2 | LE | Payload size | | Control Code | 2 | LE | 0x4510 request, 0x1510 response | | Sequence | 2 | LE | Request ID (echoed in response) | | Logger Serial | 4 | LE | Data logger serial number | | Frame Type | 1 | — | 0x02 for inverter | | Status/Sensor | 1-2 | — | Request vs response differs | | Timestamps | 12 | LE | Working time, power on, offset | | Modbus RTU | var | BE | Standard Modbus frame | | Checksum | 1 | — | sum(bytes[1..<end-1]) & 0xFF | | End | 1 | — | 0x15 |

Concurrency Model

Requests are serialized using Synchronization.Mutex. This matches:

  • pysolarmanv5: socket-based, effectively single request at a time
  • Most WiFi loggers: don't support concurrent requests

Note: Transaction ID pipelining is NOT supported (V5 protocol limitation).

Requirements

  • Swift 6.2+
  • macOS 26+, iOS 26+, or Linux (Ubuntu 24.04+)

Dependencies

| Package | Version | Purpose | |---------|---------|---------| | modbus-swift | 1.0.0+ | ModbusCore for PDU/CRC | | swift-nio | 2.91.0+ | TCP networking | | swift-log | 1.7.1+ | Structured logging | | swift-metrics | 2.7.1+ | Metrics collection | | swift-service-lifecycle | 2.9.1+ | Graceful shutdown |

Development

Setup

# Install SwiftFormat
brew install swiftformat

# Install pre-commit hook (runs SwiftFormat on staged files)
./Scripts/install-hooks.sh

Code Style

This project uses SwiftFormat with configuration in .swiftformat.

# Format all files
swiftformat .

# Check without modifying
swiftformat . --lint

Testing

swift test --filter SolarmanV5

Known Device Quirks

| Issue | Affected Devices | Solution | |-------|-----------------|----------| | Double CRC | DEYE, others | v5ErrorCorrection: true | | Response delays | Various | Increase timeout | | Connection limits | Most loggers | Use single client instance |

References

License

Apache License 2.0. See LICENSE for details.

Package Metadata

Repository: 3a4oT/solarman-swift

Stars: 0

Forks: 0

Open issues: 0

Default branch: main

Primary language: swift

License: Apache-2.0

Topics: modbus-tcp, solarmanv5, swift

README: README.md