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,802 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Handler for Bell interfaces.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Any, Dict, cast
|
|
7
|
+
|
|
8
|
+
from anyio import sleep
|
|
9
|
+
from fastapi import APIRouter, HTTPException, Request
|
|
10
|
+
from fastapi.responses import StreamingResponse
|
|
11
|
+
from opensignalbox.common.messaging import get_messager
|
|
12
|
+
from opensignalbox.common.sv_wire import decode_sv_message
|
|
13
|
+
from opensignalbox.interface.adapters.base import BaseAdapter
|
|
14
|
+
from opensignalbox.interface.interfaces.base import InterfaceHandler
|
|
15
|
+
from opensignalbox.interface.interfaces.basicio.models import IfaceChanTuple
|
|
16
|
+
from opensignalbox.interface.interfaces.bell.connections import (
|
|
17
|
+
BellConnection,
|
|
18
|
+
BellModbusConnection,
|
|
19
|
+
BellTestConnection,
|
|
20
|
+
)
|
|
21
|
+
from opensignalbox.interface.models import Interface
|
|
22
|
+
|
|
23
|
+
from .models import (
|
|
24
|
+
BellConnectionType,
|
|
25
|
+
BellForceMessage,
|
|
26
|
+
BellInterface,
|
|
27
|
+
BellInterfaceCreate,
|
|
28
|
+
BellInterfacePublic,
|
|
29
|
+
BellInterfaceUpdate,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
messager = get_messager()
|
|
34
|
+
|
|
35
|
+
STATUS_UPDATE_PERIOD = 0.2 # seconds
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class BellInterfaceHandler(InterfaceHandler):
|
|
39
|
+
"""Handler for Bell interfaces."""
|
|
40
|
+
|
|
41
|
+
# Metadata for the API discovery
|
|
42
|
+
metadata = {
|
|
43
|
+
"supported_connection_types": ["BellModbusConnection", "BellTestConnection"],
|
|
44
|
+
"required_fields": ["system_name", "user_name", "interface_type"],
|
|
45
|
+
"optional_fields": ["description", "adapter_name", "enabled", "modbus_address"],
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def get_router(cls) -> Any:
|
|
50
|
+
"""Get API router for Bell interface endpoints.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
FastAPI router with Bell interface specific endpoints
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
router = APIRouter(tags=["interfaces/bell"])
|
|
57
|
+
|
|
58
|
+
@router.get("/{system_name}", response_model=BellInterfacePublic)
|
|
59
|
+
async def get_interface(system_name: str):
|
|
60
|
+
"""Get a Bell interface by system name."""
|
|
61
|
+
if not cls.interface_controller:
|
|
62
|
+
raise HTTPException(
|
|
63
|
+
status_code=500, detail="Interface controller not initialized"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
interface = cls.interface_controller.get(system_name)
|
|
68
|
+
if interface.interface_type != "Bell":
|
|
69
|
+
raise HTTPException(
|
|
70
|
+
status_code=404,
|
|
71
|
+
detail=f"Bell interface '{system_name}' not found",
|
|
72
|
+
)
|
|
73
|
+
return BellInterfacePublic(**interface.model_dump())
|
|
74
|
+
except KeyError as exc:
|
|
75
|
+
raise HTTPException(
|
|
76
|
+
status_code=404, detail=f"Interface '{system_name}' not found"
|
|
77
|
+
) from exc
|
|
78
|
+
|
|
79
|
+
@router.post("/")
|
|
80
|
+
async def create_interface(interface: BellInterfaceCreate):
|
|
81
|
+
"""Create a new Bell interface."""
|
|
82
|
+
if not cls.interface_controller:
|
|
83
|
+
raise HTTPException(
|
|
84
|
+
status_code=500, detail="Interface controller not initialized"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
if interface.interface_type != "Bell":
|
|
88
|
+
raise HTTPException(
|
|
89
|
+
status_code=400, detail="Interface type must be Bell"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
interface_model = BellInterface(**interface.model_dump())
|
|
94
|
+
|
|
95
|
+
# Add the interface
|
|
96
|
+
new_interface = cls.interface_controller.add(interface_model)
|
|
97
|
+
return new_interface
|
|
98
|
+
except FileExistsError as exc:
|
|
99
|
+
raise HTTPException(
|
|
100
|
+
status_code=400,
|
|
101
|
+
detail=f"Interface '{interface.system_name}' already exists",
|
|
102
|
+
) from exc
|
|
103
|
+
except TypeError as exc:
|
|
104
|
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
105
|
+
|
|
106
|
+
@router.patch("/{system_name}")
|
|
107
|
+
async def update_interface(
|
|
108
|
+
system_name: str, interface_update: BellInterfaceUpdate
|
|
109
|
+
):
|
|
110
|
+
"""Update a Bell interface."""
|
|
111
|
+
if not cls.interface_controller:
|
|
112
|
+
raise HTTPException(
|
|
113
|
+
status_code=500, detail="Interface controller not initialized"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
# Check that the interface exists and is Bell type
|
|
118
|
+
interface = cls.interface_controller.get(system_name)
|
|
119
|
+
if interface.interface_type != "Bell":
|
|
120
|
+
raise HTTPException(
|
|
121
|
+
status_code=404,
|
|
122
|
+
detail=f"Bell interface '{system_name}' not found",
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# Extract update fields
|
|
126
|
+
fields = {
|
|
127
|
+
k: v
|
|
128
|
+
for k, v in interface_update.model_dump().items()
|
|
129
|
+
if v is not None
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
# Update the interface
|
|
133
|
+
updated_interface = cls.interface_controller.update(system_name, fields)
|
|
134
|
+
return updated_interface
|
|
135
|
+
except KeyError as exc:
|
|
136
|
+
raise HTTPException(
|
|
137
|
+
status_code=404, detail=f"Interface '{system_name}' not found"
|
|
138
|
+
) from exc
|
|
139
|
+
|
|
140
|
+
@router.delete("/{system_name}")
|
|
141
|
+
async def delete_interface(system_name: str):
|
|
142
|
+
"""Delete a Bell interface."""
|
|
143
|
+
if not cls.interface_controller:
|
|
144
|
+
raise HTTPException(
|
|
145
|
+
status_code=500, detail="Interface controller not initialized"
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
# Check that the interface exists and is Bell type
|
|
150
|
+
interface = cls.interface_controller.get(system_name)
|
|
151
|
+
if interface.interface_type != "Bell":
|
|
152
|
+
raise HTTPException(
|
|
153
|
+
status_code=404,
|
|
154
|
+
detail=f"Bell interface '{system_name}' not found",
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
cls.interface_controller.remove(system_name)
|
|
158
|
+
return {"status": "success"}
|
|
159
|
+
except KeyError as exc:
|
|
160
|
+
raise HTTPException(
|
|
161
|
+
status_code=404, detail=f"Interface '{system_name}' not found"
|
|
162
|
+
) from exc
|
|
163
|
+
|
|
164
|
+
@router.post("/{system_name}/connect")
|
|
165
|
+
async def connect_interface(system_name: str):
|
|
166
|
+
"""Connect to a Bell interface."""
|
|
167
|
+
if not cls.interface_controller:
|
|
168
|
+
raise HTTPException(
|
|
169
|
+
status_code=500, detail="Interface controller not initialized"
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
try:
|
|
173
|
+
interface = cls.interface_controller.get(system_name)
|
|
174
|
+
if interface.interface_type != "Bell":
|
|
175
|
+
raise HTTPException(
|
|
176
|
+
status_code=404,
|
|
177
|
+
detail=f"Bell interface '{system_name}' not found",
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
# Connect to the interface
|
|
181
|
+
success = cls.connect(interface)
|
|
182
|
+
if success:
|
|
183
|
+
return {"status": "success"}
|
|
184
|
+
else:
|
|
185
|
+
raise HTTPException(
|
|
186
|
+
status_code=500,
|
|
187
|
+
detail=f"Failed to connect to interface. Detail: {interface.error}",
|
|
188
|
+
)
|
|
189
|
+
except KeyError as exc:
|
|
190
|
+
raise HTTPException(
|
|
191
|
+
status_code=404, detail=f"Interface '{system_name}' not found"
|
|
192
|
+
) from exc
|
|
193
|
+
|
|
194
|
+
@router.get("/{system_name}/channel-status")
|
|
195
|
+
async def status_stream(request: Request, system_name: str):
|
|
196
|
+
"""Stream the channel status of a Bell interface."""
|
|
197
|
+
if not cls.interface_controller:
|
|
198
|
+
raise HTTPException(
|
|
199
|
+
status_code=500, detail="Interface controller not initialized"
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
|
|
204
|
+
async def status_generator():
|
|
205
|
+
if not cls.interface_controller:
|
|
206
|
+
raise HTTPException(
|
|
207
|
+
status_code=500,
|
|
208
|
+
detail="Interface controller not initialized",
|
|
209
|
+
)
|
|
210
|
+
interface = cls.interface_controller.get(system_name)
|
|
211
|
+
if interface.interface_type != "Bell":
|
|
212
|
+
raise HTTPException(
|
|
213
|
+
status_code=404,
|
|
214
|
+
detail=f"Bell interface '{system_name}' not found",
|
|
215
|
+
)
|
|
216
|
+
while True:
|
|
217
|
+
if await request.is_disconnected():
|
|
218
|
+
break
|
|
219
|
+
# Use controller's get_status method
|
|
220
|
+
yield f"data: {cls.get_channel_status(interface)}\n\n"
|
|
221
|
+
await sleep(STATUS_UPDATE_PERIOD)
|
|
222
|
+
|
|
223
|
+
return StreamingResponse(
|
|
224
|
+
status_generator(), media_type="text/event-stream"
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
except KeyError as exc:
|
|
228
|
+
raise HTTPException(
|
|
229
|
+
status_code=404, detail=f"Interface '{system_name}' not found"
|
|
230
|
+
) from exc
|
|
231
|
+
|
|
232
|
+
@router.put("/{system_name}/forcing")
|
|
233
|
+
async def set_forcing(system_name: str, force_message: BellForceMessage):
|
|
234
|
+
"""Set forcing for a channel in a Bell interface."""
|
|
235
|
+
if not cls.interface_controller:
|
|
236
|
+
raise HTTPException(
|
|
237
|
+
status_code=500, detail="Interface controller not initialized"
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
try:
|
|
241
|
+
interface = cls.interface_controller.get(system_name)
|
|
242
|
+
if interface.interface_type != "Bell":
|
|
243
|
+
raise HTTPException(
|
|
244
|
+
status_code=404,
|
|
245
|
+
detail=f"Bell interface '{system_name}' not found",
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
cls.set_forcing(interface, force_message.channel, force_message.state)
|
|
249
|
+
return {"status": "success"}
|
|
250
|
+
except KeyError as exc:
|
|
251
|
+
raise HTTPException(
|
|
252
|
+
status_code=404, detail=f"Interface '{system_name}' not found"
|
|
253
|
+
) from exc
|
|
254
|
+
|
|
255
|
+
@router.put("/{system_name}/send-code")
|
|
256
|
+
async def send_code(system_name: str, request: Request):
|
|
257
|
+
"""Send a code to a Bell interface."""
|
|
258
|
+
if not cls.interface_controller:
|
|
259
|
+
raise HTTPException(
|
|
260
|
+
status_code=500, detail="Interface controller not initialized"
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
try:
|
|
264
|
+
interface = cls.interface_controller.get(system_name)
|
|
265
|
+
if interface.interface_type != "Bell":
|
|
266
|
+
raise HTTPException(
|
|
267
|
+
status_code=404,
|
|
268
|
+
detail=f"Bell interface '{system_name}' not found",
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
data = await request.json()
|
|
272
|
+
if "code" not in data or "channel" not in data:
|
|
273
|
+
raise HTTPException(
|
|
274
|
+
status_code=400,
|
|
275
|
+
detail="Missing 'code' or 'channel' in request body",
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
code = data["code"]
|
|
279
|
+
channel = int(data["channel"])
|
|
280
|
+
|
|
281
|
+
# Validate channel
|
|
282
|
+
if channel < 1 or channel > 4:
|
|
283
|
+
raise HTTPException(
|
|
284
|
+
status_code=400, detail="Channel must be between 1 and 4"
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
# Send the code
|
|
288
|
+
cls.send_code(interface, code, channel)
|
|
289
|
+
return {"status": "success"}
|
|
290
|
+
except KeyError as exc:
|
|
291
|
+
raise HTTPException(
|
|
292
|
+
status_code=404, detail=f"Interface '{system_name}' not found"
|
|
293
|
+
) from exc
|
|
294
|
+
|
|
295
|
+
@router.put("/{system_name}/forceReceiveCode")
|
|
296
|
+
async def force_receive_code(system_name: str, request: Request):
|
|
297
|
+
"""Force receiving a code on a Bell interface."""
|
|
298
|
+
if not cls.interface_controller:
|
|
299
|
+
raise HTTPException(
|
|
300
|
+
status_code=500, detail="Interface controller not initialized"
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
try:
|
|
304
|
+
interface = cls.interface_controller.get(system_name)
|
|
305
|
+
if interface.interface_type != "Bell":
|
|
306
|
+
raise HTTPException(
|
|
307
|
+
status_code=404,
|
|
308
|
+
detail=f"Bell interface '{system_name}' not found",
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
data = await request.json()
|
|
312
|
+
if "code" not in data or "channel" not in data:
|
|
313
|
+
raise HTTPException(
|
|
314
|
+
status_code=400,
|
|
315
|
+
detail="Missing 'code' or 'channel' in request body",
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
code = data["code"]
|
|
319
|
+
channel = int(data["channel"])
|
|
320
|
+
|
|
321
|
+
# Validate channel
|
|
322
|
+
if channel < 1 or channel > 4:
|
|
323
|
+
raise HTTPException(
|
|
324
|
+
status_code=400, detail="Channel must be between 1 and 4"
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
# Force receiving the code
|
|
328
|
+
cls.force_receive_code(interface, code, channel)
|
|
329
|
+
return {"status": "success"}
|
|
330
|
+
except KeyError as exc:
|
|
331
|
+
raise HTTPException(
|
|
332
|
+
status_code=404, detail=f"Interface '{system_name}' not found"
|
|
333
|
+
) from exc
|
|
334
|
+
|
|
335
|
+
return router
|
|
336
|
+
|
|
337
|
+
@staticmethod
|
|
338
|
+
def add(interface: Interface) -> Interface:
|
|
339
|
+
"""
|
|
340
|
+
Initialize a BellInterface from an Interface.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
interface: The interface to initialize. Must be a BellInterface or
|
|
344
|
+
convertible to a BellInterface.
|
|
345
|
+
|
|
346
|
+
Returns:
|
|
347
|
+
The initialized BellInterface.
|
|
348
|
+
|
|
349
|
+
Raises:
|
|
350
|
+
TypeError: If the interface cannot be converted to a BellInterface.
|
|
351
|
+
"""
|
|
352
|
+
|
|
353
|
+
bell_interface = cast(BellInterface, interface)
|
|
354
|
+
|
|
355
|
+
for channel_index, channel in bell_interface.channels.items():
|
|
356
|
+
channel.in_shared_variable = (
|
|
357
|
+
BellInterfaceHandler.output_variables.new_from_bellcode(
|
|
358
|
+
system_name=channel.system_name,
|
|
359
|
+
user_name=channel.user_name,
|
|
360
|
+
description=channel.description,
|
|
361
|
+
tags=channel.tags,
|
|
362
|
+
count=channel.in_count,
|
|
363
|
+
code=channel.in_code,
|
|
364
|
+
)
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
channel_var = channel.out_shared_variable_name
|
|
368
|
+
if channel_var:
|
|
369
|
+
if channel_var not in BellInterfaceHandler.input_variable_subs:
|
|
370
|
+
BellInterfaceHandler.input_variable_subs[channel_var] = []
|
|
371
|
+
BellInterfaceHandler.input_variable_subs[channel_var].append(
|
|
372
|
+
IfaceChanTuple(bell_interface.system_name, channel_index)
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
# Create a closure that captures the interface reference
|
|
376
|
+
def create_variable_callback(iface):
|
|
377
|
+
def callback(var: str, json_data: str):
|
|
378
|
+
# Forward to controller's callback which has access to all interfaces
|
|
379
|
+
if BellInterfaceHandler.variable_update_callback:
|
|
380
|
+
BellInterfaceHandler.variable_update_callback(
|
|
381
|
+
var, json_data
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
return callback
|
|
385
|
+
|
|
386
|
+
# Subscribe with the closure
|
|
387
|
+
messager.sub(channel_var, create_variable_callback(bell_interface))
|
|
388
|
+
|
|
389
|
+
return bell_interface
|
|
390
|
+
|
|
391
|
+
@staticmethod
|
|
392
|
+
def update(interface: Interface, fields: Dict[str, Any]) -> None:
|
|
393
|
+
"""
|
|
394
|
+
Update a BellInterface.
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
interface: The interface to update. Must be a BellInterface or convertible to one.
|
|
398
|
+
fields: The fields to update.
|
|
399
|
+
|
|
400
|
+
Raises:
|
|
401
|
+
TypeError: If the interface cannot be converted to a BellInterface.
|
|
402
|
+
"""
|
|
403
|
+
|
|
404
|
+
bell_interface = cast(BellInterface, interface)
|
|
405
|
+
|
|
406
|
+
# Handle basic field updates
|
|
407
|
+
for field, value in fields.items():
|
|
408
|
+
if hasattr(bell_interface, field) and value is not None:
|
|
409
|
+
setattr(bell_interface, field, value)
|
|
410
|
+
|
|
411
|
+
# TODO handle variable updates
|
|
412
|
+
|
|
413
|
+
@staticmethod
|
|
414
|
+
def remove(interface: Interface) -> None:
|
|
415
|
+
"""
|
|
416
|
+
Remove a BellInterface.
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
interface: The interface to remove.
|
|
420
|
+
"""
|
|
421
|
+
|
|
422
|
+
# TODO remove variable subscriptions
|
|
423
|
+
|
|
424
|
+
return
|
|
425
|
+
|
|
426
|
+
@staticmethod
|
|
427
|
+
def handle_variable_update(
|
|
428
|
+
interface: Interface, variable: str, json_data: str
|
|
429
|
+
) -> None:
|
|
430
|
+
"""
|
|
431
|
+
Handle a variable update for a BellInterface.
|
|
432
|
+
|
|
433
|
+
Args:
|
|
434
|
+
interface: The interface to handle the update for.
|
|
435
|
+
variable: The name of the variable that was updated.
|
|
436
|
+
json_data: The JSON data of the updated variable.
|
|
437
|
+
"""
|
|
438
|
+
|
|
439
|
+
bell_interface = cast(BellInterface, interface)
|
|
440
|
+
|
|
441
|
+
# logger.debug(f"Handling variable update for {variable} with data: {json_data}")
|
|
442
|
+
|
|
443
|
+
# Find channels subscribed to this variable
|
|
444
|
+
channels = []
|
|
445
|
+
if variable in BellInterfaceHandler.input_variable_subs:
|
|
446
|
+
for item in BellInterfaceHandler.input_variable_subs[variable]:
|
|
447
|
+
if item.interface == bell_interface.system_name:
|
|
448
|
+
channels.append(item.channel)
|
|
449
|
+
|
|
450
|
+
# logger.debug(f"Channels subscribed to variable '{variable}': {channels}")
|
|
451
|
+
|
|
452
|
+
# No channels to update
|
|
453
|
+
if not channels:
|
|
454
|
+
return
|
|
455
|
+
|
|
456
|
+
# Parse the variable data
|
|
457
|
+
try:
|
|
458
|
+
data = decode_sv_message(json_data).data
|
|
459
|
+
code = data.get("code", "")
|
|
460
|
+
|
|
461
|
+
# Update each channel
|
|
462
|
+
for channel in channels:
|
|
463
|
+
BellInterfaceHandler.send_code(bell_interface, code, channel)
|
|
464
|
+
|
|
465
|
+
except Exception as e:
|
|
466
|
+
logger.error(f"Error parsing variable data: {e}")
|
|
467
|
+
|
|
468
|
+
@staticmethod
|
|
469
|
+
def connect(interface: Interface) -> bool:
|
|
470
|
+
"""
|
|
471
|
+
Connect to a BellInterface using the specified adapter.
|
|
472
|
+
|
|
473
|
+
Args:
|
|
474
|
+
interface: The interface to connect.
|
|
475
|
+
adapter: The adapter to use for communication.
|
|
476
|
+
|
|
477
|
+
Returns:
|
|
478
|
+
True if connection was successful, False otherwise.
|
|
479
|
+
"""
|
|
480
|
+
|
|
481
|
+
bell_interface = cast(BellInterface, interface)
|
|
482
|
+
|
|
483
|
+
# Check if the interface is enabled
|
|
484
|
+
if not bell_interface.enabled:
|
|
485
|
+
return False
|
|
486
|
+
|
|
487
|
+
# Get the appropriate connection strategy based on connection type
|
|
488
|
+
connected = False
|
|
489
|
+
match bell_interface.connection_type:
|
|
490
|
+
case BellConnectionType.TEST:
|
|
491
|
+
connected = BellTestConnection.connect(bell_interface)
|
|
492
|
+
case BellConnectionType.MODBUS_ADAPTER:
|
|
493
|
+
if BellInterfaceHandler.interface_controller:
|
|
494
|
+
adapter = BellInterfaceHandler.interface_controller.get_adapter_for_interface(
|
|
495
|
+
interface
|
|
496
|
+
)
|
|
497
|
+
if not adapter or not adapter.enabled:
|
|
498
|
+
return False # No adapter configured or adapter is disabled
|
|
499
|
+
connected = BellModbusConnection.connect(bell_interface, adapter)
|
|
500
|
+
# if connected:
|
|
501
|
+
# for channel in bell_interface.channels:
|
|
502
|
+
# in_data = BellModbusConnection.receive_code(bell_interface, channel, adapter)
|
|
503
|
+
# if in_data and len(in_data) >= 2:
|
|
504
|
+
# bell_interface.channels[channel].in_count = (in_data[0] << 8) | in_data[1]
|
|
505
|
+
# out_data = BellModbusConnection.get_sent_code(bell_interface, channel, adapter)
|
|
506
|
+
# if out_data and len(out_data) >= 2:
|
|
507
|
+
# bell_interface.channels[channel].out_count = (out_data[0] << 8) | out_data[1]
|
|
508
|
+
case _:
|
|
509
|
+
logger.error(
|
|
510
|
+
f"Unsupported connection type: {bell_interface.connection_type}"
|
|
511
|
+
)
|
|
512
|
+
interface.error = (
|
|
513
|
+
f"Unsupported connection type: {bell_interface.connection_type}"
|
|
514
|
+
)
|
|
515
|
+
interface.enabled = False
|
|
516
|
+
return False
|
|
517
|
+
return connected
|
|
518
|
+
|
|
519
|
+
@staticmethod
|
|
520
|
+
def disconnect(interface: Interface) -> None:
|
|
521
|
+
"""
|
|
522
|
+
Disconnect from a BellInterface.
|
|
523
|
+
|
|
524
|
+
Args:
|
|
525
|
+
interface: The interface to disconnect.
|
|
526
|
+
|
|
527
|
+
Returns:
|
|
528
|
+
True if disconnection was successful, False otherwise.
|
|
529
|
+
"""
|
|
530
|
+
|
|
531
|
+
bell_interface = cast(BellInterface, interface)
|
|
532
|
+
|
|
533
|
+
# Get the appropriate connection strategy based on connection type
|
|
534
|
+
match bell_interface.connection_type:
|
|
535
|
+
case BellConnectionType.TEST:
|
|
536
|
+
BellTestConnection.disconnect(bell_interface)
|
|
537
|
+
case BellConnectionType.MODBUS_ADAPTER:
|
|
538
|
+
if BellInterfaceHandler.interface_controller:
|
|
539
|
+
adapter = BellInterfaceHandler.interface_controller.get_adapter_for_interface(
|
|
540
|
+
interface
|
|
541
|
+
)
|
|
542
|
+
if not adapter:
|
|
543
|
+
return
|
|
544
|
+
BellModbusConnection.disconnect(bell_interface, adapter)
|
|
545
|
+
case _:
|
|
546
|
+
logger.error(
|
|
547
|
+
f"Unsupported connection type: {bell_interface.connection_type}"
|
|
548
|
+
)
|
|
549
|
+
interface.error = (
|
|
550
|
+
f"Unsupported connection type: {bell_interface.connection_type}"
|
|
551
|
+
)
|
|
552
|
+
interface.enabled = False
|
|
553
|
+
return
|
|
554
|
+
|
|
555
|
+
@staticmethod
|
|
556
|
+
def read_data(interface: Interface, adapter: BaseAdapter | None = None) -> None:
|
|
557
|
+
"""
|
|
558
|
+
Read data from the adapter for a BellInterface.
|
|
559
|
+
|
|
560
|
+
Args:
|
|
561
|
+
interface: The interface to read data for.
|
|
562
|
+
adapter: The adapter to read from.
|
|
563
|
+
"""
|
|
564
|
+
|
|
565
|
+
bell_interface = cast(BellInterface, interface)
|
|
566
|
+
|
|
567
|
+
# Get the appropriate connection strategy based on connection type
|
|
568
|
+
connection: type[BellConnection]
|
|
569
|
+
match bell_interface.connection_type:
|
|
570
|
+
case BellConnectionType.TEST:
|
|
571
|
+
connection = BellTestConnection
|
|
572
|
+
case BellConnectionType.MODBUS_ADAPTER:
|
|
573
|
+
connection = BellModbusConnection
|
|
574
|
+
if BellInterfaceHandler.interface_controller:
|
|
575
|
+
adapter = BellInterfaceHandler.interface_controller.get_adapter_for_interface(
|
|
576
|
+
interface
|
|
577
|
+
)
|
|
578
|
+
case _:
|
|
579
|
+
logger.error(
|
|
580
|
+
f"Unsupported connection type: {bell_interface.connection_type}"
|
|
581
|
+
)
|
|
582
|
+
interface.error = (
|
|
583
|
+
f"Unsupported connection type: {bell_interface.connection_type}"
|
|
584
|
+
)
|
|
585
|
+
interface.enabled = False
|
|
586
|
+
return
|
|
587
|
+
|
|
588
|
+
# Read from the hardware
|
|
589
|
+
try:
|
|
590
|
+
counts = [0, 0, 0, 0]
|
|
591
|
+
codes = ["", "", "", ""]
|
|
592
|
+
|
|
593
|
+
# Read the words from the adapter (starting at the configured address)
|
|
594
|
+
data = connection.receive_all_codes(bell_interface, adapter=adapter)
|
|
595
|
+
|
|
596
|
+
if not data:
|
|
597
|
+
logger.error(
|
|
598
|
+
f"No data read from adapter for interface '{bell_interface.system_name}'"
|
|
599
|
+
)
|
|
600
|
+
return
|
|
601
|
+
|
|
602
|
+
# Convert data to integer array
|
|
603
|
+
words = []
|
|
604
|
+
for i in range(0, len(data), 2):
|
|
605
|
+
if i + 1 < len(data):
|
|
606
|
+
word = (data[i] << 8) | data[i + 1]
|
|
607
|
+
words.append(word)
|
|
608
|
+
|
|
609
|
+
for channel in range(0, 4):
|
|
610
|
+
counts[channel] = words[channel * 9]
|
|
611
|
+
# Form code from the next four bytes as ints separated by commas
|
|
612
|
+
codes[channel] = (
|
|
613
|
+
f"{','.join([str(words[i]) for i in range((channel * 9 + 1), (channel * 9 + 4)) if words[i] != 0])}"
|
|
614
|
+
)
|
|
615
|
+
# Update the interface fields
|
|
616
|
+
for index, channel in bell_interface.channels.items():
|
|
617
|
+
channel.in_count = counts[index - 1]
|
|
618
|
+
channel.in_code = codes[index - 1]
|
|
619
|
+
if channel.in_shared_variable is not None:
|
|
620
|
+
channel.in_shared_variable.update(
|
|
621
|
+
{
|
|
622
|
+
"count": channel.in_count,
|
|
623
|
+
"code": channel.in_code,
|
|
624
|
+
}
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
except Exception as e:
|
|
628
|
+
logger.error(
|
|
629
|
+
f"Error reading data for interface '{bell_interface.system_name}': {e}"
|
|
630
|
+
)
|
|
631
|
+
interface.error = str(e)
|
|
632
|
+
|
|
633
|
+
@staticmethod
|
|
634
|
+
def write_data(interface: Interface, adapter: BaseAdapter | None = None) -> None:
|
|
635
|
+
"""
|
|
636
|
+
Write data to the adapter for a BellInterface.
|
|
637
|
+
|
|
638
|
+
Args:
|
|
639
|
+
interface: The interface to write data for.
|
|
640
|
+
adapter: The adapter to write to.
|
|
641
|
+
"""
|
|
642
|
+
|
|
643
|
+
bell_interface = cast(BellInterface, interface)
|
|
644
|
+
|
|
645
|
+
# Get the appropriate connection strategy based on connection type
|
|
646
|
+
connection: type[BellConnection]
|
|
647
|
+
match bell_interface.connection_type:
|
|
648
|
+
case BellConnectionType.TEST:
|
|
649
|
+
connection = BellTestConnection
|
|
650
|
+
case BellConnectionType.MODBUS_ADAPTER:
|
|
651
|
+
connection = BellModbusConnection
|
|
652
|
+
if BellInterfaceHandler.interface_controller:
|
|
653
|
+
adapter = BellInterfaceHandler.interface_controller.get_adapter_for_interface(
|
|
654
|
+
interface
|
|
655
|
+
)
|
|
656
|
+
case _:
|
|
657
|
+
logger.error(
|
|
658
|
+
f"Unsupported connection type: {bell_interface.connection_type}"
|
|
659
|
+
)
|
|
660
|
+
interface.error = (
|
|
661
|
+
f"Unsupported connection type: {bell_interface.connection_type}"
|
|
662
|
+
)
|
|
663
|
+
interface.enabled = False
|
|
664
|
+
return
|
|
665
|
+
|
|
666
|
+
for channel in range(1, 5):
|
|
667
|
+
# Prepare data to send
|
|
668
|
+
out_count = bell_interface.channels[channel].out_count
|
|
669
|
+
last_out_count = bell_interface.channels[channel].last_out_count
|
|
670
|
+
if out_count == last_out_count:
|
|
671
|
+
continue
|
|
672
|
+
bell_interface.channels[channel].last_out_count = out_count
|
|
673
|
+
out_code = bell_interface.channels[channel].out_code
|
|
674
|
+
|
|
675
|
+
words = [0] * 8 # 1 count + 7 codes
|
|
676
|
+
words[0] = out_count if out_count is not None else 0
|
|
677
|
+
for i in range(1, 8):
|
|
678
|
+
if i - 1 < len(out_code.split(",")):
|
|
679
|
+
try:
|
|
680
|
+
words[i] = int(out_code.split(",")[i - 1])
|
|
681
|
+
except ValueError:
|
|
682
|
+
words[i] = 0
|
|
683
|
+
|
|
684
|
+
# Convert words to byte array
|
|
685
|
+
data = bytearray()
|
|
686
|
+
for word in words:
|
|
687
|
+
data.append((word >> 8) & 0xFF) # High byte
|
|
688
|
+
data.append(word & 0xFF) # Low byte
|
|
689
|
+
|
|
690
|
+
# Send data to the adapter
|
|
691
|
+
try:
|
|
692
|
+
connection.send_code(bell_interface, channel, data, adapter=adapter)
|
|
693
|
+
# logger.debug(f"Sent data for interface '{bell_interface.system_name}': channel {channel} data {data.hex()}")
|
|
694
|
+
except Exception as e:
|
|
695
|
+
logger.error(
|
|
696
|
+
f"Error writing data for interface '{bell_interface.system_name}': {e}"
|
|
697
|
+
)
|
|
698
|
+
interface.error = str(e)
|
|
699
|
+
|
|
700
|
+
return
|
|
701
|
+
|
|
702
|
+
@staticmethod
|
|
703
|
+
def get_channel_status(interface: Interface):
|
|
704
|
+
"""
|
|
705
|
+
Get the channel status of a BellInterface.
|
|
706
|
+
|
|
707
|
+
Args:
|
|
708
|
+
interface: The interface to get the status of.
|
|
709
|
+
|
|
710
|
+
Returns:
|
|
711
|
+
A string describing the current status.
|
|
712
|
+
"""
|
|
713
|
+
|
|
714
|
+
bell_interface = cast(BellInterface, interface)
|
|
715
|
+
|
|
716
|
+
return bell_interface.model_dump_json(
|
|
717
|
+
include={"channels"},
|
|
718
|
+
by_alias=True,
|
|
719
|
+
)
|
|
720
|
+
|
|
721
|
+
@staticmethod
|
|
722
|
+
def set_forcing(interface: Interface, channel_index: int, state: str) -> None:
|
|
723
|
+
"""
|
|
724
|
+
Set forcing for a channel in a BellInterface.
|
|
725
|
+
|
|
726
|
+
Args:
|
|
727
|
+
interface: The interface to set forcing for.
|
|
728
|
+
channel: The channel number.
|
|
729
|
+
is_input: Whether the channel is an input.
|
|
730
|
+
enabled: Whether forcing is enabled.
|
|
731
|
+
value: The value to force.
|
|
732
|
+
"""
|
|
733
|
+
if state not in ["on", "off", "none"]:
|
|
734
|
+
raise ValueError("Invalid state. Must be 'on', 'off', or 'none'.")
|
|
735
|
+
if channel_index is None:
|
|
736
|
+
raise ValueError("Channel must be specified.")
|
|
737
|
+
|
|
738
|
+
# Set forcing for the specified channel
|
|
739
|
+
return
|
|
740
|
+
|
|
741
|
+
@staticmethod
|
|
742
|
+
def send_code(interface: Interface, code: str, channel: int) -> None:
|
|
743
|
+
"""
|
|
744
|
+
Send a code to a specific channel of a BellInterface.
|
|
745
|
+
|
|
746
|
+
Args:
|
|
747
|
+
interface: The interface to send the code to.
|
|
748
|
+
code: The code to send.
|
|
749
|
+
channel: The channel number (1-4).
|
|
750
|
+
"""
|
|
751
|
+
bell_interface = cast(BellInterface, interface)
|
|
752
|
+
|
|
753
|
+
if channel in bell_interface.channels:
|
|
754
|
+
bell_interface.channels[channel].out_code = code
|
|
755
|
+
bell_interface.channels[channel].out_count = (
|
|
756
|
+
bell_interface.channels[channel].out_count or 0
|
|
757
|
+
) + 1
|
|
758
|
+
|
|
759
|
+
return
|
|
760
|
+
|
|
761
|
+
@staticmethod
|
|
762
|
+
def force_receive_code(interface: Interface, code: str, channel: int) -> None:
|
|
763
|
+
"""
|
|
764
|
+
Force receiving a code on a specific channel of a BellInterface.
|
|
765
|
+
|
|
766
|
+
Args:
|
|
767
|
+
interface: The interface to receive the code on.
|
|
768
|
+
code: The code to receive.
|
|
769
|
+
channel: The channel number (1-4).
|
|
770
|
+
"""
|
|
771
|
+
bell_interface = cast(BellInterface, interface)
|
|
772
|
+
|
|
773
|
+
if channel in bell_interface.channels:
|
|
774
|
+
bell_interface.channels[channel].in_code = code
|
|
775
|
+
bell_interface.channels[channel].in_count = (
|
|
776
|
+
bell_interface.channels[channel].in_count or 0
|
|
777
|
+
) + 2 # So we don't miss a subsequent real input
|
|
778
|
+
var = bell_interface.channels[channel].in_shared_variable
|
|
779
|
+
if var is not None:
|
|
780
|
+
var.update(
|
|
781
|
+
{
|
|
782
|
+
"count": bell_interface.channels[channel].in_count,
|
|
783
|
+
"code": bell_interface.channels[channel].in_code,
|
|
784
|
+
}
|
|
785
|
+
)
|
|
786
|
+
|
|
787
|
+
@classmethod
|
|
788
|
+
def load_from_json(cls, interface_data: Any) -> None:
|
|
789
|
+
# Convert channel keys to integers
|
|
790
|
+
interface_data["channels"] = {
|
|
791
|
+
int(k): v for k, v in interface_data["channels"].items()
|
|
792
|
+
}
|
|
793
|
+
bell_interface = BellInterface.model_validate(interface_data)
|
|
794
|
+
if cls.interface_controller is None:
|
|
795
|
+
raise ValueError(
|
|
796
|
+
"Interface controller not set. Call set_controller() first."
|
|
797
|
+
)
|
|
798
|
+
cls.interface_controller.add(bell_interface)
|
|
799
|
+
|
|
800
|
+
@staticmethod
|
|
801
|
+
def save_to_json(interface: Interface) -> str:
|
|
802
|
+
return interface.model_dump_json()
|