onvif-tui 1.0.1__tar.gz

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.
@@ -0,0 +1,27 @@
1
+ Metadata-Version: 2.4
2
+ Name: onvif-tui
3
+ Version: 1.0.1
4
+ Author: Stephen Rhodes
5
+ Author-email: Stephen Rhodes <sr99622@gmail.com>
6
+ License-Expression: Apache-2.0
7
+ Requires-Dist: libonvif
8
+ Requires-Dist: niquests
9
+ Requires-Dist: psutil
10
+ Requires-Dist: textual
11
+ Requires-Python: >=3.10
12
+ Project-URL: Bug Tracker, https://github.com/sr99622/libonvif/issues
13
+ Project-URL: Homepage, https://github.com/sr99622/libonvif
14
+ Description-Content-Type: text/markdown
15
+
16
+ <h3>onvif-tui</h3>
17
+
18
+ This is a Terminal User Interface application that demonstrates libonvif abilities and can be used to evaluate and control camera settings. The application is launched using the command line arguments:
19
+
20
+ ```
21
+ -u username for camera authentication
22
+ -p passwword for camera authentication
23
+ -m camera ip address for manual camera discovery
24
+ -i host local ip address for binding discovery broadcast
25
+ ```
26
+
27
+ Please visit the [github repository](https://github.com/sr99622/libonvif) for more detailed information.
@@ -0,0 +1,12 @@
1
+ <h3>onvif-tui</h3>
2
+
3
+ This is a Terminal User Interface application that demonstrates libonvif abilities and can be used to evaluate and control camera settings. The application is launched using the command line arguments:
4
+
5
+ ```
6
+ -u username for camera authentication
7
+ -p passwword for camera authentication
8
+ -m camera ip address for manual camera discovery
9
+ -i host local ip address for binding discovery broadcast
10
+ ```
11
+
12
+ Please visit the [github repository](https://github.com/sr99622/libonvif) for more detailed information.
@@ -0,0 +1,29 @@
1
+ [project]
2
+ name = "onvif-tui"
3
+ version = "1.0.1"
4
+ requires-python = ">=3.10"
5
+ dependencies = [
6
+ "libonvif",
7
+ "niquests",
8
+ "psutil",
9
+ "textual",
10
+ ]
11
+ authors = [
12
+ { name = "Stephen Rhodes", email = "sr99622@gmail.com" },
13
+ ]
14
+ license = "Apache-2.0"
15
+ readme = "README.md"
16
+
17
+ [project.urls]
18
+ Homepage = "https://github.com/sr99622/libonvif"
19
+ "Bug Tracker" = "https://github.com/sr99622/libonvif/issues"
20
+
21
+ [project.scripts]
22
+ onvif-tui = "onvif_tui.main:main"
23
+
24
+ [tool.uv.sources]
25
+ libonvif = { workspace = true }
26
+
27
+ [build-system]
28
+ requires = ["uv_build>=0.8.0,<0.9.0"]
29
+ build-backend = "uv_build"
File without changes
@@ -0,0 +1,147 @@
1
+ from textual.widgets import Tree
2
+ from textual.widgets.tree import TreeNode
3
+ from dataclasses import is_dataclass, fields
4
+ from rich.text import Text
5
+ from .fields import UNUSED_FIELDS, HIDDEN_FIELDS, field_descriptions, join_fqn, is_editable_field, main_screen_text
6
+ from libonvif.devices.camera import Camera
7
+ import re
8
+
9
+ class CameraTree(Tree):
10
+ def __init__(self) -> None:
11
+ super().__init__("Cameras")
12
+ self.show_root = True
13
+
14
+ def get_fqn(self, node: TreeNode) -> str:
15
+ parts = []
16
+ current = node
17
+ while current is not None:
18
+ parent = current.parent
19
+ if parent is None:
20
+ break
21
+ data = getattr(current, "data", None)
22
+ if data and "field" in data:
23
+ parts.append(data["field"])
24
+ current = parent
25
+ return ".".join(reversed(parts))
26
+
27
+ def capture_expanded_nodes(self, node) -> set[str]:
28
+ expanded: set[str] = set()
29
+
30
+ def walk(n):
31
+ data = n.data or {}
32
+ fqn = data.get("fqn")
33
+
34
+ if fqn and n.is_expanded:
35
+ expanded.add(fqn)
36
+
37
+ for child in n.children:
38
+ walk(child)
39
+
40
+ walk(node)
41
+ return expanded
42
+
43
+ def restore_expanded_nodes(self, node, expanded: set[str]) -> None:
44
+ def walk(n):
45
+ data = n.data or {}
46
+ fqn = data.get("fqn")
47
+
48
+ if fqn in expanded:
49
+ n.allow_expand = True
50
+ n.expand()
51
+
52
+ for child in n.children:
53
+ walk(child)
54
+
55
+ walk(node)
56
+
57
+ def on_tree_node_highlighted(self, event: Tree.NodeHighlighted) -> None:
58
+
59
+ if event.node.parent is None:
60
+ self.app.debug_log.clear()
61
+ self.app.debug_log.write(main_screen_text)
62
+
63
+ if not event.node.data: return
64
+
65
+ if camera := event.node.data.get("camera"):
66
+ if event.node.label.plain == camera.name:
67
+ self.app.debug_log.clear()
68
+ self.app.debug_log.write("The camera can be rebooted using the 'r' key")
69
+
70
+ if not (fqn := event.node.data.get("fqn")): return
71
+
72
+ sfqn = re.sub(r"\[\d+\]", "[*]", fqn)
73
+ self.app.debug_log.clear()
74
+ self.app.debug_log.write(fqn)
75
+ if desc := field_descriptions.get(sfqn):
76
+ self.app.debug_log.write(desc)
77
+ if fqn == "errors" and camera.errors:
78
+ self.app.debug_log.clear()
79
+ self.app.debug_log.write(camera.errors)
80
+
81
+ def _make_editable_label(self, field: str, value: str) -> Text:
82
+ label = Text()
83
+ label.append("✎ ", style="#66cc66")
84
+ label.append(f"{field}: ")
85
+ label.append(str(value))
86
+ return label
87
+
88
+ def add_camera(self, camera: Camera) -> None:
89
+ label = camera.name
90
+ camera_node = self.root.add(label, expand=False)
91
+ camera_node.data = { "camera": camera }
92
+ for field in fields(camera):
93
+ value = getattr(camera, field.name)
94
+ self._add_value(camera_node, field.name, value, camera)
95
+ if len(self.root.children) == 1:
96
+ self.root.expand()
97
+
98
+ def _add_value(self, parent: TreeNode, name: str, value: object, camera: Camera) -> TreeNode | None:
99
+
100
+ fqn = join_fqn(self.get_fqn(parent), name)
101
+
102
+ if fqn in HIDDEN_FIELDS:
103
+ return
104
+
105
+ if is_editable_field(fqn) and value is not None:
106
+ node = parent.add_leaf(self._make_editable_label(name, str(value)))
107
+ node.data = {"camera": camera, "field": name, "fqn": fqn}
108
+ return
109
+
110
+ if value is None:
111
+ if fqn in UNUSED_FIELDS:
112
+ return
113
+ node = parent.add_leaf(Text(f"{name}: None", style="dim"))
114
+ node.data = {"camera": camera, "field": name, "fqn": fqn}
115
+ return
116
+
117
+ if is_dataclass(value):
118
+ node = parent.add(name, expand=False)
119
+ node.data = {"camera": camera, "field": name, "fqn": fqn}
120
+ for field in fields(value):
121
+ child_value = getattr(value, field.name)
122
+ self._add_value(node, field.name, child_value, camera)
123
+
124
+ elif isinstance(value, list):
125
+ if not value:
126
+ if fqn in UNUSED_FIELDS:
127
+ return
128
+ label = Text(f"{name}: list[0]", style="dim")
129
+ node = parent.add_leaf(label)
130
+ node.data = {"camera": camera, "field": name, "fqn": fqn}
131
+ return
132
+ node = parent.add(f"{name}: [{len(value)}]", expand=False)
133
+ node.data = {"camera": camera, "field": name, "fqn": fqn}
134
+ for index, item in enumerate(value):
135
+ self._add_value(node, f"[{index}]", item, camera)
136
+
137
+ elif isinstance(value, dict):
138
+ node = parent.add(f"{name}: dict[{len(value)}]", expand=False)
139
+ node.data = {"camera": camera, "field": name, "fqn": fqn}
140
+ for key, item in value.items():
141
+ self._add_value(node, str(key), item, camera)
142
+
143
+ else:
144
+ node = parent.add_leaf(f"{name}: {value}")
145
+ node.data = {"camera": camera, "field": name, "fqn": fqn}
146
+
147
+ return node