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.
- opensignalbox/interface/__init__.py +42 -0
- opensignalbox/interface/adapters/__init__.py +3 -0
- opensignalbox/interface/adapters/base.py +148 -0
- opensignalbox/interface/adapters/controller.py +286 -0
- opensignalbox/interface/adapters/modbus/__init__.py +3 -0
- opensignalbox/interface/adapters/modbus/adapter.py +485 -0
- opensignalbox/interface/assets/favicon.ico +0 -0
- opensignalbox/interface/interfaces/__init__.py +3 -0
- opensignalbox/interface/interfaces/base.py +170 -0
- opensignalbox/interface/interfaces/basicio/__init__.py +3 -0
- opensignalbox/interface/interfaces/basicio/connections.py +239 -0
- opensignalbox/interface/interfaces/basicio/handler.py +1096 -0
- opensignalbox/interface/interfaces/basicio/models.py +165 -0
- opensignalbox/interface/interfaces/bell/__init__.py +3 -0
- opensignalbox/interface/interfaces/bell/connections.py +279 -0
- opensignalbox/interface/interfaces/bell/handler.py +802 -0
- opensignalbox/interface/interfaces/bell/models.py +103 -0
- opensignalbox/interface/interfaces/controller.py +467 -0
- opensignalbox/interface/main.py +187 -0
- opensignalbox/interface/models.py +155 -0
- opensignalbox/interface/routes/__init__.py +4 -0
- opensignalbox/interface/routes/adapters.py +119 -0
- opensignalbox/interface/routes/interfaces.py +104 -0
- opensignalbox/interface/routes.py +204 -0
- opensignalbox/interface/utils.py +29 -0
- opensignalbox/interface/version.py +5 -0
- opensignalbox/interface/web_ui/.gitignore +24 -0
- opensignalbox/interface/web_ui/README.md +5 -0
- opensignalbox/interface/web_ui/WEB_UI_REFACTORING.md +136 -0
- opensignalbox/interface/web_ui/components.json +17 -0
- opensignalbox/interface/web_ui/dist/assets/index-DhsEc6uo.css +2020 -0
- opensignalbox/interface/web_ui/dist/assets/index-t__5JZjf.js +39056 -0
- opensignalbox/interface/web_ui/dist/assets/index-t__5JZjf.js.map +1 -0
- opensignalbox/interface/web_ui/dist/index.html +14 -0
- opensignalbox/interface/web_ui/index.html +13 -0
- opensignalbox/interface/web_ui/package-lock.json +3710 -0
- opensignalbox/interface/web_ui/package.json +39 -0
- opensignalbox/interface/web_ui/src/App.vue +32 -0
- opensignalbox/interface/web_ui/src/assets/favicon.ico +0 -0
- opensignalbox/interface/web_ui/src/assets/index.css +83 -0
- opensignalbox/interface/web_ui/src/components/SharedVariablePicker.vue +112 -0
- opensignalbox/interface/web_ui/src/components/adapters/AdapterList.vue +183 -0
- opensignalbox/interface/web_ui/src/components/adapters/types.ts +35 -0
- opensignalbox/interface/web_ui/src/components/interfaces/InterfaceList.vue +200 -0
- opensignalbox/interface/web_ui/src/components/ui/badge/Badge.vue +16 -0
- opensignalbox/interface/web_ui/src/components/ui/badge/index.ts +25 -0
- opensignalbox/interface/web_ui/src/components/ui/breadcrumb/Breadcrumb.vue +13 -0
- opensignalbox/interface/web_ui/src/components/ui/breadcrumb/BreadcrumbEllipsis.vue +22 -0
- opensignalbox/interface/web_ui/src/components/ui/breadcrumb/BreadcrumbItem.vue +16 -0
- opensignalbox/interface/web_ui/src/components/ui/breadcrumb/BreadcrumbLink.vue +19 -0
- opensignalbox/interface/web_ui/src/components/ui/breadcrumb/BreadcrumbList.vue +16 -0
- opensignalbox/interface/web_ui/src/components/ui/breadcrumb/BreadcrumbPage.vue +19 -0
- opensignalbox/interface/web_ui/src/components/ui/breadcrumb/BreadcrumbSeparator.vue +21 -0
- opensignalbox/interface/web_ui/src/components/ui/breadcrumb/index.ts +7 -0
- opensignalbox/interface/web_ui/src/components/ui/button/Button.vue +26 -0
- opensignalbox/interface/web_ui/src/components/ui/button/index.ts +35 -0
- opensignalbox/interface/web_ui/src/components/ui/card/Card.vue +21 -0
- opensignalbox/interface/web_ui/src/components/ui/card/CardContent.vue +14 -0
- opensignalbox/interface/web_ui/src/components/ui/card/CardDescription.vue +14 -0
- opensignalbox/interface/web_ui/src/components/ui/card/CardFooter.vue +14 -0
- opensignalbox/interface/web_ui/src/components/ui/card/CardHeader.vue +14 -0
- opensignalbox/interface/web_ui/src/components/ui/card/CardTitle.vue +18 -0
- opensignalbox/interface/web_ui/src/components/ui/card/index.ts +6 -0
- opensignalbox/interface/web_ui/src/components/ui/checkbox/Checkbox.vue +33 -0
- opensignalbox/interface/web_ui/src/components/ui/checkbox/index.ts +1 -0
- opensignalbox/interface/web_ui/src/components/ui/collapsible/Collapsible.vue +15 -0
- opensignalbox/interface/web_ui/src/components/ui/collapsible/CollapsibleContent.vue +11 -0
- opensignalbox/interface/web_ui/src/components/ui/collapsible/CollapsibleTrigger.vue +11 -0
- opensignalbox/interface/web_ui/src/components/ui/collapsible/index.ts +3 -0
- opensignalbox/interface/web_ui/src/components/ui/command/Command.vue +30 -0
- opensignalbox/interface/web_ui/src/components/ui/command/CommandDialog.vue +21 -0
- opensignalbox/interface/web_ui/src/components/ui/command/CommandEmpty.vue +20 -0
- opensignalbox/interface/web_ui/src/components/ui/command/CommandGroup.vue +29 -0
- opensignalbox/interface/web_ui/src/components/ui/command/CommandInput.vue +33 -0
- opensignalbox/interface/web_ui/src/components/ui/command/CommandItem.vue +26 -0
- opensignalbox/interface/web_ui/src/components/ui/command/CommandList.vue +27 -0
- opensignalbox/interface/web_ui/src/components/ui/command/CommandSeparator.vue +23 -0
- opensignalbox/interface/web_ui/src/components/ui/command/CommandShortcut.vue +14 -0
- opensignalbox/interface/web_ui/src/components/ui/command/index.ts +9 -0
- opensignalbox/interface/web_ui/src/components/ui/dialog/Dialog.vue +14 -0
- opensignalbox/interface/web_ui/src/components/ui/dialog/DialogClose.vue +11 -0
- opensignalbox/interface/web_ui/src/components/ui/dialog/DialogContent.vue +50 -0
- opensignalbox/interface/web_ui/src/components/ui/dialog/DialogDescription.vue +24 -0
- opensignalbox/interface/web_ui/src/components/ui/dialog/DialogFooter.vue +19 -0
- opensignalbox/interface/web_ui/src/components/ui/dialog/DialogHeader.vue +16 -0
- opensignalbox/interface/web_ui/src/components/ui/dialog/DialogScrollContent.vue +59 -0
- opensignalbox/interface/web_ui/src/components/ui/dialog/DialogTitle.vue +29 -0
- opensignalbox/interface/web_ui/src/components/ui/dialog/DialogTrigger.vue +11 -0
- opensignalbox/interface/web_ui/src/components/ui/dialog/index.ts +9 -0
- opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/DropdownMenu.vue +14 -0
- opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue +40 -0
- opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/DropdownMenuContent.vue +38 -0
- opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/DropdownMenuGroup.vue +11 -0
- opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/DropdownMenuItem.vue +28 -0
- opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/DropdownMenuLabel.vue +24 -0
- opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue +19 -0
- opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue +41 -0
- opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/DropdownMenuSeparator.vue +22 -0
- opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/DropdownMenuShortcut.vue +14 -0
- opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/DropdownMenuSub.vue +19 -0
- opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/DropdownMenuSubContent.vue +30 -0
- opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue +33 -0
- opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/DropdownMenuTrigger.vue +13 -0
- opensignalbox/interface/web_ui/src/components/ui/dropdown-menu/index.ts +16 -0
- opensignalbox/interface/web_ui/src/components/ui/form/FormControl.vue +16 -0
- opensignalbox/interface/web_ui/src/components/ui/form/FormDescription.vue +20 -0
- opensignalbox/interface/web_ui/src/components/ui/form/FormItem.vue +19 -0
- opensignalbox/interface/web_ui/src/components/ui/form/FormLabel.vue +23 -0
- opensignalbox/interface/web_ui/src/components/ui/form/FormMessage.vue +16 -0
- opensignalbox/interface/web_ui/src/components/ui/form/index.ts +7 -0
- opensignalbox/interface/web_ui/src/components/ui/form/injectionKeys.ts +4 -0
- opensignalbox/interface/web_ui/src/components/ui/form/useFormField.ts +30 -0
- opensignalbox/interface/web_ui/src/components/ui/input/Input.vue +24 -0
- opensignalbox/interface/web_ui/src/components/ui/input/index.ts +1 -0
- opensignalbox/interface/web_ui/src/components/ui/label/Label.vue +27 -0
- opensignalbox/interface/web_ui/src/components/ui/label/index.ts +1 -0
- opensignalbox/interface/web_ui/src/components/ui/number-field/NumberField.vue +23 -0
- opensignalbox/interface/web_ui/src/components/ui/number-field/NumberFieldContent.vue +14 -0
- opensignalbox/interface/web_ui/src/components/ui/number-field/NumberFieldDecrement.vue +25 -0
- opensignalbox/interface/web_ui/src/components/ui/number-field/NumberFieldIncrement.vue +25 -0
- opensignalbox/interface/web_ui/src/components/ui/number-field/NumberFieldInput.vue +16 -0
- opensignalbox/interface/web_ui/src/components/ui/number-field/index.ts +5 -0
- opensignalbox/interface/web_ui/src/components/ui/popover/Popover.vue +15 -0
- opensignalbox/interface/web_ui/src/components/ui/popover/PopoverContent.vue +48 -0
- opensignalbox/interface/web_ui/src/components/ui/popover/PopoverTrigger.vue +11 -0
- opensignalbox/interface/web_ui/src/components/ui/popover/index.ts +3 -0
- opensignalbox/interface/web_ui/src/components/ui/radio-group/RadioGroup.vue +25 -0
- opensignalbox/interface/web_ui/src/components/ui/radio-group/RadioGroupItem.vue +39 -0
- opensignalbox/interface/web_ui/src/components/ui/radio-group/index.ts +2 -0
- opensignalbox/interface/web_ui/src/components/ui/scroll-area/ScrollArea.vue +29 -0
- opensignalbox/interface/web_ui/src/components/ui/scroll-area/ScrollBar.vue +30 -0
- opensignalbox/interface/web_ui/src/components/ui/scroll-area/index.ts +2 -0
- opensignalbox/interface/web_ui/src/components/ui/select/Select.vue +15 -0
- opensignalbox/interface/web_ui/src/components/ui/select/SelectContent.vue +53 -0
- opensignalbox/interface/web_ui/src/components/ui/select/SelectGroup.vue +19 -0
- opensignalbox/interface/web_ui/src/components/ui/select/SelectItem.vue +44 -0
- opensignalbox/interface/web_ui/src/components/ui/select/SelectItemText.vue +11 -0
- opensignalbox/interface/web_ui/src/components/ui/select/SelectLabel.vue +13 -0
- opensignalbox/interface/web_ui/src/components/ui/select/SelectScrollDownButton.vue +24 -0
- opensignalbox/interface/web_ui/src/components/ui/select/SelectScrollUpButton.vue +24 -0
- opensignalbox/interface/web_ui/src/components/ui/select/SelectSeparator.vue +17 -0
- opensignalbox/interface/web_ui/src/components/ui/select/SelectTrigger.vue +31 -0
- opensignalbox/interface/web_ui/src/components/ui/select/SelectValue.vue +11 -0
- opensignalbox/interface/web_ui/src/components/ui/select/index.ts +11 -0
- opensignalbox/interface/web_ui/src/components/ui/separator/Separator.vue +35 -0
- opensignalbox/interface/web_ui/src/components/ui/separator/index.ts +1 -0
- opensignalbox/interface/web_ui/src/components/ui/sheet/Sheet.vue +14 -0
- opensignalbox/interface/web_ui/src/components/ui/sheet/SheetClose.vue +11 -0
- opensignalbox/interface/web_ui/src/components/ui/sheet/SheetContent.vue +56 -0
- opensignalbox/interface/web_ui/src/components/ui/sheet/SheetDescription.vue +22 -0
- opensignalbox/interface/web_ui/src/components/ui/sheet/SheetFooter.vue +19 -0
- opensignalbox/interface/web_ui/src/components/ui/sheet/SheetHeader.vue +16 -0
- opensignalbox/interface/web_ui/src/components/ui/sheet/SheetTitle.vue +22 -0
- opensignalbox/interface/web_ui/src/components/ui/sheet/SheetTrigger.vue +11 -0
- opensignalbox/interface/web_ui/src/components/ui/sheet/index.ts +31 -0
- opensignalbox/interface/web_ui/src/components/ui/table/Table.vue +16 -0
- opensignalbox/interface/web_ui/src/components/ui/table/TableBody.vue +14 -0
- opensignalbox/interface/web_ui/src/components/ui/table/TableCaption.vue +14 -0
- opensignalbox/interface/web_ui/src/components/ui/table/TableCell.vue +21 -0
- opensignalbox/interface/web_ui/src/components/ui/table/TableEmpty.vue +37 -0
- opensignalbox/interface/web_ui/src/components/ui/table/TableFooter.vue +14 -0
- opensignalbox/interface/web_ui/src/components/ui/table/TableHead.vue +14 -0
- opensignalbox/interface/web_ui/src/components/ui/table/TableHeader.vue +14 -0
- opensignalbox/interface/web_ui/src/components/ui/table/TableRow.vue +14 -0
- opensignalbox/interface/web_ui/src/components/ui/table/index.ts +9 -0
- opensignalbox/interface/web_ui/src/components/ui/tabs/Tabs.vue +15 -0
- opensignalbox/interface/web_ui/src/components/ui/tabs/TabsContent.vue +22 -0
- opensignalbox/interface/web_ui/src/components/ui/tabs/TabsList.vue +25 -0
- opensignalbox/interface/web_ui/src/components/ui/tabs/TabsTrigger.vue +29 -0
- opensignalbox/interface/web_ui/src/components/ui/tabs/index.ts +4 -0
- opensignalbox/interface/web_ui/src/components/ui/tags-input/TagsInput.vue +22 -0
- opensignalbox/interface/web_ui/src/components/ui/tags-input/TagsInputInput.vue +19 -0
- opensignalbox/interface/web_ui/src/components/ui/tags-input/TagsInputItem.vue +22 -0
- opensignalbox/interface/web_ui/src/components/ui/tags-input/TagsInputItemDelete.vue +24 -0
- opensignalbox/interface/web_ui/src/components/ui/tags-input/TagsInputItemText.vue +19 -0
- opensignalbox/interface/web_ui/src/components/ui/tags-input/index.ts +5 -0
- opensignalbox/interface/web_ui/src/components/ui/textarea/Textarea.vue +24 -0
- opensignalbox/interface/web_ui/src/components/ui/textarea/index.ts +1 -0
- opensignalbox/interface/web_ui/src/components/ui/tooltip/Tooltip.vue +14 -0
- opensignalbox/interface/web_ui/src/components/ui/tooltip/TooltipContent.vue +31 -0
- opensignalbox/interface/web_ui/src/components/ui/tooltip/TooltipProvider.vue +11 -0
- opensignalbox/interface/web_ui/src/components/ui/tooltip/TooltipTrigger.vue +11 -0
- opensignalbox/interface/web_ui/src/components/ui/tooltip/index.ts +4 -0
- opensignalbox/interface/web_ui/src/libs/utils.ts +6 -0
- opensignalbox/interface/web_ui/src/main.ts +141 -0
- opensignalbox/interface/web_ui/src/views/Overview.vue +11 -0
- opensignalbox/interface/web_ui/src/views/Settings.vue +6 -0
- opensignalbox/interface/web_ui/src/views/SleafordEast.vue +205 -0
- opensignalbox/interface/web_ui/src/views/adapters/modbus/ModbusAdapterEdit.vue +343 -0
- opensignalbox/interface/web_ui/src/views/adapters/modbus/ModbusAdapterView.vue +270 -0
- opensignalbox/interface/web_ui/src/views/interfaces/InterfaceCreate.vue +0 -0
- opensignalbox/interface/web_ui/src/views/interfaces/basicio/BasicIOInterfaceEdit.vue +795 -0
- opensignalbox/interface/web_ui/src/views/interfaces/basicio/BasicIOInterfaceView.vue +648 -0
- opensignalbox/interface/web_ui/src/views/interfaces/bell/BellInterfaceEdit.vue +790 -0
- opensignalbox/interface/web_ui/src/views/interfaces/bell/BellInterfaceView.vue +437 -0
- opensignalbox/interface/web_ui/src/vite-env.d.ts +1 -0
- opensignalbox/interface/web_ui/tailwind.config.js +94 -0
- opensignalbox/interface/web_ui/tsconfig.app.json +27 -0
- opensignalbox/interface/web_ui/tsconfig.json +30 -0
- opensignalbox/interface/web_ui/tsconfig.node.json +12 -0
- opensignalbox/interface/web_ui/tsconfig.tsbuildinfo +1 -0
- opensignalbox/interface/web_ui/vite.config.d.ts +2 -0
- opensignalbox/interface/web_ui/vite.config.js +60 -0
- opensignalbox/interface/web_ui/vite.config.ts +62 -0
- opensignalbox_interface-0.1.0.dist-info/METADATA +49 -0
- opensignalbox_interface-0.1.0.dist-info/RECORD +209 -0
- opensignalbox_interface-0.1.0.dist-info/WHEEL +4 -0
- opensignalbox_interface-0.1.0.dist-info/entry_points.txt +2 -0
- 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,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
|
+
)
|