第二十六夜 协议的母语
上一夜收尾的时候,山走出来了,苏轼替我们把话说完,不识庐山真面目,只缘身在此山中。但有一件事我压着没讲。那一夜的报文,名字全是 .xml 结尾。createOptIn.xml,createOptOut.xml,oadrDistributeEvent。整夜我没有解释一个字,为什么这门行业传一句话,要用尖括号。不是忘了,是那个问题的答案不在手册里,在日期里。今夜就做一件事,给这份协议立一份年谱,一年一行。立完再看,答案自己会浮上来。

2000 至 2002,先有危机,后有协议
年谱第一行,2000 年,2001 年,加州。电价一度涨到平时的几十倍,轮番停电,硅谷的机房跟着遭殃。危机过去,加州人问了一个问题,下一次,能不能不靠打电话喊企业关灯,让机器自己收到信号,自己减负荷。
2002 年,劳伦斯伯克利国家实验室的需求响应研究中心接了这个题,加州能源委员会的 PIER 计划和能源部出钱。第25夜的出生证明讲的就是这一行,今天补上半句,孩子还没出生,先有一场难产。先有危机,后有协议。
2009,出生那年的母语
2009 年,OpenADR 1.0 出生,报告编号 CEC-500-2009-063,牵头人 Piette。出生证上有一个问题,值得停下来多看一眼。为什么报文用的是 XML?
那一年,一个系统跟另一个系统说正经话,说的是 SOAP,装信封用 XML,合同是 XSD,各自带公证。那时候网页世界已经有人念 JSON 的经,可电网这个行当的客厅里,坐着的全是 XML 的老先生,电表厂商,聚合商,自动化集成商,谁的简历上写的都是 XSD。所以答案朴素得近乎扫兴,不是把各种语言摆在一起比较了一圈,挑了最好的那个,是出生那年,屋里人人说的就是 XML。孩子开口,说的自然是母语。
这一夜的第一句结论在这里落地。一份协议开口说什么语言,不看好坏,看它出生那年的通用语。
2010 至 2015,联盟、改姓与8年冻结
2010 年 10 月,联盟成立。四个创始成员,伯克利实验室,太平洋煤气电力,南加州爱迪生,霍尼韦尔。两家加州电网,一家实验室,一家设备商。加州出题,厂商答题的格局,从第一天就定了。
2011 年到 2012 年,发生了对协议命运影响最大的一件事,外表却安静得像什么都没发生。1.0 被捐给了 OASIS,一个标准组织,成为Energy Interoperation 标准。捐出去的孩子改了姓,数据模型从此姓 OASIS。你在 2.0b 的报文里看到的 xcal、emix 这些前缀,就是这段姻缘留下的历史证据,它们全来自 OASIS 家族。第25夜拍卖会上那个管报数刻度的 granularity,身份证的注册地也在 OASIS。
2013 年 ,2.0a 和 2.0b 先后发布。A 档给恒温器,B 档给聚合商,同一家两个户型。2015 年 11 月 17 日,2.0b 的 Rev 1.1 定稿,就是你在第25夜见过的那册 110 页。定稿两个字值得单独说一下,从那天起,8年,正文一个字没改。
2018 与 2021,户口颁发与持证上岗
2018 年,2.0b 升格成国际标准 IEC 62746-10-1。乡下的好孩子进了城,拿到了国际户口。
2021 年 8 月,日本側 IF 設計立字,两册 Excel,低速高速各一版,其中一册后来放在了我桌上。个人史和协议史,第一次相撞。
年谱翻完,翻到我自己的那一行。2021 年 9 月,我入职,第一天拿到三个文件,外加一个上游仓库地址。第25夜讲过残本,这一夜讲另一桩。一星期,我才让工程 build 通过。考古老实说,败因不在我的手,在年份。2015 年冻结的条约,把 2009 年选的那门语言铸成了一座 JAXB 的炉子,炉体焊死在 sun.misc.Unsafe.defineClass 这个 JDK 内部接口上。JDK 11 把这个接口移除了,到了 JDK21 上,连炉门都打不开。我在 2021 年打的,是 2015 年条约的殿后之战,败军的旗,2009 年就立起来了。
还有更细的一条,报文日志里躺着一条动态偏移,每条报文挂着十几个命名空间,前缀逐条重排,这一条叫 ns5,下一条叫 ns7。按前缀认字的解析器必死,按地址认字的,换个马甲照样上岗。
2023,搬家
2023年,3.0 发布,弃 XML,改 JSON,通路改 REST,合同从 XSD 换成 OpenAPI 加 JSON Schema,全部正典合成一份 openadr3.yaml,认证的门户也推倒重建。行业等这次搬家等了八年。不是 2.0b 不好,是 2023 年的通用语已经是 JSON,客厅里坐着的换了一代穿卫衣的年轻人。
搬家的门道要拆开看,拆开是三层。最里层,数据文体,字段和含义,格舱里装的货,一点没动。中间层,合同语汇,XSD 换成 OpenAPI 和 JSON Schema,同一份规则换了一支笔写。最外层,装订格式,尖括号换成花括号。顺带洗清一件容易冤枉的事,openadr3.yaml 的 YAML 不是新语言,只是写合同用的一支笔,写出来的还是那份合同。
XSD 过时下岗,合同还在站岗。
殿后之战
年谱翻完,翻到我自己的那一行。2021 年 9 月,boot3 立项,19 笔提交,一路打到 第二年 3 月的认证窗口。我在 2021 年打的,是 2015 年条约的殿后之战,败军的旗,2009 年就立起来了。
所以今夜的代码只讲一个道理。认字认URI,不认马甲。同一份报名单,旧格式读出来,新格式写回去。
java
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
/**
* 第26夜 可跑模型:OpenAdrMover,同一份报名单,两种格式。
*
* 旧格式:boot3 仓库 DummyVEN20b 的真实样本 createOptIn.xml(2.0b,逐字照录,
* 连 XMLSpy v2010 的生成注释都原样保留,它本身就是年代文物)。
* 新格式:按 3.0 的 JSON 口味重写同一份报名单(字段语义照搬,JSON 形态为
* 参考 3.0 风格的示意重绘,正典 openadr3.yaml 在联盟会员墙内)。
*
* 讲的道理只有一条:认字认地址(命名空间 URI),不认马甲(前缀)。
* 2021年打的是 2015 年条约的殿后之战,前缀逐条重排(动态偏移),
* 按前缀认字的解析器必死;按地址认字的,换个前缀照样上岗。
*
* 编译运行(裸 JDK 21,零第三方依赖):
* javac OpenAdrMover.java
* java -Dstdout.encoding=UTF-8 OpenAdrMover
*/
public class OpenAdrMover {
/** 旧格式原文(2.0b 真实样本,逐字照录) */
static final String OLD_TONGUE = """
<?xml version="1.0" encoding="UTF-8"?>
<!--Sample XML file generated by XMLSpy v2010 rel. 3 (x64) (http://www.altova.com) -->
<oadr:oadrCreateOpt ei:schemaVersion="2.0b"
xsi:schemaLocation="http://openadr.org/oadr-2.0b/2012/07 oadr_20b.xsd"
xmlns:emix="http://docs.oasis-open.org/ns/emix/2011/06" xmlns:power="http://docs.oasis-open.org/ns/emix/2011/06/power"
xmlns:ei="http://docs.oasis-open.org/ns/energyinterop/201110"
xmlns:pyld="http://docs.oasis-open.org/ns/energyinterop/201110/payloads"
xmlns:oadr="http://openadr.org/oadr-2.0b/2012/07" xmlns:gml="http://www.opengis.net/gml/3.2"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xcal="urn:ietf:params:xml:ns:icalendar-2.0">
<ei:optID>Opt_1234</ei:optID>
<ei:optType>optIn</ei:optType>
<ei:optReason>participating</ei:optReason>
<emix:marketContext>http://MarketContext1</emix:marketContext>
<ei:venID>Demo-RA-VEN0001</ei:venID>
<xcal:vavailability>
<xcal:components>
<xcal:available>
<xcal:properties>
<xcal:dtstart>
<xcal:date-time>2025-09-26T06:51:27.346Z</xcal:date-time>
</xcal:dtstart>
<xcal:duration>
<xcal:duration>PT24H</xcal:duration>
</xcal:duration>
</xcal:properties>
</xcal:available>
</xcal:components>
</xcal:vavailability>
<ei:createdDateTime>2025-09-26T06:51:27.346Z</ei:createdDateTime>
<pyld:requestID>0</pyld:requestID>
<ei:eiTarget>
<ei:venID>Demo-RA-VEN0001</ei:venID>
</ei:eiTarget>
<oadr:oadrDeviceClass>
<power:endDeviceAsset>
<power:mrid>x-Exterior_Lighting</power:mrid>
</power:endDeviceAsset>
</oadr:oadrDeviceClass>
</oadr:oadrCreateOpt>
""";
/** 命名空间地址表:马甲(前缀)会逐条重排,地址(URI)二十年不变 */
static final String EI = "http://docs.oasis-open.org/ns/energyinterop/201110";
static final String EMIX = "http://docs.oasis-open.org/ns/emix/2011/06";
static final String XCAL = "urn:ietf:params:xml:ns:icalendar-2.0";
public static void main(String[] args) throws Exception {
// 按地址认字的第一步:打开命名空间开关。关着的话,地址全被扔掉,只剩马甲。
var factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
Document doc = factory.newDocumentBuilder()
.parse(new ByteArrayInputStream(OLD_TONGUE.getBytes(StandardCharsets.UTF_8)));
// 逐格取货:认地址不认前缀。哪怕明天的报文把 ei 全改成 zz9,这几行一个字都不用改。
String optID = textByUri(doc, EI, "optID");
String optType = textByUri(doc, EI, "optType");
String optReason = textByUri(doc, EI, "optReason");
String marketContext = textByUri(doc, EMIX, "marketContext");
String venID = textByUri(doc, EI, "venID");
String created = textByUri(doc, EI, "createdDateTime");
String start = textByUri(doc, XCAL, "date-time");
String duration = textByUri(doc, XCAL, "duration");
// 旧格式登记:先数一数开场有几个马甲(xmlns 声明),给观众一个年代感
long hats = OLD_TONGUE.split("xmlns:", -1).length - 1;
System.out.println("[旧格式] 2.0b 报名单,开场自报 " + hats + " 个命名空间马甲,尖括号订装:");
System.out.println(" optID=" + optID + " optType=" + optType + " optReason=" + optReason);
System.out.println(" venID=" + venID + " 可用时段 " + start + " 起 " + duration);
// 新格式:同一份报名单,按 3.0 的 JSON 口味重写。字段一一对应,语义原地不动。
StringBuilder json = new StringBuilder();
json.append("{\n");
json.append(" \"optID\": \"").append(optID).append("\",\n");
json.append(" \"optType\": \"").append(optType).append("\",\n");
json.append(" \"optReason\": \"").append(optReason).append("\",\n");
json.append(" \"marketContext\": \"").append(marketContext).append("\",\n");
json.append(" \"venID\": \"").append(venID).append("\",\n");
json.append(" \"availability\": {\n");
json.append(" \"start\": \"").append(start).append("\",\n");
json.append(" \"duration\": \"").append(duration).append("\"\n");
json.append(" },\n");
json.append(" \"createdDateTime\": \"").append(created).append("\"\n");
json.append("}");
System.out.println();
System.out.println("[新格式] 3.0 风格 JSON,一对花括号订装,没有命名空间:");
System.out.println(json);
// 清点:搬家验收。数据文体(字段与含义)同一份,换的只是格式。
System.out.println();
System.out.println("[清点] 八个字段旧宅新宅一一对应:数据文体同一份,换的只是格式");
}
/** 按地址(命名空间 URI)+本名(local name)取一格文本;找不到返回占位,让断货看得见 */
static String textByUri(Document doc, String uri, String local) {
NodeList list = doc.getElementsByTagNameNS(uri, local);
return list.getLength() == 0 ? "(缺货)" : list.item(0).getTextContent().trim();
}
}
本机实跑,2026-11-05,JDK 21,零第三方依赖。
text
[旧格式] 2.0b 报名单,开场自报 8 个命名空间马甲,尖括号订装:
optID=Opt_1234 optType=optIn optReason=participating
venID=Demo-RA-VEN0001 可用时段 2025-09-26T06:51:27.346Z 起 PT24H
[新格式] 3.0 风格 JSON,一对花括号订装,没有命名空间:
{
"optID": "Opt_1234",
"optType": "optIn",
"optReason": "participating",
"marketContext": "http://MarketContext1",
"venID": "Demo-RA-VEN0001",
"availability": {
"start": "2025-09-26T06:51:27.346Z",
"duration": "PT24H"
},
"createdDateTime": "2025-09-26T06:51:27.346Z"
}
[清点] 八个字段旧宅新宅一一对应:数据文体同一份,换的只是格式
这一夜讲的是给一份协议写年谱。年谱立完,上一夜欠的那个答案也落了地。它说 XML,不是因为 XML 好,是因为它出生那年,家里说的是 XML。2009 年说得字正腔圆,2023 年改口 JSON,也不是背叛,是客厅换了主人。搬了两回家,数据文体那八个字段原地没动;格式换了两回,合同还是 2002 年危机里立下的那一份。
贺知章回乡写过,少小离家老大回,乡音无改鬓毛衰。
协议没有乡音,它只有合同。
① 附录与真伪核对
Ⅰ 出生证明 2002 年加州电力危机之后立项,加州能源委员会 PIER 计划与能源部资助,劳伦斯伯克利国家实验室需求响应研究中心承担(真,2.0b 规范前言第 5 页,第25夜已核,此处回门)。
Ⅱ 1.0 出生 2009 年发布,牵头人 Piette,报告编号 CEC-500-2009-063(真,加州能源委员会报告编号在案)。
Ⅲ 联盟 2010 年 10 月成立,四创始为伯克利实验室、太平洋煤气电力、南加州爱迪生、霍尼韦尔(真,openadr.org about-us 与 LBNL 官网在案)。
Ⅳ 捐赠 OASIS 2011 至 2012 年 1.0 捐入 OASIS 长成 Energy Interoperation;2.0b 报文的 xcal、emix 前缀与规范引用表里的 SOA-RM(2006)、WS-Calendar(2011)即这段姻缘的化石(真,2.0b 规范引用表在案)。
Ⅴ 2.0b 冻结 2013 年发布 2.0a 与 2.0b,2015 年 11 月 17 日 Rev 1.1 定稿(Document 20120912-1),此后八年正文未改(真,手册在册)。
Ⅵ 国际户口 2018 年 2.0b 升格国际标准 IEC 62746-10-1(真,openadr.org From 2.0 to 3.0 Webinar 与 about-us 在案)。
Ⅶ 3.0 搬家 2023 年发布 3.0,弃 XML 改 JSON,通路 REST,附机器可读接口文档,正典合一 openadr3.yaml(真,同上 Webinar 2023-11 在案)。
Ⅷ 本夜 JSON 形态 按 3.0 风格的示意重绘,字段语义对照为演绎层;正典 openadr3.yaml 在联盟仓库与会员墙内(演绎,重绘口径同第25夜目录示意先例)。
Ⅸ boot3 样本 createOptIn.xml 照录,XMLSpy v2010 生成注释与 2025-09-26 时间戳原样保留;venID 里的公司名按脱敏纪律替为 Demo-RA-VEN0001(真,作者开源仓库 DummyVEN20b 样本)。
Ⅹ 伤痕 一星期 build 通过;JAXB 铸炉焊死在 sun.misc.Unsafe.defineClass,JDK 11 起该接口不可用;报文前缀逐条重排的动态偏移(真,git 物证与 ven.log 在册,伤痕册第二、三、五节)。
② 术语对照
Ⅰ XML 尖括号订装的数据写法,标签成对,2009 年企业集成的普通话。
Ⅱ XSD 给 XML 立规矩的合同文本,规定哪些标签合法、按什么顺序;2.0b 的合同就是它。
Ⅲ JSON 花括号订装的数据写法,2023 年行业的新普通话。
Ⅳ OpenAPI 与 JSON Schema 给 JSON 立规矩的新合同文本;openadr3.yaml 是书写这份合同用的一支笔,不是另一种合同。
Ⅴ REST 用网址加动作调用服务的章法,3.0 的通路。
Ⅵ JAXB 把 XSD 铸成 Java 类的老炉子;JDK 11 起被请出标准库,老铸炉在新家点不着。
Ⅶ 命名空间 标签的地址;前缀是马甲,地址不变。按地址认字,不按马甲。
③ 今夜习题
Ⅰ 三层表填空。搬家搬了三层,把表补全。数据文体(字段与含义)、合同语汇(XSD 换 OpenAPI 加 JSON Schema)、装订格式(尖括号换花括号)。哪一层搬完之后语义一点没动?哪一层换的只是笔?YAML 算第几层?
Ⅱ 母语判词倒推。2015 年要是有人给 OpenADR 重新选一次语言,直接改选 JSON,对不对?用本夜判词(出生那年的通用语)作答。再想一步,一门协议应该几年搬一次家,搬家的判据是什么。评论区见。
Ⅲ 实跑题。把样本里 ei:optType 的值从 optIn 改成 optOut,重跑,新格式哪里变了?再把 ei:optReason 整个删掉,重跑,程序死了吗?答案都在那个按地址取货的函数里。
本书目录导航 上一篇:算法一千零一夜 · 第二十五夜 把电网画成UML图 下一篇(载锅中) 起点:卷首语·算法童话
版权声明:本文为作者原创,受著作权法保护。未经授权禁止转载、搬运、摘编、改编及任何形式的二次创作;个人学习引用请注明作者与原文出处。转载授权请联系作者(CSDN 私信)。侵权必究。