pumaguard 21.post29__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.post29.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.post29.dist-info/RECORD +0 -83
  185. {pumaguard-21.post29.dist-info → pumaguard-21.post83.dist-info}/WHEEL +0 -0
  186. {pumaguard-21.post29.dist-info → pumaguard-21.post83.dist-info}/entry_points.txt +0 -0
  187. {pumaguard-21.post29.dist-info → pumaguard-21.post83.dist-info}/licenses/LICENSE +0 -0
  188. {pumaguard-21.post29.dist-info → pumaguard-21.post83.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,903 @@
1
+ // Unit tests for Settings model
2
+ // Tests JSON serialization, deserialization, copyWith, and nested objects
3
+
4
+ import 'package:flutter_test/flutter_test.dart';
5
+ import 'package:pumaguard_ui/models/settings.dart';
6
+ import 'package:pumaguard_ui/models/camera.dart';
7
+ import 'package:pumaguard_ui/models/plug.dart';
8
+
9
+ void main() {
10
+ group('Settings Model Tests', () {
11
+ group('Constructor', () {
12
+ test('creates Settings with all fields', () {
13
+ final camera = Camera(
14
+ hostname: 'TestCamera',
15
+ ipAddress: '192.168.52.100',
16
+ macAddress: 'aa:bb:cc:dd:ee:ff',
17
+ lastSeen: '2024-01-15T10:30:00Z',
18
+ status: 'connected',
19
+ );
20
+
21
+ final plug = Plug(
22
+ hostname: 'TestPlug',
23
+ ipAddress: '192.168.52.200',
24
+ macAddress: 'bb:cc:dd:ee:ff:00',
25
+ lastSeen: '2024-01-15T10:30:00Z',
26
+ status: 'connected',
27
+ );
28
+
29
+ final settings = Settings(
30
+ yoloMinSize: 0.02,
31
+ yoloConfThresh: 0.25,
32
+ yoloMaxDets: 10,
33
+ yoloModelFilename: 'yolov8s_101425.pt',
34
+ classifierModelFilename: 'colorbw_111325.h5',
35
+ deterrentSoundFile: 'alarm.wav',
36
+ fileStabilizationExtraWait: 2.0,
37
+ playSound: true,
38
+ volume: 80,
39
+ cameras: [camera],
40
+ plugs: [plug],
41
+ );
42
+
43
+ expect(settings.yoloMinSize, 0.02);
44
+ expect(settings.yoloConfThresh, 0.25);
45
+ expect(settings.yoloMaxDets, 10);
46
+ expect(settings.yoloModelFilename, 'yolov8s_101425.pt');
47
+ expect(settings.classifierModelFilename, 'colorbw_111325.h5');
48
+ expect(settings.deterrentSoundFile, 'alarm.wav');
49
+ expect(settings.fileStabilizationExtraWait, 2.0);
50
+ expect(settings.playSound, true);
51
+ expect(settings.volume, 80);
52
+ expect(settings.cameras.length, 1);
53
+ expect(settings.plugs.length, 1);
54
+ });
55
+
56
+ test('creates Settings with empty lists', () {
57
+ final settings = Settings(
58
+ yoloMinSize: 0.01,
59
+ yoloConfThresh: 0.25,
60
+ yoloMaxDets: 10,
61
+ yoloModelFilename: '',
62
+ classifierModelFilename: '',
63
+ deterrentSoundFile: '',
64
+ fileStabilizationExtraWait: 2.0,
65
+ playSound: false,
66
+ volume: 80,
67
+ cameras: [],
68
+ plugs: [],
69
+ );
70
+
71
+ expect(settings.cameras.isEmpty, true);
72
+ expect(settings.plugs.isEmpty, true);
73
+ });
74
+
75
+ test('creates Settings with multiple cameras and plugs', () {
76
+ final cameras = [
77
+ Camera(
78
+ hostname: 'Camera1',
79
+ ipAddress: '192.168.52.100',
80
+ macAddress: 'aa:bb:cc:dd:ee:01',
81
+ lastSeen: '2024-01-15T10:30:00Z',
82
+ status: 'connected',
83
+ ),
84
+ Camera(
85
+ hostname: 'Camera2',
86
+ ipAddress: '192.168.52.101',
87
+ macAddress: 'aa:bb:cc:dd:ee:02',
88
+ lastSeen: '2024-01-15T10:30:00Z',
89
+ status: 'connected',
90
+ ),
91
+ ];
92
+
93
+ final plugs = [
94
+ Plug(
95
+ hostname: 'Plug1',
96
+ ipAddress: '192.168.52.200',
97
+ macAddress: 'bb:cc:dd:ee:ff:01',
98
+ lastSeen: '2024-01-15T10:30:00Z',
99
+ status: 'connected',
100
+ ),
101
+ Plug(
102
+ hostname: 'Plug2',
103
+ ipAddress: '192.168.52.201',
104
+ macAddress: 'bb:cc:dd:ee:ff:02',
105
+ lastSeen: '2024-01-15T10:30:00Z',
106
+ status: 'connected',
107
+ ),
108
+ ];
109
+
110
+ final settings = Settings(
111
+ yoloMinSize: 0.02,
112
+ yoloConfThresh: 0.25,
113
+ yoloMaxDets: 10,
114
+ yoloModelFilename: 'yolov8s.pt',
115
+ classifierModelFilename: 'classifier.h5',
116
+ deterrentSoundFile: 'sound.wav',
117
+ fileStabilizationExtraWait: 2.0,
118
+ playSound: true,
119
+ volume: 80,
120
+ cameras: cameras,
121
+ plugs: plugs,
122
+ );
123
+
124
+ expect(settings.cameras.length, 2);
125
+ expect(settings.plugs.length, 2);
126
+ expect(settings.cameras[0].hostname, 'Camera1');
127
+ expect(settings.plugs[0].hostname, 'Plug1');
128
+ });
129
+ });
130
+
131
+ group('fromJson', () {
132
+ test('parses complete JSON correctly', () {
133
+ final json = {
134
+ 'YOLO-min-size': 0.02,
135
+ 'YOLO-conf-thresh': 0.25,
136
+ 'YOLO-max-dets': 10,
137
+ 'YOLO-model-filename': 'yolov8s_101425.pt',
138
+ 'classifier-model-filename': 'colorbw_111325.h5',
139
+ 'deterrent-sound-file': 'alarm.wav',
140
+ 'file-stabilization-extra-wait': 2.0,
141
+ 'play-sound': true,
142
+ 'volume': 80,
143
+ 'cameras': [
144
+ {
145
+ 'hostname': 'TestCamera',
146
+ 'ip_address': '192.168.52.100',
147
+ 'mac_address': 'aa:bb:cc:dd:ee:ff',
148
+ 'last_seen': '2024-01-15T10:30:00Z',
149
+ 'status': 'connected',
150
+ },
151
+ ],
152
+ 'plugs': [
153
+ {
154
+ 'hostname': 'TestPlug',
155
+ 'ip_address': '192.168.52.200',
156
+ 'mac_address': 'bb:cc:dd:ee:ff:00',
157
+ 'last_seen': '2024-01-15T10:30:00Z',
158
+ 'status': 'connected',
159
+ },
160
+ ],
161
+ };
162
+
163
+ final settings = Settings.fromJson(json);
164
+
165
+ expect(settings.yoloMinSize, 0.02);
166
+ expect(settings.yoloConfThresh, 0.25);
167
+ expect(settings.yoloMaxDets, 10);
168
+ expect(settings.yoloModelFilename, 'yolov8s_101425.pt');
169
+ expect(settings.classifierModelFilename, 'colorbw_111325.h5');
170
+ expect(settings.deterrentSoundFile, 'alarm.wav');
171
+ expect(settings.fileStabilizationExtraWait, 2.0);
172
+ expect(settings.playSound, true);
173
+ expect(settings.volume, 80);
174
+ expect(settings.cameras.length, 1);
175
+ expect(settings.cameras[0].hostname, 'TestCamera');
176
+ expect(settings.plugs.length, 1);
177
+ expect(settings.plugs[0].hostname, 'TestPlug');
178
+ });
179
+
180
+ test('handles null values with defaults', () {
181
+ final json = {
182
+ 'YOLO-min-size': null,
183
+ 'YOLO-conf-thresh': null,
184
+ 'YOLO-max-dets': null,
185
+ 'YOLO-model-filename': null,
186
+ 'classifier-model-filename': null,
187
+ 'deterrent-sound-file': null,
188
+ 'file-stabilization-extra-wait': null,
189
+ 'play-sound': null,
190
+ 'volume': null,
191
+ 'cameras': null,
192
+ 'plugs': null,
193
+ };
194
+
195
+ final settings = Settings.fromJson(json);
196
+
197
+ expect(settings.yoloMinSize, 0.01);
198
+ expect(settings.yoloConfThresh, 0.25);
199
+ expect(settings.yoloMaxDets, 10);
200
+ expect(settings.yoloModelFilename, '');
201
+ expect(settings.classifierModelFilename, '');
202
+ expect(settings.deterrentSoundFile, '');
203
+ expect(settings.fileStabilizationExtraWait, 2.0);
204
+ expect(settings.playSound, false);
205
+ expect(settings.volume, 80);
206
+ expect(settings.cameras.isEmpty, true);
207
+ expect(settings.plugs.isEmpty, true);
208
+ });
209
+
210
+ test('handles missing fields with defaults', () {
211
+ final json = <String, dynamic>{};
212
+
213
+ final settings = Settings.fromJson(json);
214
+
215
+ expect(settings.yoloMinSize, 0.01);
216
+ expect(settings.yoloConfThresh, 0.25);
217
+ expect(settings.yoloMaxDets, 10);
218
+ expect(settings.yoloModelFilename, '');
219
+ expect(settings.classifierModelFilename, '');
220
+ expect(settings.deterrentSoundFile, '');
221
+ expect(settings.fileStabilizationExtraWait, 2.0);
222
+ expect(settings.playSound, false);
223
+ expect(settings.volume, 80);
224
+ expect(settings.cameras.isEmpty, true);
225
+ expect(settings.plugs.isEmpty, true);
226
+ });
227
+
228
+ test('handles partial JSON with some fields missing', () {
229
+ final json = {
230
+ 'YOLO-min-size': 0.03,
231
+ 'YOLO-conf-thresh': 0.5,
232
+ 'play-sound': true,
233
+ };
234
+
235
+ final settings = Settings.fromJson(json);
236
+
237
+ expect(settings.yoloMinSize, 0.03);
238
+ expect(settings.yoloConfThresh, 0.5);
239
+ expect(settings.playSound, true);
240
+ expect(settings.yoloMaxDets, 10); // default
241
+ expect(settings.volume, 80); // default
242
+ });
243
+
244
+ test('handles integer values for double fields', () {
245
+ final json = {
246
+ 'YOLO-min-size': 1,
247
+ 'YOLO-conf-thresh': 0,
248
+ 'file-stabilization-extra-wait': 3,
249
+ };
250
+
251
+ final settings = Settings.fromJson(json);
252
+
253
+ expect(settings.yoloMinSize, 1.0);
254
+ expect(settings.yoloConfThresh, 0.0);
255
+ expect(settings.fileStabilizationExtraWait, 3.0);
256
+ });
257
+
258
+ test('handles empty cameras list', () {
259
+ final json = {'cameras': [], 'plugs': []};
260
+
261
+ final settings = Settings.fromJson(json);
262
+
263
+ expect(settings.cameras.isEmpty, true);
264
+ expect(settings.plugs.isEmpty, true);
265
+ });
266
+
267
+ test('handles multiple cameras and plugs in JSON', () {
268
+ final json = {
269
+ 'cameras': [
270
+ {
271
+ 'hostname': 'Camera1',
272
+ 'ip_address': '192.168.52.100',
273
+ 'mac_address': 'aa:bb:cc:dd:ee:01',
274
+ 'last_seen': '2024-01-15T10:30:00Z',
275
+ 'status': 'connected',
276
+ },
277
+ {
278
+ 'hostname': 'Camera2',
279
+ 'ip_address': '192.168.52.101',
280
+ 'mac_address': 'aa:bb:cc:dd:ee:02',
281
+ 'last_seen': '2024-01-15T10:30:00Z',
282
+ 'status': 'disconnected',
283
+ },
284
+ ],
285
+ 'plugs': [
286
+ {
287
+ 'hostname': 'Plug1',
288
+ 'ip_address': '192.168.52.200',
289
+ 'mac_address': 'bb:cc:dd:ee:ff:01',
290
+ 'last_seen': '2024-01-15T10:30:00Z',
291
+ 'status': 'connected',
292
+ },
293
+ ],
294
+ };
295
+
296
+ final settings = Settings.fromJson(json);
297
+
298
+ expect(settings.cameras.length, 2);
299
+ expect(settings.cameras[0].hostname, 'Camera1');
300
+ expect(settings.cameras[1].hostname, 'Camera2');
301
+ expect(settings.cameras[1].status, 'disconnected');
302
+ expect(settings.plugs.length, 1);
303
+ expect(settings.plugs[0].hostname, 'Plug1');
304
+ });
305
+
306
+ test('handles non-list value for cameras', () {
307
+ final json = {'cameras': 'not a list', 'plugs': 'also not a list'};
308
+
309
+ final settings = Settings.fromJson(json);
310
+
311
+ expect(settings.cameras.isEmpty, true);
312
+ expect(settings.plugs.isEmpty, true);
313
+ });
314
+ });
315
+
316
+ group('toJson', () {
317
+ test('converts Settings to JSON correctly', () {
318
+ final camera = Camera(
319
+ hostname: 'TestCamera',
320
+ ipAddress: '192.168.52.100',
321
+ macAddress: 'aa:bb:cc:dd:ee:ff',
322
+ lastSeen: '2024-01-15T10:30:00Z',
323
+ status: 'connected',
324
+ );
325
+
326
+ final plug = Plug(
327
+ hostname: 'TestPlug',
328
+ ipAddress: '192.168.52.200',
329
+ macAddress: 'bb:cc:dd:ee:ff:00',
330
+ lastSeen: '2024-01-15T10:30:00Z',
331
+ status: 'connected',
332
+ );
333
+
334
+ final settings = Settings(
335
+ yoloMinSize: 0.02,
336
+ yoloConfThresh: 0.25,
337
+ yoloMaxDets: 10,
338
+ yoloModelFilename: 'yolov8s_101425.pt',
339
+ classifierModelFilename: 'colorbw_111325.h5',
340
+ deterrentSoundFile: 'alarm.wav',
341
+ fileStabilizationExtraWait: 2.0,
342
+ playSound: true,
343
+ volume: 80,
344
+ cameras: [camera],
345
+ plugs: [plug],
346
+ );
347
+
348
+ final json = settings.toJson();
349
+
350
+ expect(json['YOLO-min-size'], 0.02);
351
+ expect(json['YOLO-conf-thresh'], 0.25);
352
+ expect(json['YOLO-max-dets'], 10);
353
+ expect(json['YOLO-model-filename'], 'yolov8s_101425.pt');
354
+ expect(json['classifier-model-filename'], 'colorbw_111325.h5');
355
+ expect(json['deterrent-sound-file'], 'alarm.wav');
356
+ expect(json['file-stabilization-extra-wait'], 2.0);
357
+ expect(json['play-sound'], true);
358
+ expect(json['volume'], 80);
359
+ expect(json['cameras'], isA<List>());
360
+ expect(json['cameras'].length, 1);
361
+ expect(json['plugs'], isA<List>());
362
+ expect(json['plugs'].length, 1);
363
+ });
364
+
365
+ test('converts empty lists correctly', () {
366
+ final settings = Settings(
367
+ yoloMinSize: 0.01,
368
+ yoloConfThresh: 0.25,
369
+ yoloMaxDets: 10,
370
+ yoloModelFilename: '',
371
+ classifierModelFilename: '',
372
+ deterrentSoundFile: '',
373
+ fileStabilizationExtraWait: 2.0,
374
+ playSound: false,
375
+ volume: 80,
376
+ cameras: [],
377
+ plugs: [],
378
+ );
379
+
380
+ final json = settings.toJson();
381
+
382
+ expect(json['cameras'], isA<List>());
383
+ expect(json['cameras'].isEmpty, true);
384
+ expect(json['plugs'], isA<List>());
385
+ expect(json['plugs'].isEmpty, true);
386
+ });
387
+
388
+ test('preserves empty strings in JSON', () {
389
+ final settings = Settings(
390
+ yoloMinSize: 0.01,
391
+ yoloConfThresh: 0.25,
392
+ yoloMaxDets: 10,
393
+ yoloModelFilename: '',
394
+ classifierModelFilename: '',
395
+ deterrentSoundFile: '',
396
+ fileStabilizationExtraWait: 2.0,
397
+ playSound: false,
398
+ volume: 80,
399
+ cameras: [],
400
+ plugs: [],
401
+ );
402
+
403
+ final json = settings.toJson();
404
+
405
+ expect(json['YOLO-model-filename'], '');
406
+ expect(json['classifier-model-filename'], '');
407
+ expect(json['deterrent-sound-file'], '');
408
+ });
409
+ });
410
+
411
+ group('JSON Round Trip', () {
412
+ test('fromJson and toJson are inverses', () {
413
+ final originalJson = {
414
+ 'YOLO-min-size': 0.02,
415
+ 'YOLO-conf-thresh': 0.25,
416
+ 'YOLO-max-dets': 10,
417
+ 'YOLO-model-filename': 'yolov8s_101425.pt',
418
+ 'classifier-model-filename': 'colorbw_111325.h5',
419
+ 'deterrent-sound-file': 'alarm.wav',
420
+ 'file-stabilization-extra-wait': 2.0,
421
+ 'play-sound': true,
422
+ 'volume': 80,
423
+ 'cameras': [
424
+ {
425
+ 'hostname': 'TestCamera',
426
+ 'ip_address': '192.168.52.100',
427
+ 'mac_address': 'aa:bb:cc:dd:ee:ff',
428
+ 'last_seen': '2024-01-15T10:30:00Z',
429
+ 'status': 'connected',
430
+ },
431
+ ],
432
+ 'plugs': [
433
+ {
434
+ 'hostname': 'TestPlug',
435
+ 'ip_address': '192.168.52.200',
436
+ 'mac_address': 'bb:cc:dd:ee:ff:00',
437
+ 'last_seen': '2024-01-15T10:30:00Z',
438
+ 'status': 'connected',
439
+ },
440
+ ],
441
+ };
442
+
443
+ final settings = Settings.fromJson(originalJson);
444
+ final resultJson = settings.toJson();
445
+
446
+ expect(resultJson['YOLO-min-size'], originalJson['YOLO-min-size']);
447
+ expect(
448
+ resultJson['YOLO-conf-thresh'],
449
+ originalJson['YOLO-conf-thresh'],
450
+ );
451
+ expect(resultJson['YOLO-max-dets'], originalJson['YOLO-max-dets']);
452
+ expect(
453
+ resultJson['YOLO-model-filename'],
454
+ originalJson['YOLO-model-filename'],
455
+ );
456
+ expect(
457
+ resultJson['classifier-model-filename'],
458
+ originalJson['classifier-model-filename'],
459
+ );
460
+ expect(
461
+ resultJson['deterrent-sound-file'],
462
+ originalJson['deterrent-sound-file'],
463
+ );
464
+ expect(
465
+ resultJson['file-stabilization-extra-wait'],
466
+ originalJson['file-stabilization-extra-wait'],
467
+ );
468
+ expect(resultJson['play-sound'], originalJson['play-sound']);
469
+ expect(resultJson['volume'], originalJson['volume']);
470
+ expect(resultJson['cameras'].length, 1);
471
+ expect(resultJson['plugs'].length, 1);
472
+ });
473
+ });
474
+
475
+ group('copyWith', () {
476
+ late Settings originalSettings;
477
+
478
+ setUp(() {
479
+ final camera = Camera(
480
+ hostname: 'TestCamera',
481
+ ipAddress: '192.168.52.100',
482
+ macAddress: 'aa:bb:cc:dd:ee:ff',
483
+ lastSeen: '2024-01-15T10:30:00Z',
484
+ status: 'connected',
485
+ );
486
+
487
+ final plug = Plug(
488
+ hostname: 'TestPlug',
489
+ ipAddress: '192.168.52.200',
490
+ macAddress: 'bb:cc:dd:ee:ff:00',
491
+ lastSeen: '2024-01-15T10:30:00Z',
492
+ status: 'connected',
493
+ );
494
+
495
+ originalSettings = Settings(
496
+ yoloMinSize: 0.02,
497
+ yoloConfThresh: 0.25,
498
+ yoloMaxDets: 10,
499
+ yoloModelFilename: 'yolov8s_101425.pt',
500
+ classifierModelFilename: 'colorbw_111325.h5',
501
+ deterrentSoundFile: 'alarm.wav',
502
+ fileStabilizationExtraWait: 2.0,
503
+ playSound: true,
504
+ volume: 80,
505
+ cameras: [camera],
506
+ plugs: [plug],
507
+ );
508
+ });
509
+
510
+ test('returns identical object when no parameters provided', () {
511
+ final newSettings = originalSettings.copyWith();
512
+
513
+ expect(newSettings.yoloMinSize, originalSettings.yoloMinSize);
514
+ expect(newSettings.yoloConfThresh, originalSettings.yoloConfThresh);
515
+ expect(newSettings.yoloMaxDets, originalSettings.yoloMaxDets);
516
+ expect(
517
+ newSettings.yoloModelFilename,
518
+ originalSettings.yoloModelFilename,
519
+ );
520
+ expect(
521
+ newSettings.classifierModelFilename,
522
+ originalSettings.classifierModelFilename,
523
+ );
524
+ expect(
525
+ newSettings.deterrentSoundFile,
526
+ originalSettings.deterrentSoundFile,
527
+ );
528
+ expect(
529
+ newSettings.fileStabilizationExtraWait,
530
+ originalSettings.fileStabilizationExtraWait,
531
+ );
532
+ expect(newSettings.playSound, originalSettings.playSound);
533
+ expect(newSettings.volume, originalSettings.volume);
534
+ expect(newSettings.cameras.length, originalSettings.cameras.length);
535
+ expect(newSettings.plugs.length, originalSettings.plugs.length);
536
+ });
537
+
538
+ test('updates yoloMinSize', () {
539
+ final newSettings = originalSettings.copyWith(yoloMinSize: 0.05);
540
+
541
+ expect(newSettings.yoloMinSize, 0.05);
542
+ expect(newSettings.yoloConfThresh, originalSettings.yoloConfThresh);
543
+ });
544
+
545
+ test('updates yoloConfThresh', () {
546
+ final newSettings = originalSettings.copyWith(yoloConfThresh: 0.5);
547
+
548
+ expect(newSettings.yoloConfThresh, 0.5);
549
+ expect(newSettings.yoloMinSize, originalSettings.yoloMinSize);
550
+ });
551
+
552
+ test('updates yoloMaxDets', () {
553
+ final newSettings = originalSettings.copyWith(yoloMaxDets: 20);
554
+
555
+ expect(newSettings.yoloMaxDets, 20);
556
+ expect(newSettings.yoloMinSize, originalSettings.yoloMinSize);
557
+ });
558
+
559
+ test('updates yoloModelFilename', () {
560
+ final newSettings = originalSettings.copyWith(
561
+ yoloModelFilename: 'new_model.pt',
562
+ );
563
+
564
+ expect(newSettings.yoloModelFilename, 'new_model.pt');
565
+ expect(
566
+ newSettings.classifierModelFilename,
567
+ originalSettings.classifierModelFilename,
568
+ );
569
+ });
570
+
571
+ test('updates classifierModelFilename', () {
572
+ final newSettings = originalSettings.copyWith(
573
+ classifierModelFilename: 'new_classifier.h5',
574
+ );
575
+
576
+ expect(newSettings.classifierModelFilename, 'new_classifier.h5');
577
+ expect(
578
+ newSettings.yoloModelFilename,
579
+ originalSettings.yoloModelFilename,
580
+ );
581
+ });
582
+
583
+ test('updates deterrentSoundFile', () {
584
+ final newSettings = originalSettings.copyWith(
585
+ deterrentSoundFile: 'new_sound.wav',
586
+ );
587
+
588
+ expect(newSettings.deterrentSoundFile, 'new_sound.wav');
589
+ expect(
590
+ newSettings.yoloModelFilename,
591
+ originalSettings.yoloModelFilename,
592
+ );
593
+ });
594
+
595
+ test('updates fileStabilizationExtraWait', () {
596
+ final newSettings = originalSettings.copyWith(
597
+ fileStabilizationExtraWait: 5.0,
598
+ );
599
+
600
+ expect(newSettings.fileStabilizationExtraWait, 5.0);
601
+ expect(newSettings.yoloMinSize, originalSettings.yoloMinSize);
602
+ });
603
+
604
+ test('updates playSound', () {
605
+ final newSettings = originalSettings.copyWith(playSound: false);
606
+
607
+ expect(newSettings.playSound, false);
608
+ expect(newSettings.volume, originalSettings.volume);
609
+ });
610
+
611
+ test('updates volume', () {
612
+ final newSettings = originalSettings.copyWith(volume: 100);
613
+
614
+ expect(newSettings.volume, 100);
615
+ expect(newSettings.playSound, originalSettings.playSound);
616
+ });
617
+
618
+ test('updates cameras list', () {
619
+ final newCamera = Camera(
620
+ hostname: 'NewCamera',
621
+ ipAddress: '192.168.52.102',
622
+ macAddress: 'cc:dd:ee:ff:00:11',
623
+ lastSeen: '2024-01-15T11:00:00Z',
624
+ status: 'connected',
625
+ );
626
+
627
+ final newSettings = originalSettings.copyWith(cameras: [newCamera]);
628
+
629
+ expect(newSettings.cameras.length, 1);
630
+ expect(newSettings.cameras[0].hostname, 'NewCamera');
631
+ expect(newSettings.plugs.length, originalSettings.plugs.length);
632
+ });
633
+
634
+ test('updates plugs list', () {
635
+ final newPlug = Plug(
636
+ hostname: 'NewPlug',
637
+ ipAddress: '192.168.52.202',
638
+ macAddress: 'dd:ee:ff:00:11:22',
639
+ lastSeen: '2024-01-15T11:00:00Z',
640
+ status: 'connected',
641
+ );
642
+
643
+ final newSettings = originalSettings.copyWith(plugs: [newPlug]);
644
+
645
+ expect(newSettings.plugs.length, 1);
646
+ expect(newSettings.plugs[0].hostname, 'NewPlug');
647
+ expect(newSettings.cameras.length, originalSettings.cameras.length);
648
+ });
649
+
650
+ test('updates multiple fields at once', () {
651
+ final newSettings = originalSettings.copyWith(
652
+ yoloMinSize: 0.05,
653
+ volume: 90,
654
+ playSound: false,
655
+ );
656
+
657
+ expect(newSettings.yoloMinSize, 0.05);
658
+ expect(newSettings.volume, 90);
659
+ expect(newSettings.playSound, false);
660
+ expect(newSettings.yoloConfThresh, originalSettings.yoloConfThresh);
661
+ });
662
+
663
+ test('can set cameras to empty list', () {
664
+ final newSettings = originalSettings.copyWith(cameras: []);
665
+
666
+ expect(newSettings.cameras.isEmpty, true);
667
+ expect(originalSettings.cameras.length, 1);
668
+ });
669
+
670
+ test('can set plugs to empty list', () {
671
+ final newSettings = originalSettings.copyWith(plugs: []);
672
+
673
+ expect(newSettings.plugs.isEmpty, true);
674
+ expect(originalSettings.plugs.length, 1);
675
+ });
676
+
677
+ test('does not modify original object', () {
678
+ final newSettings = originalSettings.copyWith(
679
+ yoloMinSize: 0.99,
680
+ volume: 10,
681
+ playSound: false,
682
+ );
683
+
684
+ expect(originalSettings.yoloMinSize, 0.02);
685
+ expect(originalSettings.volume, 80);
686
+ expect(originalSettings.playSound, true);
687
+ expect(newSettings.yoloMinSize, 0.99);
688
+ });
689
+ });
690
+
691
+ group('Edge Cases and Special Values', () {
692
+ test('handles zero values', () {
693
+ final settings = Settings(
694
+ yoloMinSize: 0.0,
695
+ yoloConfThresh: 0.0,
696
+ yoloMaxDets: 0,
697
+ yoloModelFilename: '',
698
+ classifierModelFilename: '',
699
+ deterrentSoundFile: '',
700
+ fileStabilizationExtraWait: 0.0,
701
+ playSound: false,
702
+ volume: 0,
703
+ cameras: [],
704
+ plugs: [],
705
+ );
706
+
707
+ expect(settings.yoloMinSize, 0.0);
708
+ expect(settings.yoloConfThresh, 0.0);
709
+ expect(settings.yoloMaxDets, 0);
710
+ expect(settings.fileStabilizationExtraWait, 0.0);
711
+ expect(settings.volume, 0);
712
+ });
713
+
714
+ test('handles maximum values', () {
715
+ final settings = Settings(
716
+ yoloMinSize: 1.0,
717
+ yoloConfThresh: 1.0,
718
+ yoloMaxDets: 1000,
719
+ yoloModelFilename: 'model',
720
+ classifierModelFilename: 'classifier',
721
+ deterrentSoundFile: 'sound',
722
+ fileStabilizationExtraWait: 999.99,
723
+ playSound: true,
724
+ volume: 100,
725
+ cameras: [],
726
+ plugs: [],
727
+ );
728
+
729
+ expect(settings.yoloMinSize, 1.0);
730
+ expect(settings.yoloConfThresh, 1.0);
731
+ expect(settings.yoloMaxDets, 1000);
732
+ expect(settings.fileStabilizationExtraWait, 999.99);
733
+ expect(settings.volume, 100);
734
+ });
735
+
736
+ test('handles negative values', () {
737
+ final settings = Settings(
738
+ yoloMinSize: -0.5,
739
+ yoloConfThresh: -0.25,
740
+ yoloMaxDets: -10,
741
+ yoloModelFilename: '',
742
+ classifierModelFilename: '',
743
+ deterrentSoundFile: '',
744
+ fileStabilizationExtraWait: -1.0,
745
+ playSound: false,
746
+ volume: -10,
747
+ cameras: [],
748
+ plugs: [],
749
+ );
750
+
751
+ expect(settings.yoloMinSize, -0.5);
752
+ expect(settings.yoloConfThresh, -0.25);
753
+ expect(settings.yoloMaxDets, -10);
754
+ expect(settings.fileStabilizationExtraWait, -1.0);
755
+ expect(settings.volume, -10);
756
+ });
757
+
758
+ test('handles very long filenames', () {
759
+ final longFilename = 'a' * 1000;
760
+ final settings = Settings(
761
+ yoloMinSize: 0.01,
762
+ yoloConfThresh: 0.25,
763
+ yoloMaxDets: 10,
764
+ yoloModelFilename: longFilename,
765
+ classifierModelFilename: longFilename,
766
+ deterrentSoundFile: longFilename,
767
+ fileStabilizationExtraWait: 2.0,
768
+ playSound: false,
769
+ volume: 80,
770
+ cameras: [],
771
+ plugs: [],
772
+ );
773
+
774
+ expect(settings.yoloModelFilename.length, 1000);
775
+ expect(settings.classifierModelFilename.length, 1000);
776
+ expect(settings.deterrentSoundFile.length, 1000);
777
+ });
778
+
779
+ test('handles filenames with special characters', () {
780
+ final settings = Settings(
781
+ yoloMinSize: 0.01,
782
+ yoloConfThresh: 0.25,
783
+ yoloMaxDets: 10,
784
+ yoloModelFilename: 'model@#\$%^&*.pt',
785
+ classifierModelFilename: 'classifier!@#.h5',
786
+ deterrentSoundFile: 'sound-file_123.wav',
787
+ fileStabilizationExtraWait: 2.0,
788
+ playSound: false,
789
+ volume: 80,
790
+ cameras: [],
791
+ plugs: [],
792
+ );
793
+
794
+ expect(settings.yoloModelFilename, 'model@#\$%^&*.pt');
795
+ expect(settings.classifierModelFilename, 'classifier!@#.h5');
796
+ expect(settings.deterrentSoundFile, 'sound-file_123.wav');
797
+ });
798
+
799
+ test('handles very precise floating point values', () {
800
+ final settings = Settings(
801
+ yoloMinSize: 0.0123456789,
802
+ yoloConfThresh: 0.9876543210,
803
+ yoloMaxDets: 10,
804
+ yoloModelFilename: '',
805
+ classifierModelFilename: '',
806
+ deterrentSoundFile: '',
807
+ fileStabilizationExtraWait: 1.23456789,
808
+ playSound: false,
809
+ volume: 80,
810
+ cameras: [],
811
+ plugs: [],
812
+ );
813
+
814
+ expect(settings.yoloMinSize, closeTo(0.0123456789, 0.0000000001));
815
+ expect(settings.yoloConfThresh, closeTo(0.9876543210, 0.0000000001));
816
+ expect(
817
+ settings.fileStabilizationExtraWait,
818
+ closeTo(1.23456789, 0.0000000001),
819
+ );
820
+ });
821
+ });
822
+
823
+ group('Real-World Scenarios', () {
824
+ test('creates settings from typical API response', () {
825
+ final json = {
826
+ 'YOLO-min-size': 0.02,
827
+ 'YOLO-conf-thresh': 0.25,
828
+ 'YOLO-max-dets': 10,
829
+ 'YOLO-model-filename': 'yolov8s_101425.pt',
830
+ 'classifier-model-filename': 'colorbw_111325.h5',
831
+ 'deterrent-sound-file': '/path/to/sound.wav',
832
+ 'file-stabilization-extra-wait': 2.0,
833
+ 'play-sound': true,
834
+ 'volume': 80,
835
+ 'cameras': [
836
+ {
837
+ 'hostname': 'Microseven',
838
+ 'ip_address': '192.168.52.101',
839
+ 'mac_address': 'aa:bb:cc:dd:ee:01',
840
+ 'last_seen': '2024-01-15T10:30:00Z',
841
+ 'status': 'connected',
842
+ },
843
+ ],
844
+ 'plugs': [
845
+ {
846
+ 'hostname': 'KasaPlug',
847
+ 'ip_address': '192.168.52.201',
848
+ 'mac_address': 'bb:cc:dd:ee:ff:01',
849
+ 'last_seen': '2024-01-15T10:30:00Z',
850
+ 'status': 'connected',
851
+ },
852
+ ],
853
+ };
854
+
855
+ final settings = Settings.fromJson(json);
856
+
857
+ expect(settings.yoloMinSize, 0.02);
858
+ expect(settings.cameras[0].hostname, 'Microseven');
859
+ expect(settings.cameras[0].isConnected, true);
860
+ expect(settings.plugs[0].hostname, 'KasaPlug');
861
+ expect(settings.plugs[0].isConnected, true);
862
+ });
863
+
864
+ test('updates settings for user configuration change', () {
865
+ final original = Settings(
866
+ yoloMinSize: 0.02,
867
+ yoloConfThresh: 0.25,
868
+ yoloMaxDets: 10,
869
+ yoloModelFilename: 'yolov8s_101425.pt',
870
+ classifierModelFilename: 'colorbw_111325.h5',
871
+ deterrentSoundFile: 'alarm.wav',
872
+ fileStabilizationExtraWait: 2.0,
873
+ playSound: false,
874
+ volume: 80,
875
+ cameras: [],
876
+ plugs: [],
877
+ );
878
+
879
+ // User enables sound and adjusts volume
880
+ final updated = original.copyWith(playSound: true, volume: 90);
881
+
882
+ expect(updated.playSound, true);
883
+ expect(updated.volume, 90);
884
+ expect(updated.yoloMinSize, original.yoloMinSize);
885
+ });
886
+
887
+ test('handles settings with no devices', () {
888
+ final json = {
889
+ 'YOLO-min-size': 0.02,
890
+ 'YOLO-conf-thresh': 0.25,
891
+ 'YOLO-max-dets': 10,
892
+ 'cameras': [],
893
+ 'plugs': [],
894
+ };
895
+
896
+ final settings = Settings.fromJson(json);
897
+
898
+ expect(settings.cameras.isEmpty, true);
899
+ expect(settings.plugs.isEmpty, true);
900
+ });
901
+ });
902
+ });
903
+ }