DLMS/COSEM 蓝皮书解读十Schedule 类class_id 10—— “什么时候、按什么周期”Script table 的时间搭档系列说明本系列基于 DLMS UA《Blue Book蓝皮书第 16 版 · 第 2 部分 —— COSEM interface classes》一个接口类一篇。第 9 篇讲了Script tableclass_id 9它解决的是到点了要执行哪些动作把一组SET/ACTION打包成脚本。本篇的Schedule解决的是紧接着的下一个问题这些脚本在什么时候、按什么周期执行。它和Script table是一对必须同时配置才成立的搭档。上篇回顾Script table把做什么外置成了数据 ——scripts里存着若干脚本每个脚本是一串action_specification由execute(data)触发。但它有一个致命的设计前提它自己不会醒来。Script table没有时间属性没有周期属性也没有任何到点自查的逻辑。它只回答做什么把何时做整个留给了别人。本篇的Schedule就是来补这个洞的它持有一张带时间的条目表时间一到就按script_logical_namescript_selector去把对应的脚本叫起来。版本说明Schedule在本卷 raw 原文里只有version 0一个版本entry 结构为schedule_table_entry故本篇不设版本对比章节。关于某些资料中出现的另一种 entry 形态见 2.2 末尾的说明。0. 为什么必须要有 Schedule把表内自动化拆成三要素第 9 篇已经画过这张表本篇把它补全要素由谁负责class_id现在几点Clock8在什么时候、按什么周期执行Schedule10做什么Script table9被操作的对象Register activation/Profile generic/Demand register…6 / 7 / 5没有Schedule的话周期性动作只剩三条路每条都有硬伤① 主站定时下发—— 22:00 切谷费率主站要在 22:00:00 那一刻给全城 10 万块表各发一条SET拥塞、离线表漏发、下发到一半断线全都无法收拾而且那是主站的时钟不是表的时钟② 固件写死时段表—— 改一次峰谷时段就要改固件在计量行业意味着重新送检③ 厂商私有时间对象—— 各家一套格式互操作性归零。Schedule的答案和Script table一脉相承把什么时候也外置成数据。一张entries表放在attribute里表内固件只有一个通用引擎比对时间 → 命中条目 → 调脚本改时段 一次SET不动固件也不依赖主站在线。蓝皮书原文Schedule, Overview“This IC, together with the IC ‘Special days’, allows modelling time- and date-driven activities within a device.”注意这一句把Schedule10和Special days table11绑在一起提到了前者管常规日程后者管对常规日程的例外覆盖。这是本类的第一个关键点 —— 它从设计之初就不是孤立的一张表。一句话定位Schedule表内的 cron 表。它不管做什么Script table的事只管什么时候、星期几、在哪个日期区间内去叫那个脚本。它和 Activity calendar / Single action schedule 怎么分工三个类都能触发脚本工程上最容易选错判断标准很实用周期性 / 长期有效用Scheduleswitch_time可含通配符 exec_weekdaysbegin_date/end_date只干一次用Single action schedule22execution_time是精确的 time date要按季节分层、随费率切换用Activity calendar20season_start→week_profile→day_profile层层嵌套。三者可以共存于一台设备但同一个动作不要配两处 —— 那是费率被切两次这类诡异故障的常见来源。1. 类蓝图Schedule 0...n class_id 10, version 0基数0...n一台设备可以有多个Schedule实例费率切换一张、日冻结一张、负荷控制一张也可以一个都没有。属性静态/动态数据类型MinMaxDefShort namelogical_name(static)octet-stringxentries(static)arrayx 0x08方法必选/可选(m/o)Short nameenable/disable (data)ox 0x20insert (data)ox 0x28delete (data)ox 0x30注 1原文未给 Min / Max / Def 三列任何取值故留空。两个属性全是 static—— static 表示值不会由设备自发改变不等于不可写entries恰恰是主站改写最频繁的属性之一。注 2三个方法全部是ooptional—— 一块表可以完全不实现任何Schedule方法只把entries当可读写配置调用前必须确认。另外 SN 寻址时属性结束在x 0x08、方法从x 0x20起步中间0x10、0x18是空的不能按 0x08 步长顺推。只有 2 个属性但全部复杂度都藏在entries这一个array里。2. 属性逐条解读2.1logical_namestatic, octet-string“Identifies the ‘Schedule’ object instance.”6 字节OBISSchedule常见取值形如0-0:11.0.0示例值非蓝皮书原文实际以设备对象列表为准A 组 0 属于抽象/公用对象。它自己不会被谁引用Schedule是链条的发起端但它引用的script_logical_name必须存在否则命中条目后execute落空见第 9 篇软引用脱节。2.2entriesstatic, array—— 本篇的全部内容蓝皮书原文Schedule, entries“Specifies the scripts to be executed at given times. There is only one script that can be executed per entry.”“array schedule_table_entry”两句话两层意思entries是条目的数组每个 entry 只能挂一个脚本想要一个时刻做两件事那就写两个 entry或者把两件事合成一个脚本 —— 见示例 6。原文给出的structure定义10 个字段一个都不少schedule_table_entry :: structure { index: long-unsigned, enable: boolean, script_logical_name: octet-string, script_selector: long-unsigned, switch_time: octet-string, validity_window: long-unsigned, exec_weekdays: bit-string, exec_specdays: bit-string, begin_date: octet-string, end_date: octet-string }字段类型干什么用indexlong-unsigned条目主键不是数组下标。同时决定同一时刻的执行顺序enableboolean本条目是否生效enable/disable方法改的就是它script_logical_nameoctet-string目标Script table对象的OBISscript_selectorlong-unsigned目标脚本的script_identifier按编号匹配switch_timeoctet-string执行时刻允许通配符→ 用来表达重复validity_windowlong-unsigned掉电/时间跳变后补执行的容忍窗口单位分钟exec_weekdaysbit-string星期几生效Mo…Suexec_specdaysbit-string关联Special days table的day_idbegin_dateoctet-string有效期起始日允许通配符end_dateoctet-string有效期结束日允许通配符“script_logical_name: defines the logical name of the ‘Script table’ object;”“script_selector: defines the script_identifier of the script to be executed;”与第 9 篇Activity calendar的day_profile_action引用方式完全一致OBIS 编号的软引用改脚本不会触发任何一致性检查。“switch_time accepts wildcards to define repetitive entries. The format of the octet-string follows the rules set in for time;”switch_time支持通配符 —— 这是周期性重复的实现机制。按 4.6.1 的 time 编码它是 4 字节octet-stringhour | minute | second | hundredths把某个字节写成通配符0xFF就等于这一位任意switch_time4 字节触发效果16 00 00 00每天 22:00:00.00 执行一次16 FF 00 0022 点内的每一分钟都触发22:00、22:01 … 22:59FF 00 00 00每小时的第 0 分触发00:00、01:00 … 23:00FF FF FF FF全通配工程上几乎不用等于一直触发“validity_window defines a period in minutes, in which an entry shall be processed after power fail. (time between defined switch_time and actual power_up) 0xFFFF: the script is processed any time;”validity_window是全类最容易被误读的字段。它不是动作持续多久也不是执行的宽限期而是掉电恢复后判断这条还补不补的时间差上限switch_time与实际上电时刻之间相差多少分钟之内这条条目还算欠账、需要在恢复后补执行。注意单位是分钟且0xFFFF是个特殊值 任何时候都补不管隔了多久。“exec_weekdays defines the days of the week on which the entry is valid;”“exec_specdays perform the link to the IC ‘Special days table’, day_id;”两个bit-string是并列的两套日子选择exec_weekdays管星期7 bitMo…Suexec_specdays管特殊日每 bit 对应Special days table的一个day_id。两者是或的关系 —— 命中星期或命中特殊日都触发。“begin_date and end_date define the date period in which the entry is valid (wildcards are allowed). The format follows the rules set in for date.”begin_date/end_date是 5 字节octet-string按 date 编码year_hi | year_lo | month | day_of_month | day_of_week。年份写0xFFFF就是每年 —— 这正是周期性与一次性的分水岭FF FF 04 01 FF→FF FF 09 1E FF是每年 4/1–9/30 的夏季日程周期性begin_date end_date 07 EA 09 1E FF2026-09-30则只在当天有效一次性时间窗口这种需求其实更适合Single action schedule。关于另一种 entry 形态非本卷原文仅供对照在一些较新的资料 / 版本中Schedule的 entry 被重构为schedule_entry形如{index, enable, action_type, exec_time: array of date_time, script}即用一个action_typeenum区分动作类型、用一个date_time数组承载多个执行时刻、并把script_logical_name/script_selector收进一个script子 structure。本文所依据的 raw 原文Ed.16 Part 2, Schedule version 0并未包含该形态action_type的枚举取值原文亦未提供—— 故本篇不对其展开规格。若你的设备对象列表里出现这种结构请先核对所依据的蓝皮书版本与厂商文档再决定解析方式。两者的语义对应关系可粗记为exec_timearray≈ 本文的switch_timebegin_date/end_date组合script≈script_logical_namescript_selector。2.3 原文给的那张示意表raw 开头有一张Table — ScheduleTable — Special days table是蓝皮书用来演示两者配合的原表为节选排版列位对齐按语义推断仅作示意indexenableswitch_timevalidity_windowexec_weekdaysbegin_dateend_date120Yes06:000xFFFFMo–Saxx-04-01xx-09-30121Yes22:0015Mo–Frxx-04-01xx-09-30122Yes12:000Suxx-04-01xx-09-30200–202No06:30 / 21:30 / 11:00—Mo–Sa / Mo–Fr / Suxx-04-01xx-09-30配套的Special days tableindex 12 → xx-12-24 → S1每年圣诞夜、index 33 → xx-12-25 → S3、index 77 → 97-03-31 → S31997-03-31一次性。一眼能看明白三件事①xx 年份通配符所以每年 4/1–9/30是周期性的夏季日程而97-03-31写了具体年份是一次性的特殊日② 120–122 与 200–202 是两套互为备份的日程夏 / 冬靠enable或enable/disable方法整段切换见示例 5③validity_window三档——0xFFFF无条件补执行、15掉电后 15 分钟内才补、0永不补执行必须按业务重要性分配。3. 方法详解三个全是o3.1enable/disable (data)data :: structure { firstIndexA, lastIndexA, firstIndexB, lastIndexB: long-unsigned }“Sets the disabled bit of range A entries to true and then enables the entries of range B.”一次调用干两件事先把区间 A 全部enable false再把区间 B 全部enable true。设计意图非常明确 ——季节切换一步完成关旧日程、开新日程不存在中间态。区间语义由两个端点的大小关系决定原文逐条列出条件结果firstIndexA/B lastIndexA/B该区间的所有条目被 disable / enablefirstIndexA/B lastIndexA/B只 disable / enable一个条目firstIndexA/B lastIndexA/B什么都不做firstIndexA/B与lastIndexA/B 9999该区间没有任何条目被 disable / enable3.2insert (data)“Inserts a new entry in the table. If the index of the entry exists already, the existing entry is overwritten by the new entry.”“entry: schedule_table_entry / data :: corresponding to entry”data就是一个完整的schedule_table_entry。index相同时也不是报错而是覆盖—— 这实际上把insert变成了upsert改配置无需先删后插但写错index会静默覆盖掉一条正在用的日程。3.3delete (data)data :: structure { firstIndex, lastIndex: long-unsigned }“firstIndex lastIndex: all entries of the range A/B are deleted,”“firstIndex lastIndex: one entry is deleted,”“firstIndex lastIndex: nothing deleted”注意delete没有“ 9999视为空操作” 那一条那是enable/disable独有的删除按index区间匹配不是数组位置 —— 删掉中间一条后剩余条目的index不变。3.4 两条撞车规则原文 Remarks“If the same script should be executed several times at a specific time instance, then it is executed only once.”“If different scripts should be executed at the same time instance, then the execution order is according to the ‘index’. The script with the lowest ‘index’ is executed first.”同一脚本在同一时刻被多条 entry 命中 → 只执行一次天然去重不同脚本在同一时刻 → 按index升序执行小的先跑。第 9 篇Script table里同一时刻较小 index 者优先的规则与此完全同源。4. 【实战举例】以下示例中的 OBIS、配置取值与报文字节均为帮助理解而构造非蓝皮书原文实际以设备对象列表与厂商文档为准。编码按 4.6.1 的 time / date 规则与 A-XDR 规则structure0x02、array0x01、boolean0x03、bit-string0x04、octet-string0x09、long-unsigned0x12构造。示例 1工作日 22:00 切谷费率一条完整 entry 的 A-XDR承接第 9 篇示例 1 的脚本Script table0-0:10.0.0script_identifier 1Schedule logical_name 0-0:11.0.0 示例 entries[0] { index 120, # 主键不是数组下标 enable TRUE, script_logical_name 0-0:10.0.0, # 指向 Script table script_selector 1, # script_identifier 1 switch_time 22:00:00.00, validity_window 15, # 掉电后 15 分钟内补执行 exec_weekdays Mo–Fr, # 工作日 exec_specdays 无, begin_date xx-04-01, # 每年 4 月 1 日起 end_date xx-09-30 # 每年 9 月 30 日止 }SET该entries属性的 A-XDR示例01 01 array(1) entries 02 0A structure(10) schedule_table_entry 12 00 78 long-unsigned 120 index 03 01 boolean TRUE enable 09 06 00 00 0A 00 00 FF octet-string(6) 0-0:10.0.0 script_logical_name 12 00 01 long-unsigned 1 script_selector 09 04 16 00 00 00 octet-string(4) 22:00:00.00 switch_time 12 00 0F long-unsigned 15 validity_window 04 07 7C bit-string(7) Mo–Fr exec_weekdays 04 09 00 00 bit-string(9) 无特殊日 exec_specdays 09 05 FF FF 04 01 FF octet-string(5) xx-04-01 begin_date 09 05 FF FF 09 1E FF octet-string(5) xx-09-30 end_dateexec_weekdays的 7 个 bit 按 Mo→Su 从高位排Mo 0x40… Su 0x01故 Mo–Fr 0x7C、Mo–Sa 0x7E、Su 0x01、Mo–Su 0x7F。exec_specdays的 bit 数必须与Special days table里day_id的取值范围长度一致原文the range of the day_id shall match the length of the bit-string exec_specdays上表用 S1–S9故取 9 bit、2 字节。示例 2周期性 vs 一次性 —— 两种日期写法同一条23:59 结算的需求两种配置语义完全不同示例# A. 周期性每年 9 月 30 日 23:592026 执行2027 还会执行 switch_time 17 3B 00 00 # 23:59:00.00 begin_date FF FF 09 1E FF # 年份 0xFFFF 每年 end_date FF FF 09 1E FF # B. 一次性只在 2026-09-30 执行一次2027 年该条目已过期 switch_time 17 3B 00 00 begin_date 07 EA 09 1E 03 # 2026-09-30周三dow 3 end_date 07 EA 09 1E 03若按新版形态见 2.2 说明写成exec_time: array of date_time同一时刻的date_time12 字节编码为示例07 EA | 09 | 1E | 03 | 17 3B 00 00 | 01 E0 | 00—— 依次是 year0x07EA 2026、month 9、dom 30、dow 3周三、time 23:59:00.00、UTC 偏差0x01E0 480 min、clock_status0x00。对照本文 version 0 的写法date_time的前 5 字节就是 date、第 6–9 字节就是 time后 3 字节是 UTC 偏差与clock_status本类用不到。本卷Schedule用拆开的switch_time4 字节begin_date/end_date5 字节不要混用。示例 3validity_window—— 掉电恢复补不补“After a power failure, the whole schedule is processed to execute all the necessary scripts that would get lost during a power failure.”设switch_time 22:00表在 21:50 掉电、23:10 上电相差 70 minvalidity_window 0xFFFF→无条件补执行“the script is processed any time” 15→ 70 15不补这次费率切换丢失若 22:05 就上电5 ≤ 15 则补执行。三条结论① 费率切换类建议0xFFFF漏一次就是一整天的费率错② 抓冻结类反而要小窗口0也行补一次capture可能抓到时间戳错位的记录③ 单位是分钟写 3600 以为毫秒/秒会得到荒谬巨大的窗口。示例 4周末 特殊日两个 bit-string 的配合某地在 12 月 24 日执行特殊费率不走常规周末日程配置如下示例Special days table (11){ index 12, specialday_date FF FF 0C 18 FF, day_id 1 } # xx-12-24 → S1 { index 33, specialday_date FF FF 0C 19 FF, day_id 3 } # xx-12-25 → S3 Schedule (10)entry { index 300, switch_time 12:00, exec_weekdays 04 07 01, # 7 bit仅周日 exec_specdays 04 09 80 00 } # 9 bitS1 置位12 月 24 日即使是周三也因为命中exec_specdays的 S1 而触发——Special days table的 Overview 说得很直白蓝皮书原文Special days table, Overview“On such dates, a special switching behaviour overrides the normal one.”特殊日覆盖override常规日程—— 这是下篇class_id 11的主线本篇只需记住exec_specdays是这条覆盖关系的接口。示例 5夏 / 冬日程整段切换enable/disable用示意表的两套条目120–122 夏、200–202 冬4 月 1 日切换ACTION (class_id 10, logical_name 0-0:11.0.0, method_index 1) # enable/disable data :: structure { firstIndexA 200, lastIndexA 202, # 关掉冬季200 202 → 全区间 firstIndexB 120, lastIndexB 122 # 打开夏季 } → A-XDRdata 部分示例02 04 12 00 C8 12 00 CA 12 00 78 12 00 7A ↑0x00C8200 ↑0x00CA202 ↑0x0078120 ↑0x007A122若只想动一条临时禁用 121firstIndexA lastIndexA 121区间 B 写成空操作 —— 例如firstIndexB 10000, lastIndexB 10000两者均 9999 → 该区间不动作。端点大小关系本身就是语义全区间 /单条 /不做写反了不报错只会安静地没生效。示例 6同一时刻多条 entry 撞车index 就是排序键entry { index 120, switch_time 22:00, script_selector 1 } # 切谷费率 entry { index 121, switch_time 22:00, script_selector 1 } # 同一脚本写重了 entry { index 122, switch_time 22:00, script_selector 2 } # 抓冻结22:00 到来时120 与 121 指向同一脚本→ 只执行一次122 是不同脚本 → 排在后面。实际顺序切谷费率 → 抓冻结。想让某件事先发生就给它更小的index。示例 7四种时间变更各怎么处理原文把时间变更分成四类处理策略各不相同 —— 这是Schedule最硬核的一块变更类型触发方式Schedule 的处理时间向前设置写Clock的timeattribute与掉电同等处理所有被跳过的条目按validity_window决定是否补执行。很短的manufacturer specific向前调整可按时间同步处理时间向后设置写Clock的timeattribute被回拨区间内的条目会重复执行时间同步Clock的adjust_to_quarter方法算法 manufacturer specific但必须保证不丢、不重复执行validity_window不起作用夏令时切换Clock的 DST 属性时钟拨快 → 落入该区间的脚本先执行避免丢失拨慢 → 落入该区间的脚本抑制重执行“Time synchronization is used to correct small deviations between a master clock and the local clock. … It shall guarantee that no entry of the schedule gets lost, or is executed twice.”给主站的最直接结论主站校时不要动辄写Clock.time那是时间设置会触发补执行/重执行连锁小幅偏差应该用adjust_to_quarter这类同步方法。5. 工程上容易踩的坑把index当数组下标index是条目的业务主键insert/delete/enable/disable全部按index匹配同时它还是排序键同一时刻不同脚本按index升序执行排反了冻结记录就会落在错误的费率上。删掉中间一条后其余index不变按下标推算必然错位。insert是 upsertindex已存在时静默覆盖原文“the existing entry is overwritten”。批量下发日程前若没先读回entries比对可能覆盖掉现场正在用的条目。validity_window理解错它是掉电/跳变后补执行的时间差上限单位分钟0xFFFF 无条件补0 永不补。它不是动作持续时长。switch_time通配符乱填想每天 22:00却写成16 FF 00 0022 点内每分钟触发费率脚本会被执行 60 次。通配符只用来表达重复粒度配完必须在实验表上跑一遍。年份忘写通配符begin_date写成07 EA ...2026却指望它年年生效 —— 到 2027 年这条条目整体失效且没有任何告警。周期性日程的年份必须是FF FF。exec_specdays的 bit 数与day_id范围不匹配原文要求两者长度一致“the range of the day_id shall match the length of the bit-string exec_specdays”。表里有 S1–S99 bit而这里只写 7 bitS8/S9 永远命中不了 —— 表现为特殊日不生效且很难查。重复执行是允许的正常行为时间向后设置会重跑、掉电会补跑。所以脚本动作必须幂等用SET绝对值不用取反/累加这一点第 9 篇已经埋过伏笔。三个方法都是o不要假设insert/delete/enable/disable存在不支持时退化为整块SET重写entries但要先读回原值再改避免覆盖。另外主站校时别直接写Clock.time那是时间设置会触发补执行/重执行连锁。6. 小结 下期预告本篇要点Scheduleclass_id 10, version 0是表内的 cron 表Script table回答做什么它回答什么时候、星期几、哪段日期内、按什么周期去叫那个脚本只有 2 个属性 ——logical_namestatic、entriesstaticarray schedule_table_entry10 字段的 entryindex/enable/script_logical_name/script_selector/switch_time/validity_window/exec_weekdays/exec_specdays/begin_date/end_date周期性重复靠通配符switch_time的分/时字节写0xFF、日期年份写FF FF一次性窗口则给begin_date/end_date写具体年份 —— 后者更适合交给Single action schedule22validity_window是掉电/时间跳变后的补执行窗口单位分钟0xFFFF 无条件补0 永不补三个方法enable/disable一次关 A 段、开 B 段专为季节切换设计、insertupsert、delete全是o可选同一脚本同一时刻只执行一次不同脚本按index升序执行时间变更四类向前设置 / 向后设置 / 同步 / 夏令时处理策略不同 —— 主站校时优先用Clock的同步方法别直接写time。下一篇第 11 篇Special days tableclass_id 11—— 本篇的exec_specdays只留了一个接口完整规则在那边。它用entriesarray spec_day_entry每个 entry 是indexspecialday_dateday_id定义节假日与例外日让特殊日覆盖常规日程是费率日历完整性的最后一块拼图。我们还会讲清day_id的取值范围如何与exec_specdays的 bit 长度对齐、通配符怎么表达每年圣诞这类重复节日以及它与Activity calendar20里day_profile的优先级关系。参考资料DLMS UA《Blue Book Ed.16 Part 2 – COSEM interface classes》Schedule (class_id 10, version 0) 章节含 Overview、Table “Schedule”、Table “Special days table”、Attribute description、Method description、Remarks concerning inconsistencies、Recovery after power failure、Handling of time changes 各小节以及 Special days table (class_id 11) 章节中 Overview 与spec_day_entry的相关描述。文中 2 个属性的名称、static 标记、数据类型、Short name 偏移、3 个方法的名称与 m/o、Short name 偏移、以及全部英文引文均与原文一致示例中的 OBIS0-0:11.0.0/0-0:10.0.0、配置取值与报文字节为帮助理解而构造实际以设备对象列表与厂商文档为准。2.2 末尾关于schedule_entry/exec_time/action_type的对照说明非本卷 raw 原文仅为版本差异提示。