|
|
@@ -15,6 +15,10 @@ def _merge_defaults(defaults: dict[str, Any], payload: dict[str, Any]) -> dict[s
|
|
|
return merged
|
|
|
|
|
|
|
|
|
+def _success_response(response: dict[str, Any]) -> bool:
|
|
|
+ return response.get("state") == 0
|
|
|
+
|
|
|
+
|
|
|
def _require_non_empty_text(payload: dict[str, Any], field_name: str) -> str:
|
|
|
value = str(payload.get(field_name) or "").strip()
|
|
|
if not value:
|
|
|
@@ -81,11 +85,16 @@ def _normalize_modbus_device_edit_payload(payload: dict[str, Any]) -> dict[str,
|
|
|
return normalized
|
|
|
|
|
|
|
|
|
-def _normalize_modbus_point_payload(payload: dict[str, Any]) -> dict[str, Any]:
|
|
|
+def _normalize_modbus_point_payload(payload: dict[str, Any], *, require_device_id: bool = True) -> dict[str, Any]:
|
|
|
normalized = dict(payload)
|
|
|
|
|
|
normalized["name"] = _require_non_empty_text(normalized, "name")
|
|
|
_require_present(normalized, "address")
|
|
|
+ if require_device_id:
|
|
|
+ normalized["device_id"] = _normalize_positive_int(
|
|
|
+ _require_present(normalized, "device_id"),
|
|
|
+ "payload.device_id",
|
|
|
+ )
|
|
|
|
|
|
raw_type = _require_non_empty_text(normalized, "type")
|
|
|
normalized_type = MODBUS_POINT_TYPE_ALIASES.get(raw_type)
|
|
|
@@ -126,7 +135,7 @@ def _normalize_modbus_point_payload(payload: dict[str, Any]) -> dict[str, Any]:
|
|
|
|
|
|
def _normalize_modbus_point_edit_payload(payload: dict[str, Any]) -> dict[str, Any]:
|
|
|
_require_present(payload, "ori_id")
|
|
|
- normalized = _normalize_modbus_point_payload(payload)
|
|
|
+ normalized = _normalize_modbus_point_payload(payload, require_device_id=False)
|
|
|
normalized["ori_id"] = _normalize_positive_int(_require_present(normalized, "ori_id"), "payload.ori_id")
|
|
|
return normalized
|
|
|
|
|
|
@@ -141,8 +150,12 @@ def _normalize_s7_device_payload(payload: dict[str, Any]) -> dict[str, Any]:
|
|
|
if "group_id" in normalized and "device_group_id" not in normalized:
|
|
|
normalized["device_group_id"] = normalized["group_id"]
|
|
|
normalized.pop("group_id", None)
|
|
|
- if "tsap_conn_type" in normalized:
|
|
|
+ if "tsap_conn_type" in normalized and normalized["tsap_conn_type"] is not None:
|
|
|
normalized["tsap_conn_type"] = _normalize_s7_tsap_conn_type(normalized["tsap_conn_type"])
|
|
|
+ elif int(normalized.get("device_type", 1)) == 3:
|
|
|
+ normalized["tsap_conn_type"] = "OP"
|
|
|
+ else:
|
|
|
+ normalized["tsap_conn_type"] = "PG"
|
|
|
|
|
|
return normalized
|
|
|
|
|
|
@@ -239,15 +252,40 @@ def _request_collector(
|
|
|
return response_payload
|
|
|
|
|
|
|
|
|
+def _build_modbus_device_create_payload(payload: dict[str, Any]) -> dict[str, Any]:
|
|
|
+ return _merge_defaults(
|
|
|
+ MODBUS_SPEC.device_defaults,
|
|
|
+ _normalize_modbus_device_payload(payload),
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+def _build_modbus_point_create_payload(payload: dict[str, Any]) -> dict[str, Any]:
|
|
|
+ return _merge_defaults(
|
|
|
+ MODBUS_SPEC.point_defaults,
|
|
|
+ _normalize_modbus_point_payload(payload),
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+def _build_s7_device_create_payload(payload: dict[str, Any]) -> dict[str, Any]:
|
|
|
+ return _merge_defaults(
|
|
|
+ S7_SPEC.device_defaults,
|
|
|
+ _normalize_s7_device_payload(payload),
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+def _build_s7_point_create_payload(payload: dict[str, Any]) -> dict[str, Any]:
|
|
|
+ return _merge_defaults(
|
|
|
+ S7_SPEC.point_defaults,
|
|
|
+ _normalize_s7_point_payload(payload),
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
def create_modbus_device(project_key: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
|
return _request_collector(
|
|
|
project_key,
|
|
|
"POST",
|
|
|
MODBUS_SPEC.create_device_path,
|
|
|
- json_payload=_merge_defaults(
|
|
|
- MODBUS_SPEC.device_defaults,
|
|
|
- _normalize_modbus_device_payload(payload),
|
|
|
- ),
|
|
|
+ json_payload=_build_modbus_device_create_payload(payload),
|
|
|
)
|
|
|
|
|
|
|
|
|
@@ -256,10 +294,7 @@ def create_modbus_point(project_key: str, payload: dict[str, Any]) -> dict[str,
|
|
|
project_key,
|
|
|
"POST",
|
|
|
MODBUS_SPEC.create_point_path,
|
|
|
- json_payload=_merge_defaults(
|
|
|
- MODBUS_SPEC.point_defaults,
|
|
|
- _normalize_modbus_point_payload(payload),
|
|
|
- ),
|
|
|
+ json_payload=_build_modbus_point_create_payload(payload),
|
|
|
)
|
|
|
|
|
|
|
|
|
@@ -268,10 +303,7 @@ def create_s7_device(project_key: str, payload: dict[str, Any]) -> dict[str, Any
|
|
|
project_key,
|
|
|
"POST",
|
|
|
S7_SPEC.create_device_path,
|
|
|
- json_payload=_merge_defaults(
|
|
|
- S7_SPEC.device_defaults,
|
|
|
- _normalize_s7_device_payload(payload),
|
|
|
- ),
|
|
|
+ json_payload=_build_s7_device_create_payload(payload),
|
|
|
)
|
|
|
|
|
|
|
|
|
@@ -280,13 +312,258 @@ def create_s7_point(project_key: str, payload: dict[str, Any]) -> dict[str, Any]
|
|
|
project_key,
|
|
|
"POST",
|
|
|
S7_SPEC.create_point_path,
|
|
|
- json_payload=_merge_defaults(
|
|
|
- S7_SPEC.point_defaults,
|
|
|
- _normalize_s7_point_payload(payload),
|
|
|
- ),
|
|
|
+ json_payload=_build_s7_point_create_payload(payload),
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+def create_modbus_devices(project_key: str, devices: list[dict[str, Any]]) -> dict[str, Any]:
|
|
|
+ return _create_devices_batch(
|
|
|
+ project_key,
|
|
|
+ devices,
|
|
|
+ create_path=MODBUS_SPEC.create_device_path,
|
|
|
+ build_payload=_build_modbus_device_create_payload,
|
|
|
+ match_device=_match_modbus_device,
|
|
|
)
|
|
|
|
|
|
|
|
|
+def create_modbus_points(project_key: str, points: list[dict[str, Any]]) -> dict[str, Any]:
|
|
|
+ return _create_points_batch(
|
|
|
+ project_key,
|
|
|
+ points,
|
|
|
+ create_path=MODBUS_SPEC.create_point_path,
|
|
|
+ build_payload=_build_modbus_point_create_payload,
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+def create_s7_devices(project_key: str, devices: list[dict[str, Any]]) -> dict[str, Any]:
|
|
|
+ return _create_devices_batch(
|
|
|
+ project_key,
|
|
|
+ devices,
|
|
|
+ create_path=S7_SPEC.create_device_path,
|
|
|
+ build_payload=_build_s7_device_create_payload,
|
|
|
+ match_device=_match_s7_device,
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+def create_s7_points(project_key: str, points: list[dict[str, Any]]) -> dict[str, Any]:
|
|
|
+ return _create_points_batch(
|
|
|
+ project_key,
|
|
|
+ points,
|
|
|
+ create_path=S7_SPEC.create_point_path,
|
|
|
+ build_payload=_build_s7_point_create_payload,
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+def _create_devices_batch(
|
|
|
+ project_key: str,
|
|
|
+ devices: list[dict[str, Any]],
|
|
|
+ *,
|
|
|
+ create_path: str,
|
|
|
+ build_payload: Any,
|
|
|
+ match_device: Any,
|
|
|
+) -> dict[str, Any]:
|
|
|
+ results: list[dict[str, Any]] = []
|
|
|
+ errors: list[dict[str, Any]] = []
|
|
|
+ created: list[tuple[dict[str, Any], dict[str, Any]]] = []
|
|
|
+
|
|
|
+ for index, item in enumerate(devices):
|
|
|
+ if not isinstance(item, dict):
|
|
|
+ errors.append({"index": index, "stage": "create_device", "error": "device item must be an object"})
|
|
|
+ continue
|
|
|
+
|
|
|
+ result: dict[str, Any] = {
|
|
|
+ "index": index,
|
|
|
+ "name": item.get("name"),
|
|
|
+ "device_id": None,
|
|
|
+ "create_response": None,
|
|
|
+ "matched_device": None,
|
|
|
+ }
|
|
|
+ results.append(result)
|
|
|
+
|
|
|
+ try:
|
|
|
+ request_payload = build_payload(item)
|
|
|
+ response = _request_collector(project_key, "POST", create_path, json_payload=request_payload)
|
|
|
+ except Exception as exc:
|
|
|
+ errors.append({"index": index, "name": item.get("name"), "stage": "create_device", "error": str(exc)})
|
|
|
+ continue
|
|
|
+
|
|
|
+ result["create_response"] = response
|
|
|
+ if _success_response(response):
|
|
|
+ created.append((request_payload, result))
|
|
|
+ else:
|
|
|
+ errors.append(
|
|
|
+ {
|
|
|
+ "index": index,
|
|
|
+ "name": item.get("name"),
|
|
|
+ "stage": "create_device",
|
|
|
+ "error": response.get("state_info") or response.get("msg") or str(response),
|
|
|
+ }
|
|
|
+ )
|
|
|
+
|
|
|
+ if created:
|
|
|
+ try:
|
|
|
+ device_list = list_devices(project_key, num_points=False)
|
|
|
+ flattened_devices = _flatten_device_tree(device_list.get("devices", []))
|
|
|
+ except Exception as exc:
|
|
|
+ for _, result in created:
|
|
|
+ errors.append(
|
|
|
+ {
|
|
|
+ "index": result["index"],
|
|
|
+ "name": result.get("name"),
|
|
|
+ "stage": "match_device",
|
|
|
+ "error": str(exc),
|
|
|
+ }
|
|
|
+ )
|
|
|
+ else:
|
|
|
+ for request_payload, result in created:
|
|
|
+ candidates = [item for item in flattened_devices if match_device(item, request_payload)]
|
|
|
+ candidates = [item for item in candidates if _safe_int(item.get("id")) is not None]
|
|
|
+ if not candidates:
|
|
|
+ errors.append(
|
|
|
+ {
|
|
|
+ "index": result["index"],
|
|
|
+ "name": result.get("name"),
|
|
|
+ "stage": "match_device",
|
|
|
+ "error": "created device was not found in device list",
|
|
|
+ }
|
|
|
+ )
|
|
|
+ continue
|
|
|
+ selected = max(candidates, key=lambda item: _safe_int(item.get("id")) or 0)
|
|
|
+ result["device_id"] = _safe_int(selected.get("id"))
|
|
|
+ result["matched_device"] = selected
|
|
|
+
|
|
|
+ matched = sum(1 for item in results if item.get("device_id") is not None)
|
|
|
+ return {
|
|
|
+ "state": 0 if not errors else 1,
|
|
|
+ "summary": {
|
|
|
+ "total": len(devices),
|
|
|
+ "created": len(created),
|
|
|
+ "matched": matched,
|
|
|
+ "failed": len(errors),
|
|
|
+ },
|
|
|
+ "results": results,
|
|
|
+ "errors": errors,
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+def _create_points_batch(
|
|
|
+ project_key: str,
|
|
|
+ points: list[dict[str, Any]],
|
|
|
+ *,
|
|
|
+ create_path: str,
|
|
|
+ build_payload: Any,
|
|
|
+) -> dict[str, Any]:
|
|
|
+ results: list[dict[str, Any]] = []
|
|
|
+ errors: list[dict[str, Any]] = []
|
|
|
+
|
|
|
+ for index, item in enumerate(points):
|
|
|
+ if not isinstance(item, dict):
|
|
|
+ errors.append({"index": index, "stage": "create_point", "error": "point item must be an object"})
|
|
|
+ continue
|
|
|
+
|
|
|
+ result: dict[str, Any] = {
|
|
|
+ "index": index,
|
|
|
+ "name": item.get("name"),
|
|
|
+ "device_id": item.get("device_id"),
|
|
|
+ "response": None,
|
|
|
+ }
|
|
|
+ results.append(result)
|
|
|
+
|
|
|
+ try:
|
|
|
+ request_payload = build_payload(item)
|
|
|
+ response = _request_collector(project_key, "POST", create_path, json_payload=request_payload)
|
|
|
+ except Exception as exc:
|
|
|
+ errors.append({"index": index, "name": item.get("name"), "stage": "create_point", "error": str(exc)})
|
|
|
+ continue
|
|
|
+
|
|
|
+ result["response"] = response
|
|
|
+ if not _success_response(response):
|
|
|
+ errors.append(
|
|
|
+ {
|
|
|
+ "index": index,
|
|
|
+ "name": item.get("name"),
|
|
|
+ "stage": "create_point",
|
|
|
+ "error": response.get("state_info") or response.get("msg") or str(response),
|
|
|
+ }
|
|
|
+ )
|
|
|
+
|
|
|
+ success = sum(1 for item in results if isinstance(item.get("response"), dict) and _success_response(item["response"]))
|
|
|
+ return {
|
|
|
+ "state": 0 if not errors else 1,
|
|
|
+ "summary": {
|
|
|
+ "total": len(points),
|
|
|
+ "success": success,
|
|
|
+ "failed": len(errors),
|
|
|
+ },
|
|
|
+ "results": results,
|
|
|
+ "errors": errors,
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+def _flatten_device_tree(items: list[Any]) -> list[dict[str, Any]]:
|
|
|
+ flattened: list[dict[str, Any]] = []
|
|
|
+ for item in items:
|
|
|
+ if not isinstance(item, dict):
|
|
|
+ continue
|
|
|
+ flattened.append(item)
|
|
|
+ groups = item.get("groups", [])
|
|
|
+ if isinstance(groups, list):
|
|
|
+ flattened.extend(_flatten_device_tree(groups))
|
|
|
+ return flattened
|
|
|
+
|
|
|
+
|
|
|
+def _match_modbus_device(device: dict[str, Any], expected: dict[str, Any]) -> bool:
|
|
|
+ if device.get("type") != "modbus":
|
|
|
+ return False
|
|
|
+ if not _same_text(device.get("name"), expected.get("name")):
|
|
|
+ return False
|
|
|
+ if not _same_int(device.get("device_type"), expected.get("device_type")):
|
|
|
+ return False
|
|
|
+ if not _same_int(device.get("slave_id"), expected.get("slave_id")):
|
|
|
+ return False
|
|
|
+ if not _same_int(device.get("group_id"), expected.get("group_id")):
|
|
|
+ return False
|
|
|
+ if not _same_int(device.get("address_offset"), expected.get("address_offset")):
|
|
|
+ return False
|
|
|
+
|
|
|
+ if _safe_int(expected.get("device_type")) == 2:
|
|
|
+ return _same_text(device.get("serial_port"), expected.get("serial_port"))
|
|
|
+ return _same_text(device.get("ip"), expected.get("ip")) and _same_int(device.get("port"), expected.get("port"))
|
|
|
+
|
|
|
+
|
|
|
+def _match_s7_device(device: dict[str, Any], expected: dict[str, Any]) -> bool:
|
|
|
+ if device.get("type") != "s7":
|
|
|
+ return False
|
|
|
+ actual_group_id = device.get("device_group_id", device.get("group_id"))
|
|
|
+ return (
|
|
|
+ _same_text(device.get("name"), expected.get("name"))
|
|
|
+ and _same_text(device.get("ip"), expected.get("ip"))
|
|
|
+ and _same_int(device.get("port"), expected.get("port"))
|
|
|
+ and _same_int(device.get("rock"), expected.get("rock"))
|
|
|
+ and _same_int(device.get("slot"), expected.get("slot"))
|
|
|
+ and _same_int(device.get("device_type"), expected.get("device_type"))
|
|
|
+ and _same_text(device.get("tsap_conn_type"), expected.get("tsap_conn_type"))
|
|
|
+ and _same_int(actual_group_id, expected.get("device_group_id"))
|
|
|
+ )
|
|
|
+
|
|
|
+
|
|
|
+def _same_text(left: Any, right: Any) -> bool:
|
|
|
+ return str(left or "").strip() == str(right or "").strip()
|
|
|
+
|
|
|
+
|
|
|
+def _same_int(left: Any, right: Any) -> bool:
|
|
|
+ normalized_left = _safe_int(left)
|
|
|
+ normalized_right = _safe_int(right)
|
|
|
+ return normalized_left is not None and normalized_left == normalized_right
|
|
|
+
|
|
|
+
|
|
|
+def _safe_int(value: Any) -> int | None:
|
|
|
+ try:
|
|
|
+ return int(value)
|
|
|
+ except Exception:
|
|
|
+ return None
|
|
|
+
|
|
|
+
|
|
|
def edit_modbus_device(project_key: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
|
return _request_collector(
|
|
|
project_key,
|