第二十八夜 · 注册
上一夜认了四家人。注册的一家,参与的一家,事件的一家,报告的一家。这一夜拆第一家 EiRegisterParty,注册族。
为什么先拆它?因为后面三家的门,全都拴在它的锁上。没有注册,事件送不到你手上,报告交不出去,参与更是无从谈起。四家人的名册,第一页写的都是同一件事,你注册过了吗。
先从入职说起。
我入职那天,办的手续其实很简单。证件交给前台,在一张表上签了名,前台回我一张工牌。从那一刻起,这栋楼认我了。电梯愿意为我停,门禁愿意为我开,不是因为我的脸熟,是因为系统里有我的记录。
工牌上印着两个号。一个是我的名字编号,全公司就这一个;另一个是这次入职的批次号,记录我哪一天、经哪一道手续进的门。当时我没在意。多年之后我在 OpenADR 协议里写注册代码,才发现协议发给设备的也是同样两个号,一个叫 venID,一个叫 registrationID。
venID 就是设备在名册上的正式名字。registrationID 记录的是这一次注册本身的批次。往后所有的对话,这两个号成对出示,缺一个,对方就不认。
oadrCreatePartyRegistration
一台新设备想加入电网的需求响应,面对的第一步就是发一条 oadrCreatePartyRegistration 给 VTN。这条报文一共由 8 个标签组成。
| 标签名 | 说明 |
|---|---|
| requestID | 这一单注册的凭证编号 |
| venID | 设备标识。按正路这里应当空着,由 VTN 分配。但上游样本里预填了 VEN_1234,导致重复提交时直接撞墙 |
| oadrProfileName | 声明协议代次,2.0b。协议有 2.0a 和 2.0b 两代,A 档给恒温器,B 档给聚合商 |
| oadrTransportName | 传送方式,simpleHttp。候选还有 XMPP,当年有一版把聊天软件当成传话的渠道 |
| oadrReportOnly | 是否只交报告不接号令 |
| oadrXmlSignature | 信封是否要 XML 签名 |
| oadrHttpPullModel | 自己来取还是等推送 |
| oadrVenName | 设备在名册上的显示名称 |
这 8 个标签全部来自上游开源工程自带的样本报文。样本第一行写着 schemaVersion 声明 2.0a,而外层命名空间是 2.0b。版本声明和命名空间不同代,这是上游自己都没对齐的真实异常。
checkNoActiveRegistration
注册这扇门对同一个 venID 只开一次。第二次提交同一个 venID,直接判负。
这不是 OpenADR 规范的规定,是认证测试套件的机关。认证套件第一案 N1_0010_TH_VEN 的真身 N0_5010_TH_VEN 里,只有两个动作。第一步调 checkNoActiveRegistration(),查名册上有没有在册记录。查到了,判负。第二步发 oadrQueryRegistration,断言返回的 registrationID 和 venID 都必须为空。
清场用 N1_0020(只清注册库),或者手动跑 util 目录下的 TH_ClearRegistrationDatabases.java。
注册报文穿过七关
考官收到注册报文之后,不是收下就完。报文要过七关。七关不是考官拍脑袋列的清单,是需求响应这门生意能跑通的七个前提条件。抽掉任何一关,链上就断一节,断的那一节的代价不是报错重试,是真金白银的精算差错。
第一关,清场关。checkNoActiveRegistration() 查名册上有没有在册记录。同一台设备注册两次,VTN 就会向同一台物理设备派两份号令。电池只响应一次,但两次都被记为履约,精算时凭空多一份额度,结算金额失真。一个号一条命,防的是一具身体两条命。
第二关,能力声明关。注册报文里必须找到 METADATA_TELEMETRY_USAGE 且含 powerReal。找不到,考官抛 FailedException,原话是 Expected to find METADATA_TELEMETRY_USAGE with powerReal in OadrRegisterReport。这一关考的不是格式,是能力承诺。我在注册时递交的这份含 powerReal 的声明,是一份承诺书,承诺本设备能在电网喊话时实时上报功率。没有这份承诺,VTN 派了号令之后拿不到实时功率数据,无法验证设备是否响应。需求响应从可审计的制度退化成荣誉制度。
第三关,字段关。baseItemCheck 对 rID、itemUnits、siScaleCode、hertz、voltage、AC 逐项对表。rID 对不上,VTN 后续要实时数据时找不到这份报告。itemUnits 不是 W,数字含义全变。siScaleCode 缺失,1.0 到底是 1 瓦还是 1 千瓦没人知道。这些字段是后续所有数据交互的解码字典,字典错了后面全错。这就是为什么注册报文里要带相应的报告描述。报告描述就是解码字典,没有字典,后面收到的数据全是没有意义的数字。
第四关,唯一性关。同一台设备的 ReportSpecifierID 加 rID 的组合不允许重复。两份相同的话让听的人精神分裂。VTN 收到数据时分不清哪份是哪份,核对时两份数据互相冲突,精算差错。
第五关,区间关。功率值必须落在物理合理的范围内。标称 2 兆瓦的电池报出 500 兆瓦的功率,要么单位搞错了,要么设备疯了。考官不知道哪种是真的,但两种都不该通过,因为结算系统会按 500 兆瓦算钱。
第六关,时长关。报告间隔必须在合理范围内。报得太密,VTN 被数据淹没;报得太疏,需求响应事件期间 VTN 看不到设备在做什么,评估数据出现盲区。
第七关,查库关。六关过后,考官翻自己的数据库 vtndb.xml,确认注册流程完整闭环。XMLDBUtil 用 JAXB 加同步锁管着两套 XML 记录,vtndb.xml 记考官侧、vendb.xml 记被测侧。新安装时都是零字节的空文件,运行期边收边记。找不到就抛 FailedException,原话是 Refer to the db/vtndb.xml file for reports currently registered with the test harness。
七关全过,注册才算闭环。
假数据的工艺
报文过了七关,下一站是数据。认证考试跑的全是假数据,这一节讲假数据是怎么造出来的。
假电厂 SmartEnergyModuleSimulator 的全部物理,是五个写死的数字和一张查表。四级电压阶梯写死常量,level 0 到 3 分别对应 1000、1100、1200、1300 伏。LOAD_DISPATCH 号令来了直接把信号值当电压用,直通无模型。能量对时间做积分,把伏特当瓦特乘上时间除以 360 万,量纲是虚构的。
除了假电厂仿真的功率和能量,还有一种值会出现在报文里。凡假电厂没仿真的报告类型,一律填 -1.0。这个数是写死的哨兵,意思是本座没有此物。千问把它读成放电,我把它读成谜,档案把它读成负一的浮点数。而事件信号里的 SIMPLE value 1.0,那个是真的 1.0,它是考官配置里 Simple_High 的值,意思是到一档。
考官配置表里还写着 PowerReal_Range 等于 10 到 20。假数据照着这个区间落位,猫和鼠都是写死的常量,考期半年,从未有一只真猫出场。
第一次真电
你可能会问,这些假数据什么时候换成真数据。
答案是,在我离开那个项目的时候,从来没有过。接口是空的,里面没有任何业务。VTN 侧的报文格式和我们的实现对不上。上游写的也不规范。真电从未通过那条线。
这不是遗憾,这是那个项目本来的模样。空壳。协议壳完整,注册、事件、报告、Opt,全链能跑。数据全是假造的,写死的常量对着写死的区间。后来接手的人拿到了 API 文档,用 AI 补了对接,真电才第一次走了那条线。
我走的时候,真电还没有第一次。
会话不是终身
注册发的不是永久通行证。往后每一句对话都是一次全新的 HTTP 请求,用完即断。会话靠三样条件缝住。venID 加 registrationID 证明身份,VTN 侧队列暂存错过的消息,定时轮询确认线路还通着。
凭证本身有有效期,按证书寿命算。到期后走 oadrRequestReregistration 重开一次注册。
附 注册流程代码
java
import java.net.http.*;
import java.net.URI;
import java.util.UUID;
/**
* VEN 注册流程。
*
* 一台设备第一次面对 VTN 服务器时,要走的第一道手续。
* 注册做三件事:亮明身份、声明能力、领回两个号。
* 两个号分别是 venID(你从此叫什么)
* 和 registrationID(这次报到的凭据)。
*/
public class VenRegistration {
private final String vtnEndpoint;
private final HttpClient client;
public VenRegistration(String vtnEndpoint, HttpClient client) {
this.vtnEndpoint = vtnEndpoint;
this.client = client;
}
/**
* 拼装注册报文。
* 八个标签一个一个填值,缺一不可。
* venID 首次报到时留空,由 VTN 分配。
*/
public String buildRequest(String venName) {
String requestID = UUID.randomUUID().toString();
return """
<oadr:oadrCreatePartyRegistration
ei:schemaVersion="2.0b"
xmlns:ei="http://docs.oasis-open.org/ns/energyinterop/201110"
xmlns:oadr="http://openadr.org/oadr-2.0b/2012/07">
<pyld:requestID>%s</pyld:requestID>
<ei:venID></ei:venID>
<oadr:oadrProfileName>2.0b</oadr:oadrProfileName>
<oadr:oadrTransportName>simpleHttp</oadr:oadrTransportName>
<oadr:oadrReportOnly>false</oadr:oadrReportOnly>
<oadr:oadrXmlSignature>false</oadr:oadrXmlSignature>
<oadr:oadrHttpPullModel>true</oadr:oadrHttpPullModel>
<oadr:oadrVenName>%s</oadr:oadrVenName>
</oadr:oadrCreatePartyRegistration>
""".formatted(requestID, venName);
}
/**
* 发送注册请求。
* 走 HTTPS,证书链在构造 HttpClient 时已装好。
* VTN 若收了,回执里带 venID 和 registrationID。
* 若两个号都为空,说明 VTN 认为此设备已在册。
*/
public String[] send(String requestXml) throws Exception {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(vtnEndpoint))
.header("Content-Type", "application/xml")
.POST(HttpRequest.BodyPublishers.ofString(requestXml))
.build();
HttpResponse<String> response = client.send(request,
HttpResponse.BodyHandlers.ofString());
String body = response.body();
String venId = extractTag(body, "venID");
String regId = extractTag(body, "registrationID");
return new String[]{venId, regId};
}
/** 从回执 XML 中取出指定标签的文本内容。 */
private String extractTag(String xml, String tag) {
var start = xml.indexOf("<" + tag + ">");
var end = xml.indexOf("</" + tag + ">");
if (start < 0 || end < 0 || end <= start) return "";
return xml.substring(start + tag.length() + 2, end);
}
}
上面这段代码只有三个动作。拼报文,发出去,拆回执。拆开看只有三步,每一步都是协议规范里写死了的规定动作,不多了什么,也不少了什么。
结论
注册报文的门只开一次,一个 venID 一条命;证书到期重走仪式,身份连忠诚都有有效期。
今夜习题
今夜习题Ⅰ oadrCreatePartyRegistration 里 venID 标签按正路应当空着。上游样本为什么预填了 VEN_1234?这个预填会导致什么后果?
今夜习题Ⅱ venID 和 registrationID 各管什么?服务器重启清空名册后,哪一枚要重新申请,哪一枚可以留作备份凭证?
今夜习题Ⅲ 注册凭证有效期半年,到期自动作废。为什么身份的有效期不能无限长?
参考资料
- OpenADR 2.0b Profile Specification(Rev 1.1,2015)EiRegisterParty 服务定义
- 上游开源工程自带注册样本报文(schemaVersion 声明 2.0a 为该样本真实内容)
- 认证测试套件 N1 注册族 11 案(认证测试服务商实装)
本书目录导航 上一篇:算法一千零一夜 · 第二十七夜 需求与响应协议,电网里的应答哲学 下一篇(载锅中) 起点:卷首语·算法童话
版权声明:本文为作者原创,受著作权法保护。未经授权禁止转载、搬运、摘编、改编及任何形式的二次创作;个人学习引用请注明作者与原文出处。转载授权请联系作者(CSDN 私信)。侵权必究。