s7.py 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. from ipaddress import ip_address
  2. from typing import Literal
  3. from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
  4. SUPPORTED_POINT_TYPES = {
  5. "bool",
  6. "byte",
  7. "int8",
  8. "int16",
  9. "uint16",
  10. "int32",
  11. "uint32",
  12. "int64",
  13. "uint64",
  14. "float32",
  15. "float64",
  16. }
  17. class S7IpRequest(BaseModel):
  18. model_config = ConfigDict(extra="forbid")
  19. ip: str = Field(min_length=1)
  20. @field_validator("ip")
  21. @classmethod
  22. def validate_ip(cls, value: str) -> str:
  23. try:
  24. ip_address(value)
  25. except ValueError as exc:
  26. raise ValueError("ip must be a valid IP address") from exc
  27. return value
  28. class S7BaseRequest(S7IpRequest):
  29. device_type: Literal["S7-1200", "S7-Smart200"] = "S7-1200"
  30. port: int = Field(default=102, ge=1, le=65535)
  31. rock: int = Field(ge=0, le=31)
  32. slot: int = Field(ge=0, le=31)
  33. tsap_conn_type: Literal["PG", "OP", "BASIC"] = "PG"
  34. @field_validator("device_type", mode="before")
  35. @classmethod
  36. def normalize_device_type(cls, value: str) -> str:
  37. normalized = str(value).upper()
  38. if normalized == "S7-1200":
  39. return "S7-1200"
  40. if normalized == "S7-SMART200":
  41. return "S7-Smart200"
  42. return str(value)
  43. @field_validator("tsap_conn_type", mode="before")
  44. @classmethod
  45. def normalize_tsap_conn_type(cls, value: str) -> str:
  46. return str(value).upper()
  47. class S7ReadSpec(BaseModel):
  48. model_config = ConfigDict(extra="forbid")
  49. area: Literal["DB", "M", "I", "Q", "V"]
  50. db: int = Field(default=0, ge=0)
  51. start: int = Field(ge=0)
  52. size: int = Field(ge=1, le=65535)
  53. @field_validator("area", mode="before")
  54. @classmethod
  55. def normalize_area(cls, value: str) -> str:
  56. return str(value).upper()
  57. @model_validator(mode="after")
  58. def validate_read(self) -> "S7ReadSpec":
  59. if self.area == "DB" and self.db <= 0:
  60. raise ValueError("db must be greater than 0 when area is DB")
  61. return self
  62. class S7RawReadRequest(S7BaseRequest):
  63. read: S7ReadSpec
  64. @model_validator(mode="after")
  65. def validate_v_area(self) -> "S7RawReadRequest":
  66. if self.device_type != "S7-Smart200" and self.read.area == "V":
  67. raise ValueError("V area is only supported when device_type is S7-Smart200")
  68. return self
  69. class S7PointSpec(BaseModel):
  70. model_config = ConfigDict(extra="forbid")
  71. area: Literal["DB", "M", "I", "Q", "V"]
  72. db: int = Field(default=0, ge=0)
  73. start: int = Field(ge=0)
  74. type: str
  75. bit: int | None = Field(default=None, ge=0, le=7)
  76. @field_validator("area", mode="before")
  77. @classmethod
  78. def normalize_area(cls, value: str) -> str:
  79. return str(value).upper()
  80. @field_validator("type")
  81. @classmethod
  82. def validate_type(cls, value: str) -> str:
  83. normalized = value.lower()
  84. if normalized not in SUPPORTED_POINT_TYPES:
  85. raise ValueError(
  86. "type must be one of bool, byte, int8, int16, uint16, int32, uint32, int64, uint64, float32, float64"
  87. )
  88. return normalized
  89. @model_validator(mode="after")
  90. def validate_point(self) -> "S7PointSpec":
  91. if self.area == "DB" and self.db <= 0:
  92. raise ValueError("db must be greater than 0 when area is DB")
  93. if self.type != "bool" and self.bit is not None:
  94. raise ValueError("bit is only supported for bool points")
  95. return self
  96. class S7PointReadRequest(S7BaseRequest):
  97. points: list[S7PointSpec] = Field(min_length=1)
  98. @model_validator(mode="after")
  99. def validate_v_area(self) -> "S7PointReadRequest":
  100. if self.device_type != "S7-Smart200" and any(point.area == "V" for point in self.points):
  101. raise ValueError("V area is only supported when device_type is S7-Smart200")
  102. return self
  103. class S7ConnectScanRequest(S7IpRequest):
  104. device_type: Literal["S7-1200", "S7-Smart200"] = "S7-1200"
  105. @field_validator("device_type", mode="before")
  106. @classmethod
  107. def normalize_device_type(cls, value: str) -> str:
  108. return S7BaseRequest.normalize_device_type(value)