simo 1.7.19__py3-none-any.whl → 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (289) hide show
  1. simo/__pycache__/asgi.cpython-38.pyc +0 -0
  2. simo/__pycache__/on_http_start.cpython-38.pyc +0 -0
  3. simo/__pycache__/settings.cpython-38.pyc +0 -0
  4. simo/__pycache__/urls.cpython-38.pyc +0 -0
  5. simo/__pycache__/wsgi.cpython-38.pyc +0 -0
  6. simo/core/__pycache__/__init__.cpython-38.pyc +0 -0
  7. simo/core/__pycache__/admin.cpython-38.pyc +0 -0
  8. simo/core/__pycache__/api.cpython-38.pyc +0 -0
  9. simo/core/__pycache__/api_meta.cpython-38.pyc +0 -0
  10. simo/core/__pycache__/auto_urls.cpython-38.pyc +0 -0
  11. simo/core/__pycache__/autocomplete_views.cpython-38.pyc +0 -0
  12. simo/core/__pycache__/base_types.cpython-38.pyc +0 -0
  13. simo/core/__pycache__/context.cpython-38.pyc +0 -0
  14. simo/core/__pycache__/controllers.cpython-38.pyc +0 -0
  15. simo/core/__pycache__/events.cpython-38.pyc +0 -0
  16. simo/core/__pycache__/filters.cpython-38.pyc +0 -0
  17. simo/core/__pycache__/forms.cpython-38.pyc +0 -0
  18. simo/core/__pycache__/gateways.cpython-38.pyc +0 -0
  19. simo/core/__pycache__/managers.cpython-38.pyc +0 -0
  20. simo/core/__pycache__/middleware.cpython-38.pyc +0 -0
  21. simo/core/__pycache__/models.cpython-38.pyc +0 -0
  22. simo/core/__pycache__/permissions.cpython-38.pyc +0 -0
  23. simo/core/__pycache__/routing.cpython-38.pyc +0 -0
  24. simo/core/__pycache__/serializers.cpython-38.pyc +0 -0
  25. simo/core/__pycache__/signal_receivers.cpython-38.pyc +0 -0
  26. simo/core/__pycache__/socket_consumers.cpython-38.pyc +0 -0
  27. simo/core/__pycache__/tasks.cpython-38.pyc +0 -0
  28. simo/core/__pycache__/todos.cpython-38.pyc +0 -0
  29. simo/core/__pycache__/views.cpython-38.pyc +0 -0
  30. simo/core/admin.py +28 -18
  31. simo/core/api.py +157 -16
  32. simo/core/api_meta.py +87 -0
  33. simo/core/auto_urls.py +4 -1
  34. simo/core/autocomplete_views.py +8 -4
  35. simo/core/base_types.py +1 -0
  36. simo/core/context.py +3 -1
  37. simo/core/controllers.py +112 -32
  38. simo/core/db_backend/base.py +7 -22
  39. simo/core/drf_braces/README +3 -0
  40. simo/core/drf_braces/__init__.py +7 -0
  41. simo/core/drf_braces/__pycache__/__init__.cpython-38.pyc +0 -0
  42. simo/core/drf_braces/__pycache__/utils.cpython-38.pyc +0 -0
  43. simo/core/drf_braces/fields/__init__.py +5 -0
  44. simo/core/drf_braces/fields/__pycache__/__init__.cpython-38.pyc +0 -0
  45. simo/core/drf_braces/fields/__pycache__/_fields.cpython-38.pyc +0 -0
  46. simo/core/drf_braces/fields/__pycache__/custom.cpython-38.pyc +0 -0
  47. simo/core/drf_braces/fields/__pycache__/mixins.cpython-38.pyc +0 -0
  48. simo/core/drf_braces/fields/__pycache__/modified.cpython-38.pyc +0 -0
  49. simo/core/drf_braces/fields/_fields.py +48 -0
  50. simo/core/drf_braces/fields/custom.py +107 -0
  51. simo/core/drf_braces/fields/mixins.py +58 -0
  52. simo/core/drf_braces/fields/modified.py +41 -0
  53. simo/core/drf_braces/forms/__init__.py +0 -0
  54. simo/core/drf_braces/forms/fields.py +20 -0
  55. simo/core/drf_braces/forms/serializer_form.py +156 -0
  56. simo/core/drf_braces/mixins.py +52 -0
  57. simo/core/drf_braces/models.py +0 -0
  58. simo/core/drf_braces/parsers.py +72 -0
  59. simo/core/drf_braces/renderers.py +37 -0
  60. simo/core/drf_braces/serializers/__init__.py +0 -0
  61. simo/core/drf_braces/serializers/__pycache__/__init__.cpython-38.pyc +0 -0
  62. simo/core/drf_braces/serializers/__pycache__/form_serializer.cpython-38.pyc +0 -0
  63. simo/core/drf_braces/serializers/enforce_validation_serializer.py +214 -0
  64. simo/core/drf_braces/serializers/form_serializer.py +391 -0
  65. simo/core/drf_braces/serializers/swapping.py +48 -0
  66. simo/core/drf_braces/tests/__init__.py +0 -0
  67. simo/core/drf_braces/tests/fields/__init__.py +0 -0
  68. simo/core/drf_braces/tests/fields/test_custom.py +94 -0
  69. simo/core/drf_braces/tests/fields/test_fields.py +13 -0
  70. simo/core/drf_braces/tests/fields/test_mixins.py +96 -0
  71. simo/core/drf_braces/tests/fields/test_modified.py +40 -0
  72. simo/core/drf_braces/tests/forms/__init__.py +0 -0
  73. simo/core/drf_braces/tests/forms/test_fields.py +46 -0
  74. simo/core/drf_braces/tests/forms/test_serializer_form.py +256 -0
  75. simo/core/drf_braces/tests/serializers/__init__.py +0 -0
  76. simo/core/drf_braces/tests/serializers/test_enforce_validation_serializer.py +169 -0
  77. simo/core/drf_braces/tests/serializers/test_form_serializer.py +387 -0
  78. simo/core/drf_braces/tests/serializers/test_swapping.py +40 -0
  79. simo/core/drf_braces/tests/test_mixins.py +111 -0
  80. simo/core/drf_braces/tests/test_parsers.py +73 -0
  81. simo/core/drf_braces/tests/test_renderers.py +23 -0
  82. simo/core/drf_braces/tests/test_utils.py +73 -0
  83. simo/core/drf_braces/utils.py +209 -0
  84. simo/core/events.py +3 -3
  85. simo/core/forms.py +79 -37
  86. simo/core/gateways.py +31 -14
  87. simo/core/management/__pycache__/__init__.cpython-38.pyc +0 -0
  88. simo/core/management/commands/__pycache__/__init__.cpython-38.pyc +0 -0
  89. simo/core/management/commands/gateways_manager.py +0 -1
  90. simo/core/managers.py +81 -0
  91. simo/core/middleware.py +25 -0
  92. simo/core/migrations/0026_category_instance.py +20 -0
  93. simo/core/migrations/0027_remove_component_tags.py +17 -0
  94. simo/core/migrations/0028_rename_subcomponents_component_slaves.py +18 -0
  95. simo/core/migrations/0029_auto_20240229_1331.py +33 -0
  96. simo/core/migrations/__pycache__/0026_category_instance.cpython-38.pyc +0 -0
  97. simo/core/migrations/__pycache__/0027_remove_component_tags.cpython-38.pyc +0 -0
  98. simo/core/migrations/__pycache__/0028_rename_subcomponents_component_slaves.cpython-38.pyc +0 -0
  99. simo/core/migrations/__pycache__/0029_auto_20240229_1331.cpython-38.pyc +0 -0
  100. simo/core/models.py +103 -66
  101. simo/core/permissions.py +28 -2
  102. simo/core/serializers.py +330 -26
  103. simo/core/socket_consumers.py +5 -14
  104. simo/core/tasks.py +11 -1
  105. simo/core/templates/admin/base.html +37 -10
  106. simo/core/templates/admin/wizard/discovery.html +188 -0
  107. simo/core/templates/admin/wizard/wizard_add.html +5 -5
  108. simo/core/utils/__pycache__/serialization.cpython-38.pyc +0 -0
  109. simo/core/utils/admin.py +9 -2
  110. simo/core/utils/formsets.py +17 -16
  111. simo/core/utils/helpers.py +1 -0
  112. simo/core/utils/serialization.py +56 -0
  113. simo/core/utils/type_constants.py +1 -1
  114. simo/core/utils/validators.py +14 -1
  115. simo/core/views.py +13 -0
  116. simo/fleet/__pycache__/admin.cpython-38.pyc +0 -0
  117. simo/fleet/__pycache__/api.cpython-38.pyc +0 -0
  118. simo/fleet/__pycache__/auto_urls.cpython-38.pyc +0 -0
  119. simo/fleet/__pycache__/controllers.cpython-38.pyc +0 -0
  120. simo/fleet/__pycache__/forms.cpython-38.pyc +0 -0
  121. simo/fleet/__pycache__/gateways.cpython-38.pyc +0 -0
  122. simo/fleet/__pycache__/managers.cpython-38.pyc +0 -0
  123. simo/fleet/__pycache__/models.cpython-38.pyc +0 -0
  124. simo/fleet/__pycache__/serializers.cpython-38.pyc +0 -0
  125. simo/fleet/__pycache__/socket_consumers.cpython-38.pyc +0 -0
  126. simo/fleet/__pycache__/utils.cpython-38.pyc +0 -0
  127. simo/fleet/__pycache__/views.cpython-38.pyc +0 -0
  128. simo/fleet/admin.py +54 -25
  129. simo/fleet/api.py +59 -3
  130. simo/fleet/auto_urls.py +2 -3
  131. simo/fleet/controllers.py +199 -16
  132. simo/fleet/forms.py +325 -483
  133. simo/fleet/gateways.py +44 -2
  134. simo/fleet/managers.py +32 -0
  135. simo/fleet/migrations/0025_auto_20240130_1334.py +27 -0
  136. simo/fleet/migrations/0026_rename_i2cinterface_scl_pin_and_more.py +64 -0
  137. simo/fleet/migrations/0027_auto_20240306_0802.py +170 -0
  138. simo/fleet/migrations/0028_remove_i2cinterface_scl_pin_no_and_more.py +21 -0
  139. simo/fleet/migrations/0029_alter_i2cinterface_scl_pin_and_more.py +24 -0
  140. simo/fleet/migrations/0030_colonelpin_label_alter_colonel_type.py +24 -0
  141. simo/fleet/migrations/0031_alter_colonel_type.py +18 -0
  142. simo/fleet/migrations/__pycache__/0025_auto_20240130_1334.cpython-38.pyc +0 -0
  143. simo/fleet/migrations/__pycache__/0026_rename_i2cinterface_scl_pin_and_more.cpython-38.pyc +0 -0
  144. simo/fleet/migrations/__pycache__/0027_auto_20240306_0802.cpython-38.pyc +0 -0
  145. simo/fleet/migrations/__pycache__/0028_remove_i2cinterface_scl_pin_no_and_more.cpython-38.pyc +0 -0
  146. simo/fleet/migrations/__pycache__/0029_alter_i2cinterface_scl_pin_and_more.cpython-38.pyc +0 -0
  147. simo/fleet/migrations/__pycache__/0030_colonelpin_label_alter_colonel_type.cpython-38.pyc +0 -0
  148. simo/fleet/migrations/__pycache__/0031_alter_colonel_type.cpython-38.pyc +0 -0
  149. simo/fleet/models.py +134 -82
  150. simo/fleet/serializers.py +35 -1
  151. simo/fleet/socket_consumers.py +239 -76
  152. simo/fleet/utils.py +15 -53
  153. simo/fleet/views.py +28 -14
  154. simo/generic/controllers.py +13 -89
  155. simo/generic/forms.py +30 -23
  156. simo/generic/gateways.py +98 -10
  157. simo/generic/models.py +3 -3
  158. simo/multimedia/controllers.py +9 -8
  159. simo/settings.py +7 -4
  160. simo/urls.py +4 -8
  161. simo/users/__pycache__/admin.cpython-38.pyc +0 -0
  162. simo/users/__pycache__/api.cpython-38.pyc +0 -0
  163. simo/users/__pycache__/auto_urls.cpython-38.pyc +0 -0
  164. simo/users/__pycache__/models.cpython-38.pyc +0 -0
  165. simo/users/__pycache__/serializers.cpython-38.pyc +0 -0
  166. simo/users/__pycache__/sso_urls.cpython-38.pyc +0 -0
  167. simo/users/admin.py +8 -1
  168. simo/users/api.py +38 -2
  169. simo/users/auto_urls.py +2 -2
  170. simo/users/migrations/0025_rename_name_fingerprint_type_and_more.py +22 -0
  171. simo/users/migrations/__pycache__/0025_rename_name_fingerprint_type_and_more.cpython-38.pyc +0 -0
  172. simo/users/models.py +2 -3
  173. simo/users/serializers.py +15 -1
  174. simo/users/sso_urls.py +3 -3
  175. simo/wsgi.py +7 -0
  176. {simo-1.7.19.dist-info → simo-2.0.0.dist-info}/METADATA +8 -9
  177. {simo-1.7.19.dist-info → simo-2.0.0.dist-info}/RECORD +180 -210
  178. {simo-1.7.19.dist-info → simo-2.0.0.dist-info}/WHEEL +1 -1
  179. simo/core/__pycache__/__init__.cpython-37.pyc +0 -0
  180. simo/core/__pycache__/admin.cpython-37.pyc +0 -0
  181. simo/core/__pycache__/api.cpython-37.pyc +0 -0
  182. simo/core/__pycache__/apps.cpython-38.pyc +0 -0
  183. simo/core/__pycache__/controllers.cpython-37.pyc +0 -0
  184. simo/core/__pycache__/events.cpython-37.pyc +0 -0
  185. simo/core/__pycache__/forms.cpython-37.pyc +0 -0
  186. simo/core/__pycache__/gateways.cpython-37.pyc +0 -0
  187. simo/core/__pycache__/models.cpython-37.pyc +0 -0
  188. simo/core/__pycache__/scripts.cpython-37.pyc +0 -0
  189. simo/core/__pycache__/serializers.cpython-37.pyc +0 -0
  190. simo/core/__pycache__/widgets.cpython-37.pyc +0 -0
  191. simo/core/db_backend/__pycache__/__init__.cpython-38.pyc +0 -0
  192. simo/core/db_backend/__pycache__/base.cpython-38.pyc +0 -0
  193. simo/core/management/commands/__pycache__/gateways_manager.cpython-38.pyc +0 -0
  194. simo/core/management/commands/__pycache__/run_gateway.cpython-38.pyc +0 -0
  195. simo/core/migrations/__pycache__/0001_initial.cpython-38.pyc +0 -0
  196. simo/core/migrations/__pycache__/0002_load_icons.cpython-38.pyc +0 -0
  197. simo/core/migrations/__pycache__/0003_create_default_zones_and_categories.cpython-38.pyc +0 -0
  198. simo/core/migrations/__pycache__/0004_create_generic.cpython-38.pyc +0 -0
  199. simo/core/migrations/__pycache__/0005_component_subcomponents.cpython-38.pyc +0 -0
  200. simo/core/migrations/__pycache__/0006_alter_component_subcomponents.cpython-38.pyc +0 -0
  201. simo/core/migrations/__pycache__/0007_component_change_init_to.cpython-38.pyc +0 -0
  202. simo/core/migrations/__pycache__/0008_alter_component_change_init_to.cpython-38.pyc +0 -0
  203. simo/core/migrations/__pycache__/0009_auto_20220707_1404.cpython-38.pyc +0 -0
  204. simo/core/migrations/__pycache__/0010_historyaggregate.cpython-38.pyc +0 -0
  205. simo/core/migrations/__pycache__/0011_component_last_change.cpython-38.pyc +0 -0
  206. simo/core/migrations/__pycache__/0012_instance.cpython-38.pyc +0 -0
  207. simo/core/migrations/__pycache__/0013_auto_20231003_0754.cpython-38.pyc +0 -0
  208. simo/core/migrations/__pycache__/0014_zone_instance.cpython-38.pyc +0 -0
  209. simo/core/migrations/__pycache__/0015_auto_20231004_1113.cpython-38.pyc +0 -0
  210. simo/core/migrations/__pycache__/0016_auto_20231004_1113.cpython-38.pyc +0 -0
  211. simo/core/migrations/__pycache__/0017_auto_20231004_1313.cpython-38.pyc +0 -0
  212. simo/core/migrations/__pycache__/0018_auto_20231005_0622.cpython-38.pyc +0 -0
  213. simo/core/migrations/__pycache__/0019_alter_gateway_type.cpython-38.pyc +0 -0
  214. simo/core/migrations/__pycache__/0020_component_meta.cpython-38.pyc +0 -0
  215. simo/core/migrations/__pycache__/0021_auto_20231020_1041.cpython-38.pyc +0 -0
  216. simo/core/migrations/__pycache__/__init__.cpython-38.pyc +0 -0
  217. simo/core/templatetags/__pycache__/__init__.cpython-38.pyc +0 -0
  218. simo/core/templatetags/__pycache__/components_list.cpython-38.pyc +0 -0
  219. simo/core/utils/__pycache__/__init__.cpython-38.pyc +0 -0
  220. simo/core/utils/__pycache__/admin.cpython-38.pyc +0 -0
  221. simo/core/utils/__pycache__/config_values.cpython-38.pyc +0 -0
  222. simo/core/utils/__pycache__/easing.cpython-38.pyc +0 -0
  223. simo/core/utils/__pycache__/form_fields.cpython-38.pyc +0 -0
  224. simo/core/utils/__pycache__/form_widgets.cpython-38.pyc +0 -0
  225. simo/core/utils/__pycache__/formsets.cpython-38.pyc +0 -0
  226. simo/core/utils/__pycache__/helpers.cpython-38.pyc +0 -0
  227. simo/core/utils/__pycache__/logs.cpython-38.pyc +0 -0
  228. simo/core/utils/__pycache__/mixins.cpython-38.pyc +0 -0
  229. simo/core/utils/__pycache__/model_helpers.cpython-38.pyc +0 -0
  230. simo/core/utils/__pycache__/relay.cpython-38.pyc +0 -0
  231. simo/core/utils/__pycache__/type_constants.cpython-38.pyc +0 -0
  232. simo/core/utils/__pycache__/validators.cpython-38.pyc +0 -0
  233. simo/fleet/tasks.py +0 -25
  234. simo/generic/__pycache__/__init__.cpython-37.pyc +0 -0
  235. simo/generic/__pycache__/__init__.cpython-38.pyc +0 -0
  236. simo/generic/__pycache__/app_widgets.cpython-38.pyc +0 -0
  237. simo/generic/__pycache__/base_types.cpython-38.pyc +0 -0
  238. simo/generic/__pycache__/controllers.cpython-37.pyc +0 -0
  239. simo/generic/__pycache__/controllers.cpython-38.pyc +0 -0
  240. simo/generic/__pycache__/forms.cpython-38.pyc +0 -0
  241. simo/generic/__pycache__/gateways.cpython-38.pyc +0 -0
  242. simo/generic/__pycache__/models.cpython-38.pyc +0 -0
  243. simo/generic/__pycache__/routing.cpython-38.pyc +0 -0
  244. simo/generic/__pycache__/socket_consumers.cpython-38.pyc +0 -0
  245. simo/generic/__pycache__/tasks.cpython-38.pyc +0 -0
  246. simo/generic/__pycache__/widgets.cpython-37.pyc +0 -0
  247. simo/generic/tasks.py +0 -41
  248. simo/multimedia/__pycache__/__init__.cpython-38.pyc +0 -0
  249. simo/multimedia/__pycache__/admin.cpython-38.pyc +0 -0
  250. simo/multimedia/__pycache__/api.cpython-38.pyc +0 -0
  251. simo/multimedia/__pycache__/app_widgets.cpython-38.pyc +0 -0
  252. simo/multimedia/__pycache__/base_types.cpython-38.pyc +0 -0
  253. simo/multimedia/__pycache__/controllers.cpython-38.pyc +0 -0
  254. simo/multimedia/__pycache__/forms.cpython-38.pyc +0 -0
  255. simo/multimedia/__pycache__/models.cpython-38.pyc +0 -0
  256. simo/multimedia/__pycache__/serializers.cpython-38.pyc +0 -0
  257. simo/multimedia/migrations/__pycache__/0001_initial.cpython-38.pyc +0 -0
  258. simo/multimedia/migrations/__pycache__/0002_sound_length.cpython-38.pyc +0 -0
  259. simo/multimedia/migrations/__pycache__/0003_alter_sound_length.cpython-38.pyc +0 -0
  260. simo/multimedia/migrations/__pycache__/0004_auto_20231023_1055.cpython-38.pyc +0 -0
  261. simo/multimedia/migrations/__pycache__/__init__.cpython-38.pyc +0 -0
  262. simo/notifications/__pycache__/__init__.cpython-38.pyc +0 -0
  263. simo/notifications/__pycache__/admin.cpython-38.pyc +0 -0
  264. simo/notifications/__pycache__/api.cpython-38.pyc +0 -0
  265. simo/notifications/__pycache__/models.cpython-38.pyc +0 -0
  266. simo/notifications/__pycache__/serializers.cpython-38.pyc +0 -0
  267. simo/notifications/__pycache__/utils.cpython-38.pyc +0 -0
  268. simo/notifications/migrations/__pycache__/0001_initial.cpython-38.pyc +0 -0
  269. simo/notifications/migrations/__pycache__/0002_notification_instance.cpython-38.pyc +0 -0
  270. simo/notifications/migrations/__pycache__/__init__.cpython-38.pyc +0 -0
  271. simo/users/migrations/__pycache__/0001_initial.cpython-38.pyc +0 -0
  272. simo/users/migrations/__pycache__/0002_componentpermission.cpython-38.pyc +0 -0
  273. simo/users/migrations/__pycache__/0003_create_roles_and_system_user.cpython-38.pyc +0 -0
  274. simo/users/migrations/__pycache__/0004_user_secret_key.cpython-38.pyc +0 -0
  275. simo/users/migrations/__pycache__/0005_permissionsrole_instance.cpython-38.pyc +0 -0
  276. simo/users/migrations/__pycache__/0006_auto_20231003_0850.cpython-38.pyc +0 -0
  277. simo/users/migrations/__pycache__/0007_auto_20231003_1228.cpython-38.pyc +0 -0
  278. simo/users/migrations/__pycache__/0008_auto_20231003_1229.cpython-38.pyc +0 -0
  279. simo/users/migrations/__pycache__/0009_remove_user_role.cpython-38.pyc +0 -0
  280. simo/users/migrations/__pycache__/0010_auto_20231004_1313.cpython-38.pyc +0 -0
  281. simo/users/migrations/__pycache__/0011_auto_20231004_1313.cpython-38.pyc +0 -0
  282. simo/users/migrations/__pycache__/0012_alter_userinstancerole_unique_together.cpython-38.pyc +0 -0
  283. simo/users/migrations/__pycache__/0013_remove_user_roles.cpython-38.pyc +0 -0
  284. simo/users/migrations/__pycache__/0014_user_roles.cpython-38.pyc +0 -0
  285. simo/users/migrations/__pycache__/0015_remove_user_at_home.cpython-38.pyc +0 -0
  286. simo/users/migrations/__pycache__/0016_auto_20231005_1050.cpython-38.pyc +0 -0
  287. simo/users/migrations/__pycache__/__init__.cpython-38.pyc +0 -0
  288. {simo-1.7.19.dist-info → simo-2.0.0.dist-info}/LICENSE.md +0 -0
  289. {simo-1.7.19.dist-info → simo-2.0.0.dist-info}/top_level.txt +0 -0
simo/fleet/forms.py CHANGED
@@ -5,12 +5,22 @@ from django.urls.base import get_script_prefix
5
5
  from django.contrib.contenttypes.models import ContentType
6
6
  from dal import autocomplete
7
7
  from dal import forward
8
+ from simo.core.models import Component
8
9
  from simo.core.forms import BaseComponentForm, ValueLimitForm, NumericSensorForm
9
10
  from simo.core.utils.formsets import FormsetField
10
11
  from simo.core.widgets import LogOutputWidget
11
12
  from simo.core.utils.easing import EASING_CHOICES
12
- from .models import Colonel, I2CInterface, i2c_interface_no_choices
13
- from .utils import get_gpio_pins_choices, get_available_gpio_pins
13
+ from simo.core.utils.validators import validate_slaves
14
+ from simo.core.utils.admin import AdminFormActionForm
15
+ from .models import Colonel, ColonelPin, I2CInterface, i2c_interface_no_choices
16
+
17
+
18
+ class ColonelPinChoiceField(forms.ModelChoiceField):
19
+ '''
20
+ Required for API, so that SIMO app could properly handle
21
+ fleet components configuration.
22
+ '''
23
+ filter_by = 'colonel'
14
24
 
15
25
 
16
26
  class ColonelAdminForm(forms.ModelForm):
@@ -36,32 +46,32 @@ class ColonelAdminForm(forms.ModelForm):
36
46
  )
37
47
 
38
48
 
39
- class MoveColonelForm(forms.Form):
49
+ class MoveColonelForm(AdminFormActionForm):
40
50
  colonel = forms.ModelChoiceField(
41
51
  label="Move to:", queryset=Colonel.objects.filter(components=None),
42
52
  )
43
53
 
44
54
 
45
55
  class I2CInterfaceAdminForm(forms.ModelForm):
46
- scl_pin = forms.TypedChoiceField(
47
- coerce=int, choices=get_gpio_pins_choices,
56
+ scl_pin = ColonelPinChoiceField(
57
+ queryset=ColonelPin.objects.filter(output=True, native=True),
48
58
  widget=autocomplete.ListSelect2(
49
59
  url='autocomplete-colonel-pins',
50
60
  forward=[
51
61
  forward.Self(),
52
62
  forward.Field('colonel'),
53
- forward.Const({'output': True}, 'filters')
63
+ forward.Const({'output': True, 'native': True}, 'filters')
54
64
  ]
55
65
  )
56
66
  )
57
- sda_pin = forms.TypedChoiceField(
58
- coerce=int, choices=get_gpio_pins_choices,
67
+ sda_pin = ColonelPinChoiceField(
68
+ queryset=ColonelPin.objects.filter(output=True, native=True),
59
69
  widget=autocomplete.ListSelect2(
60
70
  url='autocomplete-colonel-pins',
61
71
  forward=[
62
72
  forward.Self(),
63
73
  forward.Field('colonel'),
64
- forward.Const({'output': True}, 'filters')
74
+ forward.Const({'output': True, 'native': True}, 'filters')
65
75
  ]
66
76
  )
67
77
  )
@@ -71,31 +81,21 @@ class I2CInterfaceAdminForm(forms.ModelForm):
71
81
  fields = '__all__'
72
82
 
73
83
  def clean_scl_pin(self):
74
- initial = None
75
- if self.instance.pk:
76
- initial = self.instance.scl_pin
77
- available_pins = get_available_gpio_pins(
78
- self.cleaned_data['colonel'], filters={'output': True},
79
- selected=initial
80
- )
81
- if self.cleaned_data['scl_pin'] not in available_pins:
82
- raise forms.ValidationError("Pin is unavailable.")
84
+ if self.cleaned_data['scl_pin'].occupied_by \
85
+ and self.cleaned_data['scl_pin'].occupied_by != self.instance:
86
+ raise forms.ValidationError(
87
+ f"This pin is already occupied by "
88
+ f"{self.cleaned_data['scl_pin'].occupied_by}!"
89
+ )
83
90
  return self.cleaned_data['scl_pin']
84
91
 
85
92
  def clean_sda_pin(self):
86
- initial = None
87
- if self.instance.pk:
88
- initial = self.instance.sda_pin
89
- available_pins = get_available_gpio_pins(
90
- self.cleaned_data['colonel'], filters={'output': True},
91
- selected=initial
92
- )
93
- if self.cleaned_data['sda_pin'] not in available_pins:
94
- raise forms.ValidationError("Pin is unavailable.")
95
-
96
- if self.cleaned_data.get('scl_pin') == self.cleaned_data['sda_pin']:
97
- raise forms.ValidationError("Can not be the same as SCL pin!")
98
-
93
+ if self.cleaned_data['sda_pin'].occupied_by \
94
+ and self.cleaned_data['sda_pin'].occupied_by != self.instance:
95
+ raise forms.ValidationError(
96
+ f"This pin is already occupied by "
97
+ f"{self.cleaned_data['sda_pin'].occupied_by}!"
98
+ )
99
99
  return self.cleaned_data['sda_pin']
100
100
 
101
101
 
@@ -105,14 +105,66 @@ class ColonelComponentForm(BaseComponentForm):
105
105
  help_text="ATENTION! Changing Colonel after component creation is not recommended!"
106
106
  )
107
107
 
108
- # def clean_colonel(self):
109
- # org = self.instance.config.get('colonel')
110
- # if org and org != self.cleaned_data['colonel'].id:
111
- # raise forms.ValidationError(
112
- # "Changing colonel after component is created "
113
- # "it is not allowed."
114
- # )
115
- # return self.cleaned_data['colonel']
108
+ def clean_colonel(self):
109
+ org = self.instance.config.get('colonel')
110
+ if org and org != self.cleaned_data['colonel'].id:
111
+ raise forms.ValidationError(
112
+ "Changing colonel after component is created "
113
+ "it is not allowed!"
114
+ )
115
+ return self.cleaned_data['colonel']
116
+
117
+ def _clean_pin(self, field_name):
118
+ if self.cleaned_data[field_name].colonel != self.cleaned_data['colonel']:
119
+ self.add_error(
120
+ field_name, "Pin must be from the same Colonel!"
121
+ )
122
+ return
123
+ if self.cleaned_data[field_name].occupied_by \
124
+ and self.cleaned_data[field_name].occupied_by != self.instance:
125
+ self.add_error(
126
+ field_name,
127
+ f"Pin is already occupied by {self.cleaned_data[field_name].occupied_by}!"
128
+ )
129
+
130
+ def _clean_controls(self):
131
+ # TODO: Formset factory should return proper field value types instead of str type
132
+
133
+ pin_instances = {}
134
+ for i, control in enumerate(self.cleaned_data['controls']):
135
+ updated_vals = {}
136
+ for key, val in control.items():
137
+ updated_vals[key] = val
138
+ if key == 'pin':
139
+ pin = ColonelPin.objects.get(
140
+ id=self.cleaned_data['controls'][i]['pin']
141
+ )
142
+ pin_instances[i] = pin
143
+ updated_vals['pin_no'] = pin.no
144
+ elif key == 'touch_threshold':
145
+ updated_vals[key] = int(val)
146
+ self.cleaned_data['controls'][i] = updated_vals
147
+
148
+ formset_errors = {}
149
+ for i, control in enumerate(self.cleaned_data['controls']):
150
+ if pin_instances[i].colonel != self.cleaned_data['colonel']:
151
+ formset_errors[i] = {
152
+ 'pin': f"{pin_instances[i]} must be from the same Colonel!"
153
+ }
154
+ elif pin_instances[i].occupied_by \
155
+ and pin_instances[i].occupied_by != self.instance:
156
+ formset_errors[i] = {
157
+ 'pin': f"{pin_instances[i]} is already occupied by {pin_instances[i].occupied_by}!"
158
+ }
159
+
160
+ errors_list = []
161
+ if formset_errors:
162
+ for i, control in enumerate(self.cleaned_data['controls']):
163
+ errors_list.append(formset_errors.get(i, {}))
164
+ if errors_list:
165
+ self._errors['controls'] = errors_list
166
+ if 'controls' in self.cleaned_data:
167
+ del self.cleaned_data['controls']
116
168
 
117
169
  def save(self, commit=True):
118
170
  obj = super().save(commit)
@@ -125,9 +177,8 @@ class ColonelComponentForm(BaseComponentForm):
125
177
 
126
178
 
127
179
  class ControlPinForm(forms.Form):
128
- pin = forms.TypedChoiceField(
129
- coerce=int, required=True, choices=get_gpio_pins_choices,
130
- help_text="Use this if you also want to wire up a wall switch",
180
+ pin = ColonelPinChoiceField(
181
+ queryset=ColonelPin.objects.filter(input=True),
131
182
  widget=autocomplete.ListSelect2(
132
183
  url='autocomplete-colonel-pins',
133
184
  forward=[
@@ -143,23 +194,14 @@ class ControlPinForm(forms.Form):
143
194
  method = forms.ChoiceField(
144
195
  required=True, choices=(
145
196
  ('momentary', "Momentary"), ('toggle', "Toggle"),
146
- ('touch', "Touch")
147
197
  ),
148
198
  )
149
- touch_threshold = forms.IntegerField(
150
- min_value=0, max_value=999999999, required=False, initial=1000,
151
- help_text="Used to detect touch events. "
152
- "Smaller value means a higher sensitivity. "
153
- "1000 offers good starting point. <br> "
154
- "Used only when controll method is set to Touch."
155
-
156
- )
157
199
  prefix = 'controls'
158
200
 
159
201
 
160
202
  class ColonelBinarySensorConfigForm(ColonelComponentForm):
161
- pin = forms.TypedChoiceField(
162
- coerce=int, choices=get_gpio_pins_choices,
203
+ pin = ColonelPinChoiceField(
204
+ queryset=ColonelPin.objects.filter(input=True),
163
205
  widget=autocomplete.ListSelect2(
164
206
  url='autocomplete-colonel-pins',
165
207
  forward=[
@@ -193,27 +235,15 @@ class ColonelBinarySensorConfigForm(ColonelComponentForm):
193
235
 
194
236
  def clean(self):
195
237
  super().clean()
196
- if 'colonel' not in self.cleaned_data:
238
+ if not self.cleaned_data.get('colonel'):
197
239
  return self.cleaned_data
198
240
  if 'pin' not in self.cleaned_data:
199
241
  return self.cleaned_data
200
- if self.instance.pk:
201
- selected = self.instance.config.get('pin')
202
- else:
203
- selected = None
204
- input_pins = get_available_gpio_pins(
205
- self.cleaned_data['colonel'], filters={'input': True},
206
- selected=selected
207
- )
208
- if self.cleaned_data['pin'] not in input_pins:
209
- self.add_error(
210
- 'pin',
211
- "Sorry, but GPIO%d pin can not be used as input pin "
212
- % self.cleaned_data['pin']
213
- )
214
- return
215
- if self.cleaned_data['pin'] > 100:
216
- if self.cleaned_data['pin'] < 126:
242
+
243
+ self._clean_pin('pin')
244
+
245
+ if self.cleaned_data['pin'].no > 100:
246
+ if self.cleaned_data['pin'].no < 126:
217
247
  if self.cleaned_data.get('pull') == 'HIGH':
218
248
  self.add_error(
219
249
  'pull',
@@ -233,30 +263,33 @@ class ColonelBinarySensorConfigForm(ColonelComponentForm):
233
263
  )
234
264
 
235
265
  elif self.cleaned_data.get('pull') != 'FLOATING':
236
- pins_available_for_pull = get_available_gpio_pins(
237
- self.cleaned_data['colonel'], filters={'output': True},
238
- selected=selected
239
- )
240
- if self.cleaned_data['pin'] not in pins_available_for_pull:
266
+ if not self.cleaned_data['pin'].output:
241
267
  self.add_error(
242
268
  'pin',
243
- "Sorry, but GPIO%d pin does not have internal pull HIGH/LOW"
244
- " resistance capability" % self.cleaned_data['pin']
269
+ f"Sorry, but {self.cleaned_data['pin']} "
270
+ f"does not have internal pull HIGH/LOW"
271
+ " resistance capability"
245
272
  )
246
- return
247
273
 
248
274
  return self.cleaned_data
249
275
 
250
276
 
277
+ def save(self, commit=True):
278
+ self.instance.config['pin_no'] = self.cleaned_data['pin'].no
279
+ return super().save(commit=commit)
280
+
281
+
251
282
  class ColonelNumericSensorConfigForm(ColonelComponentForm, NumericSensorForm):
252
- pin = forms.TypedChoiceField(
253
- coerce=int, choices=get_gpio_pins_choices,
283
+ pin = ColonelPinChoiceField(
284
+ queryset=ColonelPin.objects.filter(adc=True, input=True, native=True),
254
285
  widget=autocomplete.ListSelect2(
255
286
  url='autocomplete-colonel-pins',
256
287
  forward=[
257
288
  forward.Self(),
258
289
  forward.Field('colonel'),
259
- forward.Const({'adc': True}, 'filters')
290
+ forward.Const(
291
+ {'adc': True, 'native': True, 'input': True}, 'filters'
292
+ )
260
293
  ]
261
294
  )
262
295
  )
@@ -282,37 +315,32 @@ class ColonelNumericSensorConfigForm(ColonelComponentForm, NumericSensorForm):
282
315
 
283
316
  def clean(self):
284
317
  super().clean()
285
- if 'colonel' not in self.cleaned_data:
318
+ if not self.cleaned_data.get('colonel'):
286
319
  return self.cleaned_data
287
320
  if 'pin' not in self.cleaned_data:
288
321
  return self.cleaned_data
289
- if self.instance.pk:
290
- selected = self.instance.config.get('pin')
291
- else:
292
- selected = None
293
- input_pins = get_available_gpio_pins(
294
- self.cleaned_data['colonel'], filters={'adc': True},
295
- selected=selected
296
- )
297
- if self.cleaned_data['pin'] not in input_pins:
298
- self.add_error(
299
- 'pin',
300
- "Sorry, but GPIO%d pin can not be used as ADC input pin "
301
- % self.cleaned_data['pin']
302
- )
303
- return
322
+
323
+ self._clean_pin('pin')
324
+
304
325
  return self.cleaned_data
305
326
 
306
327
 
307
- class DS18B20SensorConfigForm(ColonelComponentForm):
308
- pin = forms.TypedChoiceField(
309
- coerce=int, choices=get_gpio_pins_choices,
328
+ def save(self, commit=True):
329
+ self.instance.config['pin_no'] = self.cleaned_data['pin'].no
330
+ return super().save(commit=commit)
331
+
332
+
333
+ class DS18B20SensorConfigForm(ColonelComponentForm, NumericSensorForm):
334
+ pin = ColonelPinChoiceField(
335
+ queryset=ColonelPin.objects.filter(input=True, native=True),
310
336
  widget=autocomplete.ListSelect2(
311
337
  url='autocomplete-colonel-pins',
312
338
  forward=[
313
339
  forward.Self(),
314
340
  forward.Field('colonel'),
315
- forward.Const({'input': True, 'native': True}, 'filters')
341
+ forward.Const(
342
+ {'native': True, 'input': True}, 'filters'
343
+ )
316
344
  ]
317
345
  )
318
346
  )
@@ -325,37 +353,32 @@ class DS18B20SensorConfigForm(ColonelComponentForm):
325
353
 
326
354
  def clean(self):
327
355
  super().clean()
328
- if 'colonel' not in self.cleaned_data:
356
+ if not self.cleaned_data.get('colonel'):
329
357
  return self.cleaned_data
330
358
  if 'pin' not in self.cleaned_data:
331
359
  return self.cleaned_data
332
- if self.instance.pk:
333
- selected = self.instance.config.get('pin')
334
- else:
335
- selected = None
336
- input_pins = get_available_gpio_pins(
337
- self.cleaned_data['colonel'], filters={'adc': True},
338
- selected=selected
339
- )
340
- if self.cleaned_data['pin'] not in input_pins:
341
- self.add_error(
342
- 'pin',
343
- "Sorry, but GPIO%d pin can not be used"
344
- % self.cleaned_data['pin']
345
- )
346
- return
360
+
361
+ self._clean_pin('pin')
362
+
347
363
  return self.cleaned_data
348
364
 
365
+ def save(self, commit=True):
366
+ self.instance.config['pin_no'] = self.cleaned_data['pin'].no
367
+ return super().save(commit=commit)
368
+
369
+
349
370
 
350
371
  class ColonelDHTSensorConfigForm(ColonelComponentForm):
351
- pin = forms.TypedChoiceField(
352
- coerce=int, choices=get_gpio_pins_choices,
372
+ pin = ColonelPinChoiceField(
373
+ queryset=ColonelPin.objects.filter(input=True, native=True),
353
374
  widget=autocomplete.ListSelect2(
354
375
  url='autocomplete-colonel-pins',
355
376
  forward=[
356
377
  forward.Self(),
357
378
  forward.Field('colonel'),
358
- forward.Const({'input': True, 'native': True}, 'filters')
379
+ forward.Const(
380
+ {'native': True, 'input': True}, 'filters'
381
+ )
359
382
  ]
360
383
  )
361
384
  )
@@ -376,27 +399,19 @@ class ColonelDHTSensorConfigForm(ColonelComponentForm):
376
399
 
377
400
  def clean(self):
378
401
  super().clean()
379
- if 'colonel' not in self.cleaned_data:
402
+ if not self.cleaned_data.get('colonel'):
380
403
  return self.cleaned_data
381
404
  if 'pin' not in self.cleaned_data:
382
405
  return self.cleaned_data
383
- if self.instance.pk:
384
- selected = self.instance.config.get('pin')
385
- else:
386
- selected = None
387
- input_pins = get_available_gpio_pins(
388
- self.cleaned_data['colonel'], filters={'adc': True},
389
- selected=selected
390
- )
391
- if self.cleaned_data['pin'] not in input_pins:
392
- self.add_error(
393
- 'pin',
394
- "Sorry, but GPIO%d pin can not be used"
395
- % self.cleaned_data['pin']
396
- )
397
- return
406
+
407
+ self._clean_pin('pin')
408
+
398
409
  return self.cleaned_data
399
410
 
411
+ def save(self, commit=True):
412
+ self.instance.config['pin_no'] = self.cleaned_data['pin'].no
413
+ return super().save(commit=commit)
414
+
400
415
 
401
416
  class BME680SensorConfigForm(ColonelComponentForm):
402
417
  i2c_interface = forms.TypedChoiceField(
@@ -410,7 +425,8 @@ class BME680SensorConfigForm(ColonelComponentForm):
410
425
  )
411
426
  )
412
427
  i2c_address = forms.IntegerField(
413
- help_text="Integer: 0 - 127", min_value=0, max_value=127
428
+ min_value=0, max_value=127, initial=118,
429
+ help_text="Integer: 0 - 127. Dafault: 118"
414
430
  )
415
431
  read_frequency_s = forms.IntegerField(
416
432
  initial=60, min_value=1, max_value=60*60*24,
@@ -420,15 +436,38 @@ class BME680SensorConfigForm(ColonelComponentForm):
420
436
  )
421
437
 
422
438
 
439
+ class MPC9808SensorConfigForm(ColonelComponentForm):
440
+ i2c_interface = forms.TypedChoiceField(
441
+ coerce=int, choices=i2c_interface_no_choices,
442
+ widget=autocomplete.ListSelect2(
443
+ url='autocomplete-colonel-i2c_interfaces',
444
+ forward=[
445
+ forward.Self(),
446
+ forward.Field('colonel'),
447
+ ]
448
+ )
449
+ )
450
+ i2c_address = forms.IntegerField(
451
+ min_value=0, max_value=127, initial=24,
452
+ help_text="Integer: 0 - 127. Default: 24",
453
+ )
454
+ read_frequency_s = forms.IntegerField(
455
+ initial=60, min_value=1, max_value=60 * 60 * 24,
456
+ help_text='read and report temperature value every s. '
457
+ 'Can not be less than 1s.'
458
+
459
+ )
460
+
461
+
423
462
  class ColonelTouchSensorConfigForm(ColonelComponentForm):
424
- pin = forms.TypedChoiceField(
425
- coerce=int, choices=get_gpio_pins_choices,
463
+ pin = ColonelPinChoiceField(
464
+ queryset=ColonelPin.objects.filter(input=True, capacitive=True),
426
465
  widget=autocomplete.ListSelect2(
427
466
  url='autocomplete-colonel-pins',
428
467
  forward=[
429
468
  forward.Self(),
430
469
  forward.Field('colonel'),
431
- forward.Const({'capacitive': True}, 'filters')
470
+ forward.Const({'input': True, 'capacitive': True}, 'filters')
432
471
  ]
433
472
  )
434
473
  )
@@ -439,45 +478,28 @@ class ColonelTouchSensorConfigForm(ColonelComponentForm):
439
478
  "1000 offers good starting point."
440
479
 
441
480
  )
442
- inverse = forms.ChoiceField(choices=(('no', "No"), ('yes', "Yes")))
481
+ inverse = forms.ChoiceField(choices=((0, "No"), (1, "Yes")))
443
482
 
444
483
  def clean(self):
445
484
  super().clean()
446
- if 'colonel' not in self.cleaned_data:
485
+ if not self.cleaned_data.get('colonel'):
447
486
  return self.cleaned_data
448
487
  if 'pin' not in self.cleaned_data:
449
488
  return self.cleaned_data
450
- if self.instance.pk:
451
- selected = self.instance.config.get('pin')
452
- else:
453
- selected = None
454
- free_pins = get_available_gpio_pins(
455
- self.cleaned_data['colonel'], selected=selected
456
- )
457
- if self.cleaned_data['pin'] not in free_pins:
458
- self.add_error(
459
- 'pin',
460
- "Sorry, but GPIO%d pin is occupied."
461
- % self.cleaned_data['pin']
462
- )
463
- return
464
- touch_pins = get_available_gpio_pins(
465
- self.cleaned_data['colonel'], filters={'capacitive': True},
466
- selected=selected
467
- )
468
- if self.cleaned_data['pin'] not in touch_pins:
469
- self.add_error(
470
- 'pin',
471
- "Sorry, but GPIO%d pin can not be used as input pin "
472
- % self.cleaned_data['pin']
473
- )
474
- return
489
+
490
+ self._clean_pin('pin')
491
+
475
492
  return self.cleaned_data
476
493
 
477
494
 
495
+ def save(self, commit=True):
496
+ self.instance.config['pin_no'] = self.cleaned_data['pin'].no
497
+ return super().save(commit=commit)
498
+
499
+
478
500
  class ColonelSwitchConfigForm(ColonelComponentForm):
479
- output_pin = forms.TypedChoiceField(
480
- coerce=int, choices=get_gpio_pins_choices,
501
+ output_pin = ColonelPinChoiceField(
502
+ queryset=ColonelPin.objects.filter(output=True),
481
503
  widget=autocomplete.ListSelect2(
482
504
  url='autocomplete-colonel-pins',
483
505
  forward=[
@@ -498,6 +520,20 @@ class ColonelSwitchConfigForm(ColonelComponentForm):
498
520
  inverse = forms.BooleanField(
499
521
  label=_("Inverse switch value"), required=False
500
522
  )
523
+ slaves = forms.ModelMultipleChoiceField(
524
+ required=False,
525
+ queryset=Component.objects.filter(
526
+ base_type__in=(
527
+ 'dimmer', 'switch', 'blinds', 'script'
528
+ )
529
+ ),
530
+ widget=autocomplete.ModelSelect2Multiple(
531
+ url='autocomplete-component', attrs={'data-html': True},
532
+ forward=(forward.Const(
533
+ ['dimmer', 'switch', 'blinds', 'script'], 'base_type'),
534
+ )
535
+ )
536
+ )
501
537
 
502
538
  controls = FormsetField(
503
539
  formset_factory(
@@ -505,62 +541,37 @@ class ColonelSwitchConfigForm(ColonelComponentForm):
505
541
  )
506
542
  )
507
543
 
544
+ def clean_slaves(self):
545
+ if not self.cleaned_data['slaves'] or not self.instance:
546
+ return self.cleaned_data['slaves']
547
+ return validate_slaves(self.cleaned_data['slaves'], self.instance)
548
+
549
+
508
550
  def clean(self):
509
551
  super().clean()
510
- if 'colonel' not in self.cleaned_data:
552
+ if not self.cleaned_data.get('colonel'):
511
553
  return self.cleaned_data
512
554
  if not self.cleaned_data.get('output_pin'):
513
555
  return self.cleaned_data
514
- if self.instance.pk:
515
- selected = self.instance.config.get('output_pin')
516
- else:
517
- selected = None
518
- output_pins = get_available_gpio_pins(
519
- self.cleaned_data['colonel'], filters={'output': True},
520
- selected=selected
521
- )
522
- if self.cleaned_data['output_pin'] not in output_pins:
523
- self.add_error(
524
- 'output_pin',
525
- "Sorry, but GPIO%d pin can not be used as output pin "
526
- % self.cleaned_data['output_pin']
527
- )
528
- return self.cleaned_data
529
556
 
530
- if 'controls' not in self.cleaned_data:
531
- return self.cleaned_data
557
+ self._clean_pin('output_pin')
532
558
 
533
- # TODO: Formset factory should return proper field value types instead of str type
534
- for i, control in enumerate(self.cleaned_data['controls']):
535
- for key, val in control.items():
536
- if key in ('pin', 'touch_threshold'):
537
- self.cleaned_data['controls'][i][key] = int(val)
538
- else:
539
- self.cleaned_data['controls'][i][key] = val
559
+ if not self.cleaned_data.get('controls'):
560
+ return self.cleaned_data
540
561
 
541
- for i, control in enumerate(self.cleaned_data['controls']):
542
- try:
543
- selected = self.instance.config['controls'][i]['pin']
544
- except:
545
- selected = None
546
- free_pins = get_available_gpio_pins(
547
- self.cleaned_data['colonel'], filters={'input': True},
548
- selected=selected
549
- )
550
- if control['pin'] not in free_pins:
551
- self.add_error(
552
- 'controls',
553
- "Sorry, but GPIO%d pin is occupied."
554
- % control['pin']
555
- )
556
- return
562
+ self._clean_controls()
557
563
 
558
564
  return self.cleaned_data
559
565
 
560
566
 
567
+ def save(self, commit=True):
568
+ self.instance.config['output_pin_no'] = self.cleaned_data['output_pin'].no
569
+ return super().save(commit=commit)
570
+
571
+
561
572
  class ColonelPWMOutputConfigForm(ColonelComponentForm):
562
- output_pin = forms.TypedChoiceField(
563
- coerce=int, choices=get_gpio_pins_choices,
573
+ output_pin = ColonelPinChoiceField(
574
+ queryset=ColonelPin.objects.filter(output=True),
564
575
  widget=autocomplete.ListSelect2(
565
576
  url='autocomplete-colonel-pins',
566
577
  forward=[
@@ -613,70 +624,52 @@ class ColonelPWMOutputConfigForm(ColonelComponentForm):
613
624
  required=True, initial=100,
614
625
  help_text="Component ON value when used with toggle switch"
615
626
  )
627
+ slaves = forms.ModelMultipleChoiceField(
628
+ required=False,
629
+ queryset=Component.objects.filter(
630
+ base_type__in=('dimmer', ),
631
+ ),
632
+ widget=autocomplete.ModelSelect2Multiple(
633
+ url='autocomplete-component', attrs={'data-html': True},
634
+ forward=(forward.Const(['dimmer', ], 'base_type'),)
635
+ )
636
+ )
616
637
  controls = FormsetField(
617
638
  formset_factory(
618
639
  ControlPinForm, can_delete=True, can_order=True, extra=0, max_num=1
619
640
  )
620
641
  )
621
642
 
643
+ def clean_slaves(self):
644
+ if not self.cleaned_data['slaves'] or not self.instance:
645
+ return self.cleaned_data['slaves']
646
+ return validate_slaves(self.cleaned_data['slaves'], self.instance)
647
+
622
648
  def clean(self):
623
649
  super().clean()
624
- if 'colonel' not in self.cleaned_data:
650
+ if not self.cleaned_data.get('colonel'):
625
651
  return self.cleaned_data
626
652
  if not self.cleaned_data.get('output_pin'):
627
653
  return self.cleaned_data
628
- if self.instance.pk:
629
- selected = self.instance.config.get('output_pin')
630
- else:
631
- self.cleaned_data['value_units'] = '%'
632
- selected = None
633
- output_pins = get_available_gpio_pins(
634
- self.cleaned_data['colonel'], filters={'output': True},
635
- selected=selected
636
- )
637
- if self.cleaned_data['output_pin'] not in output_pins:
638
- self.add_error(
639
- 'output_pin',
640
- "Sorry, but GPIO%d pin can not be used as output pin "
641
- % self.cleaned_data['output_pin']
642
- )
643
- return self.cleaned_data
644
654
 
645
- if 'controls' not in self.cleaned_data:
655
+ self._clean_pin('output_pin')
656
+
657
+ if not self.cleaned_data.get('controls'):
646
658
  return self.cleaned_data
647
659
 
660
+ self._clean_controls()
648
661
 
649
- # TODO: Formset factory should return proper field value types instead of str type
650
- for i, control in enumerate(self.cleaned_data['controls']):
651
- for key, val in control.items():
652
- if key in ('pin', 'touch_threshold'):
653
- self.cleaned_data['controls'][i][key] = int(val)
654
- else:
655
- self.cleaned_data['controls'][i][key] = val
662
+ return self.cleaned_data
656
663
 
657
- for i, control in enumerate(self.cleaned_data['controls']):
658
- try:
659
- selected = self.instance.config['controls'][i]['pin']
660
- except:
661
- selected = None
662
- free_pins = get_available_gpio_pins(
663
- self.cleaned_data['colonel'], filters={'input': True},
664
- selected=selected
665
- )
666
- if control['pin'] not in free_pins:
667
- self.add_error(
668
- 'controls',
669
- "Sorry, but GPIO%d pin is occupied."
670
- % control['pin']
671
- )
672
- return
673
664
 
674
- return self.cleaned_data
665
+ def save(self, commit=True):
666
+ self.instance.config['output_pin_no'] = self.cleaned_data['output_pin'].no
667
+ return super().save(commit=commit)
675
668
 
676
669
 
677
670
  class ColonelRGBLightConfigForm(ColonelComponentForm):
678
- output_pin = forms.TypedChoiceField(
679
- coerce=int, choices=get_gpio_pins_choices,
671
+ output_pin = ColonelPinChoiceField(
672
+ queryset=ColonelPin.objects.filter(output=True, native=True),
680
673
  widget=autocomplete.ListSelect2(
681
674
  url='autocomplete-colonel-pins',
682
675
  forward=[
@@ -705,6 +698,7 @@ class ColonelRGBLightConfigForm(ColonelComponentForm):
705
698
  )
706
699
 
707
700
  def save(self, commit=True):
701
+ self.instance.config['output_pin_no'] = self.cleaned_data['output_pin'].no
708
702
  if len(self.cleaned_data['order']) > 3:
709
703
  self.instance.config['has_white'] = True
710
704
  else:
@@ -714,62 +708,25 @@ class ColonelRGBLightConfigForm(ColonelComponentForm):
714
708
 
715
709
  def clean(self):
716
710
  super().clean()
717
- if 'colonel' not in self.cleaned_data:
711
+ if not self.cleaned_data.get('colonel'):
718
712
  return self.cleaned_data
719
713
  if not self.cleaned_data.get('output_pin'):
720
714
  return self.cleaned_data
721
- if self.instance.pk:
722
- selected = self.instance.config.get('output_pin')
723
- else:
724
- self.cleaned_data['value_units'] = '%'
725
- selected = None
726
- output_pins = get_available_gpio_pins(
727
- self.cleaned_data['colonel'], filters={'output': True, 'native': True},
728
- selected=selected
729
- )
730
- if self.cleaned_data['output_pin'] not in output_pins:
731
- self.add_error(
732
- 'output_pin',
733
- "Sorry, but GPIO%d pin can not be used as output pin "
734
- % self.cleaned_data['output_pin']
735
- )
736
- return self.cleaned_data
737
715
 
738
- if 'controls' not in self.cleaned_data:
739
- return self.cleaned_data
716
+ self._clean_pin('output_pin')
740
717
 
718
+ if not self.cleaned_data.get('controls'):
719
+ return self.cleaned_data
741
720
 
742
- # TODO: Formset factory should return proper field value types instead of str type
743
- for i, control in enumerate(self.cleaned_data['controls']):
744
- for key, val in control.items():
745
- if key in ('pin', 'touch_threshold'):
746
- self.cleaned_data['controls'][i][key] = int(val)
747
- else:
748
- self.cleaned_data['controls'][i][key] = val
749
-
750
- for i, control in enumerate(self.cleaned_data['controls']):
751
- try:
752
- selected = self.instance.config['controls'][i]['pin']
753
- except:
754
- selected = None
755
- free_pins = get_available_gpio_pins(
756
- self.cleaned_data['colonel'], filters={'input': True},
757
- selected=selected
758
- )
759
- if control['pin'] not in free_pins:
760
- self.add_error(
761
- 'controls',
762
- "Sorry, but GPIO%d pin is occupied."
763
- % control['pin']
764
- )
765
- return
721
+ self._clean_controls()
766
722
 
767
723
  return self.cleaned_data
768
724
 
769
725
 
726
+
770
727
  class DualMotorValveForm(ColonelComponentForm):
771
- open_pin = forms.TypedChoiceField(
772
- coerce=int, choices=get_gpio_pins_choices,
728
+ open_pin = ColonelPinChoiceField(
729
+ queryset=ColonelPin.objects.filter(output=True),
773
730
  widget=autocomplete.ListSelect2(
774
731
  url='autocomplete-colonel-pins',
775
732
  forward=[
@@ -786,8 +743,8 @@ class DualMotorValveForm(ColonelComponentForm):
786
743
  required=True, min_value=0.01, max_value=1000000000,
787
744
  help_text="Time in seconds to open."
788
745
  )
789
- close_pin = forms.TypedChoiceField(
790
- coerce=int, choices=get_gpio_pins_choices,
746
+ close_pin = ColonelPinChoiceField(
747
+ queryset=ColonelPin.objects.filter(output=True),
791
748
  widget=autocomplete.ListSelect2(
792
749
  url='autocomplete-colonel-pins',
793
750
  forward=[
@@ -805,52 +762,30 @@ class DualMotorValveForm(ColonelComponentForm):
805
762
  help_text="Time in seconds to close."
806
763
  )
807
764
 
765
+
808
766
  def clean(self):
809
767
  super().clean()
810
- if 'colonel' not in self.cleaned_data:
768
+ if not self.cleaned_data.get('colonel'):
811
769
  return self.cleaned_data
812
770
  if not self.cleaned_data.get('open_pin'):
813
771
  return self.cleaned_data
814
772
  if not self.cleaned_data.get('close_pin'):
815
773
  return self.cleaned_data
816
774
 
817
- if self.instance.pk:
818
- selected = self.instance.config.get('open_pin')
819
- else:
820
- selected = None
821
- output_pins = get_available_gpio_pins(
822
- self.cleaned_data['colonel'], filters={'output': True},
823
- selected=selected
824
- )
825
- if self.cleaned_data['open_pin'] not in output_pins:
826
- self.add_error(
827
- 'open_pin',
828
- "Sorry, but GPIO%d pin can not be used as output pin "
829
- % self.cleaned_data['open_pin']
830
- )
831
- return
775
+ self._clean_pin('open_pin')
776
+ self._clean_pin('close_pin')
832
777
 
833
- if self.instance.pk:
834
- selected = self.instance.config.get('close_pin')
835
- else:
836
- selected = None
837
- output_pins = get_available_gpio_pins(
838
- self.cleaned_data['colonel'], filters={'output': True},
839
- selected=selected
840
- )
841
- if self.cleaned_data['close_pin'] not in output_pins:
842
- self.add_error(
843
- 'close_pin',
844
- "Sorry, but GPIO%d pin can not be used as output pin "
845
- % self.cleaned_data['close_pin']
846
- )
847
- return
848
778
  return self.cleaned_data
849
779
 
780
+ def save(self, commit=True):
781
+ self.instance.config['open_pin_no'] = self.cleaned_data['open_pin'].no
782
+ self.instance.config['close_pin_no'] = self.cleaned_data['close_pin'].no
783
+ return super().save(commit=commit)
784
+
850
785
 
851
786
  class BlindsConfigForm(ColonelComponentForm):
852
- open_pin = forms.TypedChoiceField(
853
- coerce=int, choices=get_gpio_pins_choices,
787
+ open_pin = ColonelPinChoiceField(
788
+ queryset=ColonelPin.objects.filter(output=True),
854
789
  widget=autocomplete.ListSelect2(
855
790
  url='autocomplete-colonel-pins',
856
791
  forward=[
@@ -863,8 +798,8 @@ class BlindsConfigForm(ColonelComponentForm):
863
798
  open_action = forms.ChoiceField(
864
799
  choices=(('HIGH', "HIGH"), ('LOW', "LOW")),
865
800
  )
866
- close_pin = forms.TypedChoiceField(
867
- coerce=int, choices=get_gpio_pins_choices,
801
+ close_pin = ColonelPinChoiceField(
802
+ queryset=ColonelPin.objects.filter(output=True),
868
803
  widget=autocomplete.ListSelect2(
869
804
  url='autocomplete-colonel-pins',
870
805
  forward=[
@@ -922,47 +857,15 @@ class BlindsConfigForm(ColonelComponentForm):
922
857
 
923
858
  def clean(self):
924
859
  super().clean()
925
- if 'colonel' not in self.cleaned_data:
860
+ if not self.cleaned_data.get('colonel'):
926
861
  return self.cleaned_data
927
862
  if not self.cleaned_data.get('open_pin'):
928
863
  return self.cleaned_data
929
864
  if not self.cleaned_data.get('close_pin'):
930
865
  return self.cleaned_data
931
866
 
932
- if self.instance.pk:
933
- selected = self.instance.config.get('open_pin')
934
- else:
935
- selected = None
936
- output_pins = get_available_gpio_pins(
937
- self.cleaned_data['colonel'],
938
- filters={'output': True},
939
- selected=selected
940
- )
941
- if self.cleaned_data['open_pin'] not in output_pins:
942
- self.add_error(
943
- 'open_pin',
944
- "Sorry, but GPIO%d pin can not be used as output pin "
945
- % self.cleaned_data['open_pin']
946
- )
947
- return
948
-
949
- if self.instance.pk:
950
- selected = self.instance.config.get('close_pin')
951
- else:
952
- selected = None
953
- output_pins = get_available_gpio_pins(
954
- self.cleaned_data['colonel'],
955
- filters={'output': True},
956
- selected=selected
957
- )
958
- if self.cleaned_data['close_pin'] not in output_pins:
959
- self.add_error(
960
- 'close_pin',
961
- "Sorry, but GPIO%d pin can not be used as output pin "
962
- % self.cleaned_data['close_pin']
963
- )
964
- return
965
-
867
+ self._clean_pin('open_pin')
868
+ self._clean_pin('close_pin')
966
869
 
967
870
  if 'controls' not in self.cleaned_data:
968
871
  return self.cleaned_data
@@ -981,38 +884,19 @@ class BlindsConfigForm(ColonelComponentForm):
981
884
  self.add_error('controls', "Both must use the same control method.")
982
885
  return self.cleaned_data
983
886
 
984
-
985
- # TODO: Formset factory should return proper field value types instead of str type
986
- for i, control in enumerate(self.cleaned_data['controls']):
987
- for key, val in control.items():
988
- if key in ('pin', 'touch_threshold'):
989
- self.cleaned_data['controls'][i][key] = int(val)
990
- else:
991
- self.cleaned_data['controls'][i][key] = val
992
-
993
- for i, control in enumerate(self.cleaned_data['controls']):
994
- try:
995
- selected = self.instance.config['controls'][i]['pin']
996
- except:
997
- selected = None
998
- free_pins = get_available_gpio_pins(
999
- self.cleaned_data['colonel'], filters={'input': True},
1000
- selected=selected
1001
- )
1002
- if control['pin'] not in free_pins:
1003
- self.add_error(
1004
- 'controls',
1005
- "Sorry, but GPIO%d pin is occupied."
1006
- % control['pin']
1007
- )
1008
- return
887
+ self._clean_controls()
1009
888
 
1010
889
  return self.cleaned_data
1011
890
 
891
+ def save(self, commit=True):
892
+ self.instance.config['open_pin_no'] = self.cleaned_data['open_pin'].no
893
+ self.instance.config['close_pin_no'] = self.cleaned_data['close_pin'].no
894
+ return super().save(commit=commit)
895
+
1012
896
 
1013
897
  class BurglarSmokeDetectorConfigForm(ColonelComponentForm):
1014
- power_pin = forms.TypedChoiceField(
1015
- coerce=int, choices=get_gpio_pins_choices,
898
+ power_pin = ColonelPinChoiceField(
899
+ queryset=ColonelPin.objects.filter(output=True),
1016
900
  widget=autocomplete.ListSelect2(
1017
901
  url='autocomplete-colonel-pins',
1018
902
  forward=[
@@ -1025,8 +909,8 @@ class BurglarSmokeDetectorConfigForm(ColonelComponentForm):
1025
909
  power_action = forms.ChoiceField(
1026
910
  choices=(('HIGH', "HIGH"), ('LOW', "LOW")),
1027
911
  )
1028
- sensor_pin = forms.TypedChoiceField(
1029
- coerce=int, choices=get_gpio_pins_choices,
912
+ sensor_pin = ColonelPinChoiceField(
913
+ queryset=ColonelPin.objects.filter(input=True),
1030
914
  widget=autocomplete.ListSelect2(
1031
915
  url='autocomplete-colonel-pins',
1032
916
  forward=[
@@ -1053,49 +937,19 @@ class BurglarSmokeDetectorConfigForm(ColonelComponentForm):
1053
937
 
1054
938
  def clean(self):
1055
939
  super().clean()
1056
- if 'colonel' not in self.cleaned_data:
940
+ if not self.cleaned_data.get('colonel'):
1057
941
  return self.cleaned_data
1058
942
  if 'sensor_pin' not in self.cleaned_data:
1059
943
  return self.cleaned_data
1060
944
  if 'power_pin' not in self.cleaned_data:
1061
945
  return self.cleaned_data
1062
946
 
1063
-
1064
- if self.instance.pk:
1065
- selected = self.instance.config.get('power_pin')
1066
- else:
1067
- selected = None
1068
- output_pins = get_available_gpio_pins(
1069
- self.cleaned_data['colonel'], filters={'output': True},
1070
- selected=selected
1071
- )
1072
- if self.cleaned_data['power_pin'] not in output_pins:
1073
- self.add_error(
1074
- 'output_pin',
1075
- "Sorry, but GPIO%d pin can not be used as output pin "
1076
- % self.cleaned_data['power_pin']
1077
- )
1078
- return self.cleaned_data
1079
-
947
+ self._clean_pin('sensor_pin')
948
+ self._clean_pin('power_pin')
1080
949
 
1081
950
 
1082
- if self.instance.pk:
1083
- selected = self.instance.config.get('sensor_pin')
1084
- else:
1085
- selected = None
1086
- input_pins = get_available_gpio_pins(
1087
- self.cleaned_data['colonel'], filters={'input': True},
1088
- selected=selected
1089
- )
1090
- if self.cleaned_data['sensor_pin'] not in input_pins:
1091
- self.add_error(
1092
- 'pin',
1093
- "Sorry, but GPIO%d pin can not be used as input pin "
1094
- % self.cleaned_data['pin']
1095
- )
1096
- return
1097
- if self.cleaned_data['sensor_pin'] > 100:
1098
- if self.cleaned_data['sensor_pin'] < 126:
951
+ if self.cleaned_data['sensor_pin'].no > 100:
952
+ if self.cleaned_data['sensor_pin'].no < 126:
1099
953
  if self.cleaned_data.get('sensor_pull') == 'HIGH':
1100
954
  self.add_error(
1101
955
  'sensor_pull',
@@ -1114,55 +968,43 @@ class BurglarSmokeDetectorConfigForm(ColonelComponentForm):
1114
968
  "if that's what you want to do."
1115
969
  )
1116
970
  elif self.cleaned_data.get('sensor_pull') != 'FLOATING':
1117
- pins_available_for_pull = get_available_gpio_pins(
1118
- self.cleaned_data['colonel'], filters={'output': True},
1119
- selected=selected
1120
- )
1121
- if self.cleaned_data['sensor_pin'] not in pins_available_for_pull:
971
+ if not self.cleaned_data['sensor_pin'].output:
1122
972
  self.add_error(
1123
- 'pin',
1124
- "Sorry, but GPIO%d pin does not have internal pull HIGH/LOW"
1125
- " resistance capability" % self.cleaned_data['sensor_pin']
973
+ 'sensor_pin',
974
+ f"Sorry, but {self.cleaned_data['sensor_pin']} "
975
+ f"does not have internal pull HIGH/LOW"
976
+ " resistance capability"
1126
977
  )
1127
- return
1128
978
 
1129
979
  return self.cleaned_data
1130
980
 
981
+ def save(self, commit=True):
982
+ self.instance.config['sensor_pin_no'] = self.cleaned_data['sensor_pin'].no
983
+ self.instance.config['power_pin_no'] = self.cleaned_data['power_pin'].no
984
+ return super().save(commit=commit)
985
+
1131
986
 
987
+ class TTLockConfigForm(ColonelComponentForm):
988
+ pass
989
+
990
+ def clean(self):
991
+ if not self.instance or not self.instance.pk:
992
+ from .controllers import TTLock
993
+ other_lock = self.cleaned_data['colonel'].components.filter(
994
+ controller_uid=TTLock.uid
995
+ ).first()
996
+ if other_lock:
997
+ raise forms.ValidationError(
998
+ f"Single Colonel can support single TTLock only.\n"
999
+ f"You already have {other_lock} on this Colonel."
1000
+ )
1001
+ return self.cleaned_data
1002
+
1003
+
1004
+ def save(self, commit=True):
1005
+ obj = super(ColonelComponentForm, self).save(commit)
1006
+ if commit:
1007
+ self.cleaned_data['colonel'].components.add(obj)
1008
+ self.cleaned_data['colonel'].save()
1009
+ return obj
1132
1010
 
1133
- #
1134
- # class ColonelBLEClimateSensorConfigForm(
1135
- # ColonelComponentMixin, BaseComponentForm
1136
- # ):
1137
- # colonel = forms.ModelChoiceField(queryset=Colonel.objects.all())
1138
- # ble_device = forms.ModelChoiceField(
1139
- # queryset=BLEDevice.objects.filter(
1140
- # type=BLE_DEVICE_TYPE_GOVEE_MULTISENSOR
1141
- # )
1142
- # )
1143
- # additional_fields = ('colonel', 'ble_device')
1144
- #
1145
- # def __init__(self, *args, **kwargs):
1146
- # super().__init__(*args, **kwargs)
1147
- # qs = self.fields['ble_device'].queryset
1148
- # if self.instance.pk:
1149
- # self.fields['ble_device'].queryset = qs.filter(
1150
- # Q(component__isnull=True) | Q(component=self.instance)
1151
- # )
1152
- # else:
1153
- # self.fields['ble_device'].queryset = qs.filter(component__isnull=True)
1154
- #
1155
- # def clean(self):
1156
- # colonel_ble_devices = self.cleaned_data['colonel'].ble_devices.all()
1157
- # if self.cleaned_data['ble_device'] not in colonel_ble_devices:
1158
- # available_colonels = self.cleaned_data['ble_device'].colonels.all()
1159
- # self.add_error(
1160
- # 'ble_device',
1161
- # _("This BLE device is available only on colonel%s: %s" %
1162
- # (
1163
- # 's' if len(available_colonels) > 1 else '',
1164
- # ', '.join([str(c) for c in available_colonels])
1165
- # )
1166
- # )
1167
- # )
1168
- # return self.cleaned_data