:Special days table 类(class_id = 11)—— 节假日如何覆盖常规日程:费率日历的“补丁表“)
DLMS/COSEM 蓝皮书解读十一Special days table 类class_id 11—— 节假日如何覆盖常规日程费率日历的补丁表系列说明本系列基于 DLMS UA《Blue Book蓝皮书第 16 版 · 第 2 部分 —— COSEM interface classes》一个接口类一篇。第 10 篇讲了Scheduleclass_id 10它解决的是脚本在什么时候、按什么周期执行。本篇的Special days table解决的是紧随其后的那个洞节假日 / 例外日如何覆盖常规日程—— 春节、国庆这类日子不能按普通周末 / 工作日的规则走需要一张特殊日表来给日历打补丁。上篇回顾Schedule是一张表内的 cron 表靠switch_time通配符 exec_weekdaysbegin_date/end_date表达了每周一到周五 22:00 切谷这类周期性规则。但它有一个结构性盲区它只认星期几不认几月几日。2026 年春节是 2 月 17 日星期二 —— 按exec_weekdays它是一个普通工作日可业务上它必须按假日费率走。Schedule自己在exec_specdays里留了一个bit-string接口但那个接口指向的规则表正是本篇的Special days table。版本说明本卷 raw 原文中Special days table只有version 0一个版本故本篇不设版本对比章节。0. 为什么必须要有 Special days table先算一笔账。某地峰谷电价峰08:00–11:00、18:00–21:00、平、谷22:00–次日 06:00。表内靠Activity calendar的week_profile表达周一至周五用 “工作日 day_profile”周六周日用 “假日 day_profile”。现在把 2026 年的日历摊开看日期实际星期业务上应该是只靠 week_profile 的结果2026-01-01元旦星期四假日费率❌ 按工作日计费2026-02-17春节星期二假日费率❌ 按工作日计费2026-05-01劳动节星期五假日费率❌ 按工作日计费2026-10-01国庆星期四假日费率❌ 按工作日计费四个法定节假日没有一个落在周末。这不是巧合 —— 全世界的法定假日绝大多数是某月某日而不是某星期几两套体系天生对不上。少了Special days table会出现三件很难收场的事电费算错且不可逆—— 节假日那几天的峰段电量被记进峰寄存器账单已出事后补差价是商务纠纷不是技术修补只能靠主站实时下发救火—— 元旦 00:00 主站给全城表计各发一条SET切假日费率。可表计离线、通道拥塞、下发到一半断线……漏掉的表就按工作日费率跑一整天第 9 篇讲Script table时已经论证过主站实时下发这条路为什么走不通改一次就要动固件—— 如果把节假日硬编码进固件每年的放假安排都不一样2026 年春节 2/172027 年春节 2/6等于每年重新送检一次。Special days table的思路与前两篇一脉相承把例外也外置成数据。一张entries表放在attribute里表内固件只有一个通用引擎今天是不是特殊日是 → 取那个day_id→ 用它覆盖常规 day profile改节假日 一次SET/ 一次insert不动固件也不依赖主站在线。蓝皮书原文Special days table, Overview“This IC allows defining special dates. On such dates, a special switching behaviour overrides the normal one. The IC works in conjunction with the class ‘Schedule’ or ‘Activity calendar’. The linking data item is day_id.”三句话把这个类的全部定位说完了① 它定义特殊日期② 特殊日上“特殊切换行为覆盖overrides常规行为”③ 它与Schedule或Activity calendar协同工作链接两者的那个数据项叫day_id。一句话定位Special days table日历的补丁表。它自己不执行任何动作只回答一个问题“今天是不是特殊日如果是用几号 day profile / 置位哪个 specday bit”三个日历类怎么分工类class_id回答什么关键属性Schedule10什么时刻、星期几、哪段日期区间去执行哪个脚本entriesswitch_time/exec_weekdays/exec_specdays/begin_date…Special days table11哪一天是例外例外日用哪个day_identriesindex/specialday_date/day_idActivity calendar20季节 → 周 → 日层层嵌套的完整费率日历season_profile/week_profile_table/day_profile_table注意Special days table与Activity calendar是两条不同的覆盖链路见示例 3 与示例 4很多配了特殊日却不生效的工单根源就是没分清自己在配哪一条。1. 类蓝图Special days table 0...n class_id 11, version 0基数0...n一台设备可以有多个Special days table实例例如法定节假日一张“检修日一张”也可以一个都没有 ——没有实例时日历就完全按常规week_profile/exec_weekdays走这本身是合法配置。属性静态/动态数据类型MinMaxDefShort namelogical_name(static)octet-stringxentries(static)arrayx 0x08方法必选/可选(m/o)Short nameinsert (data)ox 0x10delete (data)ox 0x18注 1原文未给 Min / Max / Def 三列任何取值故留空。注 2两个属性全是 (static)—— static 表示值不会由设备自发改变不等于不可写entries恰恰是主站改写最频繁的属性之一每年都要更新。注 3两个方法都是ooptional。表计可以完全不实现任何方法只把entries当成一个可整块SET的配置数组。调用insert/delete前必须先确认设备是否支持。注 4本类的方法 Short name 从x 0x10起步紧贴entries的x 0x08对比第 10 篇的Schedule属性到x 0x08方法却从x 0x20起步中间0x10/0x18是空的——Short name 偏移不能跨类按固定步长推算必须查表。只有 2 个属性、2 个方法是全集里最轻的类之一。全部复杂度都藏在entries这一个array里。2. 属性逐条解读2.1logical_namestatic, octet-string“Identifies the ‘Special days table’ object instance.”6 字节OBIS编码。工程上常见取值形如0-0:11.0.0.255示例值非蓝皮书原文实际以设备对象列表为准A 组 0 属于抽象 / 公用对象与相邻几个日历类连号排布Script table0-0:10.0.0.255、Schedule0-0:12.0.0.255、Special days table0-0:11.0.0.255、Activity calendar0-0:13.0.0.255。关键提醒本对象的logical_name不会被任何类的属性引用。第 10 篇的Schedule是通过exec_specdays的 bit 位间接关联到day_id的Activity calendar也是通过day_id关联 —— 两边都只认day_id不认这张表是谁。这是典型的软引用改错day_id不会触发任何一致性检查详见第 5 节坑 3。2.2entriesstatic, array—— 本篇的全部内容蓝皮书原文Special days table, entries“Specifies a special day identifier for a given date. The date may have wildcards for repeating special days like Christmas.”“array spec_day_entry”两句话两层意思entries的每个条目 “给某个日期指定一个特殊日标识”日期字段允许通配符用来表达每年圣诞这类重复节日 —— 这一句直接决定了本类的使用边界见示例 2。原文给出的structure定义三个字段一个都不少spec_day_entry :: structure { index: long-unsigned, specialday_date: octet-string, day_id: unsigned }注部分资料把这个字段写作special_day_date本卷 raw 原文的拼写是specialday_date无第二个下划线本文统一按原文。字段类型干什么用indexlong-unsigned条目主键不是数组下标delete与覆盖判定都按它specialday_dateoctet-string特殊日的日期5 字节按 4.6.1 的 date 规则允许通配符day_idunsigned链接数据项对应Activity calendar的day_profile或Schedule的exec_specdaysbit 位specialday_date5 字节 date 与通配符“specialday_date formatting follows the rules set in 4.6.1 for date;”按第 8 篇讲过的 date 编码5 字节octet-stringyear_hi | year_lo | month | day_of_month | day_of_week。年份写0xFFFF not specified 每年。specialday_date5 字节含义生效次数FF FF 01 01 FF每年 1 月 1 日元旦每年永久FF FF 0A 01 FF每年 10 月 1 日国庆每年永久FF FF 0C 19 FF每年 12 月 25 日圣诞每年永久07 EA 02 11 022026-02-17周二春节仅 2026 年一次07 EA 0A 01 042026-10-01周四仅 2026 年一次年份0x07EA 2026月0x02 2 月日0x11 17 日星期0x02 周二4.6.1 规定 day_of_week 为 1…71 Monday0xFF not specified。每年重复与一次性的分水岭就是年份那两个字节—— 这一点与第 10 篇Schedule的begin_date/end_date完全同源写FF FF是周期性规则写具体年份是到点就过期。而Special days table的残酷之处在于农历节日春节、中秋、清明每年日期都不同通配符表达不了只能逐年下发具体日期详见示例 2 与坑 1。day_id整个类的链接数据项蓝皮书原文Special days table, entries, Where“the range of the day_id shall match the length of the bit-string exec_specdays in the related object of IC ‘Schedule’.”这是本类唯一一条硬性约束也是最常见的故障点day_id的取值范围必须与相关Schedule对象里exec_specdays这个bit-string的长度一致。工程上怎么落地示例非原文若Schedule的exec_specdays是 9 bit对应 S1…S9那么day_id只能取 1…9 —— 你写一个day_id 12那个 bit 根本不存在特殊日静默失效没有任何报错。原文未明确规定的两点不要当成规格①day_id的下界是否从 1 开始、0 是否保留② 第 n 个 bit 与day_id n的位序映射S1 占字节最高位还是最低位。本系列第 10 篇exec_weekdays采用第 1 个 bit 最高位Mo 0x40exec_specdays按同例类推 —— 实际请以设备对象列表与厂商文档为准并在实验表上验证。day_id的另一半语义在Activity calendar那边见示例 3本卷Special days table原文只给了day_id: unsigned和上面那条约束没有展开它在Activity calendar侧的用法。3. 方法详解两个全是o3.1insert (data)“Inserts a new entry in the table.”“entry :: spec_day_entry”“If a special day with the same index or with the same date as an already defined day is inserted, the old entry will be overwritten.”data就是一个完整的spec_day_entry。重点在第三句覆盖条件是二选一 ——index相同或**specialday_date相同**旧条目都会被覆盖。**这与第 10 篇Schedule.insert的语义不一样Schedule只说“If the index of the entry exists already, the existing entry is overwritten”—— 只按index判定。而本类多了一个日期相同也算撞车的条件后果是已存在{ index 5, specialday_date 2026-10-01, day_id 9 } # 国庆 → 假日 新下发{ index 6, specialday_date 2026-10-01, day_id 1 } # 手滑写错 day_id 结果 index 5 的条目被静默覆盖删除表里只剩 index 6 这一条也就是说insert实际上是upsert而且 upsert 的键有两个。批量下发节假日表时同一天被写了两次不会报错只会留下最后一条见示例 6。3.2delete (data)“Deletes an entry in the table.”“index Index of the entry that shall be deleted.”“data :: long-unsigned”三点data是单个long-unsigned一个index值不是区间。对比Schedule.deletedata :: structure { firstIndex, lastIndex }按区间删——这两个同名方法参数完全不同别把Schedule的写法套过来。按index匹配不是数组下标。删掉中间一条后其余条目的index不变。原文没有给出删除不存在的index时是否报错的规定 ——原文未提供该项。3.3 没有enable/disable与相邻类的能力对比类方法备注Schedule10enable/disable按区间关 A 开 B、insert、delete按区间专为季节切换设计Special days table11insert单条双条件覆盖、delete单个index无enable/disableActivity calendar20activate_passive_calendarpassive → active 一次性整体切换靠双份日历 定时激活实现无缝换季工程含义很直接想临时停用一个特殊日只能delete掉它或者把整张entries读回来改完再整块SET回去后者在两个方法都不支持时是唯一退路。另外原文未说明insert/delete之后是否立即生效、是否需要重启——原文未提供该项实践中建议insert后立刻GET entries读回校验。4. 【实战举例】以下示例中的 OBIS、配置取值与报文字节均为帮助理解而构造非蓝皮书原文实际以设备对象列表与厂商文档为准。编码按 4.6.1 的 date 规则与 A-XDR 规则structure0x02、array0x01、octet-string0x09、long-unsigned0x12、unsigned0x11、bit-string0x04构造。示例 1一条 entry 的完整构造与 A-XDR —— 2026 年春节需求2026-02-17周二春节不走工作日费率改用day_id 9的假日 day profile。Special days table logical_name 0-0:11.0.0.255 示例 entries[0] { index 1, # 主键不是数组下标 specialday_date 07 EA 02 11 02, # 2026-02-17周二 day_id 9 # → 假日 day_profile / S9 }SET该entries单条的 A-XDR示例01 01 array(1) entries 02 03 structure(3) spec_day_entry 12 00 01 long-unsigned 1 index 09 05 07 EA 02 11 02 octet-string(5) 2026-02-17 specialday_date 11 09 unsigned 9 day_id等价的增量写法若设备实现了insertACTION (class_id 11, logical_name 0-0:11.0.0.255, method_index 1) # insert data :: spec_day_entry { index 1, specialday_date 07 EA 02 11 02, day_id 9 }对比第 10 篇Schedule的 10 字段 entry20 多个字节本类一个条目只有12 个字节—— 一张 30 天的节假日表也就 360 字节左右完全可以整块SET这是它设计成这么轻的原因。示例 2国庆能用通配符春节不能 —— 农历节日的硬约束同一个法定节假日需求两种日期写法语义天差地别示例# A. 公历固定日 → 通配符一次下发永久有效 { index 10, specialday_date FF FF 0A 01 FF, day_id 9 } # 每年 10-01 国庆 { index 11, specialday_date FF FF 01 01 FF, day_id 9 } # 每年 01-01 元旦 { index 12, specialday_date FF FF 05 01 FF, day_id 9 } # 每年 05-01 劳动节 # B. 农历节日 → 每年日期不同只能逐年写具体日期 { index 20, specialday_date 07 EA 02 11 02, day_id 9 } # 2026-02-17 春节周二 # 2027 年春节是 2 月 6 日周六必须再下发一条 { index 21, specialday_date 07 EB 02 06 06, day_id 9 } # 2027-02-060x07EB 2027day_of_month通配符0xFD 倒数第二天、0xFE 最后一天能表达每月最后一天但表达不了农历正月初一—— 4.6.1 的 date 是公历结构没有农历字段。公历节日元旦 / 劳动节 / 国庆 / 圣诞→ 通配符一次配置多年农历与浮动节日春节 / 中秋 / 清明 / 复活节 / 感恩节→ 必须逐年下发具体日期。这条结论直接决定了跨年维护的工作量见示例 6。示例 3day_id→Activity calendar的 day_profile覆盖链路之一这是Special days table最正统的用法。原文对优先级的描述写在Activity calendar那一章蓝皮书原文Activity calendar, Overview“If an instance of the IC ‘Special days table’ is available, relevant entries there take precedence over the ‘Activity calendar’ object driven selection of a day profile. The day profile referenced in the ‘Special days table’ activates the day_schedule of the day_profile_table in the ‘Activity calendar’ object by referencing through the day_id.”翻成工程语言如果设备里有Special days table实例且今天命中了其中一条那么它的选择优先于Activity calendar自己按week_profile选出来的 day profile而它引用的那个 day profile正是通过day_id在Activity calendar的day_profile_table里查到的。一组完整配置示例Activity calendar (0-0:13.0.0.255) 示例 OBIS day_profile_table_active: day_id 8 → day_schedule: # 工作日 06:00 → Script(0-0:10.0.100.255, #1) # 切峰 22:00 → Script(0-0:10.0.100.255, #2) # 切谷 day_id 9 → day_schedule: # 假日全天谷 00:00 → Script(0-0:10.0.100.255, #2) # 切谷 week_profile_table_active: WORK-WEEK → { mon8, tue8, wed8, thu8, fri8, sat9, sun9 } Special days table (0-0:11.0.0.255) { index 1, specialday_date 07 EA 02 11 02, day_id 9 } # 2026-02-17 春节 → 假日 { index 10, specialday_date FF FF 0A 01 FF, day_id 9 } # 每年国庆 → 假日2026-02-17 那天week_profile说周二 → day_id 8但Special days table命中并覆盖为day_id 9→ 走假日 day_schedule → 全天谷费率。注意这里Schedule的exec_specdays完全不参与—— 这是Activity calendar侧的覆盖。示例 4day_id↔Schedule.exec_specdays覆盖链路之二另一条链路是给Schedule条目用的。第 10 篇留的接口在这里接上Special days table (0-0:11.0.0.255) { index 1, specialday_date 07 EA 02 11 02, day_id 9 } # 春节 → S9 { index 2, specialday_date FF FF 0C 19 FF, day_id 1 } # 每年圣诞 → S1 ... day_id 取值 1…9共 9 个 Schedule (0-0:12.0.0.255) entry { index 300, switch_time 12:00, exec_weekdays 04 07 01, # 7 bit仅周日0x01 Su exec_specdays 04 09 00 80 } # 9 bitS9 置位第 9 位exec_specdays的 bit 数必须等于day_id的取值范围长度表里有 S1…S9 → 9 bit → 2 字节04 09 xx xx若表里只有 S1…S4 → 4 bit → 1 字节04 04 x0。长度写错写成 7 bit时S8 / S9 永远命中不了现象是特殊日不生效且极难排查。另一条来自Activity calendar原文的排序规则值得记住“If actions in a ‘Schedule’ object are scheduled for the same activation time as in an ‘Activity calendar’ object, the actions triggered by the ‘Schedule’ object are executed first.”即同一时刻Schedule的动作先于Activity calendar的动作执行。所以如果特殊日切换和常规整点切换撞在同一分钟顺序是固定的别指望靠配两次来互相纠正。示例 5完整协作时序 —— 从 Clock 到费率寄存器把第 6 篇费率切换链路、第 9 篇Script table、第 10 篇Schedule、本篇串起来2026-02-17 春节这一天表内发生了什么示例[0] 表内时钟 (Clock, 8)2026-02-16 23:59:59 → 跨日 [1] Activity calendar (20)按 season_profile → week_profile 取 周二 → day_id 8 [2] Special days table (11)查 specialday_date 2026-02-17 → 命中 index1 → day_id 9 └─ 覆盖override上一步的 day_id 8 [3] 选中 day_profile 9 的 day_schedule假日00:00 切谷 [4] 到达激活时刻 → Script table (9).execute(#2) └─ 脚本内容SET Register activation (6).active_mask holiday [5] Register activation (6).active_mask holiday → 谷寄存器 1-0:1.8.3.255 开始累加 → 结果2026-02-17 全天电量计入谷费率寄存器而不是峰/平寄存器同一天里Schedule若也有一条exec_specdays命中 S9 的条目示例 4 的 index 300它会在 12:00 再触发一次自己的脚本 ——两条链路各走各的day_id是它们共同的锚点。工程上务必只选一条链路配置同一个动作否则就是费率被切两次。示例 6跨年下发 —— 时序、条数与那个同 date 覆盖陷阱真实运维节奏示例每年 12 月中旬主站要把次年的特殊日表下发到全网表计。T0 2026-12-10 主站生成 2027 年表12 条元旦/春节/清明/劳动/端午/中秋/国庆… T1 2026-12-15 批量 ACTION insert ×12或整块 SET entries ⚠ 每条下发后 GET entries 读回校验条数与 day_id T2 2026-12-31 抽查随机抽 1% 表计确认 entries 里 2027 年条目齐全 T3 2027-01-01 表计自动切换无需主站在线三个必须记住的点必须提前下发。如果等到 2027-01-05 才下发1/1–1/4 已经按工作日费率计费这四天的电量已经记进峰/平寄存器不可逆。这是节假日配置里唯一无法事后补救的错误。旧条目不会自动清理。specialday_date写了具体年份的一次性条目过期后仍然占着entries的位置原文未提供自动过期机制。三年不清理表里就堆了三十多条废条目既占空间又容易被insert的同index覆盖误伤 —— 建议每年下发前先delete掉上一年的一次性条目。insert的同 date 覆盖。下发 2027 年春节时若先写了一条index 21, date 2027-02-06, day_id 1day_id写错随后又补发index 22, date 2027-02-06, day_id 9按原文“with the same date … the old entry will be overwritten”结果是index 21被删掉、只留index 22—— 看起来补发成功了实际上前一条已经静默消失。批量下发时务必保证同一日期只出现一次并在下发后读回核对index集合。5. 工程上容易踩的坑通配符的两个方向都容易写错①想用通配符表达农历 / 浮动节日——specialday_date是公历 date 结构FF FF只能通配年份通配不了农历正月初一春节 / 中秋 / 清明 / 复活节这类浮动节日必须逐年下发具体日期且年底就要把次年的表配好见示例 6②反向错误—— 元旦写成07 EA 01 01 04指望它年年生效到 2027 年静默失效春节写成FF FF 02 11 FF则会每年 2 月 17 日都当春节2027 年那天是周三根本不是节日。两类错误都不会有任何告警。day_id与exec_specdays的 bit 长度不匹配原文唯一硬约束“the range of the day_id shall match the length of the bit-string exec_specdays”。表里有 S1…S9 而exec_specdays只写 7 bitS8 / S9 永远命中不了 —— 表现为特殊日不生效且没有任何报错。day_id与Activity calendar的day_profile_table对不上day_id是软引用Activity calendar里根本不存在day_id 9的 day profile 时配置照样能写进去只是到了那天什么都不发生。下发前先GET一次day_profile_table_active核对day_id全集。insert是双条件 upsertindex相同或specialday_date相同都会覆盖旧条目与Schedule只按index不同。批量下发时同一天写了两次不会报错只会留下最后一条 —— 下发后必须读回核对index集合。delete的data是单个long-unsigned不是Schedule那种{firstIndex, lastIndex}区间结构。把Schedule的参数格式套过来会得到参数类型错误。两个方法都是o可选不要假设insert/delete存在。不支持时退化为整块SET entries但必须先GET读回原值再改否则会把现场正在用的条目整表覆盖掉。搞混两条覆盖链路Activity calendar侧是特殊日覆盖 day profile 的选择示例 3Schedule侧是exec_specdays位命中触发条目示例 4。同一个切换动作在两条链路上各配一次 → 费率被切两次。另外同一时刻Schedule的动作先于Activity calendar执行“the actions triggered by the ‘Schedule’ object are executed first”别指望靠配两次互相纠正。day_of_week字节随手填4.6.1 规定day_of_week为 1…71 Monday、0xFF not specified原文未给出该字节与实际星期不符时的比对规则。保守做法是统一填0xFF或按真实星期填写后在实验表上验证不要凭猜测。6. 小结 下期预告本篇要点Special days tableclass_id 11, version 0是日历的补丁表它不执行任何动作只回答今天是不是特殊日是的话用哪个day_id只有 2 个属性 ——logical_namestatic, octet-string、entriesstatic,array spec_day_entryentry 三个字段indexlong-unsigned业务主键/specialday_dateoctet-string5 字节 date支持通配符/day_idunsigned链接数据项day_id是唯一硬约束的承载者“the range of the day_id shall match the length of the bit-string exec_specdays”同时它也是Activity calendar里day_profile_table的查找键通配符只能表达公历固定日元旦 / 国庆 / 圣诞 →FF FF年份农历与浮动节日必须逐年下发具体日期—— 这是跨年维护工作量的根源两个方法insert双条件 upsertindex相同或date相同都覆盖与delete单个long-unsigned不是区间都是o本类没有enable/disable特殊日对常规日程是**覆盖override**关系“a special switching behaviour overrides the normal one”但它与Schedule的exec_specdays是两条独立链路同一个动作不要配两处。下一篇第 12 篇Association SNclass_id 12—— 前 11 篇讲的都是数据对象从下一篇起换一个维度在 SNshort name寻址方式下会话本身如何建模。一台设备可以为它能支持的每一个 AAapplication association各建一个Association SN实例object_list/access_rights_list/security_setup_reference/user_list/current_user等属性定义了这个客户端能看到什么、能做什么。这个类有 4 个 version0 / 1 / 2 / 4我们会按版本分节逐版说明新增了哪些属性与方法read_by_logicalname、change_secret、reply_to_HLS_authentication、add_user、remove_user等并解释为什么中间跳过了 version 3。参考资料DLMS UA《Blue Book Ed.16 Part 2 – COSEM interface classes》Special days table (class_id 11, version 0) 章节含 Overview、Attribute description 与spec_day_entry定义、Method description以及 Activity calendar (class_id 20, version 0) 章节 Overview 中关于Special days table优先级与day_id引用关系的描述、Schedule (class_id 10) 章节中exec_specdays的相关描述。文中 2 个属性的名称、static 标记、数据类型、Short name 偏移、2 个方法的名称与 m/o、Short name 偏移、以及全部英文引文均与原文一致原文未提供的项Min/Max/Def 取值、insert/delete后的生效时机、删除不存在index的行为、day_id下界与 bit 位序映射、条目自动过期机制已如实标注。示例中的 OBIS0-0:12.0.0.255/0-0:11.0.0.255/0-0:10.0.0.255/0-0:13.0.0.255、day_id取值、日期字节与报文字节为帮助理解而构造实际以设备对象列表与厂商文档为准。