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,42 @@
1
+ """
2
+ opensignalbox-interface - Generic interface module for Open Signal Box
3
+ """
4
+
5
+ # Public exports
6
+ from .adapters.base import BaseAdapter as BaseAdapter
7
+ from .adapters.controller import AdapterController as AdapterController
8
+ from .interfaces.base import InterfaceHandler as InterfaceHandler
9
+ from .interfaces.controller import InterfaceController as InterfaceController
10
+
11
+ # Module classes
12
+ from .main import Module as Module
13
+ from .models import (
14
+ AdapterBase,
15
+ AdapterCreate,
16
+ AdapterModel,
17
+ AdapterPublic,
18
+ AdapterTypeEnum,
19
+ AdapterUpdate,
20
+ Interface,
21
+ InterfaceBase,
22
+ InterfacePublic,
23
+ InterfaceTypeEnum,
24
+ InterfaceUpdate,
25
+ )
26
+
27
+ __all__ = [
28
+ "AdapterBase",
29
+ "AdapterController",
30
+ "AdapterCreate",
31
+ "AdapterModel",
32
+ "AdapterPublic",
33
+ "AdapterTypeEnum",
34
+ "AdapterUpdate",
35
+ "Interface",
36
+ "InterfaceBase",
37
+ "InterfaceController",
38
+ "InterfaceHandler",
39
+ "InterfacePublic",
40
+ "InterfaceTypeEnum",
41
+ "InterfaceUpdate",
42
+ ]
@@ -0,0 +1,3 @@
1
+ """
2
+ Adapters for opensignalbox-interface.
3
+ """
@@ -0,0 +1,148 @@
1
+ """
2
+ Base adapter class and types for opensignalbox-interface module.
3
+ """
4
+
5
+ import logging
6
+ from abc import ABC, abstractmethod
7
+ from typing import Any, Optional
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+ CONNECT_RETRIES = 3
12
+ COMMAND_RETRIES = 3
13
+
14
+
15
+ class BaseAdapter(ABC):
16
+ """Base abstract class for all adapters."""
17
+
18
+ # Adapter controller reference for API routes
19
+ adapter_controller = None
20
+
21
+ @classmethod
22
+ def set_controller(cls, controller):
23
+ """Set the adapter controller reference for API routes.
24
+
25
+ Args:
26
+ controller: The adapter controller instance
27
+ """
28
+ cls.adapter_controller = controller
29
+
30
+ @classmethod
31
+ def get_router(cls) -> Optional[Any]:
32
+ """Get API router for this adapter type.
33
+
34
+ Returns:
35
+ Optional FastAPI router to be included in the API
36
+ Returns None if no custom routes are needed
37
+
38
+ Example:
39
+ ```python
40
+ @classmethod
41
+ def get_router(cls):
42
+ from fastapi import APIRouter
43
+
44
+ router = APIRouter()
45
+
46
+ @router.get("/my-adapter/status")
47
+ async def get_status():
48
+ # Access adapters via the controller reference
49
+ adapters = cls.adapter_controller.get_all()
50
+ return {"status": "ok", "adapter_count": len(adapters)}
51
+
52
+ return router
53
+ ```
54
+ """
55
+ return None
56
+
57
+ def __init__(
58
+ self, system_name: str, user_name: str, description: str = "", **kwargs: Any
59
+ ) -> None:
60
+ self.system_name = system_name
61
+ self.user_name = user_name
62
+ self.description = description
63
+ self.adapter_type: str = "BaseAdapter"
64
+ self.is_connected: bool = False
65
+ self.error: Optional[str] = None
66
+ self.enabled: bool = True
67
+
68
+ def connect(self) -> bool:
69
+ """Connect to the adapter with retry logic."""
70
+ if self.is_connected:
71
+ self.disconnect()
72
+ if not self.enabled:
73
+ return False
74
+ try:
75
+ self.attempt_connect()
76
+ self.error = None
77
+ self.is_connected = True
78
+ return True
79
+ except Exception as error:
80
+ logger.error(f"Connection to adapter '{self.user_name}' failed: {error}")
81
+ self.error = str(error)
82
+ self.disconnect()
83
+ self.enabled = False
84
+ return False
85
+
86
+ def clear_error_and_connect(self) -> None:
87
+ """Clear any errors and attempt to connect again."""
88
+ self.error = None
89
+ self.connect()
90
+
91
+ @abstractmethod
92
+ def attempt_connect(self) -> None:
93
+ """Concrete implementation of the connection logic."""
94
+ pass
95
+
96
+ def disconnect(self) -> None:
97
+ """Disconnect from the adapter."""
98
+ if self.is_connected:
99
+ self.attempt_disconnect()
100
+ self.is_connected = False
101
+
102
+ @abstractmethod
103
+ def attempt_disconnect(self) -> None:
104
+ """Concrete implementation of the disconnection logic."""
105
+ pass
106
+
107
+ def write(self, data: Any) -> None:
108
+ """Write data to the adapter with retry logic."""
109
+ write_try = 1
110
+ while write_try <= COMMAND_RETRIES:
111
+ try:
112
+ self.attempt_write(data)
113
+ return
114
+ except Exception as error:
115
+ logger.error(f"Write to adapter '{self.user_name}' failed: {error}")
116
+ self.error = str(error)
117
+ write_try += 1
118
+ if write_try <= COMMAND_RETRIES:
119
+ logger.error(
120
+ f"Retrying write {write_try} out of {COMMAND_RETRIES} attempts..."
121
+ )
122
+
123
+ @abstractmethod
124
+ def attempt_write(self, data: Any) -> None:
125
+ """Concrete implementation of the write logic."""
126
+ pass
127
+
128
+ def read(self) -> Any:
129
+ """Read data from the adapter with retry logic."""
130
+ read_try = 1
131
+ while read_try <= COMMAND_RETRIES:
132
+ try:
133
+ result = self.attempt_read()
134
+ return result
135
+ except Exception as error:
136
+ logger.error(f"Read from adapter '{self.user_name}' failed: {error}")
137
+ self.error = str(error)
138
+ read_try += 1
139
+ if read_try <= COMMAND_RETRIES:
140
+ logger.error(
141
+ f"Retrying read {read_try} out of {COMMAND_RETRIES} attempts..."
142
+ )
143
+ return None
144
+
145
+ @abstractmethod
146
+ def attempt_read(self) -> Any:
147
+ """Concrete implementation of the read logic."""
148
+ pass
@@ -0,0 +1,286 @@
1
+ """
2
+ Adapter controller for managing all adapters in the opensignalbox-interface module.
3
+ """
4
+
5
+ import json
6
+ import logging
7
+ from pathlib import Path
8
+ from typing import Any, Dict, List, Optional, Type
9
+
10
+ from opensignalbox.interface.adapters.base import BaseAdapter
11
+ from opensignalbox.interface.adapters.modbus.adapter import ModbusAdapter
12
+ from opensignalbox.interface.models import (
13
+ AdapterBase,
14
+ AdapterTypeEnum,
15
+ AdapterTypeInfo,
16
+ )
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class AdapterController:
22
+ """Controller for managing all adapters."""
23
+
24
+ def __init__(self, config_path: Path) -> None:
25
+ self.adapters: Dict[str, BaseAdapter] = {}
26
+ self.config_filepath = config_path / "adapters.json"
27
+ self.adapter_factories: Dict[AdapterTypeEnum, Type[BaseAdapter]] = {}
28
+
29
+ # Dictionary to store adapter-specific routers
30
+ self.adapter_routers: Dict[AdapterTypeEnum, Any] = {}
31
+
32
+ def register_adapter_factory(
33
+ self, adapter_type: AdapterTypeEnum, factory: Type[BaseAdapter]
34
+ ) -> None:
35
+ """Register a factory for creating adapters of a specific type.
36
+
37
+ Args:
38
+ adapter_type: The type of adapter this factory creates
39
+ factory: The adapter class that will be instantiated
40
+ """
41
+ # Set the controller reference for API routes
42
+ factory.set_controller(self)
43
+
44
+ # Register the factory
45
+ self.adapter_factories[adapter_type] = factory
46
+
47
+ # Check if the factory provides a router
48
+ router = factory.get_router()
49
+ if router:
50
+ self.adapter_routers[adapter_type] = router
51
+
52
+ def register_adapter_factories(self) -> None:
53
+ """Register all adapter factories."""
54
+ self.register_adapter_factory(AdapterTypeEnum.MODBUS, ModbusAdapter)
55
+
56
+ def add(self, adapter_base: AdapterBase, **kwargs) -> BaseAdapter:
57
+ """Add an adapter to the controller.
58
+
59
+ Args:
60
+ adapter_base: The base adapter model with common fields
61
+ **kwargs: Additional arguments specific to the adapter type
62
+
63
+ Returns:
64
+ The instantiated adapter
65
+
66
+ Raises:
67
+ FileExistsError: If an adapter with the same system name already exists
68
+ TypeError: If the adapter type is unknown
69
+ """
70
+ if adapter_base.system_name in self.adapters:
71
+ raise FileExistsError(
72
+ f"Adapter '{adapter_base.system_name}' already exists"
73
+ )
74
+
75
+ # Get the factory for this adapter type
76
+ factory = self.adapter_factories.get(adapter_base.adapter_type)
77
+ if not factory:
78
+ raise TypeError(f"Unknown adapter type: {adapter_base.adapter_type}")
79
+
80
+ # Create the adapter
81
+ try:
82
+ adapter = factory(**kwargs)
83
+ self.adapters[adapter_base.system_name] = adapter
84
+ return adapter
85
+ except Exception as e:
86
+ logger.error(f"Failed to create adapter: {e}")
87
+ raise
88
+
89
+ def get_all(self) -> Dict[str, BaseAdapter]:
90
+ """Get all adapters.
91
+
92
+ Returns:
93
+ Dictionary of adapters keyed by system name
94
+ """
95
+ return self.adapters
96
+
97
+ def get(self, system_name: str) -> BaseAdapter:
98
+ """Get an adapter by system name.
99
+
100
+ Args:
101
+ system_name: The system name of the adapter
102
+
103
+ Returns:
104
+ The adapter
105
+
106
+ Raises:
107
+ KeyError: If no adapter with the given system name exists
108
+ """
109
+ try:
110
+ return self.adapters[system_name]
111
+ except KeyError as exc:
112
+ raise KeyError(f"Cannot find adapter '{system_name}'") from exc
113
+
114
+ def update(self, system_name: str, **kwargs) -> BaseAdapter:
115
+ """Update an adapter.
116
+
117
+ Args:
118
+ system_name: The system name of the adapter to update
119
+ **kwargs: The fields to update
120
+
121
+ Returns:
122
+ The updated adapter
123
+
124
+ Raises:
125
+ KeyError: If no adapter with the given system name exists
126
+ """
127
+ try:
128
+ adapter = self.adapters[system_name]
129
+
130
+ # Update fields
131
+ for key, value in kwargs.items():
132
+ if hasattr(adapter, key) and value is not None:
133
+ setattr(adapter, key, value)
134
+
135
+ return adapter
136
+ except KeyError as exc:
137
+ raise KeyError(f"Cannot find adapter '{system_name}'") from exc
138
+
139
+ def remove(self, system_name: str) -> None:
140
+ """Remove an adapter.
141
+
142
+ Args:
143
+ system_name: The system name of the adapter to remove
144
+
145
+ Raises:
146
+ KeyError: If no adapter with the given system name exists
147
+ """
148
+ try:
149
+ adapter = self.adapters[system_name]
150
+ if adapter.is_connected:
151
+ adapter.disconnect()
152
+ del self.adapters[system_name]
153
+ except KeyError as exc:
154
+ raise KeyError(f"Cannot find adapter '{system_name}'") from exc
155
+
156
+ def load_from_json(self) -> None:
157
+ """Load adapters from a JSON file."""
158
+ if not self.config_filepath.exists():
159
+ logger.warning(f"Adapter config file not found: {self.config_filepath}")
160
+ return
161
+
162
+ try:
163
+ with open(self.config_filepath, "r") as f:
164
+ adapters_data = json.load(f)
165
+
166
+ for adapter_data in adapters_data.get("adapters", []):
167
+ try:
168
+ # Extract common fields
169
+ adapter_type = adapter_data.get("adapter_type", None)
170
+ if not adapter_type:
171
+ logger.error(f"Adapter missing type: {adapter_data}")
172
+ continue
173
+
174
+ # Create adapter base model
175
+ adapter_base = AdapterBase(**adapter_data)
176
+
177
+ # Add the adapter with specific fields
178
+ self.add(adapter_base, **adapter_data)
179
+ except Exception as e:
180
+ logger.error(f"Error loading adapter: {e}")
181
+
182
+ except Exception as e:
183
+ logger.error(f"Error loading adapters from {self.config_filepath}: {e}")
184
+
185
+ def save_to_json(self) -> None:
186
+ """Save adapters to a JSON file."""
187
+ try:
188
+ adapters_data = []
189
+ for adapter in self.adapters.values():
190
+ # Create a dictionary with all adapter fields
191
+ adapter_dict = {
192
+ "system_name": adapter.system_name,
193
+ "user_name": adapter.user_name,
194
+ "description": adapter.description,
195
+ "adapter_type": adapter.adapter_type,
196
+ "enabled": adapter.enabled,
197
+ }
198
+
199
+ # Add adapter-specific fields
200
+ for key, value in adapter.__dict__.items():
201
+ if (
202
+ key not in adapter_dict
203
+ and not key.startswith("_")
204
+ and not callable(value)
205
+ ):
206
+ adapter_dict[key] = value
207
+
208
+ adapters_data.append(adapter_dict)
209
+
210
+ with open(self.config_filepath, "w") as f:
211
+ json.dump(adapters_data, f, indent=2)
212
+
213
+ except Exception as e:
214
+ logger.error(f"Error saving adapters to {self.config_filepath}: {e}")
215
+
216
+ def get_adapter_routers(self) -> Dict[AdapterTypeEnum, Any]:
217
+ """Get all registered adapter routers.
218
+
219
+ Returns:
220
+ Dictionary of FastAPI routers keyed by adapter type
221
+ """
222
+ return self.adapter_routers
223
+
224
+ def get_adapter_class(
225
+ self, adapter_type: AdapterTypeEnum
226
+ ) -> Optional[Type[BaseAdapter]]:
227
+ """Get the adapter class for a given adapter type.
228
+
229
+ Args:
230
+ adapter_type: The adapter type to get the class for
231
+
232
+ Returns:
233
+ The adapter class, or None if not found
234
+ """
235
+ return self.adapter_factories.get(adapter_type)
236
+
237
+ def get_adapter_types(self) -> List[AdapterTypeEnum]:
238
+ """Get all available adapter types.
239
+
240
+ Returns:
241
+ List of available adapter types
242
+ """
243
+ return list(self.adapter_factories.keys())
244
+
245
+ def get_adapter_type_infos(self) -> List["AdapterTypeInfo"]:
246
+ """Get all available adapter types with their metadata.
247
+
248
+ Returns:
249
+ List of adapter type info objects
250
+ """
251
+ from opensignalbox.interface.models import AdapterTypeInfo
252
+
253
+ type_infos = []
254
+ for adapter_type in self.adapter_factories.keys():
255
+ adapter_factory = self.adapter_factories[adapter_type]
256
+
257
+ # Get metadata from the adapter class (class-level attributes)
258
+ adapter_class = adapter_factory
259
+ metadata = getattr(adapter_class, "metadata", {})
260
+
261
+ type_info = AdapterTypeInfo(
262
+ adapter_type=adapter_type,
263
+ name=metadata.get("name", adapter_type.value),
264
+ description=metadata.get(
265
+ "description", f"{adapter_type.value} adapter"
266
+ ),
267
+ supported_connection_types=metadata.get(
268
+ "supported_connection_types", []
269
+ ),
270
+ required_fields=metadata.get("required_fields", []),
271
+ optional_fields=metadata.get("optional_fields", []),
272
+ )
273
+ type_infos.append(type_info)
274
+
275
+ return type_infos
276
+
277
+ def disconnect_all(self) -> None:
278
+ """Disconnect all adapters."""
279
+ for adapter in self.adapters.values():
280
+ if adapter.is_connected:
281
+ try:
282
+ adapter.disconnect()
283
+ except Exception as e:
284
+ logger.error(
285
+ f"Error disconnecting adapter {adapter.system_name}: {e}"
286
+ )
@@ -0,0 +1,3 @@
1
+ """
2
+ Init file for Modbus adapter.
3
+ """