s7.py 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  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["S7TCP"] = "S7TCP"
  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("tsap_conn_type", mode="before")
  35. @classmethod
  36. def normalize_tsap_conn_type(cls, value: str) -> str:
  37. return str(value).upper()
  38. class S7ReadSpec(BaseModel):
  39. model_config = ConfigDict(extra="forbid")
  40. area: Literal["DB", "M", "I", "Q"]
  41. db: int = Field(default=0, ge=0)
  42. start: int = Field(ge=0)
  43. size: int = Field(ge=1, le=65535)
  44. @field_validator("area", mode="before")
  45. @classmethod
  46. def normalize_area(cls, value: str) -> str:
  47. return str(value).upper()
  48. @model_validator(mode="after")
  49. def validate_read(self) -> "S7ReadSpec":
  50. if self.area == "DB" and self.db <= 0:
  51. raise ValueError("db must be greater than 0 when area is DB")
  52. return self
  53. class S7RawReadRequest(S7BaseRequest):
  54. read: S7ReadSpec
  55. class S7PointSpec(BaseModel):
  56. model_config = ConfigDict(extra="forbid")
  57. area: Literal["DB", "M", "I", "Q"]
  58. db: int = Field(default=0, ge=0)
  59. start: int = Field(ge=0)
  60. type: str
  61. bit: int | None = Field(default=None, ge=0, le=7)
  62. @field_validator("area", mode="before")
  63. @classmethod
  64. def normalize_area(cls, value: str) -> str:
  65. return str(value).upper()
  66. @field_validator("type")
  67. @classmethod
  68. def validate_type(cls, value: str) -> str:
  69. normalized = value.lower()
  70. if normalized not in SUPPORTED_POINT_TYPES:
  71. raise ValueError(
  72. "type must be one of bool, byte, int8, int16, uint16, int32, uint32, int64, uint64, float32, float64"
  73. )
  74. return normalized
  75. @model_validator(mode="after")
  76. def validate_point(self) -> "S7PointSpec":
  77. if self.area == "DB" and self.db <= 0:
  78. raise ValueError("db must be greater than 0 when area is DB")
  79. if self.type != "bool" and self.bit is not None:
  80. raise ValueError("bit is only supported for bool points")
  81. return self
  82. class S7PointReadRequest(S7BaseRequest):
  83. points: list[S7PointSpec] = Field(min_length=1)
  84. class S7ConnectScanRequest(S7IpRequest):
  85. pass