bacnet.py 2.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
  1. from ipaddress import ip_address
  2. import re
  3. from pydantic import BaseModel, ConfigDict, Field, field_validator
  4. OBJECT_ID_MAX = 4_194_303
  5. OBJECT_TYPE_ALIASES = {
  6. "analoginput": ("AnalogInput", "analogInput"),
  7. "analogoutput": ("AnalogOutput", "analogOutput"),
  8. "analogvalue": ("AnalogValue", "analogValue"),
  9. "binaryinput": ("BinaryInput", "binaryInput"),
  10. "binaryoutput": ("BinaryOutput", "binaryOutput"),
  11. "binaryvalue": ("BinaryValue", "binaryValue"),
  12. "multistateinput": ("MultiStateInput", "multiStateInput"),
  13. "multistateoutput": ("MultiStateOutput", "multiStateOutput"),
  14. "multistatevalue": ("MultiStateValue", "multiStateValue"),
  15. "device": ("Device", "device"),
  16. "calendar": ("Calendar", "calendar"),
  17. "schedule": ("Schedule", "schedule"),
  18. "trendlog": ("TrendLog", "trendLog"),
  19. "notificationclass": ("NotificationClass", "notificationClass"),
  20. }
  21. def object_type_key(value: str) -> str:
  22. return re.sub(r"[^0-9a-z]", "", value.lower())
  23. def normalize_object_type(value: str) -> str:
  24. key = object_type_key(value)
  25. if key in OBJECT_TYPE_ALIASES:
  26. return OBJECT_TYPE_ALIASES[key][0]
  27. if not re.fullmatch(r"[A-Za-z][0-9A-Za-z_-]*", value):
  28. raise ValueError("object_type must be a valid BACnet object type")
  29. parts = re.split(r"[-_]", value)
  30. return "".join(part[:1].upper() + part[1:] for part in parts if part)
  31. def bac0_object_type(value: str) -> str:
  32. key = object_type_key(value)
  33. if key in OBJECT_TYPE_ALIASES:
  34. return OBJECT_TYPE_ALIASES[key][1]
  35. normalized = normalize_object_type(value)
  36. return normalized[:1].lower() + normalized[1:]
  37. class BacnetBaseRequest(BaseModel):
  38. model_config = ConfigDict(extra="forbid")
  39. ip: str = Field(min_length=1)
  40. bacnet_device_id: int = Field(ge=0, le=OBJECT_ID_MAX)
  41. port: int = Field(default=47808, ge=1, le=65535)
  42. @field_validator("ip")
  43. @classmethod
  44. def validate_ip(cls, value: str) -> str:
  45. try:
  46. ip_address(value)
  47. except ValueError as exc:
  48. raise ValueError("ip must be a valid IP address") from exc
  49. return value
  50. class BacnetPointSpec(BaseModel):
  51. model_config = ConfigDict(extra="forbid")
  52. object_type: str
  53. object_id: int = Field(ge=0, le=OBJECT_ID_MAX)
  54. @field_validator("object_type")
  55. @classmethod
  56. def validate_object_type(cls, value: str) -> str:
  57. return normalize_object_type(value)
  58. class BacnetPointReadRequest(BacnetBaseRequest):
  59. points: list[BacnetPointSpec] = Field(min_length=1)
  60. class BacnetPointSearchRequest(BacnetBaseRequest):
  61. pass