pumaguard 21.post27__py3-none-any.whl → 21.post83__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 (188) hide show
  1. pumaguard/presets.py +1 -0
  2. pumaguard/pumaguard-ui/.last_build_id +1 -1
  3. pumaguard/pumaguard-ui/assets/NOTICES +621 -71
  4. pumaguard/pumaguard-ui/assets/fonts/MaterialIcons-Regular.otf +0 -0
  5. pumaguard/pumaguard-ui/flutter_bootstrap.js +1 -1
  6. pumaguard/pumaguard-ui/main.dart.js +28869 -28787
  7. pumaguard/web_routes/dhcp.py +311 -54
  8. pumaguard/web_routes/diagnostics.py +6 -0
  9. pumaguard/web_routes/settings.py +13 -0
  10. pumaguard/web_ui.py +29 -0
  11. {pumaguard-21.post27.dist-info → pumaguard-21.post83.dist-info}/METADATA +1 -1
  12. pumaguard-21.post83.dist-info/RECORD +254 -0
  13. pumaguard-ui/.gitignore +48 -0
  14. pumaguard-ui/.metadata +45 -0
  15. pumaguard-ui/API_REFERENCE.md +717 -0
  16. pumaguard-ui/LICENSE +201 -0
  17. pumaguard-ui/Makefile +36 -0
  18. pumaguard-ui/README.md +371 -0
  19. pumaguard-ui/UI_DEVELOPMENT_CONTEXT.md +427 -0
  20. pumaguard-ui/analysis_options.yaml +28 -0
  21. pumaguard-ui/android/.gitignore +14 -0
  22. pumaguard-ui/android/app/build.gradle.kts +44 -0
  23. pumaguard-ui/android/app/src/debug/AndroidManifest.xml +7 -0
  24. pumaguard-ui/android/app/src/main/AndroidManifest.xml +45 -0
  25. pumaguard-ui/android/app/src/main/kotlin/com/example/pumaguard_ui/MainActivity.kt +5 -0
  26. pumaguard-ui/android/app/src/main/res/drawable/launch_background.xml +12 -0
  27. pumaguard-ui/android/app/src/main/res/drawable-v21/launch_background.xml +12 -0
  28. pumaguard-ui/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  29. pumaguard-ui/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  30. pumaguard-ui/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  31. pumaguard-ui/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  32. pumaguard-ui/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  33. pumaguard-ui/android/app/src/main/res/values/styles.xml +18 -0
  34. pumaguard-ui/android/app/src/main/res/values-night/styles.xml +18 -0
  35. pumaguard-ui/android/app/src/profile/AndroidManifest.xml +7 -0
  36. pumaguard-ui/android/build.gradle.kts +24 -0
  37. pumaguard-ui/android/gradle/wrapper/gradle-wrapper.properties +5 -0
  38. pumaguard-ui/android/gradle.properties +2 -0
  39. pumaguard-ui/android/settings.gradle.kts +26 -0
  40. pumaguard-ui/fonts/README.md +38 -0
  41. pumaguard-ui/fonts/Roboto-Bold.ttf +0 -0
  42. pumaguard-ui/fonts/Roboto-Light.ttf +0 -0
  43. pumaguard-ui/fonts/Roboto-Medium.ttf +0 -0
  44. pumaguard-ui/fonts/Roboto-Regular.ttf +0 -0
  45. pumaguard-ui/fonts/RobotoMono-Bold.ttf +0 -0
  46. pumaguard-ui/fonts/RobotoMono-Medium.ttf +0 -0
  47. pumaguard-ui/fonts/RobotoMono-Regular.ttf +0 -0
  48. pumaguard-ui/fonts/download_fonts.sh +76 -0
  49. pumaguard-ui/ios/.gitignore +34 -0
  50. pumaguard-ui/ios/Flutter/AppFrameworkInfo.plist +26 -0
  51. pumaguard-ui/ios/Flutter/Debug.xcconfig +1 -0
  52. pumaguard-ui/ios/Flutter/Release.xcconfig +1 -0
  53. pumaguard-ui/ios/Runner/AppDelegate.swift +13 -0
  54. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +122 -0
  55. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png +0 -0
  56. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png +0 -0
  57. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png +0 -0
  58. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png +0 -0
  59. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png +0 -0
  60. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png +0 -0
  61. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png +0 -0
  62. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png +0 -0
  63. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png +0 -0
  64. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png +0 -0
  65. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png +0 -0
  66. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png +0 -0
  67. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png +0 -0
  68. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png +0 -0
  69. pumaguard-ui/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png +0 -0
  70. pumaguard-ui/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json +23 -0
  71. pumaguard-ui/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png +0 -0
  72. pumaguard-ui/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png +0 -0
  73. pumaguard-ui/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png +0 -0
  74. pumaguard-ui/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md +5 -0
  75. pumaguard-ui/ios/Runner/Base.lproj/LaunchScreen.storyboard +37 -0
  76. pumaguard-ui/ios/Runner/Base.lproj/Main.storyboard +26 -0
  77. pumaguard-ui/ios/Runner/Info.plist +49 -0
  78. pumaguard-ui/ios/Runner/Runner-Bridging-Header.h +1 -0
  79. pumaguard-ui/ios/Runner.xcodeproj/project.pbxproj +616 -0
  80. pumaguard-ui/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  81. pumaguard-ui/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  82. pumaguard-ui/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +8 -0
  83. pumaguard-ui/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +101 -0
  84. pumaguard-ui/ios/Runner.xcworkspace/contents.xcworkspacedata +7 -0
  85. pumaguard-ui/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  86. pumaguard-ui/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +8 -0
  87. pumaguard-ui/ios/RunnerTests/RunnerTests.swift +12 -0
  88. pumaguard-ui/lib/main.dart +56 -0
  89. pumaguard-ui/lib/models/camera.dart +45 -0
  90. pumaguard-ui/lib/models/plug.dart +45 -0
  91. pumaguard-ui/lib/models/settings.dart +112 -0
  92. pumaguard-ui/lib/models/status.dart +58 -0
  93. pumaguard-ui/lib/screens/directories_screen.dart +319 -0
  94. pumaguard-ui/lib/screens/home_screen.dart +545 -0
  95. pumaguard-ui/lib/screens/image_browser_screen.dart +1248 -0
  96. pumaguard-ui/lib/screens/server_discovery_screen.dart +390 -0
  97. pumaguard-ui/lib/screens/settings_screen.dart +1162 -0
  98. pumaguard-ui/lib/screens/wifi_settings_screen.dart +671 -0
  99. pumaguard-ui/lib/services/api_service.dart +717 -0
  100. pumaguard-ui/lib/services/camera_events_service.dart +195 -0
  101. pumaguard-ui/lib/services/mdns_service.dart +4 -0
  102. pumaguard-ui/lib/services/mdns_service_impl.dart +282 -0
  103. pumaguard-ui/lib/services/mdns_service_io.dart +1 -0
  104. pumaguard-ui/lib/services/mdns_service_web.dart +106 -0
  105. pumaguard-ui/lib/utils/download_helper.dart +2 -0
  106. pumaguard-ui/lib/utils/download_helper_stub.dart +6 -0
  107. pumaguard-ui/lib/utils/download_helper_web.dart +14 -0
  108. pumaguard-ui/lib/utils/platform_url.dart +10 -0
  109. pumaguard-ui/lib/utils/platform_url_stub.dart +11 -0
  110. pumaguard-ui/lib/utils/platform_url_web.dart +16 -0
  111. pumaguard-ui/linux/.gitignore +1 -0
  112. pumaguard-ui/linux/CMakeLists.txt +128 -0
  113. pumaguard-ui/linux/flutter/CMakeLists.txt +88 -0
  114. pumaguard-ui/linux/flutter/generated_plugin_registrant.cc +15 -0
  115. pumaguard-ui/linux/flutter/generated_plugin_registrant.h +15 -0
  116. pumaguard-ui/linux/flutter/generated_plugins.cmake +24 -0
  117. pumaguard-ui/linux/runner/CMakeLists.txt +26 -0
  118. pumaguard-ui/linux/runner/main.cc +6 -0
  119. pumaguard-ui/linux/runner/my_application.cc +148 -0
  120. pumaguard-ui/linux/runner/my_application.h +21 -0
  121. pumaguard-ui/macos/.gitignore +7 -0
  122. pumaguard-ui/macos/Flutter/Flutter-Debug.xcconfig +1 -0
  123. pumaguard-ui/macos/Flutter/Flutter-Release.xcconfig +1 -0
  124. pumaguard-ui/macos/Flutter/GeneratedPluginRegistrant.swift +16 -0
  125. pumaguard-ui/macos/Runner/AppDelegate.swift +13 -0
  126. pumaguard-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +68 -0
  127. pumaguard-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
  128. pumaguard-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
  129. pumaguard-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
  130. pumaguard-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
  131. pumaguard-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
  132. pumaguard-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
  133. pumaguard-ui/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
  134. pumaguard-ui/macos/Runner/Base.lproj/MainMenu.xib +343 -0
  135. pumaguard-ui/macos/Runner/Configs/AppInfo.xcconfig +14 -0
  136. pumaguard-ui/macos/Runner/Configs/Debug.xcconfig +2 -0
  137. pumaguard-ui/macos/Runner/Configs/Release.xcconfig +2 -0
  138. pumaguard-ui/macos/Runner/Configs/Warnings.xcconfig +13 -0
  139. pumaguard-ui/macos/Runner/DebugProfile.entitlements +12 -0
  140. pumaguard-ui/macos/Runner/Info.plist +32 -0
  141. pumaguard-ui/macos/Runner/MainFlutterWindow.swift +15 -0
  142. pumaguard-ui/macos/Runner/Release.entitlements +8 -0
  143. pumaguard-ui/macos/Runner.xcodeproj/project.pbxproj +705 -0
  144. pumaguard-ui/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  145. pumaguard-ui/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +99 -0
  146. pumaguard-ui/macos/Runner.xcworkspace/contents.xcworkspacedata +7 -0
  147. pumaguard-ui/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  148. pumaguard-ui/macos/RunnerTests/RunnerTests.swift +12 -0
  149. pumaguard-ui/pubspec.lock +882 -0
  150. pumaguard-ui/pubspec.yaml +125 -0
  151. pumaguard-ui/test/models/camera_test.dart +515 -0
  152. pumaguard-ui/test/models/plug_test.dart +499 -0
  153. pumaguard-ui/test/models/settings_test.dart +903 -0
  154. pumaguard-ui/test/models/status_test.dart +707 -0
  155. pumaguard-ui/test/screens/image_browser_grouping_test.dart +555 -0
  156. pumaguard-ui/test/services/api_service_cameras_test.dart +580 -0
  157. pumaguard-ui/test/services/api_service_image_browser_test.dart +512 -0
  158. pumaguard-ui/test/widget_test.dart.skip +38 -0
  159. pumaguard-ui/web/favicon.png +0 -0
  160. pumaguard-ui/web/icons/Icon-192.png +0 -0
  161. pumaguard-ui/web/icons/Icon-512.png +0 -0
  162. pumaguard-ui/web/icons/Icon-maskable-192.png +0 -0
  163. pumaguard-ui/web/icons/Icon-maskable-512.png +0 -0
  164. pumaguard-ui/web/index.html +38 -0
  165. pumaguard-ui/web/manifest.json +35 -0
  166. pumaguard-ui/windows/.gitignore +17 -0
  167. pumaguard-ui/windows/CMakeLists.txt +108 -0
  168. pumaguard-ui/windows/flutter/CMakeLists.txt +109 -0
  169. pumaguard-ui/windows/flutter/generated_plugin_registrant.cc +14 -0
  170. pumaguard-ui/windows/flutter/generated_plugin_registrant.h +15 -0
  171. pumaguard-ui/windows/flutter/generated_plugins.cmake +24 -0
  172. pumaguard-ui/windows/runner/CMakeLists.txt +40 -0
  173. pumaguard-ui/windows/runner/Runner.rc +121 -0
  174. pumaguard-ui/windows/runner/flutter_window.cpp +71 -0
  175. pumaguard-ui/windows/runner/flutter_window.h +33 -0
  176. pumaguard-ui/windows/runner/main.cpp +43 -0
  177. pumaguard-ui/windows/runner/resource.h +16 -0
  178. pumaguard-ui/windows/runner/resources/app_icon.ico +0 -0
  179. pumaguard-ui/windows/runner/runner.exe.manifest +14 -0
  180. pumaguard-ui/windows/runner/utils.cpp +65 -0
  181. pumaguard-ui/windows/runner/utils.h +19 -0
  182. pumaguard-ui/windows/runner/win32_window.cpp +288 -0
  183. pumaguard-ui/windows/runner/win32_window.h +102 -0
  184. pumaguard-21.post27.dist-info/RECORD +0 -83
  185. {pumaguard-21.post27.dist-info → pumaguard-21.post83.dist-info}/WHEEL +0 -0
  186. {pumaguard-21.post27.dist-info → pumaguard-21.post83.dist-info}/entry_points.txt +0 -0
  187. {pumaguard-21.post27.dist-info → pumaguard-21.post83.dist-info}/licenses/LICENSE +0 -0
  188. {pumaguard-21.post27.dist-info → pumaguard-21.post83.dist-info}/top_level.txt +0 -0
@@ -114,65 +114,32 @@ def register_dhcp_routes(
114
114
  timestamp,
115
115
  )
116
116
 
117
- # Store camera information in webui.cameras dictionary
118
- if action in ["add", "old"]:
119
- # Camera connected or renewed lease
120
- logger.info(
121
- "Camera '%s' connected at IP %s", hostname, ip_address
122
- )
123
- # Store camera info indexed by MAC address
124
- webui.cameras[mac_address] = {
125
- "hostname": hostname,
126
- "ip_address": ip_address,
127
- "mac_address": mac_address,
128
- "last_seen": timestamp,
129
- "status": "connected",
130
- }
131
-
132
- # Notify SSE clients
133
- notify_camera_change(
134
- "camera_connected", dict(webui.cameras[mac_address])
135
- )
136
-
137
- # Update settings with camera list
138
- # Convert cameras dict to list for settings persistence
139
- camera_list = []
140
- for _, cam_info in webui.cameras.items():
141
- camera_list.append(
142
- {
143
- "hostname": cam_info["hostname"],
144
- "ip_address": cam_info["ip_address"],
145
- "mac_address": cam_info["mac_address"],
146
- "last_seen": cam_info["last_seen"],
147
- "status": cam_info["status"],
148
- }
149
- )
150
-
151
- webui.presets.cameras = camera_list
152
-
153
- # Persist to settings file
154
- try:
155
- webui.presets.save()
156
- logger.info("Camera list saved to settings")
157
- except Exception as e: # pylint: disable=broad-except
158
- logger.error(
159
- "Failed to save camera list to settings: %s", str(e)
117
+ # Determine device type based on hostname pattern
118
+ is_camera = hostname and hostname.startswith("Microseven")
119
+ is_plug = hostname and hostname.lower().startswith("shellyplug")
120
+
121
+ if is_camera:
122
+ # Handle camera events
123
+ if action in ["add", "old"]:
124
+ # Camera connected or renewed lease
125
+ logger.info(
126
+ "Camera '%s' connected at IP %s", hostname, ip_address
160
127
  )
161
-
162
- elif action == "del":
163
- # Camera disconnected
164
- logger.info("Camera '%s' disconnected", hostname)
165
- # Update camera status to disconnected (keep history)
166
- if mac_address in webui.cameras:
167
- webui.cameras[mac_address]["status"] = "disconnected"
168
- webui.cameras[mac_address]["last_seen"] = timestamp
128
+ # Store camera info indexed by MAC address
129
+ webui.cameras[mac_address] = {
130
+ "hostname": hostname,
131
+ "ip_address": ip_address,
132
+ "mac_address": mac_address,
133
+ "last_seen": timestamp,
134
+ "status": "connected",
135
+ }
169
136
 
170
137
  # Notify SSE clients
171
138
  notify_camera_change(
172
- "camera_disconnected", dict(webui.cameras[mac_address])
139
+ "camera_connected", dict(webui.cameras[mac_address])
173
140
  )
174
141
 
175
- # Update settings with updated camera list
142
+ # Update settings with camera list
176
143
  camera_list = []
177
144
  for _, cam_info in webui.cameras.items():
178
145
  camera_list.append(
@@ -190,13 +157,135 @@ def register_dhcp_routes(
190
157
  # Persist to settings file
191
158
  try:
192
159
  webui.presets.save()
193
- logger.info("Camera list updated in settings")
160
+ logger.info("Camera list saved to settings")
194
161
  except Exception as e: # pylint: disable=broad-except
195
162
  logger.error(
196
163
  "Failed to save camera list to settings: %s",
197
164
  str(e),
198
165
  )
199
166
 
167
+ elif action == "del":
168
+ # Camera disconnected
169
+ logger.info("Camera '%s' disconnected", hostname)
170
+ # Update camera status to disconnected (keep history)
171
+ if mac_address in webui.cameras:
172
+ webui.cameras[mac_address]["status"] = "disconnected"
173
+ webui.cameras[mac_address]["last_seen"] = timestamp
174
+
175
+ # Notify SSE clients
176
+ notify_camera_change(
177
+ "camera_disconnected",
178
+ dict(webui.cameras[mac_address]),
179
+ )
180
+
181
+ # Update settings with updated camera list
182
+ camera_list = []
183
+ for _, cam_info in webui.cameras.items():
184
+ camera_list.append(
185
+ {
186
+ "hostname": cam_info["hostname"],
187
+ "ip_address": cam_info["ip_address"],
188
+ "mac_address": cam_info["mac_address"],
189
+ "last_seen": cam_info["last_seen"],
190
+ "status": cam_info["status"],
191
+ }
192
+ )
193
+
194
+ webui.presets.cameras = camera_list
195
+
196
+ # Persist to settings file
197
+ try:
198
+ webui.presets.save()
199
+ logger.info("Camera list updated in settings")
200
+ except Exception as e: # pylint: disable=broad-except
201
+ logger.error(
202
+ "Failed to save camera list to settings: %s",
203
+ str(e),
204
+ )
205
+
206
+ elif is_plug:
207
+ # Handle plug events
208
+ if action in ["add", "old"]:
209
+ # Plug connected or renewed lease
210
+ logger.info(
211
+ "Plug '%s' connected at IP %s", hostname, ip_address
212
+ )
213
+ # Store plug info indexed by MAC address
214
+ webui.plugs[mac_address] = {
215
+ "hostname": hostname,
216
+ "ip_address": ip_address,
217
+ "mac_address": mac_address,
218
+ "last_seen": timestamp,
219
+ "status": "connected",
220
+ }
221
+
222
+ # Notify SSE clients
223
+ notify_camera_change(
224
+ "plug_connected", dict(webui.plugs[mac_address])
225
+ )
226
+
227
+ # Update settings with plug list
228
+ plug_list = []
229
+ for _, plug_info in webui.plugs.items():
230
+ plug_list.append(
231
+ {
232
+ "hostname": plug_info["hostname"],
233
+ "ip_address": plug_info["ip_address"],
234
+ "mac_address": plug_info["mac_address"],
235
+ "last_seen": plug_info["last_seen"],
236
+ "status": plug_info["status"],
237
+ }
238
+ )
239
+
240
+ webui.presets.plugs = plug_list
241
+
242
+ # Persist to settings file
243
+ try:
244
+ webui.presets.save()
245
+ logger.info("Plug list saved to settings")
246
+ except Exception as e: # pylint: disable=broad-except
247
+ logger.error(
248
+ "Failed to save plug list to settings: %s", str(e)
249
+ )
250
+
251
+ elif action == "del":
252
+ # Plug disconnected
253
+ logger.info("Plug '%s' disconnected", hostname)
254
+ # Update plug status to disconnected (keep history)
255
+ if mac_address in webui.plugs:
256
+ webui.plugs[mac_address]["status"] = "disconnected"
257
+ webui.plugs[mac_address]["last_seen"] = timestamp
258
+
259
+ # Notify SSE clients
260
+ notify_camera_change(
261
+ "plug_disconnected", dict(webui.plugs[mac_address])
262
+ )
263
+
264
+ # Update settings with updated plug list
265
+ plug_list = []
266
+ for _, plug_info in webui.plugs.items():
267
+ plug_list.append(
268
+ {
269
+ "hostname": plug_info["hostname"],
270
+ "ip_address": plug_info["ip_address"],
271
+ "mac_address": plug_info["mac_address"],
272
+ "last_seen": plug_info["last_seen"],
273
+ "status": plug_info["status"],
274
+ }
275
+ )
276
+
277
+ webui.presets.plugs = plug_list
278
+
279
+ # Persist to settings file
280
+ try:
281
+ webui.presets.save()
282
+ logger.info("Plug list updated in settings")
283
+ except Exception as e: # pylint: disable=broad-except
284
+ logger.error(
285
+ "Failed to save plug list to settings: %s",
286
+ str(e),
287
+ )
288
+
200
289
  return (
201
290
  jsonify(
202
291
  {
@@ -206,6 +295,11 @@ def register_dhcp_routes(
206
295
  "action": action,
207
296
  "hostname": hostname,
208
297
  "ip_address": ip_address,
298
+ "device_type": (
299
+ "camera"
300
+ if is_camera
301
+ else "plug" if is_plug else "unknown"
302
+ ),
209
303
  },
210
304
  }
211
305
  ),
@@ -446,6 +540,169 @@ def register_dhcp_routes(
446
540
  500,
447
541
  )
448
542
 
543
+ @app.route("/api/dhcp/plugs", methods=["GET"])
544
+ def get_plugs():
545
+ """
546
+ Get list of known plugs.
547
+
548
+ Returns all detected plugs with their connection status.
549
+ """
550
+ plugs_list = list(webui.plugs.values())
551
+
552
+ return (
553
+ jsonify(
554
+ {
555
+ "plugs": plugs_list,
556
+ "count": len(plugs_list),
557
+ }
558
+ ),
559
+ 200,
560
+ )
561
+
562
+ @app.route("/api/dhcp/plugs/<mac_address>", methods=["GET"])
563
+ def get_plug(mac_address: str):
564
+ """
565
+ Get information about a specific plug.
566
+
567
+ Args:
568
+ mac_address: MAC address of the plug
569
+ """
570
+ plug = webui.plugs.get(mac_address)
571
+ if not plug:
572
+ return jsonify({"error": "Plug not found"}), 404
573
+
574
+ return jsonify({"plug": plug}), 200
575
+
576
+ @app.route("/api/dhcp/plugs", methods=["POST"])
577
+ def add_plug():
578
+ """
579
+ Manually add a plug (for testing purposes).
580
+
581
+ Expected JSON payload:
582
+ {
583
+ "hostname": "plug-name",
584
+ "ip_address": "192.168.52.100",
585
+ "mac_address": "aa:bb:cc:dd:ee:ff",
586
+ "status": "connected" // optional, defaults to "connected"
587
+ }
588
+ """
589
+ try:
590
+ data = request.get_json()
591
+
592
+ if not data:
593
+ return jsonify({"error": "No JSON data provided"}), 400
594
+
595
+ hostname = data.get("hostname")
596
+ ip_address = data.get("ip_address")
597
+ mac_address = data.get("mac_address")
598
+ status = data.get("status", "connected")
599
+
600
+ # Validate required fields
601
+ if not hostname or not ip_address or not mac_address:
602
+ return (
603
+ jsonify(
604
+ {
605
+ "error": "Missing required fields: hostname, "
606
+ "ip_address, mac_address"
607
+ }
608
+ ),
609
+ 400,
610
+ )
611
+
612
+ # Generate timestamp
613
+ timestamp = datetime.now(timezone.utc).strftime(
614
+ "%Y-%m-%dT%H:%M:%SZ"
615
+ )
616
+
617
+ # Add plug to webui.plugs
618
+ webui.plugs[mac_address] = {
619
+ "hostname": hostname,
620
+ "ip_address": ip_address,
621
+ "mac_address": mac_address,
622
+ "last_seen": timestamp,
623
+ "status": status,
624
+ }
625
+
626
+ # Update settings with plug list
627
+ plug_list = []
628
+ for _, plug_info in webui.plugs.items():
629
+ plug_list.append(
630
+ {
631
+ "hostname": plug_info["hostname"],
632
+ "ip_address": plug_info["ip_address"],
633
+ "mac_address": plug_info["mac_address"],
634
+ "last_seen": plug_info["last_seen"],
635
+ "status": plug_info["status"],
636
+ }
637
+ )
638
+
639
+ webui.presets.plugs = plug_list
640
+
641
+ # Persist to settings file
642
+ try:
643
+ webui.presets.save()
644
+ logger.info(
645
+ "Manually added plug '%s' at %s", hostname, ip_address
646
+ )
647
+ except Exception as e: # pylint: disable=broad-except
648
+ logger.error(
649
+ "Failed to save plug list to settings: %s", str(e)
650
+ )
651
+
652
+ # Notify SSE clients
653
+ notify_camera_change("plug_added", dict(webui.plugs[mac_address]))
654
+
655
+ return (
656
+ jsonify(
657
+ {
658
+ "status": "success",
659
+ "message": "Plug added successfully",
660
+ "plug": webui.plugs[mac_address],
661
+ }
662
+ ),
663
+ 201,
664
+ )
665
+
666
+ except Exception as e: # pylint: disable=broad-except
667
+ logger.error("Error adding plug: %s", str(e))
668
+ return (
669
+ jsonify(
670
+ {
671
+ "error": "Failed to add plug",
672
+ }
673
+ ),
674
+ 500,
675
+ )
676
+
677
+ @app.route("/api/dhcp/plugs", methods=["DELETE"])
678
+ def clear_plugs():
679
+ """
680
+ Clear all plug records.
681
+
682
+ This removes all stored plug information from memory.
683
+ """
684
+ count = len(webui.plugs)
685
+ webui.plugs.clear()
686
+ logger.info("Cleared %d plug records", count)
687
+
688
+ # Update settings
689
+ webui.presets.plugs = []
690
+ try:
691
+ webui.presets.save()
692
+ logger.info("Plug list cleared from settings")
693
+ except Exception as e: # pylint: disable=broad-except
694
+ logger.error("Failed to save plug list to settings: %s", str(e))
695
+
696
+ return (
697
+ jsonify(
698
+ {
699
+ "status": "success",
700
+ "message": f"Cleared {count} plug record(s)",
701
+ }
702
+ ),
703
+ 200,
704
+ )
705
+
449
706
  @app.route("/api/dhcp/cameras/events", methods=["GET"])
450
707
  def camera_events():
451
708
  """
@@ -4,6 +4,7 @@ from __future__ import (
4
4
  annotations,
5
5
  )
6
6
 
7
+ import time
7
8
  from typing import (
8
9
  TYPE_CHECKING,
9
10
  )
@@ -35,6 +36,10 @@ def register_diagnostics_routes(app: "Flask", webui: "WebUI") -> None:
35
36
  def get_status():
36
37
  origin = request.headers.get("Origin", "No Origin header")
37
38
  host = request.headers.get("Host", "No Host header")
39
+
40
+ # Calculate uptime in seconds
41
+ uptime_seconds = int(time.time() - webui.start_time)
42
+
38
43
  return jsonify(
39
44
  {
40
45
  "status": "running",
@@ -42,6 +47,7 @@ def register_diagnostics_routes(app: "Flask", webui: "WebUI") -> None:
42
47
  "directories_count": len(webui.image_directories),
43
48
  "host": webui._get_local_ip(), # pylint: disable=protected-access
44
49
  "port": webui.port,
50
+ "uptime_seconds": uptime_seconds,
45
51
  "request_origin": origin,
46
52
  "request_host": host,
47
53
  }
@@ -66,6 +66,19 @@ def register_settings_routes(app: "Flask", webui: "WebUI") -> None:
66
66
  }
67
67
  )
68
68
  settings_dict["cameras"] = camera_list
69
+ # Add plugs from webui.plugs (runtime state)
70
+ plug_list = []
71
+ for _, plug_info in webui.plugs.items():
72
+ plug_list.append(
73
+ {
74
+ "hostname": plug_info["hostname"],
75
+ "ip_address": plug_info["ip_address"],
76
+ "mac_address": plug_info["mac_address"],
77
+ "last_seen": plug_info["last_seen"],
78
+ "status": plug_info["status"],
79
+ }
80
+ )
81
+ settings_dict["plugs"] = plug_list
69
82
  return jsonify(settings_dict)
70
83
 
71
84
  @app.route("/api/settings", methods=["PUT"])
pumaguard/web_ui.py CHANGED
@@ -82,6 +82,16 @@ class CameraInfo(TypedDict):
82
82
  status: str
83
83
 
84
84
 
85
+ class PlugInfo(TypedDict):
86
+ """Type definition for plug information stored in webui.plugs."""
87
+
88
+ hostname: str
89
+ ip_address: str
90
+ mac_address: str
91
+ last_seen: str
92
+ status: str
93
+
94
+
85
95
  class PhotoDict(TypedDict):
86
96
  """Type definition for photo metadata dictionary."""
87
97
 
@@ -160,10 +170,17 @@ class WebUI:
160
170
  self.image_directories: list[str] = []
161
171
  self.classification_directories: list[str] = []
162
172
 
173
+ # Track server start time for uptime calculation
174
+ self.start_time: float = time.time()
175
+
163
176
  # Camera tracking - stores detected cameras by MAC address
164
177
  # Format: {mac_address: CameraInfo}
165
178
  self.cameras: dict[str, CameraInfo] = {}
166
179
 
180
+ # Plug tracking - stores detected plugs by MAC address
181
+ # Format: {mac_address: PlugInfo}
182
+ self.plugs: dict[str, PlugInfo] = {}
183
+
167
184
  # Camera heartbeat monitoring (callback set after routes registered)
168
185
  self.heartbeat: CameraHeartbeat = CameraHeartbeat(
169
186
  webui=self,
@@ -189,6 +206,18 @@ class WebUI:
189
206
  status=camera.get("status", "disconnected"),
190
207
  )
191
208
 
209
+ # Load plugs from persisted settings
210
+ for plug in presets.plugs:
211
+ mac = plug.get("mac_address")
212
+ if mac:
213
+ self.plugs[mac] = PlugInfo(
214
+ hostname=plug.get("hostname", ""),
215
+ ip_address=plug.get("ip_address", ""),
216
+ mac_address=mac,
217
+ last_seen=plug.get("last_seen", ""),
218
+ status=plug.get("status", "disconnected"),
219
+ )
220
+
192
221
  # mDNS/Zeroconf support
193
222
  self.zeroconf: Zeroconf | None = None
194
223
  self.service_info: ServiceInfo | None = None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pumaguard
3
- Version: 21.post27
3
+ Version: 21.post83
4
4
  Author-email: Nicolas Bock <nicolasbock@gmail.com>
5
5
  Project-URL: Homepage, http://pumaguard.rtfd.io/
6
6
  Project-URL: Repository, https://github.com/PEEC-Nature-Youth-Group/pumaguard