collector_api.py 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. from __future__ import annotations
  2. from typing import Any
  3. from .auth import find_project_config, resolve_project_token
  4. from .http_client import request_json
  5. from .protocols import MODBUS_SPEC
  6. from .protocols.modbus import MODBUS_POINT_TYPE_ALIASES, MODBUS_REGISTER_TYPE_ALIASES
  7. def _merge_defaults(defaults: dict[str, Any], payload: dict[str, Any]) -> dict[str, Any]:
  8. merged = dict(defaults)
  9. merged.update(payload)
  10. return merged
  11. def _require_non_empty_text(payload: dict[str, Any], field_name: str) -> str:
  12. value = str(payload.get(field_name) or "").strip()
  13. if not value:
  14. raise ValueError(f"payload.{field_name} is required")
  15. return value
  16. def _require_present(payload: dict[str, Any], field_name: str) -> Any:
  17. if field_name not in payload or payload.get(field_name) is None:
  18. raise ValueError(f"payload.{field_name} is required")
  19. return payload[field_name]
  20. def _normalize_modbus_device_payload(payload: dict[str, Any]) -> dict[str, Any]:
  21. normalized = dict(payload)
  22. normalized["name"] = _require_non_empty_text(normalized, "name")
  23. normalized["ip"] = _require_non_empty_text(normalized, "ip")
  24. _require_present(normalized, "device_type")
  25. _require_present(normalized, "port")
  26. _require_present(normalized, "slave_id")
  27. _require_present(normalized, "word_order")
  28. _require_present(normalized, "byte_order")
  29. address_base = _require_present(normalized, "address_base")
  30. normalized["address_offset"] = address_base
  31. normalized.pop("address_base", None)
  32. return normalized
  33. def _normalize_modbus_point_payload(payload: dict[str, Any]) -> dict[str, Any]:
  34. normalized = dict(payload)
  35. normalized["name"] = _require_non_empty_text(normalized, "name")
  36. _require_present(normalized, "address")
  37. raw_type = _require_non_empty_text(normalized, "type")
  38. normalized_type = MODBUS_POINT_TYPE_ALIASES.get(raw_type)
  39. if normalized_type is None:
  40. normalized_type = MODBUS_POINT_TYPE_ALIASES.get(raw_type.lower())
  41. if normalized_type is None:
  42. raise ValueError(
  43. "payload.type is invalid; use one of bool, int16, uint16, int32, "
  44. "uint32, int64, uint64, float32, float64, or a documented alias "
  45. "such as SHORT, WORD, LONG, DWORD, FLOAT, DOUBLE"
  46. )
  47. normalized["type"] = normalized_type
  48. if normalized.get("func_code") is None:
  49. register_type = _require_non_empty_text(normalized, "register_type")
  50. func_code = MODBUS_REGISTER_TYPE_ALIASES.get(register_type)
  51. if func_code is None:
  52. func_code = MODBUS_REGISTER_TYPE_ALIASES.get(register_type.lower())
  53. if func_code is None:
  54. raise ValueError(
  55. "payload.register_type is invalid; use coil, discrete_input, "
  56. "holding_register, input_register, or func_code 1/2/3/4"
  57. )
  58. normalized["func_code"] = func_code
  59. else:
  60. try:
  61. normalized["func_code"] = int(str(normalized["func_code"]).strip())
  62. except Exception as exc:
  63. raise ValueError("payload.func_code must be one of 1, 2, 3, 4") from exc
  64. if normalized["func_code"] not in {1, 2, 3, 4}:
  65. raise ValueError("payload.func_code must be one of 1, 2, 3, 4")
  66. if "register_type" in normalized:
  67. normalized.pop("register_type")
  68. return normalized
  69. def _request_collector(
  70. project_key: str,
  71. method: str,
  72. path: str,
  73. *,
  74. json_payload: dict[str, Any] | None = None,
  75. ) -> dict[str, Any]:
  76. project = find_project_config(project_key)
  77. authorization = resolve_project_token(project)
  78. response_payload = request_json(
  79. method,
  80. f"{project['data_collector_base_url']}{path}",
  81. authorization,
  82. json_payload=json_payload,
  83. )
  84. if not isinstance(response_payload, dict):
  85. raise ValueError(f"collector API returned invalid payload for {path}: {response_payload}")
  86. return response_payload
  87. def create_modbus_device(project_key: str, payload: dict[str, Any]) -> dict[str, Any]:
  88. return _request_collector(
  89. project_key,
  90. "POST",
  91. MODBUS_SPEC.create_device_path,
  92. json_payload=_merge_defaults(
  93. MODBUS_SPEC.device_defaults,
  94. _normalize_modbus_device_payload(payload),
  95. ),
  96. )
  97. def create_modbus_point(project_key: str, payload: dict[str, Any]) -> dict[str, Any]:
  98. return _request_collector(
  99. project_key,
  100. "POST",
  101. MODBUS_SPEC.create_point_path,
  102. json_payload=_merge_defaults(
  103. MODBUS_SPEC.point_defaults,
  104. _normalize_modbus_point_payload(payload),
  105. ),
  106. )
  107. def list_devices(project_key: str, num_points: bool = False) -> dict[str, Any]:
  108. num_points_text = "true" if num_points else "false"
  109. return _request_collector(
  110. project_key,
  111. "GET",
  112. f"/api/collector/device?num_points={num_points_text}",
  113. )