bacnet_server.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  1. from __future__ import annotations
  2. from typing import Annotated, Any
  3. from pydantic import BaseModel, ConfigDict, Field
  4. from .collector_api import (
  5. create_bacnet_devices as api_create_bacnet_devices,
  6. create_bacnet_points as api_create_bacnet_points,
  7. edit_bacnet_device as api_edit_bacnet_device,
  8. edit_bacnet_point as api_edit_bacnet_point,
  9. )
  10. from .gateway_api import (
  11. bacnet_bbmd_whois as api_bacnet_bbmd_whois,
  12. bacnet_point_collect_test as api_bacnet_point_collect_test,
  13. bacnet_point_search as api_bacnet_point_search,
  14. )
  15. from .mcp_app import mcp
  16. JsonScalar = str | int | float | bool | None
  17. ProjectKeyParam = Annotated[str, Field(description="项目标识,来自 project.list 返回的 project_key。")]
  18. BacnetIpParam = Annotated[str, Field(description="BACnet/IP 设备地址。")]
  19. BacnetDeviceIdParam = Annotated[int, Field(description="BACnet 设备对象实例号,范围 0..4194303。")]
  20. BacnetPortParam = Annotated[int, Field(description="BACnet/IP UDP 端口,默认 47808。")]
  21. BacnetObjectTypeParam = Annotated[
  22. str,
  23. Field(description="BACnet 对象类型;支持 AnalogInput、analogInput、analog-input 等常见写法。"),
  24. ]
  25. BacnetObjectIdParam = Annotated[int, Field(description="BACnet 对象实例号,范围 0..4194303。")]
  26. class FlexibleModel(BaseModel):
  27. model_config = ConfigDict(extra="allow")
  28. class BacnetDeviceCreateItem(BaseModel):
  29. name: str = Field(description="设备名称。")
  30. ip: str = Field(description="BACnet/IP 设备地址。")
  31. bacnet_device_id: int = Field(description="BACnet 设备对象实例号,范围 0..4194303。")
  32. port: int = Field(default=47808, description="BACnet/IP UDP 端口。")
  33. bacnet_net: int = Field(default=0, description="BACnet 网络号;ASP 设备通常为 0。")
  34. asp_ip: str = Field(default="", description="RP 设备关联 ASP 地址;ASP 设备通常为空。")
  35. timeout: int = Field(default=3, description="连接超时时间。")
  36. is_persistent: bool = Field(default=False, description="是否持久化连接。")
  37. group_id: int = Field(default=0, description="设备分组 id。")
  38. alarm_interval: int = Field(default=90, description="告警间隔,单位秒。")
  39. collect_interval: int = Field(default=5, description="采集周期,单位秒。")
  40. class BacnetPointSpec(BaseModel):
  41. object_type: BacnetObjectTypeParam
  42. object_id: BacnetObjectIdParam
  43. name: str | None = Field(default=None, description="点位名称;读取当前值时可不传。")
  44. description: str | None = Field(default=None, description="点位描述;读取当前值时可不传。")
  45. class BacnetPointCreateItem(BaseModel):
  46. device_id: int = Field(description="所属 BACnet 设备 id,来自 collector.device_list 或设备创建结果。")
  47. object_type: BacnetObjectTypeParam
  48. object_id: BacnetObjectIdParam
  49. name: str = Field(default="", description="点位名称;为空时可由 object_name 补齐。")
  50. object_name: str = Field(default="", description="BACnet 对象名;为空时可由 name 补齐。")
  51. point_id: str = Field(default="", description="外部点位编码。")
  52. priority: int | None = Field(default=None, description="写值优先级;读取点位通常为 null。")
  53. value_type: int = Field(
  54. default=0,
  55. description="BACnet PresentValue 类型;常见值:1=Boolean,2=Unsigned,3=Signed,4=Real,5=Double,6=Enumerated。",
  56. )
  57. scale_ratio: float = Field(default=1, description="缩放系数。")
  58. value_offset: float = Field(default=0, description="值偏移。")
  59. group_id: int = Field(default=0, description="点位分组 id。")
  60. invalid_values: str = Field(default="", description="无效值列表,多个值用逗号分隔。")
  61. valid_range_start: float | None = Field(default=None, description="合法范围最小值。")
  62. valid_range_end: float | None = Field(default=None, description="合法范围最大值。")
  63. describe: str = Field(default="", description="点位描述。")
  64. class GatewayBacnetDevice(FlexibleModel):
  65. device_type: str | None = Field(default=None, description="设备类型,通常为 BACnet/IP。")
  66. ip: str | None = Field(default=None, description="BACnet/IP 设备地址。")
  67. port: int | None = Field(default=None, description="BACnet/IP UDP 端口。")
  68. bacnet_device_id: int | None = Field(default=None, description="BACnet 设备对象实例号。")
  69. class GatewayBacnetPoint(FlexibleModel):
  70. name: str | None = Field(default=None, description="点位名称;点位搜索时由 object-name 读取。")
  71. description: str | None = Field(default=None, description="点位描述;点位搜索时由 description 读取。")
  72. object_type: str | None = Field(default=None, description="BACnet 对象类型。")
  73. object_id: int | None = Field(default=None, description="BACnet 对象实例号。")
  74. present_value: JsonScalar = Field(default=None, description="当前值;读取 BACnet present-value 得到。")
  75. class GatewayBacnetPointsData(FlexibleModel):
  76. device: GatewayBacnetDevice | None = Field(default=None, description="本次读取或搜索的 BACnet 设备信息。")
  77. points: list[GatewayBacnetPoint] = Field(default_factory=list, description="BACnet 点位列表。")
  78. class BacnetGatewayPointsOutput(FlexibleModel):
  79. code: int | str | None = Field(default=None, description="网关状态码;0 表示成功。")
  80. msg: str | None = Field(default=None, description="网关状态说明或错误信息。")
  81. data: GatewayBacnetPointsData | None = Field(default=None, description="BACnet 点位读取或搜索结果。")
  82. class BacnetBbmdConfig(FlexibleModel):
  83. bbmd_ip: str | None = Field(default=None, description="BBMD 地址。")
  84. bbmd_port: int | None = Field(default=None, description="BBMD UDP 端口。")
  85. ttl: int | None = Field(default=None, description="Foreign Device 注册 TTL,单位秒。")
  86. timeout: int | None = Field(default=None, description="收集 I-Am 响应的等待时间,单位秒。")
  87. low_limit: int | None = Field(default=None, description="Who-Is 设备号下限。")
  88. high_limit: int | None = Field(default=None, description="Who-Is 设备号上限。")
  89. class BacnetDiscoveredDevice(FlexibleModel):
  90. bacnet_device_id: int | None = Field(default=None, description="发现的 BACnet 设备对象实例号。")
  91. ip: str | None = Field(default=None, description="发现的 BACnet/IP 设备地址。")
  92. port: int | None = Field(default=None, description="发现的 BACnet/IP UDP 端口。")
  93. max_apdu: int | None = Field(default=None, description="设备支持的最大 APDU 长度。")
  94. segmentation: str | None = Field(default=None, description="设备分段能力。")
  95. vendor_id: int | None = Field(default=None, description="BACnet 厂商 id。")
  96. class BacnetBbmdWhoisData(FlexibleModel):
  97. bbmd: BacnetBbmdConfig | None = Field(default=None, description="本次 BBMD Who-Is 使用的配置。")
  98. devices: list[BacnetDiscoveredDevice] = Field(default_factory=list, description="发现的 BACnet 设备列表。")
  99. class BacnetBbmdWhoisOutput(FlexibleModel):
  100. code: int | str | None = Field(default=None, description="网关状态码;0 表示成功。")
  101. msg: str | None = Field(default=None, description="网关状态说明或错误信息。")
  102. data: BacnetBbmdWhoisData | None = Field(default=None, description="BBMD Who-Is 设备发现结果。")
  103. class CollectorBaseOutput(FlexibleModel):
  104. state: int | str | None = Field(default=None, description="汇采状态码;0 表示成功。")
  105. state_info: str | None = Field(default=None, description="汇采状态说明。")
  106. class BatchError(FlexibleModel):
  107. index: int | None = Field(default=None, description="输入数组中的下标。")
  108. name: str | None = Field(default=None, description="输入项名称。")
  109. stage: str | None = Field(default=None, description="失败阶段。")
  110. error: str | None = Field(default=None, description="失败原因。")
  111. class BacnetDeviceCreateSummary(FlexibleModel):
  112. total: int | None = Field(default=None, description="输入设备总数。")
  113. created: int | None = Field(default=None, description="汇采创建设备成功数量。")
  114. matched: int | None = Field(default=None, description="创建后从设备列表匹配到设备 id 的数量。")
  115. failed: int | None = Field(default=None, description="失败数量。")
  116. class BacnetDeviceCreateResult(FlexibleModel):
  117. index: int | None = Field(default=None, description="输入 devices 数组中的下标。")
  118. name: str | None = Field(default=None, description="设备名称。")
  119. device_id: int | None = Field(default=None, description="匹配到的汇采设备 id。")
  120. create_response: dict[str, Any] | None = Field(default=None, description="汇采创建设备接口原始响应。")
  121. matched_device: dict[str, Any] | None = Field(default=None, description="设备列表中匹配到的设备详情。")
  122. class BacnetDeviceCreateOutput(FlexibleModel):
  123. state: int | str | None = Field(default=None, description="批量创建状态;0 表示全部成功,1 表示存在失败。")
  124. summary: BacnetDeviceCreateSummary | None = Field(default=None, description="批量创建设备汇总。")
  125. results: list[BacnetDeviceCreateResult] = Field(default_factory=list, description="每个设备的创建和匹配结果。")
  126. errors: list[BatchError] = Field(default_factory=list, description="失败项列表。")
  127. class BacnetPointCreateSummary(FlexibleModel):
  128. total: int | None = Field(default=None, description="输入点位总数。")
  129. success: int | None = Field(default=None, description="汇采创建点位成功数量。")
  130. failed: int | None = Field(default=None, description="失败数量。")
  131. class BacnetPointCreateResult(FlexibleModel):
  132. index: int | None = Field(default=None, description="输入 points 数组中的下标。")
  133. name: str | None = Field(default=None, description="点位名称。")
  134. device_id: int | None = Field(default=None, description="所属 BACnet 设备 id。")
  135. response: dict[str, Any] | None = Field(default=None, description="汇采创建点位接口原始响应。")
  136. class BacnetPointCreateOutput(FlexibleModel):
  137. state: int | str | None = Field(default=None, description="批量创建状态;0 表示全部成功,1 表示存在失败。")
  138. summary: BacnetPointCreateSummary | None = Field(default=None, description="批量创建点位汇总。")
  139. results: list[BacnetPointCreateResult] = Field(default_factory=list, description="每个点位的创建结果。")
  140. errors: list[BatchError] = Field(default_factory=list, description="失败项列表。")
  141. @mcp.tool(
  142. name="bacnet.point_collect_test",
  143. description=(
  144. "采集网关-读取 BACnet/IP 设备点位的当前值。"
  145. ),
  146. )
  147. def bacnet_point_collect_test(
  148. project_key: ProjectKeyParam,
  149. ip: BacnetIpParam,
  150. bacnet_device_id: BacnetDeviceIdParam,
  151. points: Annotated[list[BacnetPointSpec], Field(description="要读取的 BACnet 点位列表。")],
  152. port: BacnetPortParam = 47808,
  153. ) -> BacnetGatewayPointsOutput:
  154. return api_bacnet_point_collect_test(
  155. project_key,
  156. ip=ip,
  157. bacnet_device_id=bacnet_device_id,
  158. port=port,
  159. points=[_dump_model_or_dict(item) for item in points],
  160. )
  161. @mcp.tool(
  162. name="bacnet.point_search",
  163. description=(
  164. "采集网关-搜索 BACnet/IP 设备点位"
  165. ),
  166. )
  167. def bacnet_point_search(
  168. project_key: ProjectKeyParam,
  169. ip: BacnetIpParam,
  170. bacnet_device_id: BacnetDeviceIdParam,
  171. port: BacnetPortParam = 47808,
  172. ) -> BacnetGatewayPointsOutput:
  173. return api_bacnet_point_search(
  174. project_key,
  175. ip=ip,
  176. bacnet_device_id=bacnet_device_id,
  177. port=port,
  178. )
  179. @mcp.tool(
  180. name="bacnet.bbmd_whois",
  181. description=(
  182. "采集网关-通过 BBMD 执行 BACnet/IP Who-Is 设备发现。"
  183. ),
  184. )
  185. def bacnet_bbmd_whois(project_key: ProjectKeyParam) -> BacnetBbmdWhoisOutput:
  186. return api_bacnet_bbmd_whois(project_key)
  187. @mcp.tool(
  188. name="collector.bacnet_device_create",
  189. description=(
  190. "汇采-批量创建 BACnet 设备。创建后会读取设备列表并匹配返回设备 id。"
  191. ),
  192. )
  193. def collector_bacnet_device_create(
  194. project_key: ProjectKeyParam,
  195. devices: Annotated[list[BacnetDeviceCreateItem], Field(description="要创建的 BACnet 设备列表。")],
  196. ) -> BacnetDeviceCreateOutput:
  197. return api_create_bacnet_devices(project_key, [_dump_model_or_dict(item) for item in devices])
  198. @mcp.tool(
  199. name="collector.bacnet_device_edit",
  200. description=(
  201. "汇采-编辑 BACnet 设备。编辑前设备不能处于已连接状态;"
  202. "若已连接请先调用 collector.device_disconnect 且 device_type=bacnet。"
  203. ),
  204. )
  205. def collector_bacnet_device_edit(
  206. project_key: ProjectKeyParam,
  207. id: Annotated[int, Field(description="要编辑的原汇采设备 id。")],
  208. name: Annotated[str, Field(description="设备名称。")],
  209. ip: BacnetIpParam,
  210. bacnet_device_id: BacnetDeviceIdParam,
  211. port: BacnetPortParam = 47808,
  212. bacnet_net: Annotated[int, Field(description="BACnet 网络号;ASP 设备通常为 0。")] = 0,
  213. asp_ip: Annotated[str, Field(description="RP 设备关联 ASP 地址;ASP 设备通常为空。")] = "",
  214. is_persistent: Annotated[bool, Field(description="是否持久化连接。")] = False,
  215. device_group_id: Annotated[int, Field(description="设备分组 id。")] = 0,
  216. timeout: Annotated[int, Field(description="连接超时时间,默认3秒。")] = 3,
  217. alarm_interval: Annotated[int, Field(description="告警间隔,默认90秒。")] = 90,
  218. collect_interval: Annotated[int, Field(description="采集周期,默认5秒。")] = 5,
  219. ) -> CollectorBaseOutput:
  220. return api_edit_bacnet_device(
  221. project_key,
  222. {
  223. "ori_id": id,
  224. "name": name,
  225. "ip": ip,
  226. "bacnet_device_id": bacnet_device_id,
  227. "port": port,
  228. "bacnet_net": bacnet_net,
  229. "asp_ip": asp_ip,
  230. "is_persistent": is_persistent,
  231. "device_group_id": device_group_id,
  232. "timeout": timeout,
  233. "alarm_interval": alarm_interval,
  234. "collect_interval": collect_interval,
  235. },
  236. )
  237. @mcp.tool(
  238. name="collector.bacnet_point_create",
  239. description=(
  240. "汇采-批量创建 BACnet 采集点位。"
  241. ),
  242. )
  243. def collector_bacnet_point_create(
  244. project_key: ProjectKeyParam,
  245. points: Annotated[list[BacnetPointCreateItem], Field(description="要创建的 BACnet 采集点位列表。")],
  246. ) -> BacnetPointCreateOutput:
  247. return api_create_bacnet_points(project_key, [_dump_model_or_dict(item) for item in points])
  248. @mcp.tool(
  249. name="collector.bacnet_point_edit",
  250. description=(
  251. "汇采-编辑 BACnet 采集点位。对象类型会在调用汇采接口前规范化。"
  252. ),
  253. )
  254. def collector_bacnet_point_edit(
  255. project_key: ProjectKeyParam,
  256. id: Annotated[int, Field(description="要编辑的原采集点位 id,对应 collector.device_points 返回的点位 id。")],
  257. object_type: BacnetObjectTypeParam,
  258. object_id: BacnetObjectIdParam,
  259. name: Annotated[str, Field(description="点位名称;为空时可由 object_name 补齐。")] = "",
  260. object_name: Annotated[str, Field(description="BACnet 对象名;为空时可由 name 补齐。")] = "",
  261. point_id: Annotated[str, Field(description="外部点位编码。")] = "",
  262. priority: Annotated[int | None, Field(description="写值优先级;读取点位通常为 null。")] = None,
  263. value_type: Annotated[
  264. int,
  265. Field(description="BACnet PresentValue 类型;常见值:1=Boolean,2=Unsigned,3=Signed,4=Real,5=Double,6=Enumerated。"),
  266. ] = 0,
  267. scale_ratio: Annotated[float, Field(description="缩放系数。")] = 1,
  268. value_offset: Annotated[float, Field(description="值偏移。")] = 0,
  269. group_id: Annotated[int, Field(description="点位分组 id。")] = 0,
  270. invalid_values: Annotated[str, Field(description="无效值列表,多个值用逗号分隔。")] = "",
  271. valid_range_start: Annotated[float | None, Field(description="合法范围最小值。")] = None,
  272. valid_range_end: Annotated[float | None, Field(description="合法范围最大值。")] = None,
  273. describe: Annotated[str, Field(description="点位描述。")] = "",
  274. ) -> CollectorBaseOutput:
  275. return api_edit_bacnet_point(
  276. project_key,
  277. {
  278. "id": id,
  279. "name": name,
  280. "object_type": object_type,
  281. "object_id": object_id,
  282. "object_name": object_name,
  283. "point_id": point_id,
  284. "priority": priority,
  285. "value_type": value_type,
  286. "scale_ratio": scale_ratio,
  287. "value_offset": value_offset,
  288. "group_id": group_id,
  289. "invalid_values": invalid_values,
  290. "valid_range_start": valid_range_start,
  291. "valid_range_end": valid_range_end,
  292. "describe": describe,
  293. },
  294. )
  295. def _dump_model_or_dict(item: Any) -> dict[str, Any]:
  296. if isinstance(item, BaseModel):
  297. return item.model_dump(exclude_none=True)
  298. return dict(item)