支持设备
  • 所有能控云产品(CT电表、红外读表器、P1电表、智能插座、Linky读表器等)
  • 所有使用能控云WIFI 模块产品(储能机、逆变器、电池等)
安全鉴权与加密流程
所有开放接口调用均需通过严格的签名验证,确保通信安全与数据完整性。
1. 获取接入凭证
联系 AECC 官方获取专属接入凭证:
  • companyCode: 企业唯一识别码(用于请求头身份标识)
  • key: 接口签名密钥(仅用于本地签名生成,严禁在网络中明文传输)
凭证安全管理建议:
  • 密钥应存储在服务器端配置文件或环境变量中,禁止硬编码在客户端代码
  • 定期(建议每季度)审查密钥使用情况,发现异常立即联系官方重置
  • 不同环境(测试/生产)应使用不同的凭证,避免相互影响
  • 限制密钥访问权限,仅核心开发人员知晓,离职时需更换密钥
2. 构造待签名字符串
按以下固定规则拼接签名原文,顺序错误将导致验签失败:
1 业务参数排序
将所有业务请求参数(不包含 timesign)的参数名,按 Unicode 编码升序 排列,拼接为 key=value&key=value 格式。
2 追加时间与密钥
在排序后的字符串末尾,固定追加 time={UTC+0秒级时间戳}&key={分配的密钥}
完整示例
aiMode=0&batRatedCapacity=1&batRatedChargingPower=1000&customTimes=00:00,12:00,1000&13:00,15:00,-2000&dataTime=2025-06-26&energyMode=2&priceCompany=Germany&time=1732756652&key=2a1891544dbcf8e8b45b36d03187485a
详细构造步骤
Step 2.1 提取业务参数: 从请求体中提取所有业务参数,排除 timesign 字段。
{
  "energyMode": "2",
  "aiMode": "0",
  "customTimes": "00:00,12:00,1000&13:00,15:00,-2000",
  "batRatedCapacity": "1",
  "batRatedChargingPower": "1000",
  "dataTime": "2025-06-26",
  "priceCompany": "Germany"
}
Step 2.2 参数名排序: 按 Unicode 编码升序排列参数名。
aiMode, batRatedCapacity, batRatedChargingPower, customTimes, dataTime, energyMode, priceCompany
Step 2.3 拼接参数字符串: 按排序结果拼接为 key=value 格式,用 & 连接。
aiMode=0&batRatedCapacity=1&batRatedChargingPower=1000&customTimes=00:00,12:00,1000&13:00,15:00,-2000&dataTime=2025-06-26&energyMode=2&priceCompany=Germany
Step 2.4 追加时间戳与密钥: 在字符串末尾追加 timekey
aiMode=0&batRatedCapacity=1&batRatedChargingPower=1000&customTimes=00:00,12:00,1000&13:00,15:00,-2000&dataTime=2025-06-26&energyMode=2&priceCompany=Germany&time=1732756652&key=2a1891544dbcf8e8b45b36d03187485a
⚠️ 注意事项:
  • 参数值为空字符串时,仍需参与签名(如 status=&time=...&key=...
  • 参数值包含特殊字符无需 URL 编码,直接原值拼接
  • 时间戳必须使用 UTC+0 时区的秒级时间戳,与服务端保持一致
  • 密钥值必须使用官方分配的原始密钥,不得进行任何转换
3. 生成签名值
使用标准 MD5 算法对拼接完成的待签名字符串进行哈希运算,输出结果必须转换为 全小写 字符串,作为 sign 参数值。
多语言签名示例
Java 示例
import java.security.MessageDigest;
import java.util.TreeMap;

public class SignGenerator {
    public static String generateSign(TreeMap params, String key) {
        try {
            StringBuilder sb = new StringBuilder();
            // Concatenate business parameters
            for (String paramKey : params.keySet()) {
                if (!paramKey.equals("sign")) {
                    sb.append(paramKey).append("=").append(params.get(paramKey)).append("&");
                }
            }
            // Append time and key
            sb.append("time=").append(params.get("time")).append("&key=").append(key);
            
            // MD5 encrypt and convert to lowercase
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] digest = md.digest(sb.toString().getBytes("UTF-8"));
            StringBuilder hexString = new StringBuilder();
            for (byte b : digest) {
                String hex = Integer.toHexString(0xff & b);
                if (hex.length() == 1) hexString.append('0');
                hexString.append(hex);
            }
            return hexString.toString().toLowerCase();
        } catch (Exception e) {
            throw new RuntimeException("Signature generation failed", e);
        }
    }
}
Python 示例
import hashlib
from urllib.parse import urlencode

def generate_sign(params, key):
    """
    Generate API signature
    :param params: Business parameter dict (contains time)
    :param key: Assigned key
    :return: MD5 signature string (lowercase)
    """
    # Exclude sign field, sort by key
    sorted_params = sorted([(k, v) for k, v in params.items() if k != 'sign'])
    # Concatenate parameters
    param_str = '&'.join([f"{k}={v}" for k, v in sorted_params])
    # Append time and key
    sign_str = f"{param_str}&time={params['time']}&key={key}"
    # MD5 encrypt and convert to lowercase
    return hashlib.md5(sign_str.encode('utf-8')).hexdigest().lower()

# Usage example
params = {
    'energyMode': '2',
    'aiMode': '0',
    'customTimes': '00:00,12:00,1000&13:00,15:00,-2000',
    'batRatedCapacity': '1',
    'batRatedChargingPower': '1000',
    'dataTime': '2025-06-26',
    'priceCompany': 'Germany',
    'time': '1732756652'
}
sign = generate_sign(params, '2a1891544dbcf8e8b45b36d03187485a')
print(f"Generated signature: {sign}")
JavaScript 示例
const crypto = require('crypto');

function generateSign(params, key) {
    // Get sorted parameter keys (exclude sign)
    const sortedKeys = Object.keys(params)
        .filter(k => k !== 'sign')
        .sort();
    
    // Concatenate parameter string
    const paramString = sortedKeys
        .map(k => `=`)
        .join('&');
    
    // Append time and key
    const signString = `&time=&key=`;
    
    // MD5 encrypt and convert to lowercase
    return crypto.createHash('md5')
        .update(signString, 'utf8')
        .digest('hex')
        .toLowerCase();
}

// Usage example
const params = {
    energyMode: '2',
    aiMode: '0',
    customTimes: '00:00,12:00,1000&13:00,15:00,-2000',
    batRatedCapacity: '1',
    batRatedChargingPower: '1000',
    dataTime: '2025-06-26',
    priceCompany: 'Germany',
    time: '1732756652'
};
const sign = generateSign(params, '2a1891544dbcf8e8b45b36d03187485a');
console.log('Generated signature:', sign);
C# 示例
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;

public class SignGenerator
{
    public static string GenerateSign(Dictionary parameters, string key)
    {
        // Exclude sign field, sort by key
        var sortedParams = parameters
            .Where(p => p.Key != "sign")
            .OrderBy(p => p.Key, StringComparer.Ordinal)
            .Select(p => $"{p.Key}={p.Value}");
        
        // Concatenate parameters
        string paramString = string.Join("&", sortedParams);
        
        // Append time and key
        string signString = $"{paramString}&time={parameters["time"]}&key={key}";
        
        // MD5 encrypt and convert to lowercase
        using (var md5 = MD5.Create())
        {
            byte[] hashBytes = md5.ComputeHash(Encoding.UTF8.GetBytes(signString));
            StringBuilder sb = new StringBuilder();
            foreach (byte b in hashBytes)
            {
                sb.Append(b.ToString("x2")); // Convert to lowercase hex
            }
            return sb.ToString();
        }
    }
}
4. 发起 API 请求
  • Header: 必须携带 companyCode: {企业识别码}Accept-Language: en-US
  • Body: 除业务参数外,必须包含 time(与签名生成时完全一致的 UTC+0 秒级时间戳)和 sign(MD5 签名值)。
完整请求示例(cURL)
curl --location 'https://api.aecc.com/openApi/price/setEnergyMode' \
--header 'companyCode: AECC2024001' \
--header 'Accept-Language: en-US' \
--header 'Content-Type: application/json' \
--data '{
    "energyMode": "2",
    "aiMode": "0",
    "customTimes": "00:00,12:00,1000&13:00,15:00,-2000",
    "batRatedCapacity": "1",
    "batRatedChargingPower": "1000",
    "dataTime": "2025-06-26",
    "priceCompany": "Germany",
    "time": "1732756652",
    "sign": "c3757db87150d5efbb45009d9253d375"
}'
完整请求示例(Java - HttpClient)
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;

public class ApiClient {
    private static final String API_URL = "https://api.aecc.com/openApi/price/setEnergyMode";
    private static final String COMPANY_CODE = "AECC2024001";
    private static final String KEY = "2a1891544dbcf8e8b45b36d03187485a";

    public static void main(String[] args) throws Exception {
        CloseableHttpClient httpClient = HttpClients.createDefault();
        HttpPost httpPost = new HttpPost(API_URL);
        
        // Set headers
        httpPost.setHeader("companyCode", COMPANY_CODE);
        httpPost.setHeader("Accept-Language", "en-US");
        httpPost.setHeader("Content-Type", "application/json");
        
        // Prepare business parameters
        long currentTime = System.currentTimeMillis() / 1000; // UTC+0 second timestamp
        TreeMap params = new TreeMap<>();
        params.put("energyMode", "2");
        params.put("aiMode", "0");
        params.put("customTimes", "00:00,12:00,1000&13:00,15:00,-2000");
        params.put("batRatedCapacity", "1");
        params.put("batRatedChargingPower", "1000");
        params.put("dataTime", "2025-06-26");
        params.put("priceCompany", "Germany");
        params.put("time", String.valueOf(currentTime));
        
        // Generate signature
        String sign = SignGenerator.generateSign(params, KEY);
        params.put("sign", sign);
        
        // Construct JSON request body
        JSONObject requestBody = new JSONObject();
        for (String key : params.keySet()) {
            requestBody.put(key, params.get(key));
        }
        httpPost.setEntity(new StringEntity(requestBody.toString(), "UTF-8"));
        
        // Send request
        String response = httpClient.execute(httpPost, response -> {
            int statusCode = response.getStatusLine().getStatusCode();
            String responseBody = EntityUtils.toString(response.getEntity());
            System.out.println("Status code: " + statusCode);
            return responseBody;
        });
        System.out.println("Response: " + response);
        httpClient.close();
    }
}
服务端验签流程
  1. 身份校验:根据请求头中的 companyCode 查询对应的企业密钥
  2. 时间戳校验:验证 time 参数是否在允许的时间范围内(默认±5 分钟),防止重放攻击
  3. 签名重算:按相同规则(参数排序→追加 time 和 key→MD5 加密)重新计算签名
  4. 签名对比:对比计算结果与请求中的 sign 值,完全一致方通过验证
  5. 业务处理:验证通过后执行业务逻辑并返回结果
⚠️ 安全须知:
  • 签名有效期与时间戳强绑定,服务端会校验时间戳有效性,请勿缓存签名复用
  • 密钥泄露需立即联系官方重置,避免造成安全隐患
  • 建议在生产环境中启用 HTTPS,确保传输层安全
核心接口调用示例
以下提供高频核心接口的完整调用示例,覆盖签名生成、请求构造与响应解析全流程。
示例一:设置能源调控模式
POST
/openApi/price/setEnergyMode
该接口用于配置储能设备的运行模式(智能/自定义/关闭),并获取下发至采集器的加密控制报文。
请求体
参数 位置 类型 必填 说明
companyCode header string 唯一码
Accept-Language header string en-US
time body string 秒级时间戳(UTC+0)
sign body string 签名信息
priceCompany body string 电价区域(德国="Germany")
batRatedCapacity body string 电池额定充满电量(kWh),0~100,充满需要消耗的能量
batRatedChargingPower body string 电池额定充电功率(W),根据电池充电功率和容量计算充电时间来选择最低波谷时间
dataTime body string 当天日期(yyyy-MM-dd)
energyMode body int 能源模式:0=无模式,采集器不会对设备进行调控;1=智能模式;2=自定义模式
aiMode body int AI调控使能:0=关闭,1=开启,选择智能模式可设置此值,不传默认关闭
customTimes body string 自定义时间段:用户自定义设置的充放电时间段,多个时间段使用&隔开,最多16个,格式如:00:00,12:00,1000&13:00,15:00,-2000
请求示例
{
  "energyMode": "2",
  "aiMode": "0",
  "customTimes": "00:00,12:00,1000&13:00,15:00,-2000",
  "batRatedCapacity": "1",
  "batRatedChargingPower": "1000",
  "dataTime": "2025-06-26",
  "priceCompany": "Germany",
  "time": "1732756652",
  "sign": "c3757db87150d5efbb45009d9253d375"
}
关键响应字段
  • packet: 十六进制控制报文,需按设备协议二次加密并计算 CRC16 校验后下发至采集器。
  • powerTimes: 时段策略数组,包含各时间段的充放电功率指令。
示例二:查询分时电价数据
POST
/openApi/price/getPriceChart
该接口用于获取指定区域、指定日期的分时电价信息,为智能调控策略提供数据支撑。
请求体
参数 位置 类型 必填 说明
companyCode header string 唯一码
Accept-Language header string en-US
dataTime body string 日期(yyyy-MM-dd)
priceCompany body string 电价区域(如:Germany, France)
mode body string 电价颗粒度:0=1小时,1=15分钟,默认0
time body string 时间戳(UTC+0)
sign body string 签名信息
请求示例
{
  "dataTime": "2024-09-07",
  "priceCompany": "Germany",
  "mode": "0",
  "time": "1725677116",
  "sign": "e07b26034722d166e7f059cb728ab3fd"
}
关键响应字段
  • priceArr: 24小时电价数组,单位为 EUR/MWh。
  • pricesDayList: 时段明细列表,包含每个时间段的起止时间、电价数值及峰谷平标识。
示例三:获取设备额定参数数据
POST
/openApi/device/getBasicsInfo
该接口用于获取当前设备额定参数数据。
请求体
参数 位置 类型 必填 说明
companyCode header string 唯一码
Accept-Language header string en-US
deviceSn body string 设备序列号
time body string 秒级时间戳(UTC+0)
sign body string 签名信息
请求示例
{
  "deviceSn": "NB2548300T110CHAB",
  "time": "1725450897",
  "sign": "e83ba9c021edd831ae69033f77528ac5"
}
关键响应字段
  • obj: 设备额定数据模型。
示例四:获取设备实时信息数据
POST
/openApi/device/getRealTimeInfo
该接口用于获取当前设备实时信息数据。
请求体
参数 位置 类型 必填 说明
companyCode header string 唯一码
Accept-Language header string en-US
deviceSn body string 设备序列号
time body string 秒级时间戳(UTC+0)
sign body string 签名信息
请求示例
{
  "deviceSn": "NB2548300T110CHAB",
  "time": "1725450897",
  "sign": "e83ba9c021edd831ae69033f77528ac5"
}
关键响应字段
  • obj: 设备实时数据模型。
常见问题(FAQ)
Q1: 签名计算总返回 sign 不匹配,如何排查?

服务端在收到请求后会按相同规则(业务参数按 Unicode 升序拼接 → 末尾追加 time 和 key → MD5 → 转小写)重算签名,并与请求体中的 sign 做精确比较。任一环节不一致都会判定为签名异常,统一返回 result=10001, msg="Signature exception"

排查步骤(按命中率从高到低):

  1. 确认 sign 为 32 位小写 hex:MD5 输出必须转成全小写。若使用了大写或 Base64 编码,比对必然失败。
  2. 确认参数拼接顺序与原文一致:业务参数名必须按 Unicode(字典序)升序排列,建议直接用 TreeMap(Java)/ sorted()(Python)/ .sort()(JS)自动排序。
  3. 确认 sign 和 time 不参与业务参数排序:排序拼接时只处理业务参数,time 固定追加在末尾、key 再追加在 time 之后。sign 字段本身不参与签名原文。
  4. 确认参数值原值拼接,不做 URL 编码、不去空格:参数值为空字符串时仍要参与签名。含 &、:、, 等特殊字符(如 customTimes 字段)直接原值拼接,不要 URL encode。
  5. 确认 key 用的是官方分配的原始密钥:服务端按请求头 companyCode 查库取 keySecret 拼到签名末尾。客户端必须使用同一份原始密钥,不得做任何编码/截断/转换。
  6. 确认 time 在签名原文和请求体中完全一致:time 既要拼进签名原文,也要原值放进请求体。两者必须一字不差。
  7. 本地复现服务端算法:把拼接好的待签名字符串打印出来,用任意在线 MD5 工具计算。若仍不一致,可把待签名原文 + 你的 sign 一并提供给官方协助定位。
Q2: 时间戳有效范围是多少?如果服务器时间与标准时间有偏差怎么办?
  • 时间戳格式:UTC+0 时区的秒级 Unix 时间戳(10位数字,如1732756652),不要使用毫秒级(13位)或带时区的本地时间。
  • 有效范围:服务端会取当前 UTC+0 秒级时间戳与请求中的 time 比较,允许偏差为 ±3600 秒(即 ±1 小时)。超出该窗口的请求会被判定为过期,返回 result=10001, msg="Signature exception"

时间偏差处理建议:

  1. 以服务端为准:每次发起请求前实时生成 time = 当前 UTC+0 秒级时间戳,不要缓存复用历史时间戳,因为签名与时间强绑定。
  2. 校准服务器时钟:若服务器时间与标准时间(NTP)偏差超过几十秒,建议启用 NTP 时钟同步(如 Linux 的 ntpd/chrony、Windows 的 w32time),避免长期运行后时钟漂移。
  3. 不要尝试放宽窗口:±1 小时是服务端固定策略,无法由客户端调整。偏差超过1小时只能校准时钟。
  4. 容器/云环境注意:Docker 容器、虚拟机重启后偶发时钟回退,建议在容器启动时同步一次时间。
Q3: key 是否可以放在请求头或 URL 中传递?

不可以。

  • key 的唯一用途:仅在客户端本地参与签名计算(拼接待签名原文末尾的 &key=xxx),其本身不作为请求参数、不放在 header、不放在 URL query。
  • 服务端获取方式:服务端根据请求头中的 companyCode 查询数据库,取出对应的 keySecret 来重算签名。也就是说 companyCode 是身份标识(会随请求传输),key 是对应的密钥(仅在两端各自保存,不在网络中明文传输)。
  • 正确做法:companyCode 放在请求头:--header 'companyCode: AECC2024001'。key 仅在本地代码中参与 MD5,不进入任何请求字段。

安全要求:密钥应存储在服务器端配置文件或环境变量中,禁止硬编码到客户端、禁止打印到日志、禁止随请求外发。一旦怀疑泄露,立即联系官方重置。

Q4: 接口超时或返回 5xx 错误时应该如何处理?

开放接口的 HTTP 层基本固定返回 200,业务结果通过响应体的 result 字段表达。真正的系统级异常会统一映射为 result=4000, msg="System error, please contact the administrator!"

  1. 客户端超时设置:建议为每次调用设置合理的连接和读取超时(如连接5s、读取10s),避免因网络抖动导致线程长时间阻塞。
  2. 针对 result=4000(系统错误)的重试:这类错误通常是服务端临时故障,可重试。建议指数退避重试:间隔1s → 2s → 4s,最多3次。重试时务必重新生成 time 和 sign,不要复用原请求。
  3. 针对网络超时(无响应)的处理:客户端抛出 SocketTimeoutException/连接被拒时,同样按指数退避重试。若连续多次超时,先排查本机网络与 DNS,再联系官方确认服务状态。
  4. 不要重试的情况:result=10001(签名异常)重试不会改变结果,需先修正签名/时间戳。result=20001(参数为空)/20004(参数格式错误)等业务校验错误:修正参数后再发。result=10006(账号被禁用)/20002(权限异常):联系官方处理。
  5. 限流:部分接口配置了接口请求限流。触发限流时会通过全局异常返回错误信息,客户端应降低调用频率,不要立即重试。
Q5: 响应中的 result 和 msg 字段含义在哪里查看?

说明:响应体中的状态字段实际命名为 result(而非 code),下文为便于理解统一按"状态码"描述。

响应结构:

{
  "result": 0,
  "msg": "Request successfully.",
  "data": { /* business data */ }
}
  • result: 状态码,0 表示成功,非 0 表示失败。
  • msg: 状态描述文本,支持国际化(i18n),语言由请求头 Accept-Language 决定(如 en-US、zh-CN),未传时默认 en-US。
  • data/obj: 业务数据载体,成功时返回具体业务对象。
常见状态码对照表
result 含义 典型 msg(en-US) 触发场景
0 成功 Request successfully. 业务正常处理完成
1 通用失败 (varies) 业务逻辑校验未通过,msg 会具体说明
10000 未登录/Token失效 Please login. / token expired Token 模式下 claims 无效或过期
10001 签名异常 Signature exception 签名错误、时间戳过期、companyCode/time/sign 缺失(合并码)
10002 邮箱格式错误 Incorrect email format. 注册/绑定邮箱格式不合法
10006 用户被禁用 The user has been disabled. 账号被锁定
20000 参数类型有误 Incorrect parameter type. 请求参数类型不匹配
20001 数据不能为空 Submitted data cannot be empty. 必填参数缺失
20002 权限异常 Abnormal permissions. openApi 鉴权链路专用:companyCode 在系统中不存在、对应凭证未启用(flagState!=1)、或无操作权限
20003 时间格式错误 Time format error. 日期/时间参数格式不合法
20004 参数格式错误 Parameter format error. 参数格式校验未通过
20005 无此参数 No this parameter. 缺少必要的业务参数
20006 系统错误 System error, try later. 服务端业务异常
20007 DTC不存在 DeviceCode does not exist 设备类型编码无效
20008 设备离线 Device off-line 设备未上报数据
20009 设备不存在 Device not exist deviceSn 在系统中查不到
4000 系统全局错误 System error, please contact the administrator! 未捕获异常、事务回滚、参数解析异常等(建议重试或联系官方)
排查建议:
  • 调用时记录完整的 result、msg 以及请求参数,便于定位。
  • 若 msg 为英文且希望查看中文,可将 Accept-Language 改为 zh-CN。
👉 联系我们获得完整文档或支持 👈
联系我们