opensignalbox-interface 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. opensignalbox/interface/__init__.py +42 -0
  2. opensignalbox/interface/adapters/__init__.py +3 -0
  3. opensignalbox/interface/adapters/base.py +148 -0
  4. opensignalbox/interface/adapters/controller.py +286 -0
  5. opensignalbox/interface/adapters/modbus/__init__.py +3 -0
  6. opensignalbox/interface/adapters/modbus/adapter.py +485 -0
  7. opensignalbox/interface/assets/favicon.ico +0 -0
  8. opensignalbox/interface/interfaces/__init__.py +3 -0
  9. opensignalbox/interface/interfaces/base.py +170 -0
  10. opensignalbox/interface/interfaces/basicio/__init__.py +3 -0
  11. opensignalbox/interface/interfaces/basicio/connections.py +239 -0
  12. opensignalbox/interface/interfaces/basicio/handler.py +1096 -0
  13. opensignalbox/interface/interfaces/basicio/models.py +165 -0
  14. opensignalbox/interface/interfaces/bell/__init__.py +3 -0
  15. opensignalbox/interface/interfaces/bell/connections.py +279 -0
  16. opensignalbox/interface/interfaces/bell/handler.py +802 -0
  17. opensignalbox/interface/interfaces/bell/models.py +103 -0
  18. opensignalbox/interface/interfaces/controller.py +467 -0
  19. opensignalbox/interface/main.py +187 -0
  20. opensignalbox/interface/models.py +155 -0
  21. opensignalbox/interface/routes/__init__.py +4 -0
  22. opensignalbox/interface/routes/adapters.py +119 -0
  23. opensignalbox/interface/routes/interfaces.py +104 -0
  24. opensignalbox/interface/routes.py +204 -0
  25. opensignalbox/interface/utils.py +29 -0
  26. opensignalbox/interface/version.py +5 -0
  27. opensignalbox/interface/web_ui/.gitignore +24 -0
  28. opensignalbox/interface/web_ui/README.md +5 -0
  29. opensignalbox/interface/web_ui/WEB_UI_REFACTORING.md +136 -0
  30. opensignalbox/interface/web_ui/components.json +17 -0
  31. opensignalbox/interface/web_ui/dist/assets/index-DhsEc6uo.css +2020 -0
  32. opensignalbox/interface/web_ui/dist/assets/index-t__5JZjf.js +39056 -0
  33. opensignalbox/interface/web_ui/dist/assets/index-t__5JZjf.js.map +1 -0
  34. opensignalbox/interface/web_ui/dist/index.html +14 -0
  35. opensignalbox/interface/web_ui/index.html +13 -0
  36. opensignalbox/interface/web_ui/package-lock.json +3710 -0
  37. opensignalbox/interface/web_ui/package.json +39 -0
  38. opensignalbox/interface/web_ui/src/App.vue +32 -0
  39. opensignalbox/interface/web_ui/src/assets/favicon.ico +0 -0
  40. opensignalbox/interface/web_ui/src/assets/index.css +83 -0
  41. opensignalbox/interface/web_ui/src/components/SharedVariablePicker.vue +112 -0
  42. opensignalbox/interface/web_ui/src/components/adapters/AdapterList.vue +183 -0
  43. opensignalbox/interface/web_ui/src/components/adapters/types.ts +35 -0
  44. opensignalbox/interface/web_ui/src/components/interfaces/InterfaceList.vue +200 -0
  45. opensignalbox/interface/web_ui/src/components/ui/badge/Badge.vue +16 -0
  46. opensignalbox/interface/web_ui/src/components/ui/badge/index.ts +25 -0
  47. opensignalbox/interface/web_ui/src/components/ui/breadcrumb/Breadcrumb.vue +13 -0
  48. opensignalbox/interface/web_ui/src/components/ui/breadcrumb/BreadcrumbEllipsis.vue +22 -0
  49. opensignalbox/interface/web_ui/src/components/ui/breadcrumb/BreadcrumbItem.vue +16 -0
  50. opensignalbox/interface/web_ui/src/components/ui/breadcrumb/BreadcrumbLink.vue +19 -0
  51. opensignalbox/interface/web_ui/src/components/ui/breadcrumb/BreadcrumbList.vue +16 -0
  52. opensignalbox/interface/web_ui/src/components/ui/breadcrumb/BreadcrumbPage.vue +19 -0
  53. opensignalbox/interface/web_ui/src/components/ui/breadcrumb/BreadcrumbSeparator.vue +21 -0
  54. opensignalbox/interface/web_ui/src/components/ui/breadcrumb/index.ts +7 -0
  55. opensignalbox/interface/web_ui/src/components/ui/button/Button.vue +26 -0
  56. opensignalbox/interface/web_ui/src/components/ui/button/index.ts +35 -0
  57. opensignalbox/interface/web_ui/src/components/ui/card/Card.vue +21 -0
  58. opensignalbox/interface/web_ui/src/components/ui/card/CardContent.vue +14 -0
  59. opensignalbox/interface/web_ui/src/components/ui/card/CardDescription.vue +14 -0
  60. opensignalbox/interface/web_ui/src/components/ui/card/CardFooter.vue +14 -0
  61. opensignalbox/interface/web_ui/src/components/ui/card/CardHeader.vue +14 -0
  62. opensignalbox/interface/web_ui/src/components/ui/card/CardTitle.vue +18 -0
  63. opensignalbox/interface/web_ui/src/components/ui/card/index.ts +6 -0
  64. opensignalbox/interface/web_ui/src/components/ui/checkbox/Checkbox.vue +33 -0
  65. opensignalbox/interface/web_ui/src/components/ui/checkbox/index.ts +1 -0
  66. opensignalbox/interface/web_ui/src/components/ui/collapsible/Collapsible.vue +15 -0
  67. opensignalbox/interface/web_ui/src/components/ui/collapsible/CollapsibleContent.vue +11 -0
  68. opensignalbox/interface/web_ui/src/components/ui/collapsible/CollapsibleTrigger.vue +11 -0
  69. opensignalbox/interface/web_ui/src/components/ui/collapsible/index.ts +3 -0
  70. opensignalbox/interface/web_ui/src/components/ui/command/Command.vue +30 -0
  71. opensignalbox/interface/web_ui/src/components/ui/command/CommandDialog.vue +21 -0
  72. opensignalbox/interface/web_ui/src/components/ui/command/CommandEmpty.vue +20 -0
  73. opensignalbox/interface/web_ui/src/components/ui/command/CommandGroup.vue +29 -0
  74. opensignalbox/interface/web_ui/src/components/ui/command/CommandInput.vue +33 -0
  75. opensignalbox/interface/web_ui/src/components/ui/command/CommandItem.vue +26 -0
  76. opensignalbox/interface/web_ui/src/components/ui/command/CommandList.vue +27 -0
  77. opensignalbox/interface/web_ui/src/components/ui/command/CommandSeparator.vue +23 -0
  78. opensignalbox/interface/web_ui/src/components/ui/command/CommandShortcut.vue +14 -0
  79. opensignalbox/interface/web_ui/src/components/ui/command/index.ts +9 -0
  80. opensignalbox/interface/web_ui/src/components/ui/dialog/Dialog.vue +14 -0
  81. opensignalbox/interface/web_ui/src/components/ui/dialog/DialogClose.vue +11 -0
  82. opensignalbox/interface/web_ui/src/components/ui/dialog/DialogContent.vue +50 -0
  83. opensignalbox/interface/web_ui/src/components/ui/dialog/DialogDescription.vue +24 -0
  84. opensignalbox/interface/web_ui/src/components/ui/dialog/DialogFooter.vue +19 -0
  85. opensignalbox/interface/web_ui/src/components/ui/dialog/DialogHeader.vue +16 -0
  86. opensignalbox/interface/web_ui/src/components/ui/dialog/DialogScrollContent.vue +59 -0
  87. opensignalbox/interface/web_ui/src/components/ui/dialog/DialogTitle.vue +29 -0
  88. opensignalbox/interface/web_ui/src/components/ui/dialog/DialogTrigger.vue +11 -0
  89. opensignalbox/interface/web_ui/src/components/ui/dialog/index.ts +9 -0
  90. opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/DropdownMenu.vue +14 -0
  91. opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +40 -0
  92. opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/DropdownMenuContent.vue +38 -0
  93. opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/DropdownMenuGroup.vue +11 -0
  94. opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/DropdownMenuItem.vue +28 -0
  95. opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/DropdownMenuLabel.vue +24 -0
  96. opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue +19 -0
  97. opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +41 -0
  98. opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/DropdownMenuSeparator.vue +22 -0
  99. opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/DropdownMenuShortcut.vue +14 -0
  100. opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/DropdownMenuSub.vue +19 -0
  101. opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/DropdownMenuSubContent.vue +30 -0
  102. opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +33 -0
  103. opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/DropdownMenuTrigger.vue +13 -0
  104. opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/index.ts +16 -0
  105. opensignalbox/interface/web_ui/src/components/ui/form/FormControl.vue +16 -0
  106. opensignalbox/interface/web_ui/src/components/ui/form/FormDescription.vue +20 -0
  107. opensignalbox/interface/web_ui/src/components/ui/form/FormItem.vue +19 -0
  108. opensignalbox/interface/web_ui/src/components/ui/form/FormLabel.vue +23 -0
  109. opensignalbox/interface/web_ui/src/components/ui/form/FormMessage.vue +16 -0
  110. opensignalbox/interface/web_ui/src/components/ui/form/index.ts +7 -0
  111. opensignalbox/interface/web_ui/src/components/ui/form/injectionKeys.ts +4 -0
  112. opensignalbox/interface/web_ui/src/components/ui/form/useFormField.ts +30 -0
  113. opensignalbox/interface/web_ui/src/components/ui/input/Input.vue +24 -0
  114. opensignalbox/interface/web_ui/src/components/ui/input/index.ts +1 -0
  115. opensignalbox/interface/web_ui/src/components/ui/label/Label.vue +27 -0
  116. opensignalbox/interface/web_ui/src/components/ui/label/index.ts +1 -0
  117. opensignalbox/interface/web_ui/src/components/ui/number-field/NumberField.vue +23 -0
  118. opensignalbox/interface/web_ui/src/components/ui/number-field/NumberFieldContent.vue +14 -0
  119. opensignalbox/interface/web_ui/src/components/ui/number-field/NumberFieldDecrement.vue +25 -0
  120. opensignalbox/interface/web_ui/src/components/ui/number-field/NumberFieldIncrement.vue +25 -0
  121. opensignalbox/interface/web_ui/src/components/ui/number-field/NumberFieldInput.vue +16 -0
  122. opensignalbox/interface/web_ui/src/components/ui/number-field/index.ts +5 -0
  123. opensignalbox/interface/web_ui/src/components/ui/popover/Popover.vue +15 -0
  124. opensignalbox/interface/web_ui/src/components/ui/popover/PopoverContent.vue +48 -0
  125. opensignalbox/interface/web_ui/src/components/ui/popover/PopoverTrigger.vue +11 -0
  126. opensignalbox/interface/web_ui/src/components/ui/popover/index.ts +3 -0
  127. opensignalbox/interface/web_ui/src/components/ui/radio-group/RadioGroup.vue +25 -0
  128. opensignalbox/interface/web_ui/src/components/ui/radio-group/RadioGroupItem.vue +39 -0
  129. opensignalbox/interface/web_ui/src/components/ui/radio-group/index.ts +2 -0
  130. opensignalbox/interface/web_ui/src/components/ui/scroll-area/ScrollArea.vue +29 -0
  131. opensignalbox/interface/web_ui/src/components/ui/scroll-area/ScrollBar.vue +30 -0
  132. opensignalbox/interface/web_ui/src/components/ui/scroll-area/index.ts +2 -0
  133. opensignalbox/interface/web_ui/src/components/ui/select/Select.vue +15 -0
  134. opensignalbox/interface/web_ui/src/components/ui/select/SelectContent.vue +53 -0
  135. opensignalbox/interface/web_ui/src/components/ui/select/SelectGroup.vue +19 -0
  136. opensignalbox/interface/web_ui/src/components/ui/select/SelectItem.vue +44 -0
  137. opensignalbox/interface/web_ui/src/components/ui/select/SelectItemText.vue +11 -0
  138. opensignalbox/interface/web_ui/src/components/ui/select/SelectLabel.vue +13 -0
  139. opensignalbox/interface/web_ui/src/components/ui/select/SelectScrollDownButton.vue +24 -0
  140. opensignalbox/interface/web_ui/src/components/ui/select/SelectScrollUpButton.vue +24 -0
  141. opensignalbox/interface/web_ui/src/components/ui/select/SelectSeparator.vue +17 -0
  142. opensignalbox/interface/web_ui/src/components/ui/select/SelectTrigger.vue +31 -0
  143. opensignalbox/interface/web_ui/src/components/ui/select/SelectValue.vue +11 -0
  144. opensignalbox/interface/web_ui/src/components/ui/select/index.ts +11 -0
  145. opensignalbox/interface/web_ui/src/components/ui/separator/Separator.vue +35 -0
  146. opensignalbox/interface/web_ui/src/components/ui/separator/index.ts +1 -0
  147. opensignalbox/interface/web_ui/src/components/ui/sheet/Sheet.vue +14 -0
  148. opensignalbox/interface/web_ui/src/components/ui/sheet/SheetClose.vue +11 -0
  149. opensignalbox/interface/web_ui/src/components/ui/sheet/SheetContent.vue +56 -0
  150. opensignalbox/interface/web_ui/src/components/ui/sheet/SheetDescription.vue +22 -0
  151. opensignalbox/interface/web_ui/src/components/ui/sheet/SheetFooter.vue +19 -0
  152. opensignalbox/interface/web_ui/src/components/ui/sheet/SheetHeader.vue +16 -0
  153. opensignalbox/interface/web_ui/src/components/ui/sheet/SheetTitle.vue +22 -0
  154. opensignalbox/interface/web_ui/src/components/ui/sheet/SheetTrigger.vue +11 -0
  155. opensignalbox/interface/web_ui/src/components/ui/sheet/index.ts +31 -0
  156. opensignalbox/interface/web_ui/src/components/ui/table/Table.vue +16 -0
  157. opensignalbox/interface/web_ui/src/components/ui/table/TableBody.vue +14 -0
  158. opensignalbox/interface/web_ui/src/components/ui/table/TableCaption.vue +14 -0
  159. opensignalbox/interface/web_ui/src/components/ui/table/TableCell.vue +21 -0
  160. opensignalbox/interface/web_ui/src/components/ui/table/TableEmpty.vue +37 -0
  161. opensignalbox/interface/web_ui/src/components/ui/table/TableFooter.vue +14 -0
  162. opensignalbox/interface/web_ui/src/components/ui/table/TableHead.vue +14 -0
  163. opensignalbox/interface/web_ui/src/components/ui/table/TableHeader.vue +14 -0
  164. opensignalbox/interface/web_ui/src/components/ui/table/TableRow.vue +14 -0
  165. opensignalbox/interface/web_ui/src/components/ui/table/index.ts +9 -0
  166. opensignalbox/interface/web_ui/src/components/ui/tabs/Tabs.vue +15 -0
  167. opensignalbox/interface/web_ui/src/components/ui/tabs/TabsContent.vue +22 -0
  168. opensignalbox/interface/web_ui/src/components/ui/tabs/TabsList.vue +25 -0
  169. opensignalbox/interface/web_ui/src/components/ui/tabs/TabsTrigger.vue +29 -0
  170. opensignalbox/interface/web_ui/src/components/ui/tabs/index.ts +4 -0
  171. opensignalbox/interface/web_ui/src/components/ui/tags-input/TagsInput.vue +22 -0
  172. opensignalbox/interface/web_ui/src/components/ui/tags-input/TagsInputInput.vue +19 -0
  173. opensignalbox/interface/web_ui/src/components/ui/tags-input/TagsInputItem.vue +22 -0
  174. opensignalbox/interface/web_ui/src/components/ui/tags-input/TagsInputItemDelete.vue +24 -0
  175. opensignalbox/interface/web_ui/src/components/ui/tags-input/TagsInputItemText.vue +19 -0
  176. opensignalbox/interface/web_ui/src/components/ui/tags-input/index.ts +5 -0
  177. opensignalbox/interface/web_ui/src/components/ui/textarea/Textarea.vue +24 -0
  178. opensignalbox/interface/web_ui/src/components/ui/textarea/index.ts +1 -0
  179. opensignalbox/interface/web_ui/src/components/ui/tooltip/Tooltip.vue +14 -0
  180. opensignalbox/interface/web_ui/src/components/ui/tooltip/TooltipContent.vue +31 -0
  181. opensignalbox/interface/web_ui/src/components/ui/tooltip/TooltipProvider.vue +11 -0
  182. opensignalbox/interface/web_ui/src/components/ui/tooltip/TooltipTrigger.vue +11 -0
  183. opensignalbox/interface/web_ui/src/components/ui/tooltip/index.ts +4 -0
  184. opensignalbox/interface/web_ui/src/libs/utils.ts +6 -0
  185. opensignalbox/interface/web_ui/src/main.ts +141 -0
  186. opensignalbox/interface/web_ui/src/views/Overview.vue +11 -0
  187. opensignalbox/interface/web_ui/src/views/Settings.vue +6 -0
  188. opensignalbox/interface/web_ui/src/views/SleafordEast.vue +205 -0
  189. opensignalbox/interface/web_ui/src/views/adapters/modbus/ModbusAdapterEdit.vue +343 -0
  190. opensignalbox/interface/web_ui/src/views/adapters/modbus/ModbusAdapterView.vue +270 -0
  191. opensignalbox/interface/web_ui/src/views/interfaces/InterfaceCreate.vue +0 -0
  192. opensignalbox/interface/web_ui/src/views/interfaces/basicio/BasicIOInterfaceEdit.vue +795 -0
  193. opensignalbox/interface/web_ui/src/views/interfaces/basicio/BasicIOInterfaceView.vue +648 -0
  194. opensignalbox/interface/web_ui/src/views/interfaces/bell/BellInterfaceEdit.vue +790 -0
  195. opensignalbox/interface/web_ui/src/views/interfaces/bell/BellInterfaceView.vue +437 -0
  196. opensignalbox/interface/web_ui/src/vite-env.d.ts +1 -0
  197. opensignalbox/interface/web_ui/tailwind.config.js +94 -0
  198. opensignalbox/interface/web_ui/tsconfig.app.json +27 -0
  199. opensignalbox/interface/web_ui/tsconfig.json +30 -0
  200. opensignalbox/interface/web_ui/tsconfig.node.json +12 -0
  201. opensignalbox/interface/web_ui/tsconfig.tsbuildinfo +1 -0
  202. opensignalbox/interface/web_ui/vite.config.d.ts +2 -0
  203. opensignalbox/interface/web_ui/vite.config.js +60 -0
  204. opensignalbox/interface/web_ui/vite.config.ts +62 -0
  205. opensignalbox_interface-0.1.0.dist-info/METADATA +49 -0
  206. opensignalbox_interface-0.1.0.dist-info/RECORD +209 -0
  207. opensignalbox_interface-0.1.0.dist-info/WHEEL +4 -0
  208. opensignalbox_interface-0.1.0.dist-info/entry_points.txt +2 -0
  209. opensignalbox_interface-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,485 @@
1
+ """
2
+ Modbus adapter implementation.
3
+ """
4
+
5
+ import logging
6
+ import socket
7
+ from typing import Any, Optional, Union
8
+
9
+ import serial
10
+ from opensignalbox.interface.adapters.base import BaseAdapter
11
+ from opensignalbox.interface.models import AdapterTypeEnum
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ SERIAL_TIMEOUT = 1 # seconds
16
+ NETWORK_TIMEOUT = 1 # seconds
17
+ STATUS_UPDATE_PERIOD = 1 # seconds
18
+
19
+
20
+ def modbus_crc(msg: bytearray) -> int:
21
+ """
22
+ Calculate Modbus CRC for a message.
23
+
24
+ Args:
25
+ msg: The message to calculate CRC for
26
+
27
+ Returns:
28
+ The calculated CRC
29
+ """
30
+ crc = 0xFFFF
31
+ for n in range(len(msg)):
32
+ crc ^= msg[n]
33
+ for _ in range(8):
34
+ if crc & 1:
35
+ crc >>= 1
36
+ crc ^= 0xA001
37
+ else:
38
+ crc >>= 1
39
+ return crc
40
+
41
+
42
+ class ModbusAdapter(BaseAdapter):
43
+ """Adapter for Modbus communication."""
44
+
45
+ # Metadata for the API discovery
46
+ metadata = {
47
+ "supported_connection_types": [
48
+ "BasicIOModbusConnection",
49
+ "BellModbusConnection",
50
+ ],
51
+ "required_fields": ["systemName", "userName", "isNetwork"],
52
+ "optional_fields": [
53
+ "description",
54
+ "baud",
55
+ "address",
56
+ "serialPort",
57
+ "networkPort",
58
+ "enabled",
59
+ ],
60
+ }
61
+
62
+ @classmethod
63
+ def get_router(cls) -> Any:
64
+ """Get API router for Modbus adapter endpoints.
65
+
66
+ Returns:
67
+ FastAPI router with Modbus adapter specific endpoints
68
+ """
69
+ from fastapi import APIRouter, HTTPException
70
+ from opensignalbox.interface.models import (
71
+ AdapterBase,
72
+ AdapterCreate,
73
+ AdapterPublic,
74
+ AdapterUpdate,
75
+ )
76
+
77
+ router = APIRouter(tags=["adapters/modbus"])
78
+
79
+ @router.get("/{system_name}")
80
+ async def get_adapter(system_name: str):
81
+ """Get a Modbus adapter by system name."""
82
+ if not cls.adapter_controller:
83
+ raise HTTPException(
84
+ status_code=500, detail="Adapter controller not initialized"
85
+ )
86
+
87
+ try:
88
+ adapter = cls.adapter_controller.get(system_name)
89
+ if adapter.adapter_type != "Modbus":
90
+ raise HTTPException(
91
+ status_code=404,
92
+ detail=f"Modbus adapter '{system_name}' not found",
93
+ )
94
+
95
+ # Convert BaseAdapter to AdapterPublic
96
+ adapter_dict = {
97
+ "system_name": adapter.system_name,
98
+ "user_name": adapter.user_name,
99
+ "description": adapter.description,
100
+ "adapter_type": adapter.adapter_type,
101
+ "enabled": adapter.enabled,
102
+ "error": adapter.error,
103
+ "is_connected": adapter.is_connected,
104
+ "is_network": adapter.is_network,
105
+ "serial_port": adapter.serial_port,
106
+ "baud": adapter.baud,
107
+ "address": adapter.address,
108
+ "network_port": adapter.network_port,
109
+ }
110
+ return AdapterPublic(**adapter_dict)
111
+ except KeyError as exc:
112
+ raise HTTPException(
113
+ status_code=404, detail=f"Adapter '{system_name}' not found"
114
+ ) from exc
115
+
116
+ @router.post("/")
117
+ async def create_adapter(adapter: AdapterCreate):
118
+ """Create a new Modbus adapter."""
119
+ if not cls.adapter_controller:
120
+ raise HTTPException(
121
+ status_code=500, detail="Adapter controller not initialized"
122
+ )
123
+
124
+ try:
125
+ # Extract common fields
126
+ adapter_base = AdapterBase(
127
+ **adapter.model_dump(), adapter_type=AdapterTypeEnum.MODBUS
128
+ )
129
+
130
+ # Create the adapter
131
+ new_adapter = cls.adapter_controller.add(
132
+ adapter_base, **adapter.model_dump()
133
+ )
134
+
135
+ # Convert BaseAdapter to AdapterPublic
136
+ adapter_dict = {
137
+ "system_name": new_adapter.system_name,
138
+ "user_name": new_adapter.user_name,
139
+ "description": new_adapter.description,
140
+ "adapter_type": new_adapter.adapter_type,
141
+ "enabled": new_adapter.enabled,
142
+ "is_connected": new_adapter.is_connected,
143
+ }
144
+ return AdapterPublic(**adapter_dict)
145
+ except FileExistsError as exc:
146
+ raise HTTPException(
147
+ status_code=400,
148
+ detail=f"Adapter '{adapter.system_name}' already exists",
149
+ ) from exc
150
+ except TypeError as exc:
151
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
152
+
153
+ @router.patch("/{system_name}")
154
+ async def update_adapter(system_name: str, adapter_update: AdapterUpdate):
155
+ """Update a Modbus adapter."""
156
+ if not cls.adapter_controller:
157
+ raise HTTPException(
158
+ status_code=500, detail="Adapter controller not initialized"
159
+ )
160
+
161
+ try:
162
+ # Check that the adapter exists and is Modbus type
163
+ adapter = cls.adapter_controller.get(system_name)
164
+ if adapter.adapter_type != "Modbus":
165
+ raise HTTPException(
166
+ status_code=404,
167
+ detail=f"Modbus adapter '{system_name}' not found",
168
+ )
169
+
170
+ # Extract update fields
171
+ kwargs = {
172
+ k: v
173
+ for k, v in adapter_update.model_dump().items()
174
+ if v is not None and k != "system_name"
175
+ }
176
+
177
+ # Update the adapter
178
+ updated_adapter = cls.adapter_controller.update(system_name, **kwargs)
179
+
180
+ # Convert BaseAdapter to AdapterPublic
181
+ adapter_dict = {
182
+ "system_name": updated_adapter.system_name,
183
+ "user_name": updated_adapter.user_name,
184
+ "description": updated_adapter.description,
185
+ "adapter_type": updated_adapter.adapter_type,
186
+ "enabled": updated_adapter.enabled,
187
+ "is_connected": updated_adapter.is_connected,
188
+ }
189
+ return AdapterPublic(**adapter_dict)
190
+ except KeyError as exc:
191
+ raise HTTPException(
192
+ status_code=404, detail=f"Adapter '{system_name}' not found"
193
+ ) from exc
194
+
195
+ @router.delete("/{system_name}")
196
+ async def delete_adapter(system_name: str):
197
+ """Delete a Modbus adapter."""
198
+ if not cls.adapter_controller:
199
+ raise HTTPException(
200
+ status_code=500, detail="Adapter controller not initialized"
201
+ )
202
+
203
+ try:
204
+ # Check that the adapter exists and is Modbus type
205
+ adapter = cls.adapter_controller.get(system_name)
206
+ if adapter.adapter_type != "Modbus":
207
+ raise HTTPException(
208
+ status_code=404,
209
+ detail=f"Modbus adapter '{system_name}' not found",
210
+ )
211
+
212
+ cls.adapter_controller.remove(system_name)
213
+ return {"status": "success"}
214
+ except KeyError as exc:
215
+ raise HTTPException(
216
+ status_code=404, detail=f"Adapter '{system_name}' not found"
217
+ ) from exc
218
+
219
+ @router.post("/{system_name}/connect")
220
+ async def connect_adapter(system_name: str):
221
+ """Connect a Modbus adapter."""
222
+ if not cls.adapter_controller:
223
+ raise HTTPException(
224
+ status_code=500, detail="Adapter controller not initialized"
225
+ )
226
+
227
+ try:
228
+ adapter = cls.adapter_controller.get(system_name)
229
+ if adapter.adapter_type != "Modbus":
230
+ raise HTTPException(
231
+ status_code=404,
232
+ detail=f"Modbus adapter '{system_name}' not found",
233
+ )
234
+
235
+ adapter.connect()
236
+ return {"status": "success", "connected": adapter.is_connected}
237
+ except KeyError as exc:
238
+ raise HTTPException(
239
+ status_code=404, detail=f"Adapter '{system_name}' not found"
240
+ ) from exc
241
+
242
+ @router.post("/{system_name}/disconnect")
243
+ async def disconnect_adapter(system_name: str):
244
+ """Disconnect a Modbus adapter."""
245
+ if not cls.adapter_controller:
246
+ raise HTTPException(
247
+ status_code=500, detail="Adapter controller not initialized"
248
+ )
249
+
250
+ try:
251
+ adapter = cls.adapter_controller.get(system_name)
252
+ if adapter.adapter_type != "Modbus":
253
+ raise HTTPException(
254
+ status_code=404,
255
+ detail=f"Modbus adapter '{system_name}' not found",
256
+ )
257
+
258
+ adapter.disconnect()
259
+ return {"status": "success", "connected": adapter.is_connected}
260
+ except KeyError as exc:
261
+ raise HTTPException(
262
+ status_code=404, detail=f"Adapter '{system_name}' not found"
263
+ ) from exc
264
+
265
+ return router
266
+
267
+ def __init__(
268
+ self,
269
+ system_name: str,
270
+ user_name: str,
271
+ description: str = "",
272
+ enabled: bool = True,
273
+ is_network: bool = False,
274
+ serial_port: Optional[str] = None,
275
+ baud: int = 9600,
276
+ address: Optional[str] = None,
277
+ network_port: Optional[int] = 0,
278
+ **kwargs: Any,
279
+ ) -> None:
280
+ super().__init__(system_name, user_name, description)
281
+ self.adapter_type = AdapterTypeEnum.MODBUS
282
+ self.enabled = enabled
283
+ self.is_network = is_network
284
+ self.serial_port = serial_port
285
+ self.baud = baud
286
+ self.address = address
287
+ self.network_port = network_port
288
+ self.connection: Union[serial.Serial, socket.socket, None] = None
289
+ self.tid = 0 # Transaction ID for Modbus TCP
290
+
291
+ def attempt_connect(self) -> None:
292
+ """Attempt to connect to the Modbus device."""
293
+ logger.info(
294
+ f"Connecting to Modbus device: {self.system_name} ({self.adapter_type})"
295
+ )
296
+ if self.is_network:
297
+ self._connect_network()
298
+ else:
299
+ self._connect_serial()
300
+
301
+ def _connect_network(self) -> None:
302
+ """Connect to a Modbus TCP device."""
303
+ if not self.address:
304
+ raise ValueError("Network address is required for network connection")
305
+
306
+ self.connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
307
+ self.connection.settimeout(NETWORK_TIMEOUT)
308
+ logger.info(
309
+ f"Connecting to Modbus TCP device at {self.address}:{self.network_port}"
310
+ )
311
+ self.connection.connect((self.address, self.network_port))
312
+
313
+ def _connect_serial(self) -> None:
314
+ """Connect to a Modbus RTU device."""
315
+ if not isinstance(self.serial_port, str):
316
+ self.serial_port = f"COM{self.serial_port}"
317
+
318
+ self.connection = serial.Serial(
319
+ port=self.serial_port,
320
+ baudrate=self.baud,
321
+ bytesize=serial.EIGHTBITS,
322
+ parity=serial.PARITY_NONE,
323
+ stopbits=serial.STOPBITS_ONE,
324
+ timeout=SERIAL_TIMEOUT,
325
+ )
326
+
327
+ def attempt_disconnect(self) -> None:
328
+ """Attempt to disconnect from the Modbus device."""
329
+ if self.connection:
330
+ self.connection.close()
331
+ self.connection = None
332
+ self.is_connected = False
333
+ logger.debug(
334
+ f"Disconnected from Modbus device: {self.system_name} ({self.adapter_type})"
335
+ )
336
+
337
+ def attempt_write(self, data: bytearray) -> None:
338
+ """
339
+ Write data to the Modbus device.
340
+
341
+ Args:
342
+ data: The data to write
343
+
344
+ Raises:
345
+ ConnectionError: If not connected to a device
346
+ IOError: If the write fails
347
+ """
348
+ if not self.connection:
349
+ raise ConnectionError("Not connected to a device")
350
+
351
+ if self.is_network:
352
+ self._write_network(data)
353
+ else:
354
+ self._write_serial(data)
355
+
356
+ def _write_network(self, data: bytearray) -> None:
357
+ """Write data to a Modbus TCP device."""
358
+ if not isinstance(self.connection, socket.socket):
359
+ raise ConnectionError("Invalid network connection")
360
+
361
+ # Increment transaction ID
362
+ self.tid = (self.tid + 1) % 65536
363
+
364
+ # Create MBAP header
365
+ mbap = bytearray(
366
+ [
367
+ (self.tid >> 8) & 0xFF, # Transaction ID high byte
368
+ self.tid & 0xFF, # Transaction ID low byte
369
+ 0x00, # Protocol ID high byte
370
+ 0x00, # Protocol ID low byte
371
+ (len(data) >> 8) & 0xFF, # Length high byte
372
+ len(data) & 0xFF, # Length low byte
373
+ ]
374
+ )
375
+
376
+ # Prepend MBAP header to data
377
+ message = mbap + data
378
+
379
+ # Send data
380
+ self.connection.sendall(message)
381
+
382
+ def _write_serial(self, data: bytearray) -> None:
383
+ """Write data to a Modbus RTU device."""
384
+ if not isinstance(self.connection, serial.Serial):
385
+ raise ConnectionError("Invalid serial connection")
386
+
387
+ # Calculate CRC
388
+ crc = modbus_crc(data)
389
+
390
+ # Append CRC to data
391
+ message = data + bytearray([(crc & 0xFF), (crc >> 8) & 0xFF])
392
+
393
+ # Send data
394
+ self.connection.write(message)
395
+
396
+ def attempt_read(self) -> bytearray:
397
+ """
398
+ Read data from the Modbus device.
399
+
400
+ Returns:
401
+ The read data
402
+
403
+ Raises:
404
+ ConnectionError: If not connected to a device
405
+ IOError: If the read fails
406
+ """
407
+ if not self.connection:
408
+ raise ConnectionError("Not connected to a device")
409
+
410
+ # Read response
411
+ if self.is_network:
412
+ return self._read_network()
413
+ else:
414
+ return self._read_serial()
415
+
416
+ def _read_network(self) -> bytearray:
417
+ """Read data from a Modbus TCP device."""
418
+ if not isinstance(self.connection, socket.socket):
419
+ raise ConnectionError("Invalid network connection")
420
+
421
+ # Read MBAP header (7 bytes)
422
+ mbap = self.connection.recv(7)
423
+ if len(mbap) != 7:
424
+ raise IOError("Failed to read MBAP header")
425
+
426
+ # Extract length from MBAP header
427
+ length = (mbap[4] << 8) | mbap[5] - 1 # Subtract 1 for unit ID byte
428
+
429
+ # Read data
430
+ data = self.connection.recv(length)
431
+ if len(data) != length:
432
+ raise IOError("Failed to read complete response")
433
+
434
+ # Check transaction ID
435
+ tid = (mbap[0] << 8) | mbap[1]
436
+ if tid != self.tid:
437
+ self.connection.recv(1024) # Flush buffer
438
+ raise IOError(
439
+ f"Transaction ID mismatch: expected {self.tid}, got {tid}. Flushing buffer."
440
+ )
441
+
442
+ return bytearray(data[2:]) # Skip function code and unit ID byte
443
+
444
+ def _read_serial(self) -> bytearray:
445
+ """Read data from a Modbus RTU device."""
446
+ if not isinstance(self.connection, serial.Serial):
447
+ raise ConnectionError("Invalid serial connection")
448
+
449
+ # Read function code and length (2 bytes)
450
+ header = self.connection.read(2)
451
+ if len(header) != 2:
452
+ raise IOError("Failed to read response header")
453
+
454
+ # Extract function code
455
+ function_code = header[1]
456
+
457
+ # Check for exception response
458
+ if function_code & 0x80:
459
+ exception_code = self.connection.read(1)
460
+ raise IOError(f"Modbus exception: {exception_code[0]}")
461
+
462
+ # Select length based on function code
463
+ if function_code == 15: # Write multiple coils
464
+ length = 4
465
+ elif function_code == 3: # Read holding registers
466
+ header += self.connection.read(1)
467
+ length = header[-1]
468
+ else:
469
+ length = 2
470
+
471
+ # Read data and CRC
472
+ data = self.connection.read(length + 2) # +2 for CRC
473
+ if len(data) != length + 2:
474
+ raise IOError("Failed to read complete response")
475
+
476
+ # Verify CRC
477
+ message = header + data[:-2]
478
+ crc = modbus_crc(bytearray(message))
479
+ received_crc = (data[-1] << 8) | data[-2]
480
+
481
+ if crc != received_crc:
482
+ raise IOError("CRC check failed")
483
+
484
+ # Return data without CRC
485
+ return bytearray(data[:-2])
@@ -0,0 +1,3 @@
1
+ """
2
+ Interface implementations for opensignalbox-interface.
3
+ """
@@ -0,0 +1,170 @@
1
+ """
2
+ Base interface classes and types for opensignalbox-interface module.
3
+ """
4
+
5
+ from abc import ABC, abstractmethod
6
+ from typing import Any, Dict, Optional
7
+
8
+ from opensignalbox.interface.models import Interface
9
+
10
+
11
+ class InterfaceHandler(ABC):
12
+ """Base abstract class for all interface handlers."""
13
+
14
+ # Interface controller reference for API routes
15
+ interface_controller = None
16
+ adapter_controller = None
17
+
18
+ @classmethod
19
+ def init(cls, output_variables, input_variable_subs, variable_update_callback):
20
+ """Initialize handler with controller references."""
21
+ cls.output_variables = output_variables
22
+ cls.input_variable_subs = input_variable_subs
23
+ cls.variable_update_callback = variable_update_callback
24
+
25
+ @classmethod
26
+ def set_controllers(cls, interface_controller, adapter_controller):
27
+ """Set the interface and adapter controller references for API routes.
28
+
29
+ Args:
30
+ interface_controller: The interface controller instance
31
+ adapter_controller: The adapter controller instance
32
+ """
33
+ cls.interface_controller = interface_controller
34
+ cls.adapter_controller = adapter_controller
35
+
36
+ @staticmethod
37
+ @abstractmethod
38
+ def add(interface: Interface) -> Interface:
39
+ """Add a new interface.
40
+
41
+ Args:
42
+ interface: The interface to add
43
+
44
+ Returns:
45
+ The initialized interface with additional runtime fields
46
+ """
47
+ pass
48
+
49
+ @staticmethod
50
+ @abstractmethod
51
+ def update(interface: Interface, fields: Dict[str, Any]) -> None:
52
+ """Update an existing interface.
53
+
54
+ Args:
55
+ interface: The interface to update
56
+ fields: The fields to update
57
+ """
58
+ pass
59
+
60
+ @staticmethod
61
+ @abstractmethod
62
+ def remove(interface: Interface) -> None:
63
+ """Remove an interface.
64
+
65
+ Args:
66
+ interface: The interface to remove
67
+ """
68
+ pass
69
+
70
+ @staticmethod
71
+ @abstractmethod
72
+ def handle_variable_update(
73
+ interface: Interface, variable: str, json_data: str
74
+ ) -> None:
75
+ """Handle an update to a subscribed variable.
76
+
77
+ Args:
78
+ interface: The interface associated with the variable
79
+ variable: The name of the variable that was updated
80
+ json_data: The serialized JSON data of the variable update
81
+ """
82
+ pass
83
+
84
+ @staticmethod
85
+ @abstractmethod
86
+ def connect(interface: Interface) -> bool:
87
+ """Connect to the interface.
88
+
89
+ Args:
90
+ interface: The interface to connect
91
+
92
+ Returns:
93
+ True if connection was successful, False otherwise
94
+ """
95
+ pass
96
+
97
+ @staticmethod
98
+ @abstractmethod
99
+ def disconnect(interface: Interface) -> None:
100
+ """Disconnect from the interface.
101
+
102
+ Args:
103
+ interface: The interface to disconnect
104
+ """
105
+ pass
106
+
107
+ @staticmethod
108
+ @abstractmethod
109
+ def read_data(interface: Interface) -> None:
110
+ """Read data.
111
+
112
+ Args:
113
+ interface: The interface to read data for
114
+ """
115
+ pass
116
+
117
+ @staticmethod
118
+ @abstractmethod
119
+ def write_data(interface: Interface) -> None:
120
+ """Write data to hardware using the adapter.
121
+
122
+ Args:
123
+ interface: The interface to write data for
124
+ """
125
+ pass
126
+
127
+ @classmethod
128
+ def get_router(cls) -> Optional[Any]:
129
+ """Get API router for this interface type.
130
+
131
+ Returns:
132
+ Optional FastAPI router to be included in the API
133
+ Returns None if no custom routes are needed
134
+
135
+ Example:
136
+ ```python
137
+ @classmethod
138
+ def get_router(cls):
139
+ from fastapi import APIRouter
140
+
141
+ router = APIRouter()
142
+
143
+ @router.get("/my-interface/status")
144
+ async def get_status():
145
+ # Access interfaces via the controller reference
146
+ interfaces = cls.interface_controller.get_all()
147
+ return {"status": "ok", "interface_count": len(interfaces)}
148
+
149
+ return router
150
+ ```
151
+ """
152
+ return None
153
+
154
+ @classmethod
155
+ @abstractmethod
156
+ def load_from_json(cls, interface_data: Any) -> None:
157
+ """Load interface data from JSON."""
158
+
159
+ pass
160
+
161
+ @staticmethod
162
+ @abstractmethod
163
+ def save_to_json(interface: Interface) -> Any:
164
+ """Save interface data to JSON.
165
+
166
+ Returns:
167
+ A dictionary representation of the interface data
168
+ """
169
+ # Default implementation returns simple model dump
170
+ pass
@@ -0,0 +1,3 @@
1
+ """
2
+ Basic IO interface implementation.
3
+ """