bacnet_server.py 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. from __future__ import annotations
  2. from typing import Any, NotRequired, TypedDict
  3. from pydantic import BaseModel
  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_point_collect_test as api_bacnet_point_collect_test,
  12. bacnet_point_search as api_bacnet_point_search,
  13. )
  14. from .mcp_app import mcp
  15. class BacnetDeviceCreateItem(BaseModel):
  16. name: str
  17. ip: str
  18. bacnet_device_id: int
  19. port: int = 47808
  20. bacnet_net: int = 0
  21. asp_ip: str = ""
  22. timeout: int = 3
  23. is_persistent: bool = False
  24. group_id: int = 0
  25. alarm_interval: int = 90
  26. collect_interval: int = 5
  27. class BacnetPointSpec(TypedDict):
  28. object_type: str
  29. object_id: int
  30. name: NotRequired[str]
  31. description: NotRequired[str]
  32. class BacnetPointCreateItem(TypedDict):
  33. device_id: int
  34. object_type: str
  35. object_id: int
  36. name: NotRequired[str]
  37. object_name: NotRequired[str]
  38. point_id: NotRequired[str]
  39. priority: NotRequired[int | None]
  40. units: NotRequired[str]
  41. value_type: NotRequired[int]
  42. group_id: NotRequired[int]
  43. scale_ratio: NotRequired[float]
  44. value_offset: NotRequired[float]
  45. describe: NotRequired[str]
  46. @mcp.tool(
  47. name="bacnet.point_collect_test",
  48. description=(
  49. "通过采集网关读取 BACnet/IP 点位 present-value。调用 "
  50. "{base_url}/api/dc-gateway/bacnet/read_points。ip 为 BACnet/IP 设备地址,"
  51. "bacnet_device_id 为 BACnet 设备对象实例号,范围 0..4194303;port 默认 47808。"
  52. "points 每项必须包含 object_type 和 object_id;object_type 支持 AnalogInput 这类格式。"
  53. "如果传入 analogInput、analog-input 等常见写法,需要先转换为 AnalogInput 这类格式;"
  54. "网关响应也会规范为 AnalogInput 形式。"
  55. "响应 data.points[].present_value 为当前值;响应透传上游 JSON,code=0 表示业务成功。"
  56. ),
  57. )
  58. def bacnet_point_collect_test(
  59. project_key: str,
  60. ip: str,
  61. bacnet_device_id: int,
  62. points: list[BacnetPointSpec],
  63. port: int = 47808,
  64. ) -> dict[str, Any]:
  65. return api_bacnet_point_collect_test(
  66. project_key,
  67. ip=ip,
  68. bacnet_device_id=bacnet_device_id,
  69. port=port,
  70. points=points,
  71. )
  72. @mcp.tool(
  73. name="bacnet.point_search",
  74. description=(
  75. "通过采集网关搜索 BACnet/IP 设备点位。调用 "
  76. "{base_url}/api/dc-gateway/bacnet/search_points。ip 为 BACnet/IP 设备地址,"
  77. "bacnet_device_id 为 BACnet 设备对象实例号,范围 0..4194303;port 默认 47808。"
  78. "网关读取设备对象的 object-list,并过滤常见点位对象类型:AnalogInput、AnalogOutput、"
  79. "AnalogValue、BinaryInput、BinaryOutput、BinaryValue、MultiStateInput、MultiStateOutput、"
  80. "MultiStateValue。响应 data.points[] 包含 name、description、object_type、object_id、"
  81. "present_value;单点 description 或 present_value 读取失败时字段为 null。"
  82. "响应透传上游 JSON,code=0 表示业务成功。"
  83. ),
  84. )
  85. def bacnet_point_search(
  86. project_key: str,
  87. ip: str,
  88. bacnet_device_id: int,
  89. port: int = 47808,
  90. ) -> dict[str, Any]:
  91. return api_bacnet_point_search(
  92. project_key,
  93. ip=ip,
  94. bacnet_device_id=bacnet_device_id,
  95. port=port,
  96. )
  97. @mcp.tool(
  98. name="collector.bacnet_device_create",
  99. description=(
  100. "汇采-批量创建 BACnet 设备。依次调用 {data_collector_base_url}/api/collector/device,"
  101. "请求体固定 type=bacnet 且 device_type=1(ASP 设备)。全部设备创建调用完成后会内部调用"
  102. "设备列表并匹配每个设备 id;如果匹配到多个设备,选择 id 最大的。devices 每项必须传 "
  103. "name、ip、bacnet_device_id;port 默认 47808,bacnet_net 默认 0,asp_ip 默认空字符串。"
  104. "默认参数: timeout=3, is_persistent=false, group_id=0, alarm_interval=90, collect_interval=5。"
  105. "响应返回批量结果,state=0 表示全部创建并匹配成功。"
  106. ),
  107. )
  108. def collector_bacnet_device_create(
  109. project_key: str,
  110. devices: list[BacnetDeviceCreateItem],
  111. ) -> dict[str, Any]:
  112. return api_create_bacnet_devices(project_key, [_dump_model_or_dict(item) for item in devices])
  113. @mcp.tool(
  114. name="collector.bacnet_device_edit",
  115. description=(
  116. "汇采-编辑 BACnet 设备。调用 {data_collector_base_url}/api/collector/bacnet/device/edit。"
  117. "必须传 ori_id 原设备 id、name、ip、bacnet_device_id;MCP 会把 bacnet_device_id 映射为"
  118. "汇采旧接口的 device_id 字符串,并固定 type=1(ASP 设备)。编辑前设备不能处于已连接状态;"
  119. "若已连接,请先调用 collector.device_disconnect,并传 device_type=bacnet。该接口是全量更新语义。"
  120. "默认参数: port=47808, net=0, asp_ip='', timeout=3, is_persistent=false, "
  121. "device_group_id=0, alarm_interval=90, collect_interval=5。响应透传上游 JSON,state=0 表示业务成功。"
  122. ),
  123. )
  124. def collector_bacnet_device_edit(
  125. project_key: str,
  126. ori_id: int,
  127. name: str,
  128. ip: str,
  129. bacnet_device_id: int,
  130. port: int = 47808,
  131. bacnet_net: int = 0,
  132. asp_ip: str = "",
  133. is_persistent: bool = False,
  134. device_group_id: int = 0,
  135. timeout: int = 3,
  136. alarm_interval: int = 90,
  137. collect_interval: int = 5,
  138. ) -> dict[str, Any]:
  139. return api_edit_bacnet_device(
  140. project_key,
  141. {
  142. "ori_id": ori_id,
  143. "name": name,
  144. "ip": ip,
  145. "bacnet_device_id": bacnet_device_id,
  146. "port": port,
  147. "bacnet_net": bacnet_net,
  148. "asp_ip": asp_ip,
  149. "is_persistent": is_persistent,
  150. "device_group_id": device_group_id,
  151. "timeout": timeout,
  152. "alarm_interval": alarm_interval,
  153. "collect_interval": collect_interval,
  154. },
  155. )
  156. @mcp.tool(
  157. name="collector.bacnet_point_create",
  158. description=(
  159. "汇采-批量创建 BACnet 采集点位。依次调用 "
  160. "{data_collector_base_url}/api/collector/bacnet/point/add_collect_point。"
  161. "points 每项必须传 device_id、object_type、object_id,并传 name 或 object_name;"
  162. "object_type 支持 AnalogInput 这类格式;如果传入 analogInput、analog-input 等常见写法,"
  163. "需要先转换为 AnalogInput 这类格式;"
  164. "MCP 会将单个点位包装成汇采接口要求的 {device_id, points:[...]}。"
  165. "每个点位会补齐默认值: point_id='', priority=null, units='', value_type=0, "
  166. "group_id=0, scale_ratio=1, value_offset=0, describe=''。"
  167. "常见 value_type: 1=Boolean, 2=Unsigned, 3=Signed, 4=Real, 5=Double, 6=Enumerated。"
  168. "响应返回批量结果,state=0 表示全部创建成功。"
  169. ),
  170. )
  171. def collector_bacnet_point_create(
  172. project_key: str,
  173. points: list[BacnetPointCreateItem],
  174. ) -> dict[str, Any]:
  175. return api_create_bacnet_points(project_key, points)
  176. @mcp.tool(
  177. name="collector.bacnet_point_edit",
  178. description=(
  179. "汇采-编辑 BACnet 采集点位。调用 {data_collector_base_url}/api/collector/bacnet/point/edit。"
  180. "必须传 ori_id 原点位 id、object_type、object_id,并传 name 或 object_name;ori_id 对应 "
  181. "collector.device_points 返回的 data.point[].id,MCP 会映射为汇采接口字段 id。"
  182. "object_type 支持 AnalogInput 这类格式;如果传入 analogInput、analog-input 等常见写法,"
  183. "MCP 会先转换为 AnalogInput 这类格式再调用汇采接口。"
  184. "该接口是全量更新语义,未传字段可能被默认值覆盖。默认参数: point_id='', priority=null, "
  185. "units='', value_type=0, group_id=0, scale_ratio=1, value_offset=0, describe='', "
  186. "invalid_values='', valid_range_start=null, valid_range_end=null。"
  187. "响应透传上游 JSON,state=0 表示业务成功。"
  188. ),
  189. )
  190. def collector_bacnet_point_edit(
  191. project_key: str,
  192. ori_id: int,
  193. object_type: str,
  194. object_id: int,
  195. name: str = "",
  196. object_name: str = "",
  197. point_id: str = "",
  198. priority: int | None = None,
  199. units: str = "",
  200. value_type: int = 0,
  201. group_id: int = 0,
  202. scale_ratio: float = 1,
  203. value_offset: float = 0,
  204. describe: str = "",
  205. invalid_values: str = "",
  206. valid_range_start: float | None = None,
  207. valid_range_end: float | None = None,
  208. ) -> dict[str, Any]:
  209. return api_edit_bacnet_point(
  210. project_key,
  211. {
  212. "ori_id": ori_id,
  213. "name": name,
  214. "object_type": object_type,
  215. "object_id": object_id,
  216. "object_name": object_name,
  217. "point_id": point_id,
  218. "priority": priority,
  219. "units": units,
  220. "value_type": value_type,
  221. "group_id": group_id,
  222. "scale_ratio": scale_ratio,
  223. "value_offset": value_offset,
  224. "describe": describe,
  225. "invalid_values": invalid_values,
  226. "valid_range_start": valid_range_start,
  227. "valid_range_end": valid_range_end,
  228. },
  229. )
  230. def _dump_model_or_dict(item: Any) -> dict[str, Any]:
  231. if isinstance(item, BaseModel):
  232. return item.model_dump(exclude_none=True)
  233. return dict(item)