simo 1.7.20__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 (267) hide show
  1. simo/__pycache__/asgi.cpython-38.pyc +0 -0
  2. simo/__pycache__/settings.cpython-38.pyc +0 -0
  3. simo/__pycache__/urls.cpython-38.pyc +0 -0
  4. simo/__pycache__/wsgi.cpython-38.pyc +0 -0
  5. simo/core/__pycache__/admin.cpython-38.pyc +0 -0
  6. simo/core/__pycache__/api.cpython-38.pyc +0 -0
  7. simo/core/__pycache__/api_meta.cpython-38.pyc +0 -0
  8. simo/core/__pycache__/auto_urls.cpython-38.pyc +0 -0
  9. simo/core/__pycache__/autocomplete_views.cpython-38.pyc +0 -0
  10. simo/core/__pycache__/base_types.cpython-38.pyc +0 -0
  11. simo/core/__pycache__/context.cpython-38.pyc +0 -0
  12. simo/core/__pycache__/controllers.cpython-38.pyc +0 -0
  13. simo/core/__pycache__/events.cpython-38.pyc +0 -0
  14. simo/core/__pycache__/forms.cpython-38.pyc +0 -0
  15. simo/core/__pycache__/gateways.cpython-38.pyc +0 -0
  16. simo/core/__pycache__/managers.cpython-38.pyc +0 -0
  17. simo/core/__pycache__/middleware.cpython-38.pyc +0 -0
  18. simo/core/__pycache__/models.cpython-38.pyc +0 -0
  19. simo/core/__pycache__/permissions.cpython-38.pyc +0 -0
  20. simo/core/__pycache__/serializers.cpython-38.pyc +0 -0
  21. simo/core/__pycache__/signal_receivers.cpython-38.pyc +0 -0
  22. simo/core/__pycache__/socket_consumers.cpython-38.pyc +0 -0
  23. simo/core/__pycache__/tasks.cpython-38.pyc +0 -0
  24. simo/core/__pycache__/views.cpython-38.pyc +0 -0
  25. simo/core/admin.py +28 -18
  26. simo/core/api.py +157 -16
  27. simo/core/api_meta.py +87 -0
  28. simo/core/auto_urls.py +4 -1
  29. simo/core/autocomplete_views.py +8 -4
  30. simo/core/base_types.py +1 -0
  31. simo/core/context.py +3 -1
  32. simo/core/controllers.py +112 -32
  33. simo/core/db_backend/base.py +7 -22
  34. simo/core/drf_braces/README +3 -0
  35. simo/core/drf_braces/__init__.py +7 -0
  36. simo/core/drf_braces/__pycache__/__init__.cpython-38.pyc +0 -0
  37. simo/core/drf_braces/__pycache__/utils.cpython-38.pyc +0 -0
  38. simo/core/drf_braces/fields/__init__.py +5 -0
  39. simo/core/drf_braces/fields/__pycache__/__init__.cpython-38.pyc +0 -0
  40. simo/core/drf_braces/fields/__pycache__/_fields.cpython-38.pyc +0 -0
  41. simo/core/drf_braces/fields/__pycache__/custom.cpython-38.pyc +0 -0
  42. simo/core/drf_braces/fields/__pycache__/mixins.cpython-38.pyc +0 -0
  43. simo/core/drf_braces/fields/__pycache__/modified.cpython-38.pyc +0 -0
  44. simo/core/drf_braces/fields/_fields.py +48 -0
  45. simo/core/drf_braces/fields/custom.py +107 -0
  46. simo/core/drf_braces/fields/mixins.py +58 -0
  47. simo/core/drf_braces/fields/modified.py +41 -0
  48. simo/core/drf_braces/forms/__init__.py +0 -0
  49. simo/core/drf_braces/forms/fields.py +20 -0
  50. simo/core/drf_braces/forms/serializer_form.py +156 -0
  51. simo/core/drf_braces/mixins.py +52 -0
  52. simo/core/drf_braces/models.py +0 -0
  53. simo/core/drf_braces/parsers.py +72 -0
  54. simo/core/drf_braces/renderers.py +37 -0
  55. simo/core/drf_braces/serializers/__init__.py +0 -0
  56. simo/core/drf_braces/serializers/__pycache__/__init__.cpython-38.pyc +0 -0
  57. simo/core/drf_braces/serializers/__pycache__/form_serializer.cpython-38.pyc +0 -0
  58. simo/core/drf_braces/serializers/enforce_validation_serializer.py +214 -0
  59. simo/core/drf_braces/serializers/form_serializer.py +391 -0
  60. simo/core/drf_braces/serializers/swapping.py +48 -0
  61. simo/core/drf_braces/tests/__init__.py +0 -0
  62. simo/core/drf_braces/tests/fields/__init__.py +0 -0
  63. simo/core/drf_braces/tests/fields/test_custom.py +94 -0
  64. simo/core/drf_braces/tests/fields/test_fields.py +13 -0
  65. simo/core/drf_braces/tests/fields/test_mixins.py +96 -0
  66. simo/core/drf_braces/tests/fields/test_modified.py +40 -0
  67. simo/core/drf_braces/tests/forms/__init__.py +0 -0
  68. simo/core/drf_braces/tests/forms/test_fields.py +46 -0
  69. simo/core/drf_braces/tests/forms/test_serializer_form.py +256 -0
  70. simo/core/drf_braces/tests/serializers/__init__.py +0 -0
  71. simo/core/drf_braces/tests/serializers/test_enforce_validation_serializer.py +169 -0
  72. simo/core/drf_braces/tests/serializers/test_form_serializer.py +387 -0
  73. simo/core/drf_braces/tests/serializers/test_swapping.py +40 -0
  74. simo/core/drf_braces/tests/test_mixins.py +111 -0
  75. simo/core/drf_braces/tests/test_parsers.py +73 -0
  76. simo/core/drf_braces/tests/test_renderers.py +23 -0
  77. simo/core/drf_braces/tests/test_utils.py +73 -0
  78. simo/core/drf_braces/utils.py +209 -0
  79. simo/core/events.py +3 -3
  80. simo/core/forms.py +79 -37
  81. simo/core/gateways.py +31 -14
  82. simo/core/management/commands/gateways_manager.py +0 -1
  83. simo/core/managers.py +81 -0
  84. simo/core/middleware.py +25 -0
  85. simo/core/migrations/0026_category_instance.py +20 -0
  86. simo/core/migrations/0027_remove_component_tags.py +17 -0
  87. simo/core/migrations/0028_rename_subcomponents_component_slaves.py +18 -0
  88. simo/core/migrations/0029_auto_20240229_1331.py +33 -0
  89. simo/core/migrations/__pycache__/0026_category_instance.cpython-38.pyc +0 -0
  90. simo/core/migrations/__pycache__/0027_remove_component_tags.cpython-38.pyc +0 -0
  91. simo/core/migrations/__pycache__/0028_rename_subcomponents_component_slaves.cpython-38.pyc +0 -0
  92. simo/core/migrations/__pycache__/0029_auto_20240229_1331.cpython-38.pyc +0 -0
  93. simo/core/models.py +103 -66
  94. simo/core/permissions.py +28 -2
  95. simo/core/serializers.py +330 -26
  96. simo/core/socket_consumers.py +5 -14
  97. simo/core/tasks.py +11 -1
  98. simo/core/templates/admin/base.html +37 -10
  99. simo/core/templates/admin/wizard/discovery.html +188 -0
  100. simo/core/templates/admin/wizard/wizard_add.html +5 -5
  101. simo/core/utils/__pycache__/serialization.cpython-38.pyc +0 -0
  102. simo/core/utils/admin.py +9 -2
  103. simo/core/utils/formsets.py +17 -16
  104. simo/core/utils/helpers.py +1 -0
  105. simo/core/utils/serialization.py +56 -0
  106. simo/core/utils/type_constants.py +1 -1
  107. simo/core/utils/validators.py +14 -1
  108. simo/core/views.py +13 -0
  109. simo/fleet/__pycache__/admin.cpython-38.pyc +0 -0
  110. simo/fleet/__pycache__/api.cpython-38.pyc +0 -0
  111. simo/fleet/__pycache__/auto_urls.cpython-38.pyc +0 -0
  112. simo/fleet/__pycache__/controllers.cpython-38.pyc +0 -0
  113. simo/fleet/__pycache__/forms.cpython-38.pyc +0 -0
  114. simo/fleet/__pycache__/gateways.cpython-38.pyc +0 -0
  115. simo/fleet/__pycache__/managers.cpython-38.pyc +0 -0
  116. simo/fleet/__pycache__/models.cpython-38.pyc +0 -0
  117. simo/fleet/__pycache__/serializers.cpython-38.pyc +0 -0
  118. simo/fleet/__pycache__/socket_consumers.cpython-38.pyc +0 -0
  119. simo/fleet/__pycache__/utils.cpython-38.pyc +0 -0
  120. simo/fleet/__pycache__/views.cpython-38.pyc +0 -0
  121. simo/fleet/admin.py +54 -25
  122. simo/fleet/api.py +59 -3
  123. simo/fleet/auto_urls.py +2 -3
  124. simo/fleet/controllers.py +199 -16
  125. simo/fleet/forms.py +325 -483
  126. simo/fleet/gateways.py +44 -2
  127. simo/fleet/managers.py +32 -0
  128. simo/fleet/migrations/0025_auto_20240130_1334.py +27 -0
  129. simo/fleet/migrations/0026_rename_i2cinterface_scl_pin_and_more.py +64 -0
  130. simo/fleet/migrations/0027_auto_20240306_0802.py +170 -0
  131. simo/fleet/migrations/0028_remove_i2cinterface_scl_pin_no_and_more.py +21 -0
  132. simo/fleet/migrations/0029_alter_i2cinterface_scl_pin_and_more.py +24 -0
  133. simo/fleet/migrations/0030_colonelpin_label_alter_colonel_type.py +24 -0
  134. simo/fleet/migrations/0031_alter_colonel_type.py +18 -0
  135. simo/fleet/migrations/__pycache__/0025_auto_20240130_1334.cpython-38.pyc +0 -0
  136. simo/fleet/migrations/__pycache__/0026_rename_i2cinterface_scl_pin_and_more.cpython-38.pyc +0 -0
  137. simo/fleet/migrations/__pycache__/0027_auto_20240306_0802.cpython-38.pyc +0 -0
  138. simo/fleet/migrations/__pycache__/0028_remove_i2cinterface_scl_pin_no_and_more.cpython-38.pyc +0 -0
  139. simo/fleet/migrations/__pycache__/0029_alter_i2cinterface_scl_pin_and_more.cpython-38.pyc +0 -0
  140. simo/fleet/migrations/__pycache__/0030_colonelpin_label_alter_colonel_type.cpython-38.pyc +0 -0
  141. simo/fleet/migrations/__pycache__/0031_alter_colonel_type.cpython-38.pyc +0 -0
  142. simo/fleet/models.py +134 -82
  143. simo/fleet/serializers.py +35 -1
  144. simo/fleet/socket_consumers.py +239 -76
  145. simo/fleet/utils.py +15 -53
  146. simo/fleet/views.py +28 -14
  147. simo/generic/controllers.py +13 -89
  148. simo/generic/forms.py +29 -18
  149. simo/generic/gateways.py +73 -2
  150. simo/generic/models.py +3 -3
  151. simo/multimedia/controllers.py +9 -8
  152. simo/settings.py +7 -4
  153. simo/urls.py +4 -8
  154. simo/users/__pycache__/admin.cpython-38.pyc +0 -0
  155. simo/users/__pycache__/api.cpython-38.pyc +0 -0
  156. simo/users/__pycache__/auto_urls.cpython-38.pyc +0 -0
  157. simo/users/__pycache__/models.cpython-38.pyc +0 -0
  158. simo/users/__pycache__/serializers.cpython-38.pyc +0 -0
  159. simo/users/__pycache__/sso_urls.cpython-38.pyc +0 -0
  160. simo/users/admin.py +8 -1
  161. simo/users/api.py +38 -2
  162. simo/users/auto_urls.py +2 -2
  163. simo/users/migrations/0025_rename_name_fingerprint_type_and_more.py +22 -0
  164. simo/users/migrations/__pycache__/0025_rename_name_fingerprint_type_and_more.cpython-38.pyc +0 -0
  165. simo/users/models.py +2 -3
  166. simo/users/serializers.py +15 -1
  167. simo/users/sso_urls.py +3 -3
  168. simo/wsgi.py +7 -0
  169. {simo-1.7.20.dist-info → simo-2.0.0.dist-info}/METADATA +8 -9
  170. {simo-1.7.20.dist-info → simo-2.0.0.dist-info}/RECORD +173 -189
  171. {simo-1.7.20.dist-info → simo-2.0.0.dist-info}/WHEEL +1 -1
  172. simo/core/db_backend/__pycache__/__init__.cpython-38.pyc +0 -0
  173. simo/core/db_backend/__pycache__/base.cpython-38.pyc +0 -0
  174. simo/core/management/commands/__pycache__/gateways_manager.cpython-38.pyc +0 -0
  175. simo/core/migrations/__pycache__/0001_initial.cpython-38.pyc +0 -0
  176. simo/core/migrations/__pycache__/0002_load_icons.cpython-38.pyc +0 -0
  177. simo/core/migrations/__pycache__/0003_create_default_zones_and_categories.cpython-38.pyc +0 -0
  178. simo/core/migrations/__pycache__/0004_create_generic.cpython-38.pyc +0 -0
  179. simo/core/migrations/__pycache__/0005_component_subcomponents.cpython-38.pyc +0 -0
  180. simo/core/migrations/__pycache__/0006_alter_component_subcomponents.cpython-38.pyc +0 -0
  181. simo/core/migrations/__pycache__/0007_component_change_init_to.cpython-38.pyc +0 -0
  182. simo/core/migrations/__pycache__/0008_alter_component_change_init_to.cpython-38.pyc +0 -0
  183. simo/core/migrations/__pycache__/0009_auto_20220707_1404.cpython-38.pyc +0 -0
  184. simo/core/migrations/__pycache__/0010_historyaggregate.cpython-38.pyc +0 -0
  185. simo/core/migrations/__pycache__/0011_component_last_change.cpython-38.pyc +0 -0
  186. simo/core/migrations/__pycache__/0012_instance.cpython-38.pyc +0 -0
  187. simo/core/migrations/__pycache__/0013_auto_20231003_0754.cpython-38.pyc +0 -0
  188. simo/core/migrations/__pycache__/0014_zone_instance.cpython-38.pyc +0 -0
  189. simo/core/migrations/__pycache__/0015_auto_20231004_1113.cpython-38.pyc +0 -0
  190. simo/core/migrations/__pycache__/0016_auto_20231004_1113.cpython-38.pyc +0 -0
  191. simo/core/migrations/__pycache__/0017_auto_20231004_1313.cpython-38.pyc +0 -0
  192. simo/core/migrations/__pycache__/0018_auto_20231005_0622.cpython-38.pyc +0 -0
  193. simo/core/migrations/__pycache__/0019_alter_gateway_type.cpython-38.pyc +0 -0
  194. simo/core/migrations/__pycache__/0020_component_meta.cpython-38.pyc +0 -0
  195. simo/core/migrations/__pycache__/0021_auto_20231020_1041.cpython-38.pyc +0 -0
  196. simo/core/migrations/__pycache__/__init__.cpython-38.pyc +0 -0
  197. simo/core/templatetags/__pycache__/__init__.cpython-38.pyc +0 -0
  198. simo/core/templatetags/__pycache__/components_list.cpython-38.pyc +0 -0
  199. simo/core/utils/__pycache__/__init__.cpython-38.pyc +0 -0
  200. simo/core/utils/__pycache__/admin.cpython-38.pyc +0 -0
  201. simo/core/utils/__pycache__/config_values.cpython-38.pyc +0 -0
  202. simo/core/utils/__pycache__/easing.cpython-38.pyc +0 -0
  203. simo/core/utils/__pycache__/form_fields.cpython-38.pyc +0 -0
  204. simo/core/utils/__pycache__/form_widgets.cpython-38.pyc +0 -0
  205. simo/core/utils/__pycache__/formsets.cpython-38.pyc +0 -0
  206. simo/core/utils/__pycache__/helpers.cpython-38.pyc +0 -0
  207. simo/core/utils/__pycache__/logs.cpython-38.pyc +0 -0
  208. simo/core/utils/__pycache__/mixins.cpython-38.pyc +0 -0
  209. simo/core/utils/__pycache__/model_helpers.cpython-38.pyc +0 -0
  210. simo/core/utils/__pycache__/relay.cpython-38.pyc +0 -0
  211. simo/core/utils/__pycache__/type_constants.cpython-38.pyc +0 -0
  212. simo/core/utils/__pycache__/validators.cpython-38.pyc +0 -0
  213. simo/fleet/tasks.py +0 -25
  214. simo/generic/__pycache__/__init__.cpython-37.pyc +0 -0
  215. simo/generic/__pycache__/__init__.cpython-38.pyc +0 -0
  216. simo/generic/__pycache__/app_widgets.cpython-38.pyc +0 -0
  217. simo/generic/__pycache__/base_types.cpython-38.pyc +0 -0
  218. simo/generic/__pycache__/controllers.cpython-37.pyc +0 -0
  219. simo/generic/__pycache__/controllers.cpython-38.pyc +0 -0
  220. simo/generic/__pycache__/forms.cpython-38.pyc +0 -0
  221. simo/generic/__pycache__/gateways.cpython-38.pyc +0 -0
  222. simo/generic/__pycache__/models.cpython-38.pyc +0 -0
  223. simo/generic/__pycache__/routing.cpython-38.pyc +0 -0
  224. simo/generic/__pycache__/socket_consumers.cpython-38.pyc +0 -0
  225. simo/generic/__pycache__/widgets.cpython-37.pyc +0 -0
  226. simo/multimedia/__pycache__/__init__.cpython-38.pyc +0 -0
  227. simo/multimedia/__pycache__/admin.cpython-38.pyc +0 -0
  228. simo/multimedia/__pycache__/api.cpython-38.pyc +0 -0
  229. simo/multimedia/__pycache__/app_widgets.cpython-38.pyc +0 -0
  230. simo/multimedia/__pycache__/base_types.cpython-38.pyc +0 -0
  231. simo/multimedia/__pycache__/controllers.cpython-38.pyc +0 -0
  232. simo/multimedia/__pycache__/forms.cpython-38.pyc +0 -0
  233. simo/multimedia/__pycache__/models.cpython-38.pyc +0 -0
  234. simo/multimedia/__pycache__/serializers.cpython-38.pyc +0 -0
  235. simo/multimedia/migrations/__pycache__/0001_initial.cpython-38.pyc +0 -0
  236. simo/multimedia/migrations/__pycache__/0002_sound_length.cpython-38.pyc +0 -0
  237. simo/multimedia/migrations/__pycache__/0003_alter_sound_length.cpython-38.pyc +0 -0
  238. simo/multimedia/migrations/__pycache__/0004_auto_20231023_1055.cpython-38.pyc +0 -0
  239. simo/multimedia/migrations/__pycache__/__init__.cpython-38.pyc +0 -0
  240. simo/notifications/__pycache__/__init__.cpython-38.pyc +0 -0
  241. simo/notifications/__pycache__/admin.cpython-38.pyc +0 -0
  242. simo/notifications/__pycache__/api.cpython-38.pyc +0 -0
  243. simo/notifications/__pycache__/models.cpython-38.pyc +0 -0
  244. simo/notifications/__pycache__/serializers.cpython-38.pyc +0 -0
  245. simo/notifications/__pycache__/utils.cpython-38.pyc +0 -0
  246. simo/notifications/migrations/__pycache__/0001_initial.cpython-38.pyc +0 -0
  247. simo/notifications/migrations/__pycache__/0002_notification_instance.cpython-38.pyc +0 -0
  248. simo/notifications/migrations/__pycache__/__init__.cpython-38.pyc +0 -0
  249. simo/users/migrations/__pycache__/0001_initial.cpython-38.pyc +0 -0
  250. simo/users/migrations/__pycache__/0002_componentpermission.cpython-38.pyc +0 -0
  251. simo/users/migrations/__pycache__/0003_create_roles_and_system_user.cpython-38.pyc +0 -0
  252. simo/users/migrations/__pycache__/0004_user_secret_key.cpython-38.pyc +0 -0
  253. simo/users/migrations/__pycache__/0005_permissionsrole_instance.cpython-38.pyc +0 -0
  254. simo/users/migrations/__pycache__/0006_auto_20231003_0850.cpython-38.pyc +0 -0
  255. simo/users/migrations/__pycache__/0007_auto_20231003_1228.cpython-38.pyc +0 -0
  256. simo/users/migrations/__pycache__/0008_auto_20231003_1229.cpython-38.pyc +0 -0
  257. simo/users/migrations/__pycache__/0009_remove_user_role.cpython-38.pyc +0 -0
  258. simo/users/migrations/__pycache__/0010_auto_20231004_1313.cpython-38.pyc +0 -0
  259. simo/users/migrations/__pycache__/0011_auto_20231004_1313.cpython-38.pyc +0 -0
  260. simo/users/migrations/__pycache__/0012_alter_userinstancerole_unique_together.cpython-38.pyc +0 -0
  261. simo/users/migrations/__pycache__/0013_remove_user_roles.cpython-38.pyc +0 -0
  262. simo/users/migrations/__pycache__/0014_user_roles.cpython-38.pyc +0 -0
  263. simo/users/migrations/__pycache__/0015_remove_user_at_home.cpython-38.pyc +0 -0
  264. simo/users/migrations/__pycache__/0016_auto_20231005_1050.cpython-38.pyc +0 -0
  265. simo/users/migrations/__pycache__/__init__.cpython-38.pyc +0 -0
  266. {simo-1.7.20.dist-info → simo-2.0.0.dist-info}/LICENSE.md +0 -0
  267. {simo-1.7.20.dist-info → simo-2.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,73 @@
1
+ from __future__ import absolute_import, print_function, unicode_literals
2
+ import unittest
3
+
4
+ from rest_framework import fields
5
+
6
+ from ..utils import (
7
+ find_class_args,
8
+ find_function_args,
9
+ get_attr_from_base_classes,
10
+ get_class_name_with_new_suffix,
11
+ )
12
+
13
+
14
+ class TestUtils(unittest.TestCase):
15
+ def test_get_class_name_with_new_suffix(self):
16
+ new_name = get_class_name_with_new_suffix(
17
+ klass=fields.IntegerField,
18
+ existing_suffix='Field',
19
+ new_suffix='StrawberryFields'
20
+ )
21
+ self.assertEqual(new_name, 'IntegerStrawberryFields')
22
+
23
+ new_name = get_class_name_with_new_suffix(
24
+ klass=fields.IntegerField,
25
+ existing_suffix='straws',
26
+ new_suffix='Blueberries'
27
+ )
28
+ self.assertEqual(new_name, 'IntegerFieldBlueberries')
29
+
30
+ def test_get_attr_from_base_classes(self):
31
+ Parent = type(str('Parent'), (), {'fields': 'pancakes'})
32
+
33
+ self.assertEqual(
34
+ get_attr_from_base_classes((Parent,), [], 'fields'), 'pancakes'
35
+ )
36
+
37
+ self.assertEqual(
38
+ get_attr_from_base_classes(
39
+ (Parent,), {'fields': 'mushrooms'}, 'fields'
40
+ ),
41
+ 'mushrooms'
42
+ )
43
+
44
+ self.assertEqual(
45
+ get_attr_from_base_classes((Parent,), [], '', default='maple_syrup'),
46
+ 'maple_syrup'
47
+ )
48
+
49
+ with self.assertRaises(AttributeError):
50
+ get_attr_from_base_classes(
51
+ (Parent,), {'fields': 'mushrooms'}, 'catchmeifyoucan'
52
+ )
53
+
54
+ def test_find_function_args(self):
55
+ def foo(a, b, c):
56
+ pass
57
+
58
+ self.assertListEqual(find_function_args(foo), ['a', 'b', 'c'])
59
+
60
+ def test_find_function_args_invalid(self):
61
+ self.assertListEqual(find_function_args(None), [])
62
+
63
+ def test_find_class_args(self):
64
+ class Bar(object):
65
+ def __init__(self, a, b):
66
+ pass
67
+
68
+ class Foo(Bar):
69
+ def __init__(self, c, d):
70
+ super(Foo, self).__init__(None, None)
71
+ pass
72
+
73
+ self.assertSetEqual(set(find_class_args(Foo)), {'a', 'b', 'c', 'd'})
@@ -0,0 +1,209 @@
1
+ from __future__ import absolute_import, print_function, unicode_literals
2
+ import inspect
3
+ import itertools
4
+
5
+
6
+ IGNORE_ARGS = ['self', 'cls']
7
+
8
+
9
+ def find_function_args(func):
10
+ """
11
+ Get the list of parameter names which function accepts.
12
+ """
13
+ func_args = []
14
+ try:
15
+ spec = inspect.getfullargspec(func) if hasattr(inspect, 'getfullargspec') else inspect.getargspec(func)
16
+ except TypeError:
17
+ return []
18
+
19
+ func_args.extend([i for i in spec[0] if i not in IGNORE_ARGS])
20
+ func_args.extend([i for i in spec.kwonlyargs])
21
+
22
+ return func_args
23
+
24
+ def find_class_args(klass):
25
+ """
26
+ Find all class arguments (parameters) which can be passed in ``__init__``.
27
+ """
28
+ args = set()
29
+
30
+ for i in klass.__mro__:
31
+ if i is object or not hasattr(i, '__init__'):
32
+ continue
33
+ args |= set(find_function_args(i.__init__))
34
+
35
+ return list(args)
36
+
37
+
38
+ def find_matching_class_kwargs(reference_object, klass):
39
+ return {
40
+ i: getattr(reference_object, i) for i in find_class_args(klass)
41
+ if hasattr(reference_object, i)
42
+ }
43
+
44
+
45
+ def add_base_class_to_instance(instance, base_class=None, new_name=None):
46
+ """
47
+ Generic utility for adding a base class to an instance.
48
+
49
+ This function returns a copy of the given instance which
50
+ will then include the new base_class in its ``__mro__``.
51
+
52
+ The way that is done internally is it creates a brand new
53
+ class with correct bases. Then the newly created class is
54
+ instantiated. Since ``__init__`` could be expensive operation
55
+ in any of the base classes of the original instance mro,
56
+ nto make it cheap, we temporarily switch __init__ with
57
+ super simple implementation which does nothing but only
58
+ instantiates class. Once instantiated, then we copy all of the
59
+ instance attributes to the newly created instance.
60
+ Finally, then we pop our mock ``__init__`` implementation.
61
+
62
+ Args:
63
+ instance (object): Instance of any object
64
+ base_class (type): Any class which will be added as first class
65
+ in the newly copied instance mro.
66
+
67
+ Returns:
68
+ Shallow copy of ``instance`` which will also inherit ``base_class``.
69
+ """
70
+ # overwrite __init__ since that is mainly responsible for setting
71
+ # instance state but since we explicitly copy it, we can
72
+ # make __init__ a noop method
73
+ def __init__(self, *args, **kwargs):
74
+ pass
75
+
76
+ if base_class is not None and base_class not in instance.__class__.mro():
77
+ base_classes = (base_class, instance.__class__)
78
+ else:
79
+ base_classes = (instance.__class__,)
80
+
81
+ new_field_class = type(
82
+ str(new_name or instance.__class__.__name__),
83
+ base_classes,
84
+ {'__init__': __init__}
85
+ )
86
+
87
+ new_instance = new_field_class()
88
+ new_instance.__dict__.update(instance.__dict__)
89
+
90
+ # we added __init__ just for faster instantiation
91
+ # since then we dont have to copy all the parameters
92
+ # when creating new instance and then update its state
93
+ # however after we instantiated the class, we want to
94
+ # pop our silly __init__ implementation so that if somebody
95
+ # wants to instantiate instance.__class__(), it will
96
+ # use the original __init__ method
97
+ del new_field_class.__init__
98
+
99
+ return new_instance
100
+
101
+
102
+ def initialize_class_using_reference_object(reference_object, klass, **kwargs):
103
+ """
104
+ Utility function which instantiates ``klass`` by extracting ``__init__``
105
+ kwargs from ``reference_object`` attributes.
106
+
107
+ Args:
108
+ reference_object (object): Any object instance from which matching
109
+ attributes will be used as ``klass``'s ``__init__`` kwargs.
110
+ klass (type): Class which will be instantiated by using
111
+ ``reference_object`` attributes.
112
+ **kwargs: Any additional kwargs which will be passed during instantiation.
113
+
114
+ Returns:
115
+ Instantiated ``klass`` object.
116
+ """
117
+ _kwargs = find_matching_class_kwargs(reference_object, klass)
118
+ _kwargs.update(kwargs)
119
+
120
+ return klass(**_kwargs)
121
+
122
+
123
+ def get_class_name_with_new_suffix(klass, existing_suffix, new_suffix):
124
+ """
125
+ Generates new name by replacing the existing suffix with a new one.
126
+
127
+ Args:
128
+ klass (type): original class from which new name is generated
129
+ existing_suffix (str): the suffix which needs to remain where it is
130
+ new_suffix (str): the new suffix desired
131
+
132
+ Example:
133
+ >>> get_class_name_with_new_suffix(FooForm, 'Form', 'NewForm')
134
+ 'FooNewForm'
135
+
136
+ Returns:
137
+ new_name (str): the name with the new suffix
138
+ """
139
+ class_name = klass.__name__
140
+
141
+ if existing_suffix in class_name:
142
+ prefix, suffix = class_name.rsplit(existing_suffix, 1)
143
+ else:
144
+ prefix, suffix = class_name, ''
145
+
146
+ new_name = str('{}{}{}'.format(prefix, new_suffix, suffix))
147
+
148
+ return new_name
149
+
150
+
151
+ def get_attr_from_base_classes(bases, attrs, attr, default=None):
152
+ """
153
+ The attribute is retrieved from the base classes if they are not already
154
+ present on the object.
155
+
156
+ Args:
157
+ bases (tuple, list): The base classes for a class.
158
+ attrs (dict): The attributes of the class.
159
+ attr (str): Specific attribute being looked for.
160
+ default (any): Whatever default value is expected if the
161
+ attr is not found.
162
+
163
+ Returns:
164
+ attribute value as found in base classes or a default when attribute
165
+ is not found and default is provided.
166
+
167
+ Raises:
168
+ AttributeError: When the attribute is not present anywhere in the
169
+ call chain hierarchy specified through bases and the attributes
170
+ of the class itself
171
+ """
172
+ if attr in attrs:
173
+ return attrs[attr]
174
+
175
+ for base in bases:
176
+ try:
177
+ return getattr(base, attr)
178
+ except AttributeError:
179
+ continue
180
+
181
+ if default is not None:
182
+ return default
183
+
184
+ raise AttributeError(
185
+ 'None of the bases have {} attribute'
186
+ ''.format(attr)
187
+ )
188
+
189
+
190
+ def reduce_attr_dict_from_base_classes(bases, getter, default=None):
191
+ data = (default or {}).copy()
192
+
193
+ # get all field mappings from super methods
194
+ for base in itertools.chain(*[reversed(i.mro()) for i in reversed(bases)]):
195
+ data.update(getter(base) or {})
196
+
197
+ return data
198
+
199
+
200
+ def reduce_attr_dict_from_instance(self, getter, default=None):
201
+ data = reduce_attr_dict_from_base_classes(type(self).mro(), getter, default)
202
+
203
+ # this should of been picked by reduce_attr_dict_from_base_classes
204
+ # however that only accounts base classes and not instance
205
+ # attribute which can be modified in __init__, etc so we
206
+ # explicitly account for it
207
+ data.update(getter(self) or {})
208
+
209
+ return data
simo/core/events.py CHANGED
@@ -30,10 +30,10 @@ class ObjMqttAnnouncement:
30
30
  assert self.data is not None
31
31
  mqtt_publish.single(
32
32
  self.get_topic(), json.dumps(self.data, default=str),
33
+ retain=retain,
33
34
  hostname=settings.MQTT_HOST,
34
35
  port=settings.MQTT_PORT,
35
36
  auth={'username': 'root', 'password': settings.SECRET_KEY},
36
- retain=retain
37
37
  )
38
38
 
39
39
  def get_topic(self):
@@ -54,8 +54,8 @@ class ObjectChangeEvent(ObjMqttAnnouncement):
54
54
  return f"{self.TOPIC}/{self.instance.id if self.instance else 'global'}/" \
55
55
  f"{type(self.obj).__name__}-{self.data['obj_pk']}"
56
56
 
57
- def publish(self, retain=False):
58
- return super().publish(retain=True)
57
+ def publish(self, retain=True):
58
+ return super().publish(retain=retain)
59
59
 
60
60
 
61
61
  class GatewayObjectCommand(ObjMqttAnnouncement):
simo/core/forms.py CHANGED
@@ -1,6 +1,7 @@
1
1
  import os
2
2
  import traceback
3
3
  import requests
4
+ from dal import forward
4
5
  from django import forms
5
6
  from django.contrib.admin.forms import AdminAuthenticationForm as OrgAdminAuthenticationForm
6
7
  from django.db import models
@@ -22,6 +23,7 @@ from .widgets import SVGFileWidget, PythonCode, LogOutputWidget
22
23
  from .widgets import ImageWidget
23
24
  from .utils.helpers import get_random_string
24
25
  from .utils.formsets import FormsetField
26
+ from .utils.validators import validate_slaves
25
27
 
26
28
 
27
29
  class HubConfigForm(forms.Form):
@@ -156,7 +158,9 @@ class ConfigFieldsMixin:
156
158
 
157
159
  def __init__(self, *args, **kwargs):
158
160
  super().__init__(*args, **kwargs)
159
- self.model_fields = [f.name for f in Component._meta.fields]
161
+ self.model_fields = [
162
+ f.name for f in Component._meta.fields
163
+ ] + ['slaves', ]
160
164
  self.config_fields = []
161
165
  for field_name, field in self.fields.items():
162
166
  if field_name in self.model_fields:
@@ -164,21 +168,22 @@ class ConfigFieldsMixin:
164
168
  self.config_fields.append(field_name)
165
169
  if self.instance.pk:
166
170
  for field_name in self.config_fields:
167
- if self.instance.config.get(field_name) != None:
168
- if hasattr(self.fields[field_name], 'queryset'):
169
- if isinstance(self.instance.config[field_name], list):
170
- self.fields[field_name].initial = \
171
- self.fields[field_name].queryset.filter(
172
- pk__in=self.instance.config[field_name]
173
- )
174
- else:
175
- self.fields[field_name].initial = \
176
- self.fields[field_name].queryset.filter(
177
- pk=self.instance.config[field_name]
178
- ).first()
171
+ if field_name not in self.instance.config:
172
+ continue
173
+ if hasattr(self.fields[field_name], 'queryset'):
174
+ if isinstance(self.instance.config.get(field_name), list):
175
+ self.fields[field_name].initial = \
176
+ self.fields[field_name].queryset.filter(
177
+ pk__in=self.instance.config.get(field_name)
178
+ )
179
179
  else:
180
180
  self.fields[field_name].initial = \
181
- self.instance.config[field_name]
181
+ self.fields[field_name].queryset.filter(
182
+ pk=self.instance.config.get(field_name)
183
+ ).first()
184
+ else:
185
+ self.fields[field_name].initial = \
186
+ self.instance.config.get(field_name)
182
187
 
183
188
  def save(self, commit=True):
184
189
  for field_name in self.config_fields:
@@ -272,7 +277,7 @@ class ComponentAdminForm(forms.ModelForm):
272
277
  fields = '__all__'
273
278
  exclude = (
274
279
  'gateway', 'controller_uid', 'base_type',
275
- 'alive', 'value_type', 'value'
280
+ 'alive', 'value_type', 'value', 'arm_status',
276
281
  )
277
282
  widgets = {
278
283
  'icon': autocomplete.ModelSelect2(
@@ -289,29 +294,31 @@ class ComponentAdminForm(forms.ModelForm):
289
294
 
290
295
  def __init__(self, *args, **kwargs):
291
296
  self.request = kwargs.pop('request', None)
292
- if 'gateway' in kwargs:
293
- self.gateway = kwargs.pop('gateway')
294
- self.controller_cls = kwargs.pop('controller_cls')
297
+ self.controller_uid = kwargs.pop('controller_uid', '')
295
298
  super().__init__(*args, **kwargs)
296
299
  if self.instance.pk:
297
300
  self.gateway = self.instance.gateway
298
301
  self.controller = self.instance.controller
299
302
  else:
300
- self.controller = self.controller_cls(self.instance)
301
- self.instance.gateway = self.gateway
302
- self.instance.controller_uid = self.controller_cls.uid
303
- self.instance.base_type = self.controller.base_type
304
- self.instance.value = self.controller.default_value
305
- self.instance.value_previous = self.controller.default_value
306
- self.instance.config = self.controller.default_config
307
- self.instance.meta = self.controller.default_meta
308
-
309
-
303
+ from .utils.type_constants import get_controller_types_map
304
+ ControllerClass = get_controller_types_map().get(self.controller_uid)
305
+ if ControllerClass:
306
+ self.controller = ControllerClass(self.instance)
307
+ self.gateway = Gateway.objects.filter(
308
+ type=ControllerClass.gateway_class.uid
309
+ ).first()
310
+ self.instance.gateway = self.gateway
311
+ self.instance.controller_uid = ControllerClass.uid
312
+ self.instance.base_type = self.controller.base_type
313
+ self.instance.value = self.controller.default_value
314
+ self.instance.value_previous = self.controller.default_value
315
+ self.instance.config = self.controller.default_config
316
+ self.instance.meta = self.controller.default_meta
310
317
 
311
318
  @classmethod
312
319
  def get_admin_fieldsets(cls, request, obj=None):
313
320
  main_fields = (
314
- 'name', 'icon', 'zone', 'category', 'tags',
321
+ 'name', 'icon', 'zone', 'category',
315
322
  'show_in_app', 'battery_level',
316
323
  'instance_methods', 'value_units',
317
324
  'alarm_category', 'arm_status',
@@ -323,7 +330,7 @@ class ComponentAdminForm(forms.ModelForm):
323
330
  base_fields.append('zone')
324
331
  base_fields.append('category')
325
332
 
326
- for field_name in cls.base_fields:
333
+ for field_name in cls.declared_fields:
327
334
  if field_name not in main_fields:
328
335
  base_fields.append(field_name)
329
336
 
@@ -384,6 +391,8 @@ class ComponentAdminForm(forms.ModelForm):
384
391
  return self.cleaned_data['instance_methods']
385
392
 
386
393
 
394
+
395
+
387
396
  class BaseComponentForm(ConfigFieldsMixin, ComponentAdminForm):
388
397
  pass
389
398
 
@@ -405,8 +414,8 @@ class ValueLimitsMixin:
405
414
 
406
415
 
407
416
  class NumericSensorForm(BaseComponentForm):
408
- widget = forms.TypedChoiceField(
409
- coerce=str, initial='numeric-sensor', choices=(
417
+ widget = forms.ChoiceField(
418
+ initial='numeric-sensor', choices=(
410
419
  ('numeric-sensor', "Basic Sensor"),
411
420
  ('numeric-sensor-graph', "Graph"),
412
421
  )
@@ -470,6 +479,28 @@ class MultiSensorConfigForm(BaseComponentForm):
470
479
  has_icon = False
471
480
 
472
481
 
482
+ class SwitchForm(BaseComponentForm):
483
+ slaves = forms.ModelMultipleChoiceField(
484
+ required=False,
485
+ queryset=Component.objects.filter(
486
+ base_type__in=(
487
+ 'dimmer', 'switch', 'blinds', 'script'
488
+ )
489
+ ),
490
+ widget=autocomplete.ModelSelect2Multiple(
491
+ url='autocomplete-component', attrs={'data-html': True},
492
+ forward=(forward.Const(
493
+ ['dimmer', 'switch', 'blinds', 'script'], 'base_type'),
494
+ )
495
+ )
496
+ )
497
+
498
+ def clean_slaves(self):
499
+ if not self.cleaned_data['slaves'] or not self.instance:
500
+ return self.cleaned_data['slaves']
501
+ return validate_slaves(self.cleaned_data['slaves'], self.instance)
502
+
503
+
473
504
  class DoubleSwitchConfigForm(BaseComponentForm):
474
505
  icon_1 = forms.ModelChoiceField(
475
506
  queryset=Icon.objects.all(),
@@ -580,6 +611,21 @@ class DimmerConfigForm(BaseComponentForm):
580
611
  inverse = forms.BooleanField(
581
612
  label=_("Inverse dimmer signal"), required=False
582
613
  )
614
+ slaves = forms.ModelMultipleChoiceField(
615
+ required=False,
616
+ queryset=Component.objects.filter(
617
+ base_type__in='dimmer',
618
+ ),
619
+ widget=autocomplete.ModelSelect2Multiple(
620
+ url='autocomplete-component', attrs={'data-html': True},
621
+ forward=(forward.Const(['dimmer', ], 'base_type'),)
622
+ )
623
+ )
624
+
625
+ def clean_slaves(self):
626
+ if not self.cleaned_data['slaves'] or not self.instance:
627
+ return self.cleaned_data['slaves']
628
+ return validate_slaves(self.cleaned_data['slaves'], self.instance)
583
629
 
584
630
 
585
631
  class DimmerPlusConfigForm(BaseComponentForm):
@@ -600,8 +646,4 @@ class DimmerPlusConfigForm(BaseComponentForm):
600
646
  class RGBWConfigForm(BaseComponentForm):
601
647
  has_white = forms.BooleanField(
602
648
  label=_("Has WHITE color channel"), required=False,
603
- )
604
-
605
-
606
-
607
-
649
+ )
simo/core/gateways.py CHANGED
@@ -3,6 +3,7 @@ import time
3
3
  import json
4
4
  import paho.mqtt.client as mqtt
5
5
  from django.conf import settings
6
+ from django.db import close_old_connections
6
7
  from abc import ABC, abstractmethod
7
8
  from simo.core.utils.helpers import classproperty
8
9
  from simo.core.events import GatewayObjectCommand, get_event_obj
@@ -59,7 +60,7 @@ class BaseObjectCommandsGatewayHandler(BaseGatewayHandler):
59
60
 
60
61
  for task, period in self.periodic_tasks:
61
62
  threading.Thread(
62
- target=self._run_periodic_task, args=(task, period), daemon=True
63
+ target=self._run_periodic_task, args=(self.exit, task, period), daemon=True
63
64
  ).start()
64
65
 
65
66
  self.mqtt_client.connect(host=settings.MQTT_HOST, port=settings.MQTT_PORT)
@@ -70,8 +71,8 @@ class BaseObjectCommandsGatewayHandler(BaseGatewayHandler):
70
71
 
71
72
  self.mqtt_client.loop_stop()
72
73
 
73
- def _run_periodic_task(self, task, period):
74
- while not self.exit.is_set():
74
+ def _run_periodic_task(self, exit, task, period):
75
+ while not exit.is_set():
75
76
  try:
76
77
  print(f"Run periodic task {task}!")
77
78
  getattr(self, task)()
@@ -86,19 +87,35 @@ class BaseObjectCommandsGatewayHandler(BaseGatewayHandler):
86
87
  self.mqtt_client.subscribe(command.get_topic())
87
88
 
88
89
  def _on_mqtt_message(self, client, userdata, msg):
89
- payload = json.loads(msg.payload)
90
- if 'set_val' not in payload:
91
- return
92
90
  from simo.core.models import Component
93
- component = get_event_obj(payload, Component)
94
- if not component:
95
- return
96
- print(f"Perform Value ({str(payload['set_val'])}) Send to {component}")
97
- try:
98
- self.perform_value_send(component, payload['set_val'])
99
- except Exception as e:
100
- self.logger.error(e, exc_info=True)
91
+ payload = json.loads(msg.payload)
92
+ if 'set_val' in payload:
93
+ component = get_event_obj(payload, Component)
94
+ if not component:
95
+ return
96
+ print(f"Perform Value ({str(payload['set_val'])}) Send to {component}")
97
+ try:
98
+ self.perform_value_send(component, payload['set_val'])
99
+ except Exception as e:
100
+ self.logger.error(e, exc_info=True)
101
+
102
+ if 'bulk_send' in payload:
103
+ self.perform_bulk_send(payload['bulk_send'])
101
104
 
102
105
  def perform_value_send(self, component, value):
103
106
  raise NotImplemented()
104
107
 
108
+ def perform_bulk_send(self, data):
109
+ from simo.core.models import Component
110
+ for comp_id, val in data.items():
111
+ component = Component.objects.filter(
112
+ pk=comp_id, gateway=self.gateway_instance
113
+ ).first()
114
+ if not component:
115
+ continue
116
+ try:
117
+ self.perform_value_send(component, val)
118
+ except Exception as e:
119
+ self.logger.error(e, exc_info=True)
120
+
121
+
@@ -31,7 +31,6 @@ class GatewayRunHandler(multiprocessing.Process):
31
31
  self.exit_event = multiprocessing.Event()
32
32
  self.logger = get_gw_logger(self.gateway_id)
33
33
 
34
-
35
34
  def run(self):
36
35
  db_connection.connect()
37
36
  try:
simo/core/managers.py ADDED
@@ -0,0 +1,81 @@
1
+ import sys
2
+ import traceback
3
+ from .middleware import get_current_instance
4
+ from django.utils import timezone
5
+ from django.db import models
6
+
7
+
8
+ class ZonesManager(models.Manager):
9
+
10
+ def get_queryset(self):
11
+ qs = super().get_queryset()
12
+ instance = get_current_instance()
13
+ if instance:
14
+ qs = qs.filter(instance=instance)
15
+ return qs
16
+
17
+
18
+ class CategoriesManager(models.Manager):
19
+
20
+ def get_queryset(self):
21
+ qs = super().get_queryset()
22
+ instance = get_current_instance()
23
+ if instance:
24
+ qs = qs.filter(instance=instance)
25
+ return qs
26
+
27
+
28
+ class ComponentsManager(models.Manager):
29
+
30
+ def get_queryset(self):
31
+ qs = super().get_queryset()
32
+ instance = get_current_instance()
33
+ if instance:
34
+ qs = qs.filter(zone__instance=instance)
35
+ return qs
36
+
37
+ def bulk_send(self, data):
38
+ """
39
+ :param data: {component1: True, component2: False, component3: 55.0}
40
+ :return:
41
+ """
42
+ from .models import Component
43
+ from .controllers import BEFORE_SEND
44
+ from simo.users.middleware import get_current_user
45
+ from .events import GatewayObjectCommand
46
+
47
+ for component, value in data.items():
48
+ assert isinstance(component, Component), \
49
+ "Component: value map is required!"
50
+
51
+ gateway_components = {}
52
+ for comp, value in data.items():
53
+ try:
54
+ value = comp.controller._validate_val(value, BEFORE_SEND)
55
+ except:
56
+ print(traceback.format_exc(), file=sys.stderr)
57
+ continue
58
+
59
+ comp.change_init_by = get_current_user()
60
+ comp.change_init_date = timezone.now()
61
+ comp.save(
62
+ update_fields=['change_init_by', 'change_init_date']
63
+ )
64
+ try:
65
+ value = comp.controller._prepare_for_send(value)
66
+ except:
67
+ print(traceback.format_exc(), file=sys.stderr)
68
+ continue
69
+
70
+ if comp.gateway not in gateway_components:
71
+ gateway_components[comp.gateway] = {}
72
+ gateway_components[comp.gateway][comp.id] = value
73
+
74
+ print("BULK SEND: ", gateway_components)
75
+ for gateway, send_vals in gateway_components.items():
76
+ GatewayObjectCommand(gateway, bulk_send=send_vals).publish(
77
+ retain=False
78
+ )
79
+
80
+
81
+