Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

draft: (blueprint) add SMA cluster controller #203

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions solar_inverters/sma_cluster_controller/DEVELOPMENT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# SMA Sunny Tripower Blueprint Development

## Working With Modbus

### Download All Modbus Documents

SMA provides HTML files with Modbus registers specifications. Files can be manually downloaded from [SMA website](https://www.sma.de/en/products/product-features-interfaces/modbus-protocol-interface) or using the automated script: `scripts/download_modbus.sh`.

### Easy Way to Extract Enum Values

The following script will collect enum values from all downloaded Modbus HTML documents.

Note: [ripgrep](https://github.com/BurntSushi/ripgrep) must be installed.

```bash
rg --no-ignore -g '*_en.html' 40009 | grep -o '[0-9]*: [^<]*' | sort -u
```

## Working With Alerts

Run `scripts/generate_alerts.rb`, it will generate YAML declaration for all known alerts in an appropriate format.

## List of All SMA Sunny Boy Models

### Active Models

How to build this list:

1. Download all .zip files from [SMA documentation](https://www.sma.de/en/products/product-features-interfaces/modbus-protocol-interface)
2. Run: `for f in modbus/*/*_en.html; do grep 30053 "$f" | grep -o '[0-9]*:[^<]*'; done`
3. Copy here, sort, remove duplicates (vim: `sort u`)
4. Leave only names in brakets, remove marketing names. E.g. "Sunny Island 6.0H (SI 6.0H-12)" -> "SI 6.0H-12". vim: `s/\v: .*\((.*)\)/: \1`
5. Leave only Sunny Boy models (starts with `SB`).

```txt
19048: STP5.0-3SE-40
19049: STP6.0-3SE-40
19050: STP8.0-3SE-40
19051: STP10.0-3SE-40
9284: STP 20000TL-30
9285: STP 25000TL-30
9336: STP 15000TL-30
9337: STP 17000TL-30
9338: STP50-40
9339: STP50-US-40
9340: STP50-JP-40
9344: STP4.0-3AV-40
9345: STP5.0-3AV-40
9346: STP6.0-3AV-40
9347: STP8.0-3AV-40
9348: STP10.0-3AV-40
9366: STP3.0-3AV-40
9428: STP62-US-41
9429: STP50-US-41
9430: STP33-US-41
9431: STP50-41
9432: STP50-JP-41
```

### Archive Models

These models are no longer available in Modbus documentation and left here for backward compatibility.

```txt
9067: STP 10000TL-10
9068: STP 12000TL-10
9069: STP 15000TL-10
9070: STP 17000TL-10
9098: STP 5000TL-20
9099: STP 6000TL-20
9100: STP 7000TL-20
9101: STP 8000TL-10
9102: STP 9000TL-20
9103: STP 8000TL-20
9131: STP 20000TL-10
9139: STP 20000TLHE-10
9140: STP 15000TLHE-10
9181: STP 20000TLEE-10
9182: STP 15000TLEE-10
9281: STP 10000TL-20
9282: STP 11000TL-20
9283: STP 12000TL-20
```
46 changes: 46 additions & 0 deletions solar_inverters/sma_cluster_controller/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# SMA Sunny Tripower

This [Enapter Device Blueprint](https://go.enapter.com/marketplace-readme) integrates the **SMA Sunny Tripower** solar inverter via [Modbus TCP API](https://go.enapter.com/developers-modbustcp) implemented on the [Enapter Virtual UCM](https://go.enapter.com/handbook-vucm).

## Supported SMA Sunny Tripower Models

```txt
STP 15000TL-30 (STP 15000TL-30)
STP 17000TL-30 (STP 17000TL-30)
STP 20000TL-30 (STP 20000TL-30)
STP 25000TL-30 (STP 25000TL-30)
STP3.0-3AV-40 (STP3.0-3AV-40)
STP33-US-41 (STP33-US-41)
STP50-40 (STP50-40)
STP50-41 (STP50-41)
STP50-JP-40 (STP50-JP-40)
STP50-JP-41 (STP50-JP-41)
STP50-US-40 (STP50-US-40)
STP50-US-41 (STP50-US-41)
STP62-US-41 (STP62-US-41)
Sunny Tripower 10.0 (STP10.0-3AV-40)
Sunny Tripower 10.0 SE (STP10.0-3SE-40)
Sunny Tripower 4.0 (STP4.0-3AV-40)
Sunny Tripower 5.0 (STP5.0-3AV-40)
Sunny Tripower 5.0 SE (STP5.0-3SE-40)
Sunny Tripower 6.0 (STP6.0-3AV-40)
Sunny Tripower 6.0 SE (STP6.0-3SE-40)
Sunny Tripower 8.0 (STP8.0-3AV-40)
Sunny Tripower 8.0 SE (STP8.0-3SE-40)
```

## Connect to Enapter

- Sign up to Enapter Cloud using [Web](https://cloud.enapter.com/) or mobile app ([iOS](https://apps.apple.com/app/id1388329910), [Android](https://play.google.com/store/apps/details?id=com.enapter&hl=en)).
- Install [Enapter Gateway](https://go.enapter.com/handbook-gateway-setup) to run Virtual UCM.
- Create [Enapter Virtual UCM](https://go.enapter.com/handbook-vucm).
- [Upload](https://go.enapter.com/developers-upload-blueprint) this blueprint to Enapter Virtual UCM.
- Use `Configure` command in Enapter mobile app or Web to set up SMA Sunny Tripower communication parameters:
- _Modbus IP address_, use either static IP or DHCP reservation. Check your network router manual for configuration instructions.
- _Modbus Unit ID_, can be found in SMA Web interface, default value is `3`.

## References

- [SMA Sunny Tripower manuals](https://my.sma-service.com/s/article/Sunny-Tripower-Manuals?language=en_US)
- [SMA Sunny Tripower Modbus parameters and measured values](https://www.sma.de/en/products/product-features-interfaces/modbus-protocol-interface)
- [SMA Modbus interface](https://files.sma.de/downloads/EDMx-Modbus-TI-en-16.pdf)
116 changes: 116 additions & 0 deletions solar_inverters/sma_cluster_controller/firmware.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
local smamodbus = require('enapter.sma.modbustcp')
local config = require('enapter.ucm.config')

ADDRESS_CONFIG = 'address'
UNIT_ID_CONFIG = 'unit_id'

-- global SMA Modbus TCP connection, initialized below
sma = nil

function main()
scheduler.add(30000, send_properties)
scheduler.add(1000, send_realtime_telemetry)

config.init({
[ADDRESS_CONFIG] = { type = 'string', required = true },
[UNIT_ID_CONFIG] = { type = 'number', required = true }
})
end

function send_properties()
local properties = {}

local sma, _ = connect_sma()
if sma then
properties.serial_num = sma:read_u32_fix0(30057)
properties.fw_ver = parse_firmware_version(sma:read_u32_fix0(30059))
end

local values, err = config.read_all()
if err then
enapter.log('cannot read config: '..tostring(err), 'error')
else
for name, val in pairs(values) do
properties[name] = val
end
end

enapter.send_properties(properties)
end

function send_realtime_telemetry()
local sma, err = connect_sma()
if not sma then
if err == 'cannot_read_config' then
enapter.send_telemetry({ status = 'error', alerts = {'cannot_read_config'} })
elseif err == 'not_configured' then
enapter.send_telemetry({ status = 'warning', alerts = {'not_configured'} })
end
return
end

local telemetry = {
active_power = sma:read_s32_fix0(30775),
reactive_power = sma:read_s32_fix0(30805),
total_energy = sma:read_u64_fix0(30513),
energy_fed_today = sma:read_u64_fix0(30517),
ambient_temp = sma:read_s32_fix1(34609),
pv_temp = sma:read_s32_fix1(34621),
external_total_irradiation = sma:read_u32_fix0(34623),
status = 'ok',
alerts = {},
}

if telemetry.total_energy then
telemetry.total_energy = telemetry.total_energy / 1000
end
if telemetry.energy_fed_today then
telemetry.energy_fed_today = telemetry.energy_fed_today / 1000
end

enapter.send_telemetry(telemetry)
end

function connect_sma()
if sma then
return sma, nil end

local values, err = config.read_all()
if err then
enapter.log('cannot read config: '..tostring(err), 'error')
return nil, 'cannot_read_config'
else
local address, unit_id = values[ADDRESS_CONFIG], values[UNIT_ID_CONFIG]
if not address or not unit_id then
return nil, 'not_configured'
else
-- Declare global variable to reuse connection between function calls
sma = smamodbus.new(address, tonumber(unit_id))
sma:connect()
return sma, nil
end
end
end

function parse_firmware_version(value)
if not value then return end

local release_id = value & 0xFF
local build = (value >> 8) & 0xFF
local minor = (value >> 16) & 0xFF
local major = (value >> 24) & 0xFF

local release
if release_id == 0 then release = 'N'
elseif release_id == 1 then release = 'E'
elseif release_id == 2 then release = 'A'
elseif release_id == 3 then release = 'B'
elseif release_id == 4 then release = 'R'
elseif release_id == 4 then release = 'S'
else release = tostring(release_id)
end

return major..'.'..minor..'.'..build..'.'..release
end

main()
Loading