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,103 @@
1
+ """
2
+ Bell interface models.
3
+ """
4
+
5
+ from enum import Enum
6
+ from typing import List, Literal, Optional
7
+
8
+ from opensignalbox.common.models import CamelModel
9
+ from opensignalbox.common.variables import SharedVariable
10
+ from opensignalbox.interface.models import (
11
+ Interface,
12
+ InterfaceBase,
13
+ InterfaceTypeEnum,
14
+ InterfaceUpdate,
15
+ )
16
+
17
+
18
+ class BellConnectionType(str, Enum):
19
+ """Connection types for Bell interfaces."""
20
+
21
+ MODBUS_ADAPTER = "ModbusAdapter"
22
+ DIRECT_TCP = "DirectTCP"
23
+ TEST = "Test" # For testing purposes only
24
+
25
+
26
+ class ForceStateEnum(str, Enum):
27
+ """Enum for force states."""
28
+
29
+ ON = "on"
30
+ OFF = "off"
31
+ NONE = "none" # To remove forcing
32
+
33
+
34
+ class BellChannelConfig(CamelModel):
35
+ pass
36
+
37
+
38
+ class BellChannel(CamelModel):
39
+ """Model for Bell channels."""
40
+
41
+ system_name: str
42
+ user_name: str
43
+ description: str = ""
44
+ tags: List[str] = []
45
+ in_count: int = 0
46
+ in_code: str = ""
47
+ in_shared_variable: Optional[SharedVariable] = None
48
+ out_count: int = 0
49
+ last_out_count: int = 0
50
+ out_code: str = ""
51
+ out_shared_variable_name: str | None = None
52
+
53
+
54
+ class BellInterfaceFields(CamelModel):
55
+ """Mixin with Bell-specific fields."""
56
+
57
+ connection_type: BellConnectionType = BellConnectionType.MODBUS_ADAPTER
58
+
59
+ channels: dict[int, BellChannel] = {}
60
+
61
+ # Fields for modbus connection
62
+ modbus_address: Optional[int] = None
63
+
64
+ # Fields for TCP connection
65
+ tcp_host: Optional[str] = None
66
+ tcp_port: Optional[int] = None
67
+
68
+
69
+ class BellInterfaceBase(Interface, BellInterfaceFields):
70
+ """Base model with Bell-specific fields."""
71
+
72
+ pass
73
+
74
+
75
+ class BellInterface(BellInterfaceBase):
76
+ """Runtime model for Bell interfaces."""
77
+
78
+ pass
79
+
80
+
81
+ class BellInterfaceCreate(InterfaceBase, BellInterfaceFields):
82
+ """API model for creating Bell interfaces."""
83
+
84
+ interface_type: Literal[InterfaceTypeEnum.BELL] = InterfaceTypeEnum.BELL
85
+
86
+
87
+ class BellInterfacePublic(BellInterfaceBase):
88
+ """API model for Bell interface responses."""
89
+
90
+ pass
91
+
92
+
93
+ class BellInterfaceUpdate(InterfaceUpdate):
94
+ """API model for updating Bell interfaces."""
95
+
96
+ pass
97
+
98
+
99
+ class BellForceMessage(CamelModel):
100
+ """Message for setting force state on Bell channels."""
101
+
102
+ channel: int
103
+ state: ForceStateEnum
@@ -0,0 +1,467 @@
1
+ """
2
+ Interface controller for managing all interfaces 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 anyio import sleep
11
+ from opensignalbox.common.messaging import get_messager
12
+ from opensignalbox.common.variables import SharedVariableCollection
13
+ from opensignalbox.interface.adapters.controller import AdapterController
14
+ from opensignalbox.interface.interfaces.base import InterfaceHandler
15
+ from opensignalbox.interface.interfaces.basicio.handler import BasicIOInterfaceHandler
16
+ from opensignalbox.interface.interfaces.bell.handler import BellInterfaceHandler
17
+ from opensignalbox.interface.models import (
18
+ Interface,
19
+ InterfaceTypeEnum,
20
+ InterfaceTypeInfo,
21
+ ModuleConfig,
22
+ )
23
+
24
+ logger = logging.getLogger(__name__)
25
+ messager = get_messager()
26
+
27
+ MAX_RETRIES = 3
28
+
29
+
30
+ class InterfaceController:
31
+ """Controller for managing all interfaces."""
32
+
33
+ def __init__(
34
+ self,
35
+ config_path: Path,
36
+ output_variables: SharedVariableCollection,
37
+ adapters: AdapterController,
38
+ ) -> None:
39
+ self.interfaces: Dict[str, Interface] = {}
40
+ self.config_path = config_path
41
+ self.output_variables = output_variables
42
+ self.input_variable_subs: Dict[str, List[Any]] = {}
43
+ self.adapters = adapters
44
+ self.module_config = ModuleConfig()
45
+ self.output_sync_countdown: int = 0
46
+
47
+ # Initialize interface handlers dictionary (will be populated by register_handler)
48
+ self.interface_handlers: Dict[InterfaceTypeEnum, Type[InterfaceHandler]] = {}
49
+
50
+ # Dictionary to store interface-specific routers
51
+ self.interface_routers: Dict[InterfaceTypeEnum, Any] = {}
52
+
53
+ def register_handler(
54
+ self, interface_type: InterfaceTypeEnum, handler: Type[InterfaceHandler]
55
+ ) -> None:
56
+ """Register a handler for an interface type.
57
+
58
+ Args:
59
+ interface_type: The type of interface this handler handles
60
+ handler: The handler class for this interface type
61
+ """
62
+ # Initialize the handler with controller references
63
+ handler.init(
64
+ output_variables=self.output_variables,
65
+ input_variable_subs=self.input_variable_subs,
66
+ variable_update_callback=self.variable_update_callback,
67
+ )
68
+
69
+ # Set the controllers for the handler
70
+ handler.set_controllers(self, self.adapters)
71
+
72
+ # Register the handler
73
+ self.interface_handlers[interface_type] = handler
74
+
75
+ # Check if the handler provides a router
76
+ router = handler.get_router()
77
+ if router:
78
+ self.interface_routers[interface_type] = router
79
+
80
+ def register_interface_handlers(self) -> None:
81
+ """Register all interface handlers."""
82
+ self.register_handler(InterfaceTypeEnum.BASICIO, BasicIOInterfaceHandler)
83
+ self.register_handler(InterfaceTypeEnum.BELL, BellInterfaceHandler)
84
+
85
+ def add(self, interface: Interface) -> Interface:
86
+ """Add an interface to the controller.
87
+
88
+ Args:
89
+ interface: The interface to add
90
+
91
+ Returns:
92
+ The initialized interface
93
+
94
+ Raises:
95
+ FileExistsError: If an interface with the same system name already exists
96
+ TypeError: If the interface type is unknown
97
+ """
98
+ if interface.system_name in self.interfaces:
99
+ raise FileExistsError(
100
+ f"Interface with system name '{interface.system_name}' already exists"
101
+ )
102
+
103
+ try:
104
+ # Use the handler based on interface type
105
+ handler = self.interface_handlers.get(interface.interface_type)
106
+ if not handler:
107
+ raise TypeError(f"Unknown interface type: {interface.interface_type}")
108
+
109
+ # Handler.add already does type conversion if needed
110
+ specialized_interface = handler.add(interface)
111
+ self.interfaces[interface.system_name] = specialized_interface
112
+ return specialized_interface
113
+ except Exception as e:
114
+ logger.error(f"Failed to add interface: {e}")
115
+ raise
116
+
117
+ def get_all(self) -> Dict[str, Interface]:
118
+ """Get all interfaces.
119
+
120
+ Returns:
121
+ Dictionary of interfaces keyed by system name
122
+ """
123
+ return self.interfaces
124
+
125
+ def get_interface_type_infos(self) -> List["InterfaceTypeInfo"]:
126
+ """Get all available interface types with their metadata.
127
+
128
+ Returns:
129
+ List of interface type info objects
130
+ """
131
+ from opensignalbox.interface.models import InterfaceTypeInfo
132
+
133
+ type_infos = []
134
+ for interface_type, handler_class in self.interface_handlers.items():
135
+ # Get metadata from the handler class
136
+ metadata = getattr(handler_class, "metadata", {})
137
+
138
+ type_info = InterfaceTypeInfo(
139
+ interface_type=interface_type,
140
+ name=metadata.get("name", interface_type.value),
141
+ description=metadata.get(
142
+ "description", f"{interface_type.value} interface"
143
+ ),
144
+ supported_connection_types=metadata.get(
145
+ "supported_connection_types", []
146
+ ),
147
+ required_fields=metadata.get("required_fields", []),
148
+ optional_fields=metadata.get("optional_fields", []),
149
+ )
150
+ type_infos.append(type_info)
151
+
152
+ return type_infos
153
+
154
+ def get(self, system_name: str) -> Interface:
155
+ """Get an interface by system name.
156
+
157
+ Args:
158
+ system_name: The system name of the interface
159
+
160
+ Returns:
161
+ The interface
162
+
163
+ Raises:
164
+ KeyError: If no interface with the given system name exists
165
+ """
166
+ try:
167
+ return self.interfaces[system_name]
168
+ except KeyError as exc:
169
+ raise KeyError(f"Cannot find interface '{system_name}'") from exc
170
+
171
+ def update(self, system_name: str, fields: Dict[str, Any]) -> Interface:
172
+ """Update an interface.
173
+
174
+ Args:
175
+ system_name: The system name of the interface to update
176
+ fields: The fields to update
177
+
178
+ Returns:
179
+ The updated interface
180
+
181
+ Raises:
182
+ KeyError: If no interface with the given system name exists
183
+ TypeError: If the interface type is unknown
184
+ """
185
+ try:
186
+ interface = self.interfaces[system_name]
187
+ # Use the handler based on interface type
188
+ handler = self.interface_handlers.get(interface.interface_type)
189
+ if not handler:
190
+ raise TypeError(f"Unknown interface type: {interface.interface_type}")
191
+ # Update the interface using the handler
192
+ handler.update(interface, fields)
193
+ return self.interfaces[system_name]
194
+ except KeyError as exc:
195
+ raise KeyError(f"Cannot find interface '{system_name}'") from exc
196
+
197
+ def remove(self, system_name: str) -> None:
198
+ """Remove an interface.
199
+
200
+ Args:
201
+ system_name: The system name of the interface to remove
202
+
203
+ Raises:
204
+ KeyError: If no interface with the given system name exists
205
+ TypeError: If the interface type is unknown
206
+ """
207
+ try:
208
+ interface = self.interfaces[system_name]
209
+ # Use the handler based on interface type
210
+ handler = self.interface_handlers.get(interface.interface_type)
211
+ if not handler:
212
+ raise TypeError(f"Unknown interface type: {interface.interface_type}")
213
+ # Remove the interface using the handler
214
+ handler.remove(interface)
215
+ del self.interfaces[system_name]
216
+ except KeyError as exc:
217
+ raise KeyError(f"Cannot find interface '{system_name}'") from exc
218
+
219
+ def variable_update_callback(self, variable: str, json_data: str) -> None:
220
+ """Callback for variable updates.
221
+
222
+ Args:
223
+ variable: The name of the variable that was updated
224
+ json_data: The serialized JSON data of the variable update
225
+ """
226
+ # Check if we have any interfaces subscribed to this variable
227
+ if variable in self.input_variable_subs:
228
+ for iface_tuple in self.input_variable_subs[variable]:
229
+ interface_name = iface_tuple[0] # Assuming the tuple structure
230
+
231
+ # Get the interface and its handler
232
+ try:
233
+ interface = self.interfaces[interface_name]
234
+ handler = self.interface_handlers.get(interface.interface_type)
235
+ if handler:
236
+ # Let the handler process the variable update
237
+ handler.handle_variable_update(interface, variable, json_data)
238
+ except (KeyError, TypeError) as e:
239
+ logger.error(f"Error handling variable update: {e}")
240
+
241
+ def sync_interfaces(self) -> None:
242
+ """Synchronize all interfaces with their adapters."""
243
+ for interface_name, interface in self.interfaces.items():
244
+ # logger.debug(f"Syncing interface '{interface_name}', with enabled={interface.enabled}, connected={interface.is_connected}")
245
+ if not interface.enabled or not interface.is_connected:
246
+ continue
247
+
248
+ handler = self.interface_handlers.get(interface.interface_type)
249
+ if not handler:
250
+ logger.error(
251
+ f"Unknown interface type for '{interface_name}': {interface.interface_type}"
252
+ )
253
+ continue
254
+
255
+ retries = 0
256
+ while retries < MAX_RETRIES:
257
+ try:
258
+ # Attempt to sync the interface
259
+ # Read from hardware
260
+ handler.read_data(interface)
261
+ # Write to hardware
262
+ handler.write_data(interface)
263
+ interface.error = None # Clear any previous error
264
+ break
265
+ except Exception as e:
266
+ logger.error(f"Error syncing interface '{interface_name}': {e}")
267
+ logger.info(
268
+ f"Retrying sync for interface '{interface_name}' ({retries + 1}/{MAX_RETRIES})"
269
+ )
270
+ retries += 1
271
+ if retries >= MAX_RETRIES:
272
+ interface.error = str(e)
273
+ logger.error(
274
+ f"Max retries reached for interface '{interface_name}', disabling it."
275
+ )
276
+ interface.enabled = False
277
+ handler.disconnect(interface) # Ensure we disconnect on failure
278
+ handler.connect(interface) # Attempt to reconnect
279
+
280
+ async def tick(self) -> None:
281
+ """Perform a tick update for interfaces not synchronized on systick."""
282
+ self.sync_interfaces()
283
+ await sleep(self.module_config.input_update_period)
284
+
285
+ def load_from_json(self) -> None:
286
+ """Load configuration from a JSON file."""
287
+
288
+ # First the module config
289
+ self.config_filepath = self.config_path / "module_config.json"
290
+ if not self.config_filepath.exists():
291
+ logger.warning(f"Module config file not found: {self.config_filepath}")
292
+ logger.info("Creating default module config")
293
+ # Create default config if it doesn't exist
294
+ self.module_config = ModuleConfig()
295
+ self.save_module_config()
296
+
297
+ try:
298
+ with open(self.config_filepath, "r") as f:
299
+ config_data = json.load(f)
300
+ self.module_config.input_update_period = config_data.get(
301
+ "input_update_period", 0.1
302
+ )
303
+ self.module_config.output_update_period = config_data.get(
304
+ "output_update_period", 1
305
+ )
306
+ self.module_config.sync_on_systick = config_data.get(
307
+ "sync_on_systick", False
308
+ )
309
+ except Exception as e:
310
+ logger.error(
311
+ f"Error loading module config from {self.config_filepath}, using defaults: {e}"
312
+ )
313
+
314
+ # Now load each interface in the interfaces directory
315
+ interfaces_config_dir = self.config_path / "interfaces"
316
+ if not interfaces_config_dir.exists():
317
+ logger.warning(
318
+ f"Interfaces config directory not found: {interfaces_config_dir}"
319
+ )
320
+ return
321
+
322
+ # Clear existing interfaces
323
+ for interface in self.interfaces.values():
324
+ try:
325
+ handler = self.interface_handlers.get(interface.interface_type)
326
+ if handler:
327
+ handler.remove(interface)
328
+ except Exception as e:
329
+ logger.error(f"Error removing interface '{interface.system_name}': {e}")
330
+
331
+ for interface_file in interfaces_config_dir.glob("*.json"):
332
+ try:
333
+ with open(interface_file, "r") as f:
334
+ interface_data = json.load(f)
335
+ # Use the handler based on interface type
336
+ interface_name = interface_data.get("system_name")
337
+ interface_type = interface_data.get("interface_type")
338
+ adapter_name = interface_data.get("adapter_name")
339
+
340
+ if adapter_name:
341
+ adapter = self.adapters.get(adapter_name)
342
+ if not adapter:
343
+ logger.error(
344
+ f"Adapter '{adapter_name}' not found for interface '{interface_name}'"
345
+ )
346
+ raise KeyError(f"Adapter '{adapter_name}' not found")
347
+
348
+ handler = self.interface_handlers.get(interface_type)
349
+ if not handler:
350
+ logger.error(
351
+ f"Unknown interface type for '{interface_name}': {interface_type}"
352
+ )
353
+ raise TypeError(f"Unknown interface type: {interface_type}")
354
+ handler.load_from_json(interface_data)
355
+ except Exception as e:
356
+ logger.error(f"Error loading interface from {interface_file}: {e}")
357
+
358
+ def save_module_config(self) -> None:
359
+ """Save the module configuration to a JSON file."""
360
+ try:
361
+ with open(self.config_filepath, "w") as f:
362
+ json.dump(self.module_config.model_dump(), f, indent=2)
363
+ except Exception as e:
364
+ logger.error(f"Error saving module config to {self.config_filepath}: {e}")
365
+
366
+ def save_to_json(self, suffix: str | None = None) -> None:
367
+ """Save configuration to a JSON file.
368
+ Args:
369
+ suffix: Optional suffix to append to the interface filenames
370
+ """
371
+
372
+ # Save the module config first
373
+ self.save_module_config()
374
+
375
+ # Save each interface
376
+ interfaces_config_dir = self.config_path / "interfaces"
377
+ interfaces_config_dir.mkdir(parents=True, exist_ok=True)
378
+
379
+ for interface in self.interfaces.values():
380
+ interface_file = (
381
+ interfaces_config_dir / f"{interface.system_name}{suffix or ''}.json"
382
+ )
383
+ try:
384
+ with open(interface_file, "w") as f:
385
+ # Use the handler based on interface type
386
+ handler = self.interface_handlers.get(interface.interface_type)
387
+ if handler:
388
+ json.dump(handler.save_to_json(interface), f, indent=2)
389
+ else:
390
+ logger.error(
391
+ f"Unknown interface type for '{interface.system_name}': {interface.interface_type}"
392
+ )
393
+ except Exception as e:
394
+ logger.error(
395
+ f"Error saving interface '{interface.system_name}' to {interface_file}: {e}"
396
+ )
397
+
398
+ def get_interface_routers(self) -> Dict[InterfaceTypeEnum, Any]:
399
+ """Get all registered interface routers.
400
+
401
+ Returns:
402
+ Dictionary of FastAPI routers keyed by interface type
403
+ """
404
+ return self.interface_routers
405
+
406
+ def connect_all(self) -> None:
407
+ """Connect all interfaces."""
408
+ for interface in self.interfaces.values():
409
+ if not interface.enabled:
410
+ continue
411
+ handler = self.interface_handlers.get(interface.interface_type)
412
+ if handler:
413
+ retries = 0
414
+ while retries < MAX_RETRIES and not interface.is_connected:
415
+ retries += 1
416
+ logger.info(
417
+ f"Connecting interface '{interface.system_name}' (attempt {retries}/{MAX_RETRIES})"
418
+ )
419
+ handler.connect(interface)
420
+ if interface.is_connected:
421
+ logger.info(
422
+ f"Interface '{interface.system_name}' connected successfully"
423
+ )
424
+ interface.error = None
425
+ break
426
+ else:
427
+ logger.error(
428
+ f"Failed to connect interface '{interface.system_name}'"
429
+ )
430
+ interface.error = "Connection failed"
431
+ interface.enabled = False
432
+ else:
433
+ logger.error(
434
+ f"Unknown interface type for '{interface.system_name}': {interface.interface_type}"
435
+ )
436
+
437
+ def disconnect_all(self) -> None:
438
+ """Disconnect all interfaces."""
439
+ for interface in self.interfaces.values():
440
+ handler = self.interface_handlers.get(interface.interface_type)
441
+ if handler:
442
+ handler.disconnect(interface)
443
+ else:
444
+ logger.error(
445
+ f"Unknown interface type for '{interface.system_name}': {interface.interface_type}"
446
+ )
447
+
448
+ def get_adapter_for_interface(self, interface: Interface) -> Optional[Any]:
449
+ """Get the adapter instance for a given interface.
450
+
451
+ Args:
452
+ interface: The interface to get the adapter for
453
+
454
+ Returns:
455
+ The adapter instance, or None if no adapter is assigned
456
+
457
+ Raises:
458
+ KeyError: If the adapter name is not found
459
+ """
460
+ if not hasattr(interface, "adapter_name") or not interface.adapter_name:
461
+ return None
462
+
463
+ adapter = self.adapters.get(interface.adapter_name)
464
+ if not adapter:
465
+ raise KeyError(f"Adapter '{interface.adapter_name}' not found")
466
+
467
+ return adapter