simo 2.10.9__py3-none-any.whl → 2.11.1__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.

Potentially problematic release.


This version of simo might be problematic. Click here for more details.

Files changed (379) hide show
  1. simo/__pycache__/__init__.cpython-312.pyc +0 -0
  2. simo/__pycache__/asgi.cpython-312.pyc +0 -0
  3. simo/__pycache__/celeryc.cpython-312.pyc +0 -0
  4. simo/__pycache__/conf.cpython-312.pyc +0 -0
  5. simo/__pycache__/settings.cpython-312.pyc +0 -0
  6. simo/__pycache__/urls.cpython-312.pyc +0 -0
  7. simo/automation/__pycache__/__init__.cpython-312.pyc +0 -0
  8. simo/automation/__pycache__/app_widgets.cpython-312.pyc +0 -0
  9. simo/automation/__pycache__/controllers.cpython-312.pyc +0 -0
  10. simo/automation/__pycache__/forms.cpython-312.pyc +0 -0
  11. simo/automation/__pycache__/gateways.cpython-312.pyc +0 -0
  12. simo/automation/__pycache__/helpers.cpython-312.pyc +0 -0
  13. simo/automation/__pycache__/models.cpython-312.pyc +0 -0
  14. simo/automation/__pycache__/serializers.cpython-312.pyc +0 -0
  15. simo/automation/__pycache__/state.cpython-312.pyc +0 -0
  16. simo/automation/migrations/__pycache__/0001_initial.cpython-312.pyc +0 -0
  17. simo/automation/migrations/__pycache__/0002_update_helpers_in_scripts.cpython-312.pyc +0 -0
  18. simo/automation/migrations/__pycache__/__init__.cpython-312.pyc +0 -0
  19. simo/automation/templates/automations/__pycache__/auto_away.cpython-312.pyc +0 -0
  20. simo/automation/templates/automations/__pycache__/auto_state_script.cpython-312.pyc +0 -0
  21. simo/automation/templates/automations/__pycache__/phones_sleep_script.cpython-312.pyc +0 -0
  22. simo/backups/__pycache__/__init__.cpython-312.pyc +0 -0
  23. simo/backups/__pycache__/admin.cpython-312.pyc +0 -0
  24. simo/backups/__pycache__/dynamic_settings.cpython-312.pyc +0 -0
  25. simo/backups/__pycache__/models.cpython-312.pyc +0 -0
  26. simo/backups/__pycache__/tasks.cpython-312.pyc +0 -0
  27. simo/backups/migrations/__pycache__/0001_initial.cpython-312.pyc +0 -0
  28. simo/backups/migrations/__pycache__/0002_backuplog_backup_level_backup_size.cpython-312.pyc +0 -0
  29. simo/backups/migrations/__pycache__/0003_alter_backuplog_options_alter_backup_size.cpython-312.pyc +0 -0
  30. simo/backups/migrations/__pycache__/0004_alter_backup_options_alter_backuplog_options_and_more.cpython-312.pyc +0 -0
  31. simo/backups/migrations/__pycache__/__init__.cpython-312.pyc +0 -0
  32. simo/backups/rescue.img.xz +0 -0
  33. simo/backups/tasks.py +361 -17
  34. simo/core/__pycache__/__init__.cpython-312.pyc +0 -0
  35. simo/core/__pycache__/admin.cpython-312.pyc +0 -0
  36. simo/core/__pycache__/api.cpython-312.pyc +0 -0
  37. simo/core/__pycache__/api_auth.cpython-312.pyc +0 -0
  38. simo/core/__pycache__/api_meta.cpython-312.pyc +0 -0
  39. simo/core/__pycache__/app_widgets.cpython-312.pyc +0 -0
  40. simo/core/__pycache__/apps.cpython-312.pyc +0 -0
  41. simo/core/__pycache__/auto_urls.cpython-312.pyc +0 -0
  42. simo/core/__pycache__/autocomplete_views.cpython-312.pyc +0 -0
  43. simo/core/__pycache__/base_types.cpython-312.pyc +0 -0
  44. simo/core/__pycache__/context.cpython-312.pyc +0 -0
  45. simo/core/__pycache__/controllers.cpython-312.pyc +0 -0
  46. simo/core/__pycache__/dynamic_settings.cpython-312.pyc +0 -0
  47. simo/core/__pycache__/events.cpython-312.pyc +0 -0
  48. simo/core/__pycache__/filters.cpython-312.pyc +0 -0
  49. simo/core/__pycache__/form_fields.cpython-312.pyc +0 -0
  50. simo/core/__pycache__/forms.cpython-312.pyc +0 -0
  51. simo/core/__pycache__/gateways.cpython-312.pyc +0 -0
  52. simo/core/__pycache__/loggers.cpython-312.pyc +0 -0
  53. simo/core/__pycache__/managers.cpython-312.pyc +0 -0
  54. simo/core/__pycache__/middleware.cpython-312.pyc +0 -0
  55. simo/core/__pycache__/models.cpython-312.pyc +0 -0
  56. simo/core/__pycache__/permissions.cpython-312.pyc +0 -0
  57. simo/core/__pycache__/routing.cpython-312.pyc +0 -0
  58. simo/core/__pycache__/serializers.cpython-312.pyc +0 -0
  59. simo/core/__pycache__/signal_receivers.cpython-312.pyc +0 -0
  60. simo/core/__pycache__/socket_consumers.cpython-312.pyc +0 -0
  61. simo/core/__pycache__/storage.cpython-312.pyc +0 -0
  62. simo/core/__pycache__/tasks.cpython-312.pyc +0 -0
  63. simo/core/__pycache__/todos.cpython-312.pyc +0 -0
  64. simo/core/__pycache__/types.cpython-312.pyc +0 -0
  65. simo/core/__pycache__/views.cpython-312.pyc +0 -0
  66. simo/core/__pycache__/widgets.cpython-312.pyc +0 -0
  67. simo/core/controllers.py +6 -3
  68. simo/core/db_backend/__pycache__/__init__.cpython-312.pyc +0 -0
  69. simo/core/db_backend/__pycache__/base.cpython-312.pyc +0 -0
  70. simo/core/drf_braces/__pycache__/__init__.cpython-312.pyc +0 -0
  71. simo/core/drf_braces/__pycache__/mixins.cpython-312.pyc +0 -0
  72. simo/core/drf_braces/__pycache__/models.cpython-312.pyc +0 -0
  73. simo/core/drf_braces/__pycache__/parsers.cpython-312.pyc +0 -0
  74. simo/core/drf_braces/__pycache__/renderers.cpython-312.pyc +0 -0
  75. simo/core/drf_braces/__pycache__/utils.cpython-312.pyc +0 -0
  76. simo/core/drf_braces/fields/__pycache__/__init__.cpython-312.pyc +0 -0
  77. simo/core/drf_braces/fields/__pycache__/_fields.cpython-312.pyc +0 -0
  78. simo/core/drf_braces/fields/__pycache__/custom.cpython-312.pyc +0 -0
  79. simo/core/drf_braces/fields/__pycache__/mixins.cpython-312.pyc +0 -0
  80. simo/core/drf_braces/fields/__pycache__/modified.cpython-312.pyc +0 -0
  81. simo/core/drf_braces/forms/__pycache__/__init__.cpython-312.pyc +0 -0
  82. simo/core/drf_braces/forms/__pycache__/fields.cpython-312.pyc +0 -0
  83. simo/core/drf_braces/forms/__pycache__/serializer_form.cpython-312.pyc +0 -0
  84. simo/core/drf_braces/serializers/__pycache__/__init__.cpython-312.pyc +0 -0
  85. simo/core/drf_braces/serializers/__pycache__/enforce_validation_serializer.cpython-312.pyc +0 -0
  86. simo/core/drf_braces/serializers/__pycache__/form_serializer.cpython-312.pyc +0 -0
  87. simo/core/drf_braces/serializers/__pycache__/swapping.cpython-312.pyc +0 -0
  88. simo/core/drf_braces/tests/__pycache__/__init__.cpython-312.pyc +0 -0
  89. simo/core/drf_braces/tests/__pycache__/test_mixins.cpython-312.pyc +0 -0
  90. simo/core/drf_braces/tests/__pycache__/test_parsers.cpython-312.pyc +0 -0
  91. simo/core/drf_braces/tests/__pycache__/test_renderers.cpython-312.pyc +0 -0
  92. simo/core/drf_braces/tests/__pycache__/test_utils.cpython-312.pyc +0 -0
  93. simo/core/drf_braces/tests/fields/__pycache__/__init__.cpython-312.pyc +0 -0
  94. simo/core/drf_braces/tests/fields/__pycache__/test_custom.cpython-312.pyc +0 -0
  95. simo/core/drf_braces/tests/fields/__pycache__/test_fields.cpython-312.pyc +0 -0
  96. simo/core/drf_braces/tests/fields/__pycache__/test_mixins.cpython-312.pyc +0 -0
  97. simo/core/drf_braces/tests/fields/__pycache__/test_modified.cpython-312.pyc +0 -0
  98. simo/core/drf_braces/tests/forms/__pycache__/__init__.cpython-312.pyc +0 -0
  99. simo/core/drf_braces/tests/forms/__pycache__/test_fields.cpython-312.pyc +0 -0
  100. simo/core/drf_braces/tests/forms/__pycache__/test_serializer_form.cpython-312.pyc +0 -0
  101. simo/core/drf_braces/tests/serializers/__pycache__/__init__.cpython-312.pyc +0 -0
  102. simo/core/drf_braces/tests/serializers/__pycache__/test_enforce_validation_serializer.cpython-312.pyc +0 -0
  103. simo/core/drf_braces/tests/serializers/__pycache__/test_form_serializer.cpython-312.pyc +0 -0
  104. simo/core/drf_braces/tests/serializers/__pycache__/test_swapping.cpython-312.pyc +0 -0
  105. simo/core/management/__pycache__/__init__.cpython-312.pyc +0 -0
  106. simo/core/management/__pycache__/update.cpython-312.pyc +0 -0
  107. simo/core/management/_hub_template/hub/__pycache__/asgi.cpython-312.pyc +0 -0
  108. simo/core/management/_hub_template/hub/__pycache__/celeryc.cpython-312.pyc +0 -0
  109. simo/core/management/_hub_template/hub/__pycache__/manage.cpython-312.pyc +0 -0
  110. simo/core/management/_hub_template/hub/__pycache__/settings.cpython-312.pyc +0 -0
  111. simo/core/management/_hub_template/hub/__pycache__/urls.cpython-312.pyc +0 -0
  112. simo/core/management/_hub_template/hub/__pycache__/wsgi.cpython-312.pyc +0 -0
  113. simo/core/management/commands/__pycache__/__init__.cpython-312.pyc +0 -0
  114. simo/core/management/commands/__pycache__/gateways_manager.cpython-312.pyc +0 -0
  115. simo/core/management/commands/__pycache__/on_http_start.cpython-312.pyc +0 -0
  116. simo/core/management/commands/__pycache__/run_gateway.cpython-312.pyc +0 -0
  117. simo/core/migrations/__pycache__/0001_initial.cpython-312.pyc +0 -0
  118. simo/core/migrations/__pycache__/0002_load_icons.cpython-312.pyc +0 -0
  119. simo/core/migrations/__pycache__/0003_create_default_zones_and_categories.cpython-312.pyc +0 -0
  120. simo/core/migrations/__pycache__/0004_create_generic.cpython-312.pyc +0 -0
  121. simo/core/migrations/__pycache__/0005_component_subcomponents.cpython-312.pyc +0 -0
  122. simo/core/migrations/__pycache__/0006_alter_component_subcomponents.cpython-312.pyc +0 -0
  123. simo/core/migrations/__pycache__/0007_component_change_init_to.cpython-312.pyc +0 -0
  124. simo/core/migrations/__pycache__/0008_alter_component_change_init_to.cpython-312.pyc +0 -0
  125. simo/core/migrations/__pycache__/0009_auto_20220707_1404.cpython-312.pyc +0 -0
  126. simo/core/migrations/__pycache__/0010_historyaggregate.cpython-312.pyc +0 -0
  127. simo/core/migrations/__pycache__/0011_component_last_change.cpython-312.pyc +0 -0
  128. simo/core/migrations/__pycache__/0012_instance.cpython-312.pyc +0 -0
  129. simo/core/migrations/__pycache__/0013_auto_20231003_0754.cpython-312.pyc +0 -0
  130. simo/core/migrations/__pycache__/0014_zone_instance.cpython-312.pyc +0 -0
  131. simo/core/migrations/__pycache__/0015_auto_20231004_1113.cpython-312.pyc +0 -0
  132. simo/core/migrations/__pycache__/0016_auto_20231004_1113.cpython-312.pyc +0 -0
  133. simo/core/migrations/__pycache__/0017_auto_20231004_1313.cpython-312.pyc +0 -0
  134. simo/core/migrations/__pycache__/0018_auto_20231005_0622.cpython-312.pyc +0 -0
  135. simo/core/migrations/__pycache__/0019_alter_gateway_type.cpython-312.pyc +0 -0
  136. simo/core/migrations/__pycache__/0020_component_meta.cpython-312.pyc +0 -0
  137. simo/core/migrations/__pycache__/0021_auto_20231020_1041.cpython-312.pyc +0 -0
  138. simo/core/migrations/__pycache__/0022_auto_20231221_0735.cpython-312.pyc +0 -0
  139. simo/core/migrations/__pycache__/0023_auto_20231229_1352.cpython-312.pyc +0 -0
  140. simo/core/migrations/__pycache__/0024_alter_instance_device_report_history_days.cpython-312.pyc +0 -0
  141. simo/core/migrations/__pycache__/0025_auto_20240122_1321.cpython-312.pyc +0 -0
  142. simo/core/migrations/__pycache__/0026_category_instance.cpython-312.pyc +0 -0
  143. simo/core/migrations/__pycache__/0027_remove_component_tags.cpython-312.pyc +0 -0
  144. simo/core/migrations/__pycache__/0028_rename_subcomponents_component_slaves.cpython-312.pyc +0 -0
  145. simo/core/migrations/__pycache__/0029_auto_20240229_1331.cpython-312.pyc +0 -0
  146. simo/core/migrations/__pycache__/0030_alter_instance_timezone.cpython-312.pyc +0 -0
  147. simo/core/migrations/__pycache__/0031_auto_20240429_1231.cpython-312.pyc +0 -0
  148. simo/core/migrations/__pycache__/0032_auto_20240506_0834.cpython-312.pyc +0 -0
  149. simo/core/migrations/__pycache__/0033_auto_20240509_0821.cpython-312.pyc +0 -0
  150. simo/core/migrations/__pycache__/0034_component_error_msg.cpython-312.pyc +0 -0
  151. simo/core/migrations/__pycache__/0035_remove_instance_share_location.cpython-312.pyc +0 -0
  152. simo/core/migrations/__pycache__/0036_auto_20240521_0823.cpython-312.pyc +0 -0
  153. simo/core/migrations/__pycache__/0037_auto_20240606_1057.cpython-312.pyc +0 -0
  154. simo/core/migrations/__pycache__/0038_remove_instance_cover_image_and_more.cpython-312.pyc +0 -0
  155. simo/core/migrations/__pycache__/0039_instance_is_active_alter_instance_timezone.cpython-312.pyc +0 -0
  156. simo/core/migrations/__pycache__/0040_alter_instance_name.cpython-312.pyc +0 -0
  157. simo/core/migrations/__pycache__/0041_alter_instance_slug.cpython-312.pyc +0 -0
  158. simo/core/migrations/__pycache__/0042_alter_instance_timezone.cpython-312.pyc +0 -0
  159. simo/core/migrations/__pycache__/0043_alter_category_instance_alter_instance_timezone_and_more.cpython-312.pyc +0 -0
  160. simo/core/migrations/__pycache__/0044_alter_gateway_type.cpython-312.pyc +0 -0
  161. simo/core/migrations/__pycache__/0045_alter_instance_device_report_history_days_and_more.cpython-312.pyc +0 -0
  162. simo/core/migrations/__pycache__/0046_component_value_translation_alter_gateway_type.cpython-312.pyc +0 -0
  163. simo/core/migrations/__pycache__/0047_alter_component_value_translation.cpython-312.pyc +0 -0
  164. simo/core/migrations/__pycache__/0048_publicfile_privatefile.cpython-312.pyc +0 -0
  165. simo/core/migrations/__pycache__/0049_alter_gateway_type.cpython-312.pyc +0 -0
  166. simo/core/migrations/__pycache__/0050_componenthistory_alive.cpython-312.pyc +0 -0
  167. simo/core/migrations/__pycache__/__init__.cpython-312.pyc +0 -0
  168. simo/core/templates/core/__pycache__/value_translation.cpython-312.pyc +0 -0
  169. simo/core/templatetags/__pycache__/__init__.cpython-312.pyc +0 -0
  170. simo/core/templatetags/__pycache__/components_list.cpython-312.pyc +0 -0
  171. simo/core/utils/__pycache__/__init__.cpython-312.pyc +0 -0
  172. simo/core/utils/__pycache__/admin.cpython-312.pyc +0 -0
  173. simo/core/utils/__pycache__/api.cpython-312.pyc +0 -0
  174. simo/core/utils/__pycache__/cache.cpython-312.pyc +0 -0
  175. simo/core/utils/__pycache__/config_values.cpython-312.pyc +0 -0
  176. simo/core/utils/__pycache__/converters.cpython-312.pyc +0 -0
  177. simo/core/utils/__pycache__/easing.cpython-312.pyc +0 -0
  178. simo/core/utils/__pycache__/form_fields.cpython-312.pyc +0 -0
  179. simo/core/utils/__pycache__/form_widgets.cpython-312.pyc +0 -0
  180. simo/core/utils/__pycache__/formsets.cpython-312.pyc +0 -0
  181. simo/core/utils/__pycache__/helpers.cpython-312.pyc +0 -0
  182. simo/core/utils/__pycache__/json.cpython-312.pyc +0 -0
  183. simo/core/utils/__pycache__/logs.cpython-312.pyc +0 -0
  184. simo/core/utils/__pycache__/mixins.cpython-312.pyc +0 -0
  185. simo/core/utils/__pycache__/model_helpers.cpython-312.pyc +0 -0
  186. simo/core/utils/__pycache__/operations.cpython-312.pyc +0 -0
  187. simo/core/utils/__pycache__/relay.cpython-312.pyc +0 -0
  188. simo/core/utils/__pycache__/serialization.cpython-312.pyc +0 -0
  189. simo/core/utils/__pycache__/type_constants.cpython-312.pyc +0 -0
  190. simo/core/utils/__pycache__/validators.cpython-312.pyc +0 -0
  191. simo/fleet/__pycache__/__init__.cpython-312.pyc +0 -0
  192. simo/fleet/__pycache__/admin.cpython-312.pyc +0 -0
  193. simo/fleet/__pycache__/api.cpython-312.pyc +0 -0
  194. simo/fleet/__pycache__/apps.cpython-312.pyc +0 -0
  195. simo/fleet/__pycache__/auto_urls.cpython-312.pyc +0 -0
  196. simo/fleet/__pycache__/base_types.cpython-312.pyc +0 -0
  197. simo/fleet/__pycache__/ble.cpython-312.pyc +0 -0
  198. simo/fleet/__pycache__/controllers.cpython-312.pyc +0 -0
  199. simo/fleet/__pycache__/custom_dali_operations.cpython-312.pyc +0 -0
  200. simo/fleet/__pycache__/forms.cpython-312.pyc +0 -0
  201. simo/fleet/__pycache__/gateways.cpython-312.pyc +0 -0
  202. simo/fleet/__pycache__/managers.cpython-312.pyc +0 -0
  203. simo/fleet/__pycache__/models.cpython-312.pyc +0 -0
  204. simo/fleet/__pycache__/routing.cpython-312.pyc +0 -0
  205. simo/fleet/__pycache__/serializers.cpython-312.pyc +0 -0
  206. simo/fleet/__pycache__/socket_consumers.cpython-312.pyc +0 -0
  207. simo/fleet/__pycache__/tasks.cpython-312.pyc +0 -0
  208. simo/fleet/__pycache__/utils.cpython-312.pyc +0 -0
  209. simo/fleet/__pycache__/views.cpython-312.pyc +0 -0
  210. simo/fleet/controllers.py +102 -47
  211. simo/fleet/custom_dali_operations.py +14 -2
  212. simo/fleet/forms.py +112 -101
  213. simo/fleet/migrations/0054_auto_20250507_1256.py +36 -0
  214. simo/fleet/migrations/__pycache__/0001_initial.cpython-312.pyc +0 -0
  215. simo/fleet/migrations/__pycache__/0002_auto_20220422_0743.cpython-312.pyc +0 -0
  216. simo/fleet/migrations/__pycache__/0003_auto_20220422_0752.cpython-312.pyc +0 -0
  217. simo/fleet/migrations/__pycache__/0004_auto_20220422_0818.cpython-312.pyc +0 -0
  218. simo/fleet/migrations/__pycache__/0005_auto_20220428_0900.cpython-312.pyc +0 -0
  219. simo/fleet/migrations/__pycache__/0006_rename_mac_colonel_uid.cpython-312.pyc +0 -0
  220. simo/fleet/migrations/__pycache__/0007_colonel_socket_connected.cpython-312.pyc +0 -0
  221. simo/fleet/migrations/__pycache__/0008_i2cinterface.cpython-312.pyc +0 -0
  222. simo/fleet/migrations/__pycache__/0009_i2cinterface_name.cpython-312.pyc +0 -0
  223. simo/fleet/migrations/__pycache__/0010_auto_20220602_0746.cpython-312.pyc +0 -0
  224. simo/fleet/migrations/__pycache__/0011_i2cinterface_freq.cpython-312.pyc +0 -0
  225. simo/fleet/migrations/__pycache__/0012_colonel_logs_stream.cpython-312.pyc +0 -0
  226. simo/fleet/migrations/__pycache__/0013_alter_colonel_last_seen.cpython-312.pyc +0 -0
  227. simo/fleet/migrations/__pycache__/0014_auto_20220614_0659.cpython-312.pyc +0 -0
  228. simo/fleet/migrations/__pycache__/0015_auto_20220614_0754.cpython-312.pyc +0 -0
  229. simo/fleet/migrations/__pycache__/0016_auto_20220704_0840.cpython-312.pyc +0 -0
  230. simo/fleet/migrations/__pycache__/0017_alter_colonel_secret.cpython-312.pyc +0 -0
  231. simo/fleet/migrations/__pycache__/0018_colonel_instance.cpython-312.pyc +0 -0
  232. simo/fleet/migrations/__pycache__/0019_auto_20231006_0749.cpython-312.pyc +0 -0
  233. simo/fleet/migrations/__pycache__/0020_instanceoptions.cpython-312.pyc +0 -0
  234. simo/fleet/migrations/__pycache__/0021_auto_20231006_0819.cpython-312.pyc +0 -0
  235. simo/fleet/migrations/__pycache__/0022_remove_colonel_secret.cpython-312.pyc +0 -0
  236. simo/fleet/migrations/__pycache__/0023_colonel_is_authorized.cpython-312.pyc +0 -0
  237. simo/fleet/migrations/__pycache__/0024_colonel_pwm_frequency.cpython-312.pyc +0 -0
  238. simo/fleet/migrations/__pycache__/0025_auto_20240130_1334.cpython-312.pyc +0 -0
  239. simo/fleet/migrations/__pycache__/0026_rename_i2cinterface_scl_pin_and_more.cpython-312.pyc +0 -0
  240. simo/fleet/migrations/__pycache__/0027_auto_20240306_0802.cpython-312.pyc +0 -0
  241. simo/fleet/migrations/__pycache__/0028_remove_i2cinterface_scl_pin_no_and_more.cpython-312.pyc +0 -0
  242. simo/fleet/migrations/__pycache__/0029_alter_i2cinterface_scl_pin_and_more.cpython-312.pyc +0 -0
  243. simo/fleet/migrations/__pycache__/0030_colonelpin_label_alter_colonel_type.cpython-312.pyc +0 -0
  244. simo/fleet/migrations/__pycache__/0031_alter_colonel_type.cpython-312.pyc +0 -0
  245. simo/fleet/migrations/__pycache__/0032_auto_20240415_0736.cpython-312.pyc +0 -0
  246. simo/fleet/migrations/__pycache__/0033_auto_20240415_0736.cpython-312.pyc +0 -0
  247. simo/fleet/migrations/__pycache__/0034_auto_20240418_0735.cpython-312.pyc +0 -0
  248. simo/fleet/migrations/__pycache__/0035_auto_20240514_0855.cpython-312.pyc +0 -0
  249. simo/fleet/migrations/__pycache__/0036_auto_20240605_0702.cpython-312.pyc +0 -0
  250. simo/fleet/migrations/__pycache__/0037_alter_colonelpin_options_alter_colonelpin_no_and_more.cpython-312.pyc +0 -0
  251. simo/fleet/migrations/__pycache__/0038_alter_colonel_type.cpython-312.pyc +0 -0
  252. simo/fleet/migrations/__pycache__/0039_auto_20241016_1047.cpython-312.pyc +0 -0
  253. simo/fleet/migrations/__pycache__/0040_alter_colonel_pwm_frequency.cpython-312.pyc +0 -0
  254. simo/fleet/migrations/__pycache__/0041_alter_colonel_instance_and_more.cpython-312.pyc +0 -0
  255. simo/fleet/migrations/__pycache__/0042_auto_20241120_1028.cpython-312.pyc +0 -0
  256. simo/fleet/migrations/__pycache__/0043_auto_20241203_0930.cpython-312.pyc +0 -0
  257. simo/fleet/migrations/__pycache__/0044_auto_20241210_0707.cpython-312.pyc +0 -0
  258. simo/fleet/migrations/__pycache__/0045_alter_colonel_type_customdalidevice.cpython-312.pyc +0 -0
  259. simo/fleet/migrations/__pycache__/0046_delete_customdalidevice.cpython-312.pyc +0 -0
  260. simo/fleet/migrations/__pycache__/0047_customdalidevice.cpython-312.pyc +0 -0
  261. simo/fleet/migrations/__pycache__/0048_remove_customdalidevice_colonel_and_more.cpython-312.pyc +0 -0
  262. simo/fleet/migrations/__pycache__/0049_alter_customdalidevice_interface.cpython-312.pyc +0 -0
  263. simo/fleet/migrations/__pycache__/0050_customdalidevice_uid.cpython-312.pyc +0 -0
  264. simo/fleet/migrations/__pycache__/0051_customdalidevice_components.cpython-312.pyc +0 -0
  265. simo/fleet/migrations/__pycache__/0052_colonelpin_interface.cpython-312.pyc +0 -0
  266. simo/fleet/migrations/__pycache__/0053_auto_20250507_0713.cpython-312.pyc +0 -0
  267. simo/fleet/migrations/__pycache__/0054_auto_20250507_1256.cpython-312.pyc +0 -0
  268. simo/fleet/migrations/__pycache__/__init__.cpython-312.pyc +0 -0
  269. simo/fleet/models.py +1 -1
  270. simo/generic/__pycache__/__init__.cpython-312.pyc +0 -0
  271. simo/generic/__pycache__/app_widgets.cpython-312.pyc +0 -0
  272. simo/generic/__pycache__/base_types.cpython-312.pyc +0 -0
  273. simo/generic/__pycache__/controllers.cpython-312.pyc +0 -0
  274. simo/generic/__pycache__/forms.cpython-312.pyc +0 -0
  275. simo/generic/__pycache__/gateways.cpython-312.pyc +0 -0
  276. simo/generic/__pycache__/models.cpython-312.pyc +0 -0
  277. simo/generic/__pycache__/routing.cpython-312.pyc +0 -0
  278. simo/generic/__pycache__/socket_consumers.cpython-312.pyc +0 -0
  279. simo/generic/__pycache__/tasks.cpython-312.pyc +0 -0
  280. simo/generic/migrations/__pycache__/0001_initial.cpython-312.pyc +0 -0
  281. simo/generic/migrations/__pycache__/0002_auto_20241126_0726.cpython-312.pyc +0 -0
  282. simo/generic/migrations/__pycache__/0003_auto_20250409_1404.cpython-312.pyc +0 -0
  283. simo/generic/migrations/__pycache__/__init__.cpython-312.pyc +0 -0
  284. simo/multimedia/__pycache__/__init__.cpython-312.pyc +0 -0
  285. simo/multimedia/__pycache__/admin.cpython-312.pyc +0 -0
  286. simo/multimedia/__pycache__/api.cpython-312.pyc +0 -0
  287. simo/multimedia/__pycache__/app_widgets.cpython-312.pyc +0 -0
  288. simo/multimedia/__pycache__/auto_urls.cpython-312.pyc +0 -0
  289. simo/multimedia/__pycache__/base_types.cpython-312.pyc +0 -0
  290. simo/multimedia/__pycache__/controllers.cpython-312.pyc +0 -0
  291. simo/multimedia/__pycache__/forms.cpython-312.pyc +0 -0
  292. simo/multimedia/__pycache__/models.cpython-312.pyc +0 -0
  293. simo/multimedia/__pycache__/serializers.cpython-312.pyc +0 -0
  294. simo/multimedia/__pycache__/views.cpython-312.pyc +0 -0
  295. simo/multimedia/migrations/__pycache__/0001_initial.cpython-312.pyc +0 -0
  296. simo/multimedia/migrations/__pycache__/0002_sound_length.cpython-312.pyc +0 -0
  297. simo/multimedia/migrations/__pycache__/0003_alter_sound_length.cpython-312.pyc +0 -0
  298. simo/multimedia/migrations/__pycache__/0004_auto_20231023_1055.cpython-312.pyc +0 -0
  299. simo/multimedia/migrations/__pycache__/0005_remove_sound_slug_sound_date_uploaded.cpython-312.pyc +0 -0
  300. simo/multimedia/migrations/__pycache__/0006_remove_sound_length_sound_duration.cpython-312.pyc +0 -0
  301. simo/multimedia/migrations/__pycache__/__init__.cpython-312.pyc +0 -0
  302. simo/notifications/__pycache__/__init__.cpython-312.pyc +0 -0
  303. simo/notifications/__pycache__/admin.cpython-312.pyc +0 -0
  304. simo/notifications/__pycache__/api.cpython-312.pyc +0 -0
  305. simo/notifications/__pycache__/models.cpython-312.pyc +0 -0
  306. simo/notifications/__pycache__/serializers.cpython-312.pyc +0 -0
  307. simo/notifications/__pycache__/utils.cpython-312.pyc +0 -0
  308. simo/notifications/migrations/__pycache__/0001_initial.cpython-312.pyc +0 -0
  309. simo/notifications/migrations/__pycache__/0002_notification_instance.cpython-312.pyc +0 -0
  310. simo/notifications/migrations/__pycache__/0003_alter_notification_instance.cpython-312.pyc +0 -0
  311. simo/notifications/migrations/__pycache__/__init__.cpython-312.pyc +0 -0
  312. simo/users/__pycache__/__init__.cpython-312.pyc +0 -0
  313. simo/users/__pycache__/admin.cpython-312.pyc +0 -0
  314. simo/users/__pycache__/api.cpython-312.pyc +0 -0
  315. simo/users/__pycache__/apps.cpython-312.pyc +0 -0
  316. simo/users/__pycache__/auth_backends.cpython-312.pyc +0 -0
  317. simo/users/__pycache__/auto_urls.cpython-312.pyc +0 -0
  318. simo/users/__pycache__/dynamic_settings.cpython-312.pyc +0 -0
  319. simo/users/__pycache__/managers.cpython-312.pyc +0 -0
  320. simo/users/__pycache__/middleware.cpython-312.pyc +0 -0
  321. simo/users/__pycache__/models.cpython-312.pyc +0 -0
  322. simo/users/__pycache__/permissions.cpython-312.pyc +0 -0
  323. simo/users/__pycache__/serializers.cpython-312.pyc +0 -0
  324. simo/users/__pycache__/sso_urls.cpython-312.pyc +0 -0
  325. simo/users/__pycache__/sso_views.cpython-312.pyc +0 -0
  326. simo/users/__pycache__/tasks.cpython-312.pyc +0 -0
  327. simo/users/__pycache__/utils.cpython-312.pyc +0 -0
  328. simo/users/__pycache__/views.cpython-312.pyc +0 -0
  329. simo/users/migrations/__pycache__/0001_initial.cpython-312.pyc +0 -0
  330. simo/users/migrations/__pycache__/0002_componentpermission.cpython-312.pyc +0 -0
  331. simo/users/migrations/__pycache__/0003_create_roles_and_system_user.cpython-312.pyc +0 -0
  332. simo/users/migrations/__pycache__/0004_user_secret_key.cpython-312.pyc +0 -0
  333. simo/users/migrations/__pycache__/0005_permissionsrole_instance.cpython-312.pyc +0 -0
  334. simo/users/migrations/__pycache__/0006_auto_20231003_0850.cpython-312.pyc +0 -0
  335. simo/users/migrations/__pycache__/0007_auto_20231003_1228.cpython-312.pyc +0 -0
  336. simo/users/migrations/__pycache__/0008_auto_20231003_1229.cpython-312.pyc +0 -0
  337. simo/users/migrations/__pycache__/0009_remove_user_role.cpython-312.pyc +0 -0
  338. simo/users/migrations/__pycache__/0010_auto_20231004_1313.cpython-312.pyc +0 -0
  339. simo/users/migrations/__pycache__/0011_auto_20231004_1313.cpython-312.pyc +0 -0
  340. simo/users/migrations/__pycache__/0012_alter_userinstancerole_unique_together.cpython-312.pyc +0 -0
  341. simo/users/migrations/__pycache__/0013_remove_user_roles.cpython-312.pyc +0 -0
  342. simo/users/migrations/__pycache__/0014_user_roles.cpython-312.pyc +0 -0
  343. simo/users/migrations/__pycache__/0015_remove_user_at_home.cpython-312.pyc +0 -0
  344. simo/users/migrations/__pycache__/0016_auto_20231005_1050.cpython-312.pyc +0 -0
  345. simo/users/migrations/__pycache__/0017_auto_20231221_0735.cpython-312.pyc +0 -0
  346. simo/users/migrations/__pycache__/0018_user_is_god.cpython-312.pyc +0 -0
  347. simo/users/migrations/__pycache__/0019_auto_20231221_1155.cpython-312.pyc +0 -0
  348. simo/users/migrations/__pycache__/0020_rename_is_god_user_is_master.cpython-312.pyc +0 -0
  349. simo/users/migrations/__pycache__/0021_alter_permissionsrole_instance.cpython-312.pyc +0 -0
  350. simo/users/migrations/__pycache__/0022_userdevicereportlog_instance.cpython-312.pyc +0 -0
  351. simo/users/migrations/__pycache__/0023_auto_20240105_0719.cpython-312.pyc +0 -0
  352. simo/users/migrations/__pycache__/0024_fingerprint.cpython-312.pyc +0 -0
  353. simo/users/migrations/__pycache__/0025_rename_name_fingerprint_type_and_more.cpython-312.pyc +0 -0
  354. simo/users/migrations/__pycache__/0026_fingerprint_name.cpython-312.pyc +0 -0
  355. simo/users/migrations/__pycache__/0027_permissionsrole_can_manage_components.cpython-312.pyc +0 -0
  356. simo/users/migrations/__pycache__/0028_auto_20240506_1146.cpython-312.pyc +0 -0
  357. simo/users/migrations/__pycache__/0029_alter_instanceuser_instance.cpython-312.pyc +0 -0
  358. simo/users/migrations/__pycache__/0030_userdevice_users.cpython-312.pyc +0 -0
  359. simo/users/migrations/__pycache__/0031_auto_20240923_1115.cpython-312.pyc +0 -0
  360. simo/users/migrations/__pycache__/0032_remove_userdevice_user_alter_userdevice_users.cpython-312.pyc +0 -0
  361. simo/users/migrations/__pycache__/0033_alter_user_ssh_key.cpython-312.pyc +0 -0
  362. simo/users/migrations/__pycache__/0034_instanceuser_last_seen_location_and_more.cpython-312.pyc +0 -0
  363. simo/users/migrations/__pycache__/0035_instanceuser_last_seen_speed_kmh_and_more.cpython-312.pyc +0 -0
  364. simo/users/migrations/__pycache__/0036_instanceuser_phone_on_charge_user_phone_on_charge.cpython-312.pyc +0 -0
  365. simo/users/migrations/__pycache__/0037_rename_last_seen_location_datetime_instanceuser_last_seen_and_more.cpython-312.pyc +0 -0
  366. simo/users/migrations/__pycache__/0038_userdevicereportlog_at_home_and_more.cpython-312.pyc +0 -0
  367. simo/users/migrations/__pycache__/0039_auto_20241117_1039.cpython-312.pyc +0 -0
  368. simo/users/migrations/__pycache__/0040_userdevicereportlog_location_smoothed_and_more.cpython-312.pyc +0 -0
  369. simo/users/migrations/__pycache__/0041_userdevicereportlog_speed_kmh_received.cpython-312.pyc +0 -0
  370. simo/users/migrations/__pycache__/0042_remove_userdevicereportlog_location_smoothed_and_more.cpython-312.pyc +0 -0
  371. simo/users/migrations/__pycache__/0043_userdevicereportlog_avg_speed_kmh.cpython-312.pyc +0 -0
  372. simo/users/migrations/__pycache__/0044_permissionsrole_is_person.cpython-312.pyc +0 -0
  373. simo/users/migrations/__pycache__/__init__.cpython-312.pyc +0 -0
  374. {simo-2.10.9.dist-info → simo-2.11.1.dist-info}/METADATA +1 -1
  375. {simo-2.10.9.dist-info → simo-2.11.1.dist-info}/RECORD +379 -376
  376. {simo-2.10.9.dist-info → simo-2.11.1.dist-info}/WHEEL +1 -1
  377. {simo-2.10.9.dist-info → simo-2.11.1.dist-info}/entry_points.txt +0 -0
  378. {simo-2.10.9.dist-info → simo-2.11.1.dist-info}/licenses/LICENSE.md +0 -0
  379. {simo-2.10.9.dist-info → simo-2.11.1.dist-info}/top_level.txt +0 -0
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
simo/backups/tasks.py CHANGED
@@ -140,26 +140,311 @@ def create_snap(lv_group, lv_name, snap_name=None, size=None, try_no=1):
140
140
 
141
141
 
142
142
  def get_lvm_partition(lsblk_data):
143
+ """Return the *lsblk* entry describing the logical volume mounted as "/".
144
+
145
+ The original implementation returned prematurely when the first top-level
146
+ device contained any children – even if none of them matched the search
147
+ criteria. As a result the search stopped after inspecting just a single
148
+ branch of the device tree which broke setups where the root logical
149
+ volume was not located under the very first block device listed by
150
+ *lsblk* (e.g. when the machine had multiple drives).
151
+
152
+ The fixed version walks the whole tree depth-first and stops only after a
153
+ matching entry is found or the entire structure has been inspected.
154
+ """
155
+
143
156
  for device in lsblk_data:
144
- if device['type'] == 'lvm' and device['mountpoint'] == '/':
157
+ # Check the current node first.
158
+ if device.get('type') == 'lvm' and device.get('mountpoint') == '/':
145
159
  return device
146
- if 'children' in device:
147
- return get_lvm_partition(device['children'])
160
+
161
+ # Recursively search children (if any). The recursive call returns
162
+ # either the desired dictionary or *None* – propagate the first truthy
163
+ # value up the call stack so that the outermost caller gets the
164
+ # matching entry.
165
+ child_match = get_lvm_partition(device.get('children', [])) if device.get('children') else None
166
+ if child_match:
167
+ return child_match
168
+
169
+ # Nothing found on this branch.
170
+ return None
171
+
172
+
173
+ def _has_backup_label(dev: dict) -> bool:
174
+ """Return ``True`` when the given *lsblk* device description represents
175
+ the desired "backup" partition. The logic is kept in one place to make
176
+ future adjustments simpler.
177
+
178
+ The criteria as of now are:
179
+
180
+ The filesystem label (``label`` field) is exactly ``BACKUP`` – this is
181
+ how the pre-built *rescue.img* image names the 3rd partition that
182
+ will be used for storing backups.
183
+ """
184
+
185
+ label = (dev.get("label") or dev.get("partlabel") or "").upper()
186
+ if label == "BACKUP":
187
+ return True
188
+ return False
148
189
 
149
190
 
150
191
  def get_backup_device(lsblk_data):
192
+ """Locate a removable partition that should be used to store backups.
193
+
194
+ Priority is given to a partition explicitly labelled ``BACKUP``. If such
195
+ a partition isn't found, the legacy rule – ‘any removable exFAT
196
+ partition’ – is used.
197
+ """
198
+
151
199
  for device in lsblk_data:
152
- if not device['hotplug']:
200
+ if not device.get("hotplug"):
153
201
  continue
154
- target_device = None
155
- if device.get('fstype') == 'exfat':
156
- target_device = device
157
- elif device.get('children'):
158
- for child in device.get('children'):
159
- if child.get('fstype') == 'exfat':
160
- target_device = child
161
- if target_device:
162
- return target_device
202
+
203
+ # Prefer partitions explicitly labelled "BACKUP".
204
+ for child in device.get("children", []):
205
+ if _has_backup_label(child):
206
+ return child
207
+
208
+ # Legacy fallback – whole-disk filesystems or partitions formatted as
209
+ # exFAT are still recognised in order to stay compatible with drives
210
+ # prepared by older software versions.
211
+ # NOTE: We intentionally keep this logic after the new BACKUP label
212
+ # check so that freshly provisioned media (ext4+label) wins.
213
+
214
+ # 1. Whole-disk (no partition table) exFAT volume.
215
+ if (device.get("fstype") or "").lower() == "exfat":
216
+ return device
217
+
218
+ # 2. Partitioned removable drive – look for any exFAT child.
219
+ for child in device.get("children", []):
220
+ if (child.get("fstype") or "").lower() == "exfat":
221
+ return child
222
+
223
+ # Nothing has been found.
224
+ return None
225
+
226
+
227
+ def _find_blank_removable_device(lsblk_data):
228
+ """Return the first removable block *device* that looks empty.
229
+
230
+ A device is considered *blank* when one of the following conditions is
231
+ met:
232
+
233
+ 1. It has no children (partitions) **and** no recognised filesystem – the
234
+ original behaviour that covers brand-new, uninitialised drives.
235
+ 2. It has no children (partitions) **and** an existing filesystem that is
236
+ effectively empty (e.g. a freshly formatted card).
237
+
238
+ Determining if a filesystem is *empty* is tricky without mounting it, but
239
+ for the purpose of automatically provisioning backup media we can use a
240
+ pragmatic heuristic: if the device is not mounted we temporarily mount it
241
+ read-only to a throw-away directory, inspect its contents and then unmount
242
+ it again. If it **is** already mounted we reuse the existing
243
+ mount-point. In both cases we treat the device as blank when the root of
244
+ the filesystem contains no entries other than implementation-specific
245
+ placeholders like the *lost+found* directory created by *mkfs.ext4*.
246
+
247
+ This relaxed definition allows the backup subsystem to reuse drives that
248
+ have been pre-formatted by the user but never actually used to store any
249
+ files.
250
+ """
251
+
252
+ # --- Helper inner functions ------------------------------------------------
253
+
254
+ def _device_size_bytes(dev_name: str):
255
+ """Return size of *dev_name* in bytes (or ``None`` on failure)."""
256
+
257
+ for cmd in (
258
+ f"blockdev --getsize64 /dev/{dev_name}",
259
+ f"lsblk -b -dn -o SIZE /dev/{dev_name}",
260
+ ):
261
+ try:
262
+ out = subprocess.check_output(
263
+ cmd, shell=True, stderr=subprocess.DEVNULL
264
+ ).strip()
265
+ return int(out)
266
+ except Exception:
267
+ continue
268
+ return None
269
+
270
+ def _fs_is_empty(entry: dict) -> bool:
271
+ """Heuristic to decide if a filesystem represented by *entry* is empty."""
272
+
273
+ fstype = entry.get("fstype")
274
+ if not fstype:
275
+ # Unformatted – treat as empty.
276
+ return True
277
+
278
+ mountpoint = entry.get("mountpoint")
279
+ cleanup = False
280
+
281
+ if not mountpoint:
282
+ tmp_dir = f"/tmp/simo-bk-{uuid.uuid4().hex[:8]}"
283
+ try:
284
+ os.makedirs(tmp_dir, exist_ok=True)
285
+ res = subprocess.run(
286
+ f"mount -o ro /dev/{entry['name']} {tmp_dir}",
287
+ shell=True,
288
+ stderr=subprocess.PIPE,
289
+ )
290
+ if res.returncode:
291
+ shutil.rmtree(tmp_dir, ignore_errors=True)
292
+ print(f"Unable to mount {entry['name']} to inspect contents – skip")
293
+ return False
294
+ mountpoint = tmp_dir
295
+ cleanup = True
296
+ except Exception as exc:
297
+ shutil.rmtree(tmp_dir, ignore_errors=True)
298
+ print(f"Exception while mounting {entry['name']}: {exc}")
299
+ return False
300
+
301
+ try:
302
+ with os.scandir(mountpoint) as it:
303
+ entries = [e.name for e in it if not e.name.startswith('.')]
304
+ except Exception as exc:
305
+ print(f"Unable to read directory listing for {entry['name']}: {exc}")
306
+ if cleanup:
307
+ subprocess.run(f"umount {mountpoint}", shell=True)
308
+ shutil.rmtree(mountpoint, ignore_errors=True)
309
+ return False
310
+
311
+ if cleanup:
312
+ subprocess.run(f"umount {mountpoint}", shell=True)
313
+ shutil.rmtree(mountpoint, ignore_errors=True)
314
+
315
+ meaningful = [e for e in entries if e not in {"lost+found"}]
316
+ return not meaningful
317
+
318
+ # ---------------------------------------------------------------------------
319
+
320
+ _MIN_SIZE_BYTES = 32 * 1024 * 1024 * 1024 # 32 GiB
321
+
322
+ for device in lsblk_data:
323
+ if not device.get("hotplug"):
324
+ continue
325
+
326
+ size_bytes = _device_size_bytes(device["name"])
327
+ if size_bytes is None:
328
+ print(f"Could not obtain capacity of: {device['name']}")
329
+ continue
330
+
331
+ if size_bytes < _MIN_SIZE_BYTES:
332
+ print(f"Too small (<32 GiB): {device['name']}")
333
+ continue
334
+
335
+ children = device.get("children") or []
336
+
337
+ if not children:
338
+ # Whole-disk filesystem.
339
+ if _fs_is_empty(device):
340
+ return device
341
+ print(f"Whole-disk filesystem on {device['name']} is not empty – skip")
342
+ continue
343
+
344
+ if len(children) == 1:
345
+ child = children[0]
346
+ if _fs_is_empty(child):
347
+ return device
348
+ print(f"Single partition {child['name']} on {device['name']} is not empty – skip")
349
+ continue
350
+
351
+ print(f"More than one partition on {device['name']} – skip")
352
+
353
+ return None
354
+
355
+
356
+ def _ensure_rescue_image_written(blank_device_name: str):
357
+ """Write *rescue.img* to the given **whole-disk** device.
358
+
359
+ The function is intentionally idempotent – if writing fails the caller can
360
+ attempt to call it again (e.g. the next time the periodic task runs).
361
+
362
+ It raises an exception on irrecoverable errors so that the caller can log
363
+ the failure.
364
+ """
365
+
366
+ import tarfile, time
367
+
368
+ img_path = os.path.join(os.path.dirname(__file__), "rescue.img.xz")
369
+
370
+ # Write the image. We deliberately avoid using *python-dd* wrappers and
371
+ # rely on the time-tested `dd(1)` command.
372
+ dd_cmd = (
373
+ f"xzcat {img_path} | dd of=/dev/{blank_device_name} bs=4M conv=fsync"
374
+ )
375
+ res = subprocess.run(dd_cmd, shell=True, stderr=subprocess.PIPE)
376
+ if res.returncode:
377
+ raise RuntimeError(
378
+ f"Writing rescue image failed: {res.stderr.decode(errors='ignore')}"
379
+ )
380
+
381
+ # Make sure the kernel notices the new partition table.
382
+ subprocess.run(f"partprobe /dev/{blank_device_name}", shell=True)
383
+
384
+ # Give the device a moment to settle.
385
+ time.sleep(2)
386
+
387
+ # Enlarge the 3rd partition (BACKUP) to the rest of the disk and create /
388
+ # extend the exFAT filesystem. This is wrapped in a helper to keep the
389
+ # main flow readable.
390
+ _expand_backup_partition(blank_device_name)
391
+
392
+
393
+ def _expand_backup_partition(device_name: str):
394
+ """Make partition 3 span leftover space and be ext4 labelled BACKUP.
395
+
396
+ Implementation is intentionally minimal and resilient:
397
+ – Use *sgdisk* only (no interactive prompts).
398
+ – Delete partition 3 (if present) and create a new one that fills all
399
+ remaining free space.
400
+ – Always create a fresh ext4 filesystem labelled BACKUP.
401
+ Because the rescue-image just flashed is empty, data loss is not a
402
+ concern and this deterministic route avoids edge-case errors.
403
+ """
404
+
405
+ import time, shutil
406
+
407
+ def _dev_path(base: str) -> str:
408
+ """Return /dev/<base>3 path handling devices that need 'p3'."""
409
+ direct = f"/dev/{base}3"
410
+ with_p = f"/dev/{base}p3"
411
+ return direct if os.path.exists(direct) else with_p
412
+
413
+ # 1. Ensure GPT headers cover the whole disk (harmless if already OK).
414
+ subprocess.run(f"sgdisk -e /dev/{device_name}", shell=True)
415
+
416
+ # 2. Drop existing partition 3 (ignore errors when it does not exist).
417
+ subprocess.run(f"sgdisk -d 3 /dev/{device_name}", shell=True)
418
+
419
+ # 3. Create new Linux filesystem partition occupying the rest of the disk.
420
+ create_cmd = f"sgdisk -n 3:0:0 -t 3:8300 -c 3:BACKUP /dev/{device_name}"
421
+ res = subprocess.run(create_cmd, shell=True, stderr=subprocess.PIPE)
422
+ if res.returncode:
423
+ raise RuntimeError(
424
+ "sgdisk failed to create BACKUP partition: " +
425
+ res.stderr.decode(errors="ignore")
426
+ )
427
+
428
+ # 4. Inform kernel and wait for udev.
429
+ subprocess.run(f"partprobe /dev/{device_name}", shell=True)
430
+ subprocess.run("udevadm settle", shell=True)
431
+
432
+ part_path = _dev_path(device_name)
433
+ for _ in range(5):
434
+ if os.path.exists(part_path):
435
+ break
436
+ time.sleep(1)
437
+ else:
438
+ raise RuntimeError("/dev node for new BACKUP partition did not appear")
439
+
440
+ # 5. Always create a fresh ext4 filesystem; wipe old signatures first.
441
+ subprocess.run(f"wipefs -a {part_path}", shell=True)
442
+ mkfs_cmd = f"mkfs.ext4 -F -L BACKUP {part_path}"
443
+ res = subprocess.run(mkfs_cmd, shell=True, stderr=subprocess.PIPE)
444
+ if res.returncode:
445
+ raise RuntimeError(
446
+ "mkfs.ext4 failed for BACKUP partition: " + res.stderr.decode(errors="ignore")
447
+ )
163
448
 
164
449
 
165
450
  def get_partitions():
@@ -205,16 +490,39 @@ def get_partitions():
205
490
 
206
491
  backup_device = get_backup_device(lsblk_data)
207
492
 
493
+ # If no suitable partition is available try to prepare one automatically.
494
+ if not backup_device:
495
+ blank_dev = _find_blank_removable_device(lsblk_data)
496
+ if blank_dev:
497
+ try:
498
+ _ensure_rescue_image_written(blank_dev["name"])
499
+ except Exception as exc:
500
+ BackupLog.objects.create(
501
+ level="error",
502
+ msg=(
503
+ "Can't prepare backup drive automatically.\n\n" +
504
+ str(exc)
505
+ ),
506
+ )
507
+ else:
508
+ # Re-read block devices so that the freshly written partition
509
+ # table appears in *lsblk* output.
510
+ lsblk_data = json.loads(subprocess.check_output(
511
+ 'lsblk --output NAME,HOTPLUG,MOUNTPOINT,FSTYPE,TYPE,LABEL,PARTLABEL --json',
512
+ shell=True
513
+ ).decode())['blockdevices']
514
+ backup_device = get_backup_device(lsblk_data)
515
+
208
516
  if not backup_device:
209
517
  BackupLog.objects.create(
210
518
  level='warning',
211
- msg="Can't backup. No external exFAT backup device on this machine."
519
+ msg="Can't backup. No external BACKUP partition found and no blank removable device was available."
212
520
  )
213
521
  return
214
522
 
215
- if lvm_partition.get('partlabel'):
523
+ if backup_device.get('partlabel'):
216
524
  sd_mountpoint = f"/media/{backup_device['partlabel']}"
217
- elif lvm_partition.get('label'):
525
+ elif backup_device.get('label'):
218
526
  sd_mountpoint = f"/media/{backup_device['label']}"
219
527
  else:
220
528
  sd_mountpoint = f"/media/{backup_device['name']}"
@@ -237,6 +545,8 @@ def get_partitions():
237
545
 
238
546
  @celery_app.task
239
547
  def perform_backup():
548
+ from simo.core.models import Instance
549
+ from simo.core.middleware import drop_current_instance
240
550
  from simo.backups.models import BackupLog
241
551
  try:
242
552
  lv_group, lv_name, sd_mountpoint = get_partitions()
@@ -267,6 +577,12 @@ def perform_backup():
267
577
  mac = str(hex(uuid.getnode()))
268
578
  device_backups_path = f'{sd_mountpoint}/simo_backups/hub-{mac}'
269
579
 
580
+ drop_current_instance()
581
+ hub_meta = {
582
+ 'instances': [inst.name for inst in Instance.objects.all()]
583
+ }
584
+ with open(os.path.join(device_backups_path, 'hub_meta.json'), 'w') as f:
585
+ f.write(json.dumps(hub_meta))
270
586
 
271
587
  now = datetime.now()
272
588
  month_folder = os.path.join(
@@ -288,6 +604,29 @@ def perform_backup():
288
604
  f'borg init --encryption=none {month_folder}', shell=True
289
605
  )
290
606
 
607
+ # ------------------------------------------------------------------
608
+ # Ensure that files stored on *separate* partitions – most importantly
609
+ # the /boot (kernel & initrd images) and /boot/efi (EFI System
610
+ # Partition) – are included in the snapshot. Otherwise the rescue
611
+ # procedure restores an empty /boot which leaves the system un-bootable
612
+ # once GRUB hands over control to the (missing) kernel.
613
+ #
614
+ # We temporarily bind-mount those paths into the read-only snapshot so
615
+ # that Borg treats them as regular directories residing on the same
616
+ # filesystem tree.
617
+ # ------------------------------------------------------------------
618
+
619
+ bind_mounts = []
620
+ for path in ("/boot", "/boot/efi"):
621
+ target = os.path.join(snap_mount_point, path.lstrip("/"))
622
+ # Create the mount-point inside the snapshot and bind-mount the live
623
+ # directory if it exists.
624
+ if os.path.ismount(path):
625
+ os.makedirs(target, exist_ok=True)
626
+ subprocess.run(["mount", "--bind", path, target], check=True)
627
+ bind_mounts.append(target)
628
+
629
+ # Directories that are safe to exclude – keep /boot out of this list!
291
630
  exclude_dirs = (
292
631
  'tmp', 'lost+found', 'proc', 'cdrom', 'dev', 'mnt', 'sys', 'run',
293
632
  'var/tmp', 'var/cache', 'var/log', 'media',
@@ -326,6 +665,11 @@ def perform_backup():
326
665
  stdout=subprocess.PIPE, stderr=subprocess.PIPE
327
666
  )
328
667
 
668
+ # Unmount previously created bind-mounts (boot / boot/efi) *before*
669
+ # removing the snapshot so that no busy references remain.
670
+ for mnt in reversed(bind_mounts):
671
+ subprocess.run(["umount", mnt])
672
+
329
673
  subprocess.run(["umount", snap_mount_point])
330
674
  subprocess.run(
331
675
  f"lvremove -f {lv_group}/{snap_name}", shell=True
@@ -421,4 +765,4 @@ def setup_periodic_tasks(sender, **kwargs):
421
765
  sender.add_periodic_task(60 * 60, check_backups.s())
422
766
  # perform auto backup every 12 hours
423
767
  sender.add_periodic_task(60 * 60 * 12, perform_backup.s())
424
- sender.add_periodic_task(60 * 60, clean_old_logs.s())
768
+ sender.add_periodic_task(60 * 60, clean_old_logs.s())
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
simo/core/controllers.py CHANGED
@@ -265,9 +265,7 @@ class ControllerBase(ABC):
265
265
  except:
266
266
  pass
267
267
 
268
- GatewayObjectCommand(
269
- self.component.gateway, self.component, set_val=value
270
- ).publish()
268
+ self._send_to_device(value)
271
269
  if value != self.component.value:
272
270
  self.component.value_previous = self.component.value
273
271
  self.component.value = value
@@ -316,6 +314,11 @@ class ControllerBase(ABC):
316
314
  ).first()
317
315
  self.component.save()
318
316
 
317
+ def _send_to_device(self, value):
318
+ GatewayObjectCommand(
319
+ self.component.gateway, self.component, set_val=value
320
+ ).publish()
321
+
319
322
  def _receive_from_device(
320
323
  self, value, is_alive=True, battery_level=None, error_msg=None
321
324
  ):