算法一千零一夜 · 第二十八夜 注册

第二十八夜 · 注册

上一夜认了四家人。注册的一家,参与的一家,事件的一家,报告的一家。这一夜拆第一家 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 各管什么?服务器重启清空名册后,哪一枚要重新申请,哪一枚可以留作备份凭证?

今夜习题Ⅲ 注册凭证有效期半年,到期自动作废。为什么身份的有效期不能无限长?

参考资料

  1. OpenADR 2.0b Profile Specification(Rev 1.1,2015)EiRegisterParty 服务定义
  2. 上游开源工程自带注册样本报文(schemaVersion 声明 2.0a 为该样本真实内容)
  3. 认证测试套件 N1 注册族 11 案(认证测试服务商实装)

本书目录导航 上一篇:算法一千零一夜 · 第二十七夜 需求与响应协议,电网里的应答哲学 下一篇(载锅中) 起点:卷首语·算法童话


版权声明:本文为作者原创,受著作权法保护。未经授权禁止转载、搬运、摘编、改编及任何形式的二次创作;个人学习引用请注明作者与原文出处。转载授权请联系作者(CSDN 私信)。侵权必究。

相关推荐
Java码农也是农14 分钟前
Spring 事务详解:@Transactional 如何生效、何时回滚、传播行为怎么选
java·spring boot·spring·事务
学长毕设1 小时前
基于SpringBoot的游乐园管理系统-附源码
java·spring boot·后端
SimonKing1 小时前
震惊:怎么回事,opencode一轮对话耗尽免费额度
java·后端·程序员
装不满的克莱因瓶1 小时前
【SpringAI Alibaba】ReactAgent响应SSE实时流式传输案例实战
java·人工智能·ai·sse·springai
杨凯凡2 小时前
Spring Boot 文件上传怎么做才安全?流式落盘、路径校验与数据库一致性实战
java·spring boot·文件上传·后端安全·数据库一致性
这个昵称不容易2 小时前
旅游管理系统的设计与实现(SpringBoot + MySQL + Vue 前后端分离)
java·springboot·毕设
Rain的Java大神之路2 小时前
LeetCode 37 解数独面试攻略:回溯 + 位掩码 + MRV,从暴力到毫秒级求解
java·后端·leetcode
NEFU_Jyj2 小时前
贝尔数(组合数学)
算法
希和20273 小时前
C语言程序设计题|有序数组插入数字冒泡排序
c语言·数据结构·算法