simo 1.7.20__py3-none-any.whl → 2.0.1__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (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 +134 -36
  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 +53 -29
  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 +140 -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.1.dist-info}/METADATA +8 -9
  170. {simo-1.7.20.dist-info → simo-2.0.1.dist-info}/RECORD +173 -189
  171. {simo-1.7.20.dist-info → simo-2.0.1.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.1.dist-info}/LICENSE.md +0 -0
  267. {simo-1.7.20.dist-info → simo-2.0.1.dist-info}/top_level.txt +0 -0
simo/core/controllers.py CHANGED
@@ -11,11 +11,13 @@ from django.utils.translation import gettext_lazy as _
11
11
  from simo.users.middleware import introduce, get_current_user
12
12
  from simo.users.utils import get_device_user
13
13
  from .utils.helpers import is_hex_color, classproperty
14
+ # from django.utils.functional import classproperty
14
15
  from .gateways import BaseGatewayHandler
15
16
  from .app_widgets import *
16
17
  from .forms import (
17
18
  BaseComponentForm, NumericSensorForm,
18
- MultiSensorConfigForm, DoubleSwitchConfigForm,
19
+ MultiSensorConfigForm,
20
+ SwitchForm, DoubleSwitchConfigForm,
19
21
  TrippleSwitchConfigForm, QuadrupleSwitchConfigForm,
20
22
  QuintupleSwitchConfigForm, DimmerConfigForm, DimmerPlusConfigForm,
21
23
  RGBWConfigForm
@@ -31,6 +33,7 @@ class ControllerBase(ABC):
31
33
  admin_widget_template = 'admin/controller_widgets/generic.html'
32
34
  default_config = {}
33
35
  default_meta = {}
36
+ discovery_msg = None
34
37
 
35
38
  @property
36
39
  @abstractmethod
@@ -83,7 +86,7 @@ class ControllerBase(ABC):
83
86
  assert issubclass(self.config_form, BaseComponentForm)
84
87
  assert issubclass(self.app_widget, BaseAppWidget)
85
88
  assert self.base_type in ALL_BASE_TYPES, \
86
- "base_type must be defined in BASE_TYPES"
89
+ f"{self.base_type} must be defined in BASE_TYPES!"
87
90
 
88
91
  @classproperty
89
92
  @classmethod
@@ -98,6 +101,15 @@ class ControllerBase(ABC):
98
101
  """
99
102
  return cls.config_form
100
103
 
104
+ @classproperty
105
+ @classmethod
106
+ def is_discoverable(cls):
107
+ return hasattr(
108
+ cls, 'init_discovery'
109
+ ) and hasattr(
110
+ cls, '_process_discovery'
111
+ )
112
+
101
113
  def _aggregate_values(self, values):
102
114
  if type(values[0]) in (float, int):
103
115
  return [statistics.mean(values)]
@@ -165,7 +177,38 @@ class ControllerBase(ABC):
165
177
  else:
166
178
  return self.component.change_init_by
167
179
 
180
+ def send(self, value):
181
+ self.component.refresh_from_db()
182
+
183
+ # Bulk send if it is a switch or dimmer and has slaves
184
+ if self.component.base_type in ('switch', 'dimmer') \
185
+ and self.component.slaves.count():
186
+ bulk_send_map = {self.component: value}
187
+ for slave in self.component.slaves.all():
188
+ bulk_send_map[slave] = value
189
+ from .models import Component
190
+ Component.objects.bulk_send(bulk_send_map)
191
+ return
192
+
193
+ # Regular send
194
+ value = self._validate_val(value, BEFORE_SEND)
195
+
196
+ self.component.change_init_by = get_current_user()
197
+ self.component.change_init_date = timezone.now()
198
+ self.component.save(
199
+ update_fields=['change_init_by', 'change_init_date']
200
+ )
201
+ value = self._prepare_for_send(value)
202
+ GatewayObjectCommand(
203
+ self.component.gateway, self.component, set_val=value
204
+ ).publish()
205
+ if value != self.component.value:
206
+ self.component.value_previous = self.component.value
207
+ self.component.value = value
208
+
168
209
  def set(self, value, actor=None):
210
+ value = self._validate_val(value, BEFORE_SET)
211
+
169
212
  if not actor:
170
213
  actor = self._get_actor(value)
171
214
  if not actor:
@@ -175,8 +218,6 @@ class ControllerBase(ABC):
175
218
  # in relation to the change of this component
176
219
  introduce(actor)
177
220
 
178
- value = self.component.translate_before_set(value)
179
- value = self._validate_val(value, BEFORE_SET)
180
221
  self.component.refresh_from_db()
181
222
  if value != self.component.value:
182
223
  self.component.value_previous = self.component.value
@@ -187,16 +228,15 @@ class ControllerBase(ABC):
187
228
  self.component.change_init_fingerprint = None
188
229
  self.component.save()
189
230
 
190
- def _send_to_device(self, value):
191
- GatewayObjectCommand(
192
- self.component.gateway, self.component, set_val=value
193
- ).publish()
194
-
195
231
  def _receive_from_device(self, value, is_alive=True):
196
232
  value = self._prepare_for_set(value)
197
233
  actor = self._get_actor(value)
234
+
235
+ init_by_device = False
198
236
  if not actor:
237
+ init_by_device = True
199
238
  actor = get_device_user()
239
+
200
240
  # Introducing user to this thread for changes that might happen to other components
201
241
  # in relation to the change of this component
202
242
  introduce(actor)
@@ -204,27 +244,17 @@ class ControllerBase(ABC):
204
244
  self.component.save(update_fields=['alive'])
205
245
  self.set(value, actor)
206
246
 
207
- def _prepare_for_send(self, value):
208
- return value
209
-
210
- def _prepare_for_set(self, value):
211
- return value
212
-
213
- def send(self, value):
214
- self.component.refresh_from_db()
215
- value = self.component.translate_before_send(value)
216
- value = self._validate_val(value, BEFORE_SEND)
217
-
218
- self.component.change_init_by = get_current_user()
219
- self.component.change_init_date = timezone.now()
220
- self.component.save(
221
- update_fields=['change_init_by', 'change_init_date']
222
- )
223
- value = self._prepare_for_send(value)
224
- self._send_to_device(value)
225
- if value != self.component.value:
226
- self.component.value_previous = self.component.value
227
- self.component.value = value
247
+ if init_by_device and self.component.slaves.count():
248
+ slaves_qs = self.component.slaves.all()
249
+ # slaves are being controlled by colonels internally
250
+ if self.component.controller_uid.startswith('simo.fleet.') \
251
+ and self.component.config.get('colonel'):
252
+ slaves_qs = slaves_qs.exclude(
253
+ config__colonel=self.component.config['colonel']
254
+ )
255
+ bulk_send_map = {s: value for s in slaves_qs}
256
+ from .models import Component
257
+ Component.objects.bulk_send(bulk_send_map)
228
258
 
229
259
  def history_display(self, values):
230
260
  assert type(values) in (list, tuple)
@@ -245,6 +275,16 @@ class ControllerBase(ABC):
245
275
  'val': icon if any(values) else None}
246
276
  ]
247
277
 
278
+ def poke(self):
279
+ '''Use this when component is dead to try and wake it up'''
280
+ pass
281
+
282
+ def _prepare_for_send(self, value):
283
+ return value
284
+
285
+ def _prepare_for_set(self, value):
286
+ return value
287
+
248
288
 
249
289
  class TimerMixin:
250
290
 
@@ -399,7 +439,19 @@ class BinarySensor(ControllerBase):
399
439
  return value
400
440
 
401
441
 
402
- class Dimmer(ControllerBase, TimerMixin):
442
+ class OnOffPokerMixin:
443
+ _poke_toggle = False
444
+
445
+ def poke(self):
446
+ if self._poke_toggle:
447
+ self._poke_toggle = False
448
+ self.turn_on()
449
+ else:
450
+ self._poke_toggle = True
451
+ self.turn_off()
452
+
453
+
454
+ class Dimmer(ControllerBase, TimerMixin, OnOffPokerMixin):
403
455
  name = _("Dimmer")
404
456
  base_type = 'dimmer'
405
457
  app_widget = KnobWidget
@@ -408,8 +460,23 @@ class Dimmer(ControllerBase, TimerMixin):
408
460
  default_config = {'min': 0.0, 'max': 100.0, 'inverse': False}
409
461
  default_value = 0
410
462
 
463
+
464
+ def _prepare_for_send(self, value):
465
+ if isinstance(value, bool):
466
+ if value:
467
+ self.component.refresh_from_db()
468
+ if self.component.value:
469
+ return self.component.value
470
+ else:
471
+ if self.component.value_previous:
472
+ return self.component.value_previous
473
+ return self.component.config.get('max', 100.0)
474
+ else:
475
+ return 0
476
+ return value
477
+
411
478
  def _validate_val(self, value, occasion=None):
412
- if value > self.component.config.get('max', 1.0):
479
+ if value > self.component.config.get('max', 100.0):
413
480
  raise ValidationError("Value to big.")
414
481
  elif value < self.component.config.get('min', 0.0):
415
482
  raise ValidationError("Value to small.")
@@ -434,7 +501,8 @@ class Dimmer(ControllerBase, TimerMixin):
434
501
  self.turn_on()
435
502
 
436
503
 
437
- class DimmerPlus(ControllerBase, TimerMixin):
504
+
505
+ class DimmerPlus(ControllerBase, TimerMixin, OnOffPokerMixin):
438
506
  name = _("Dimmer Plus")
439
507
  base_type = 'dimmer-plus'
440
508
  app_widget = KnobPlusWidget
@@ -508,13 +576,13 @@ class DimmerPlus(ControllerBase, TimerMixin):
508
576
  })
509
577
 
510
578
  def toggle(self):
511
- if self.component.value:
579
+ if self.component.value.get('main'):
512
580
  self.turn_off()
513
581
  else:
514
582
  self.turn_on()
515
583
 
516
584
 
517
- class RGBWLight(ControllerBase, TimerMixin):
585
+ class RGBWLight(ControllerBase, TimerMixin, OnOffPokerMixin):
518
586
  name = _("RGB(W) Light")
519
587
  base_type = 'rgbw-light'
520
588
  app_widget = RGBWidget
@@ -598,10 +666,11 @@ class MultiSwitchBase(ControllerBase):
598
666
  return value
599
667
 
600
668
 
601
- class Switch(MultiSwitchBase, TimerMixin):
669
+ class Switch(MultiSwitchBase, TimerMixin, OnOffPokerMixin):
602
670
  name = _("Switch")
603
671
  base_type = 'switch'
604
672
  app_widget = SingleSwitchWidget
673
+ config_form = SwitchForm
605
674
  admin_widget_template = 'admin/controller_widgets/switch.html'
606
675
  default_value = False
607
676
 
@@ -642,6 +711,11 @@ class DoubleSwitch(MultiSwitchBase):
642
711
  config_form = DoubleSwitchConfigForm
643
712
  default_value = [False, False]
644
713
 
714
+ def _prepare_for_send(self, value):
715
+ if isinstance(value, bool):
716
+ return [value, value]
717
+ return value
718
+
645
719
 
646
720
  class TripleSwitch(MultiSwitchBase):
647
721
  name = _("Triple Switch")
@@ -650,6 +724,11 @@ class TripleSwitch(MultiSwitchBase):
650
724
  config_form = TrippleSwitchConfigForm
651
725
  default_value = [False, False, False]
652
726
 
727
+ def _prepare_for_send(self, value):
728
+ if isinstance(value, bool):
729
+ return [value, value, value]
730
+ return value
731
+
653
732
 
654
733
  class QuadrupleSwitch(MultiSwitchBase):
655
734
  name = _("Quadruple Switch")
@@ -658,6 +737,11 @@ class QuadrupleSwitch(MultiSwitchBase):
658
737
  config_form = QuadrupleSwitchConfigForm
659
738
  default_value = [False, False, False, False]
660
739
 
740
+ def _prepare_for_send(self, value):
741
+ if isinstance(value, bool):
742
+ return [value, value, value, value]
743
+ return value
744
+
661
745
 
662
746
  class QuintupleSwitch(MultiSwitchBase):
663
747
  name = _("Quintuple Switch")
@@ -666,6 +750,11 @@ class QuintupleSwitch(MultiSwitchBase):
666
750
  config_form = QuintupleSwitchConfigForm
667
751
  default_value = [False, False, False, False, False]
668
752
 
753
+ def _prepare_for_send(self, value):
754
+ if isinstance(value, bool):
755
+ return [value, value, value, value, value]
756
+ return value
757
+
669
758
 
670
759
  class Lock(Switch):
671
760
  name = _("Lock")
@@ -702,3 +791,12 @@ class Lock(Switch):
702
791
  f"one of available values [{available_values}] for lock."
703
792
  )
704
793
  return value
794
+
795
+ def set(self, value, actor=None):
796
+ super().set(value, actor=actor)
797
+ if actor and value in ('locking', 'unlocking'):
798
+ self.component.change_init_by = actor
799
+ self.component.change_init_date = timezone.now()
800
+ self.component.save(
801
+ update_fields=['change_init_by', 'change_init_date']
802
+ )
@@ -2,6 +2,7 @@ import random, time
2
2
  from django.contrib.gis.db.backends.postgis.base import (
3
3
  DatabaseWrapper as PostGisPsycopg2DatabaseWrapper
4
4
  )
5
+ from django.db import close_old_connections, connection as db_connection
5
6
  from django.utils.asyncio import async_unsafe
6
7
  from django.db.utils import InterfaceError
7
8
  from django.conf import settings
@@ -9,28 +10,12 @@ from django.conf import settings
9
10
 
10
11
  class DatabaseWrapper(PostGisPsycopg2DatabaseWrapper):
11
12
 
12
- # connection already closed raised by default django postgresql db backend
13
- # which means that ensure_connection didn't ensured connection!
14
- # It happens when connection to the postgresql server is already lost,
15
- # but this backend handler knows nothing about it.
16
- # So we set connection to None immediately, therefore if there
17
- # is other thread trying to access the cursor
18
- # new connection will be established by ensure_connection() method.
19
- # We also wait for 0 - 10s, to not overwhelm anything in case
20
- # there are many multiple threads trying to access the same
21
- # db connection.
22
-
23
-
24
- def _cursor(self, name=None):
25
- self.ensure_connection()
26
- with self.wrap_database_errors:
27
- try:
28
- return self._prepare_cursor(self.create_cursor(name))
29
- except InterfaceError:
30
-
31
- self.connection = None
32
- time.sleep(random.randint(0, 100) / 10)
33
- return self._cursor(name)
13
+ @async_unsafe
14
+ def create_cursor(self, name=None):
15
+ if not self.is_usable():
16
+ close_old_connections()
17
+ db_connection.connect()
18
+ return super().create_cursor(name=name)
34
19
 
35
20
 
36
21
 
@@ -0,0 +1,3 @@
1
+ Remove this from here once this pulll request is resolved
2
+
3
+ https://github.com/dealertrack/django-rest-framework-braces/pull/36
@@ -0,0 +1,7 @@
1
+ # -*- coding: utf-8 -*-
2
+ from __future__ import absolute_import, print_function, unicode_literals
3
+
4
+
5
+ __author__ = 'Miroslav Shubernetskiy'
6
+ __email__ = 'miroslav@miki725.com'
7
+ __version__ = '0.3.4'
@@ -0,0 +1,5 @@
1
+ from __future__ import absolute_import, print_function, unicode_literals
2
+
3
+ from ._fields import * # noqa
4
+ from .custom import * # noqa
5
+ from .modified import * # noqa
@@ -0,0 +1,48 @@
1
+ from __future__ import absolute_import, print_function, unicode_literals
2
+ import inspect
3
+
4
+ from rest_framework import fields
5
+ from rest_framework.fields import * # noqa
6
+ from rest_framework.fields import _UnvalidatedField # noqa
7
+
8
+ from .mixins import AllowBlankNullFieldMixin, EmptyStringFieldMixin
9
+
10
+
11
+ FIELDS = [
12
+ 'BooleanField',
13
+ 'CharField',
14
+ 'ChoiceField',
15
+ 'DateField',
16
+ 'DateTimeField',
17
+ 'DecimalField',
18
+ 'DurationField',
19
+ 'EmailField',
20
+ 'FileField',
21
+ 'FloatField',
22
+ 'HiddenField',
23
+ 'ImageField',
24
+ 'IntegerField',
25
+ 'IPAddressField',
26
+ 'MultipleChoiceField',
27
+ 'RegexField',
28
+ 'SlugField',
29
+ 'TimeField',
30
+ 'URLField',
31
+ 'UUIDField',
32
+ ]
33
+
34
+
35
+ def get_updated_fields(fields, base_classes):
36
+ fields = [globals()[i] for i in fields]
37
+ return {
38
+ field.__name__: type(field.__name__, base_classes + (field,), {})
39
+ for field in fields
40
+ }
41
+
42
+
43
+ locals().update(
44
+ get_updated_fields(FIELDS, (EmptyStringFieldMixin, AllowBlankNullFieldMixin))
45
+ )
46
+
47
+ __all__ = [name for name, value in locals().items()
48
+ if inspect.isclass(value) and issubclass(value, fields.Field)]
@@ -0,0 +1,107 @@
1
+ from __future__ import absolute_import, print_function, unicode_literals
2
+ import inspect
3
+ from decimal import Decimal, getcontext
4
+
5
+ import pytz
6
+ import six
7
+ from django.utils.translation import gettext as _
8
+
9
+ from . import _fields as fields
10
+ from .mixins import ValueAsTextFieldMixin
11
+
12
+
13
+ class UnvalidatedField(fields._UnvalidatedField):
14
+ """
15
+ Same as DRF's ``_UnvalidatedField``, except this is a public class.
16
+ """
17
+
18
+ def run_validators(self, value):
19
+ return
20
+
21
+
22
+ class PositiveIntegerField(fields.IntegerField):
23
+ """
24
+ Enhanced DRF's ``IntegerField`` as this default ``min_value`` to be 0
25
+ hence only allowing positive numbers.
26
+ """
27
+
28
+ def __init__(self, *args, **kwargs):
29
+ kwargs.setdefault('min_value', 0)
30
+ super(PositiveIntegerField, self).__init__(*args, **kwargs)
31
+
32
+
33
+ class UTCDateTimeField(fields.DateTimeField):
34
+ """
35
+ Same as DateTimeField except this field guarantees to return time-zone aware dates.
36
+ """
37
+
38
+ def __init__(self, *args, **kwargs):
39
+ kwargs.setdefault('default_timezone', pytz.utc)
40
+ super(UTCDateTimeField, self).__init__(*args, **kwargs)
41
+
42
+
43
+ class NonValidatingChoiceField(fields.ChoiceField):
44
+ """
45
+ ChoiceField subclass that skips the validation of "choices".
46
+ It does apply 'required' validation, and any other validation
47
+ done by the parent drf.Field class.
48
+ """
49
+
50
+ def __init__(self, *args, **kwargs):
51
+ kwargs.setdefault('choices', [])
52
+ super(NonValidatingChoiceField, self).__init__(*args, **kwargs)
53
+
54
+ def to_internal_value(self, data):
55
+ try:
56
+ return self.choice_strings_to_values[six.text_type(data)]
57
+ except KeyError:
58
+ return six.text_type(data)
59
+
60
+
61
+ class NumericField(ValueAsTextFieldMixin, fields.IntegerField):
62
+ default_error_messages = {
63
+ 'invalid': _('Enter a whole number.'),
64
+ }
65
+
66
+
67
+ class RoundedDecimalField(fields.DecimalField):
68
+ """
69
+ Currency field subclass of Decimal used for rounding currencies
70
+ to two decimal places.
71
+ """
72
+
73
+ def __init__(self, max_digits=None, decimal_places=2, rounding=None, *args, **kwargs):
74
+ super(RoundedDecimalField, self).__init__(
75
+ max_digits=max_digits,
76
+ decimal_places=decimal_places,
77
+ *args, **kwargs
78
+ )
79
+ self.rounding = rounding
80
+
81
+ def to_internal_value(self, data):
82
+ return self.quantize(super(RoundedDecimalField, self).to_internal_value(data))
83
+
84
+ def validate_precision(self, data):
85
+ return data
86
+
87
+ def quantize(self, data):
88
+ """
89
+ Quantize the decimal value to the configured precision.
90
+ """
91
+ if self.decimal_places is None:
92
+ return data
93
+
94
+ context = getcontext().copy()
95
+
96
+ if self.max_digits is not None:
97
+ context.prec = self.max_digits
98
+ if self.rounding is not None:
99
+ context.rounding = self.rounding
100
+ return data.quantize(
101
+ Decimal('.1') ** self.decimal_places,
102
+ context=context
103
+ )
104
+
105
+
106
+ __all__ = [name for name, value in locals().items()
107
+ if inspect.isclass(value) and issubclass(value, fields.Field)]
@@ -0,0 +1,58 @@
1
+ from __future__ import absolute_import, print_function, unicode_literals
2
+
3
+ import six
4
+ from rest_framework.fields import CharField, empty
5
+
6
+
7
+ class EmptyStringFieldMixin(object):
8
+ def validate_empty_values(self, data):
9
+ is_empty, data = super(EmptyStringFieldMixin, self).validate_empty_values(data)
10
+ if not is_empty and data == '':
11
+ if self.required:
12
+ self.fail('required')
13
+ else:
14
+ return True, data
15
+ return is_empty, data
16
+
17
+ def to_representation(self, value):
18
+ if value in ('', None) and not self.required:
19
+ return value
20
+ return super(EmptyStringFieldMixin, self).to_representation(value)
21
+
22
+
23
+ class AllowBlankNullFieldMixin(object):
24
+ def __init__(self, *args, **kwargs):
25
+ super(AllowBlankNullFieldMixin, self).__init__(*args, **kwargs)
26
+
27
+ # some DRF fields explicitly do not allow some kwargs
28
+ # therefore we adjust the field attributes directly
29
+ if not self.required:
30
+ if all([isinstance(self, CharField),
31
+ hasattr(self, 'allow_blank'),
32
+ 'allow_blank' not in kwargs]):
33
+ self.allow_blank = True
34
+ if all([hasattr(self, 'allow_null'),
35
+ 'allow_null' not in kwargs]):
36
+ self.allow_null = True
37
+
38
+
39
+ class ValueAsTextFieldMixin(object):
40
+ def to_string_value(self, data):
41
+ if data:
42
+ return six.text_type(data)
43
+ return data
44
+
45
+ def prepare_value_for_validation(self, data):
46
+ return data
47
+
48
+ def run_validation(self, value=empty):
49
+ (is_empty_value, value) = self.validate_empty_values(value)
50
+ if is_empty_value:
51
+ return value
52
+
53
+ value = self.prepare_value_for_validation(value)
54
+ value = self.to_internal_value(value)
55
+ self.run_validators(value)
56
+ value = self.to_string_value(value)
57
+
58
+ return value
@@ -0,0 +1,41 @@
1
+ from __future__ import absolute_import, print_function, unicode_literals
2
+ import inspect
3
+
4
+ from . import _fields as fields
5
+
6
+
7
+ class BooleanField(fields.BooleanField):
8
+ def __init__(self, *args, **kwargs):
9
+ self.TRUE_VALUES = self.TRUE_VALUES | set(kwargs.pop('true_values', []))
10
+ self.FALSE_VALUES = self.FALSE_VALUES | set(kwargs.pop('false_values', []))
11
+ super(BooleanField, self).__init__(*args, **kwargs)
12
+
13
+
14
+ class DecimalField(fields.DecimalField):
15
+ def __init__(self, max_digits=None, decimal_places=None, *args, **kwargs):
16
+ super(DecimalField, self).__init__(
17
+ max_digits=max_digits, decimal_places=decimal_places,
18
+ *args, **kwargs
19
+ )
20
+
21
+ def quantize(self, value):
22
+ if self.max_digits is None or self.decimal_places is None:
23
+ return value
24
+ return super(DecimalField, self).quantize(value)
25
+
26
+
27
+ class DateTimeField(fields.DateTimeField):
28
+ def __init__(self, *args, **kwargs):
29
+ super(DateTimeField, self).__init__(*args, **kwargs)
30
+ # allow fo the case when default_timezone is passed as None
31
+ # since super will in that use Django's default timezone
32
+ if 'default_timezone' in kwargs:
33
+ # DRF >= 3.3
34
+ if callable(self.default_timezone):
35
+ self.timezone = kwargs['default_timezone']
36
+ else:
37
+ self.default_timezone = kwargs['default_timezone']
38
+
39
+
40
+ __all__ = [name for name, value in locals().items()
41
+ if inspect.isclass(value) and issubclass(value, fields.Field)]
File without changes
@@ -0,0 +1,20 @@
1
+ from __future__ import absolute_import, print_function, unicode_literals
2
+
3
+ import six
4
+ from dateutil.parser import parse
5
+ from django import forms
6
+ from rest_framework import ISO_8601
7
+
8
+
9
+ class ISO8601DateTimeField(forms.DateTimeField):
10
+ def to_python(self, value):
11
+ if value in self.empty_values:
12
+ return
13
+
14
+ if isinstance(value, six.string_types) and ISO_8601 in self.input_formats:
15
+ try:
16
+ return parse(value)
17
+ except ValueError:
18
+ raise forms.ValidationError(self.error_messages['invalid'], code='invalid')
19
+
20
+ return super(ISO8601DateTimeField, self).to_python(value)