simo 2.0.42__py3-none-any.whl → 2.1.2__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 (124) hide show
  1. simo/__pycache__/asgi.cpython-38.pyc +0 -0
  2. simo/__pycache__/settings.cpython-38.pyc +0 -0
  3. simo/__pycache__/wsgi.cpython-38.pyc +0 -0
  4. simo/asgi.py +1 -1
  5. simo/core/__init__.py +1 -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__/app_widgets.cpython-38.pyc +0 -0
  11. simo/core/__pycache__/apps.cpython-38.pyc +0 -0
  12. simo/core/__pycache__/auto_urls.cpython-38.pyc +0 -0
  13. simo/core/__pycache__/base_types.cpython-38.pyc +0 -0
  14. simo/core/__pycache__/controllers.cpython-38.pyc +0 -0
  15. simo/core/__pycache__/dynamic_settings.cpython-38.pyc +0 -0
  16. simo/core/__pycache__/form_fields.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__/models.cpython-38.pyc +0 -0
  21. simo/core/__pycache__/permissions.cpython-38.pyc +0 -0
  22. simo/core/__pycache__/serializers.cpython-38.pyc +0 -0
  23. simo/core/__pycache__/signal_receivers.cpython-38.pyc +0 -0
  24. simo/core/__pycache__/tasks.cpython-38.pyc +0 -0
  25. simo/core/__pycache__/views.cpython-38.pyc +0 -0
  26. simo/core/admin.py +26 -26
  27. simo/core/api.py +22 -2
  28. simo/core/api_meta.py +23 -13
  29. simo/core/app_widgets.py +6 -0
  30. simo/core/apps.py +13 -0
  31. simo/core/auto_urls.py +2 -3
  32. simo/core/base_types.py +1 -0
  33. simo/core/controllers.py +57 -0
  34. simo/core/dynamic_settings.py +0 -8
  35. simo/core/form_fields.py +93 -0
  36. simo/core/forms.py +16 -101
  37. simo/core/gateways.py +1 -1
  38. simo/core/managers.py +14 -1
  39. simo/core/migrations/0037_auto_20240606_1057.py +33 -0
  40. simo/core/migrations/0038_remove_instance_cover_image_and_more.py +30 -0
  41. simo/core/migrations/__pycache__/0037_auto_20240606_1057.cpython-38.pyc +0 -0
  42. simo/core/migrations/__pycache__/0038_remove_instance_cover_image_and_more.cpython-38.pyc +0 -0
  43. simo/core/models.py +30 -16
  44. simo/core/permissions.py +6 -3
  45. simo/core/serializers.py +77 -5
  46. simo/core/signal_receivers.py +25 -0
  47. simo/core/static/admin/css/simo.css +14 -0
  48. simo/core/tasks.py +82 -49
  49. simo/core/templates/admin/controller_widgets/button.html +8 -0
  50. simo/core/templates/admin/core/component_change_form.html +97 -0
  51. simo/core/templates/admin/formset_widget.html +88 -118
  52. simo/core/templates/admin/formset_widget_old.html +122 -0
  53. simo/core/templates/admin/user_tools.html +0 -3
  54. simo/core/templates/admin/wizard/wizard_add.html +16 -9
  55. simo/core/utils/__pycache__/admin.cpython-38.pyc +0 -0
  56. simo/core/utils/__pycache__/cache.cpython-38.pyc +0 -0
  57. simo/core/utils/__pycache__/formsets.cpython-38.pyc +0 -0
  58. simo/core/utils/admin.py +11 -0
  59. simo/core/utils/cache.py +15 -0
  60. simo/core/utils/formsets.py +11 -18
  61. simo/core/views.py +2 -85
  62. simo/fleet/__pycache__/auto_urls.cpython-38.pyc +0 -0
  63. simo/fleet/__pycache__/controllers.cpython-38.pyc +0 -0
  64. simo/fleet/__pycache__/forms.cpython-38.pyc +0 -0
  65. simo/fleet/__pycache__/gateways.cpython-38.pyc +0 -0
  66. simo/fleet/__pycache__/models.cpython-38.pyc +0 -0
  67. simo/fleet/__pycache__/socket_consumers.cpython-38.pyc +0 -0
  68. simo/fleet/__pycache__/utils.cpython-38.pyc +0 -0
  69. simo/fleet/__pycache__/views.cpython-38.pyc +0 -0
  70. simo/fleet/auto_urls.py +7 -1
  71. simo/fleet/controllers.py +194 -31
  72. simo/fleet/forms.py +223 -87
  73. simo/fleet/gateways.py +53 -2
  74. simo/fleet/migrations/0036_auto_20240605_0702.py +68 -0
  75. simo/fleet/migrations/0037_alter_colonelpin_options_alter_colonelpin_no_and_more.py +27 -0
  76. simo/fleet/migrations/__pycache__/0036_auto_20240605_0702.cpython-38.pyc +0 -0
  77. simo/fleet/migrations/__pycache__/0037_alter_colonelpin_options_alter_colonelpin_no_and_more.cpython-38.pyc +0 -0
  78. simo/fleet/models.py +35 -6
  79. simo/fleet/socket_consumers.py +1 -1
  80. simo/fleet/templates/fleet/controllers_info/button.md +16 -0
  81. simo/fleet/utils.py +31 -1
  82. simo/fleet/views.py +45 -0
  83. simo/generic/__pycache__/controllers.cpython-38.pyc +0 -0
  84. simo/generic/__pycache__/forms.cpython-38.pyc +0 -0
  85. simo/generic/__pycache__/gateways.cpython-38.pyc +0 -0
  86. simo/generic/controllers.py +61 -16
  87. simo/generic/forms.py +0 -3
  88. simo/generic/gateways.py +2 -0
  89. simo/generic/templates/admin/controller_widgets/blinds.html +2 -1
  90. simo/generic/templates/admin/controller_widgets/weather_forecast.html +1 -1
  91. simo/generic/templates/generic/controllers_info/dummy.md +3 -0
  92. simo/generic/templates/generic/controllers_info/stateselect.md +2 -0
  93. simo/management/__init__.py +0 -0
  94. simo/management/__pycache__/__init__.cpython-38.pyc +0 -0
  95. simo/management/__pycache__/on_http_start.cpython-38.pyc +0 -0
  96. simo/{_hub_template → management/_hub_template}/hub/nginx.conf +2 -2
  97. simo/{auto_update.py → management/auto_update.py} +3 -0
  98. simo/{cli.py → management/copy_template.py} +3 -16
  99. simo/management/install.py +258 -0
  100. simo/{on_http_start.py → management/on_http_start.py} +22 -2
  101. simo/settings.py +20 -4
  102. simo/users/__init__.py +1 -0
  103. simo/users/__pycache__/__init__.cpython-38.pyc +0 -0
  104. simo/users/__pycache__/admin.cpython-38.pyc +0 -0
  105. simo/users/__pycache__/apps.cpython-38.pyc +0 -0
  106. simo/users/__pycache__/models.cpython-38.pyc +0 -0
  107. simo/users/apps.py +9 -0
  108. simo/users/migrations/__pycache__/0029_alter_instanceuser_options_instanceuser_order.cpython-38.pyc +0 -0
  109. simo/users/migrations/__pycache__/0030_alter_instanceuser_options_remove_instanceuser_order.cpython-38.pyc +0 -0
  110. simo/users/models.py +16 -3
  111. {simo-2.0.42.dist-info → simo-2.1.2.dist-info}/METADATA +5 -3
  112. {simo-2.0.42.dist-info → simo-2.1.2.dist-info}/RECORD +122 -95
  113. simo-2.1.2.dist-info/entry_points.txt +2 -0
  114. simo/__pycache__/on_http_start.cpython-38.pyc +0 -0
  115. simo/wsgi.py +0 -7
  116. /simo/{_hub_template → management/_hub_template}/hub/asgi.py +0 -0
  117. /simo/{_hub_template → management/_hub_template}/hub/celeryc.py +0 -0
  118. /simo/{_hub_template → management/_hub_template}/hub/manage.py +0 -0
  119. /simo/{_hub_template → management/_hub_template}/hub/settings.py +0 -0
  120. /simo/{_hub_template → management/_hub_template}/hub/supervisor.conf +0 -0
  121. /simo/{_hub_template → management/_hub_template}/hub/urls.py +0 -0
  122. {simo-2.0.42.dist-info → simo-2.1.2.dist-info}/LICENSE.md +0 -0
  123. {simo-2.0.42.dist-info → simo-2.1.2.dist-info}/WHEEL +0 -0
  124. {simo-2.0.42.dist-info → simo-2.1.2.dist-info}/top_level.txt +0 -0
Binary file
Binary file
Binary file
simo/asgi.py CHANGED
@@ -21,7 +21,7 @@ for name, app in apps.app_configs.items():
21
21
  if isinstance(item, list) and var_name == 'urlpatterns':
22
22
  urlpatterns.extend(item)
23
23
 
24
- from .on_http_start import *
24
+ from .management.on_http_start import *
25
25
 
26
26
  application = ProtocolTypeRouter({
27
27
  "http": get_asgi_application(),
simo/core/__init__.py CHANGED
@@ -0,0 +1 @@
1
+ default_app_config = 'core.apps.SIMOCoreAppConfig'
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
simo/core/admin.py CHANGED
@@ -1,6 +1,8 @@
1
+ import markdown
1
2
  from django.utils.translation import gettext_lazy as _
2
3
  from django.contrib import admin
3
4
  from django.urls import reverse
5
+ from django.utils.safestring import mark_safe
4
6
  from easy_thumbnails.fields import ThumbnailerField
5
7
  from adminsortable2.admin import SortableAdminMixin
6
8
  from django.template.loader import render_to_string
@@ -8,6 +10,7 @@ from django.utils.decorators import method_decorator
8
10
  from django.views.decorators.csrf import csrf_protect
9
11
  from django.shortcuts import redirect, render
10
12
  from simo.users.models import ComponentPermission
13
+ from simo.core.utils.admin import EasyObjectsDeleteMixin
11
14
  from .utils.type_constants import (
12
15
  ALL_BASE_TYPES, GATEWAYS_MAP, CONTROLLERS_BY_GATEWAY
13
16
  )
@@ -27,7 +30,7 @@ csrf_protect_m = method_decorator(csrf_protect)
27
30
 
28
31
 
29
32
  @admin.register(Icon)
30
- class IconAdmin(admin.ModelAdmin):
33
+ class IconAdmin(EasyObjectsDeleteMixin, admin.ModelAdmin):
31
34
  form = IconForm
32
35
  list_display = 'slug', 'preview', 'copyright'
33
36
  search_fields = 'slug', 'keywords',
@@ -57,25 +60,8 @@ class IconAdmin(admin.ModelAdmin):
57
60
 
58
61
 
59
62
 
60
- @admin.register(Instance)
61
- class InstanceAdmin(admin.ModelAdmin):
62
- list_display = 'name', 'timezone', 'uid'
63
- exclude = 'learn_fingerprints_start', 'learn_fingerprints'
64
-
65
-
66
- def has_module_permission(self, request):
67
- return request.user.is_master
68
-
69
- def has_view_permission(self, request, obj=None):
70
- return self.has_module_permission(request)
71
-
72
- def has_change_permission(self, request, obj=None):
73
- return self.has_module_permission(request)
74
-
75
-
76
-
77
63
  @admin.register(Zone)
78
- class ZoneAdmin(SortableAdminMixin, admin.ModelAdmin):
64
+ class ZoneAdmin(EasyObjectsDeleteMixin, SortableAdminMixin, admin.ModelAdmin):
79
65
  list_display = 'name', 'instance'
80
66
  search_fields = 'name',
81
67
  list_filter = 'instance',
@@ -91,7 +77,7 @@ class ZoneAdmin(SortableAdminMixin, admin.ModelAdmin):
91
77
 
92
78
 
93
79
  @admin.register(Category)
94
- class CategoryAdmin(SortableAdminMixin, admin.ModelAdmin):
80
+ class CategoryAdmin(EasyObjectsDeleteMixin, SortableAdminMixin, admin.ModelAdmin):
95
81
  form = CategoryAdminForm
96
82
  list_display = 'name_display', 'all'
97
83
  search_fields = 'name',
@@ -118,7 +104,7 @@ class CategoryAdmin(SortableAdminMixin, admin.ModelAdmin):
118
104
 
119
105
 
120
106
  @admin.register(Gateway)
121
- class GatewayAdmin(admin.ModelAdmin):
107
+ class GatewayAdmin(EasyObjectsDeleteMixin, admin.ModelAdmin):
122
108
  list_display = 'type', 'status'
123
109
  readonly_fields = ('type', 'control')
124
110
 
@@ -254,14 +240,14 @@ class ComponentPermissionInline(admin.TabularInline):
254
240
 
255
241
 
256
242
  @admin.register(Component)
257
- class ComponentAdmin(admin.ModelAdmin):
243
+ class ComponentAdmin(EasyObjectsDeleteMixin, admin.ModelAdmin):
258
244
  form = BaseComponentForm
259
245
  list_display = (
260
246
  'id', 'name_display', 'value_display', 'base_type', 'alive', 'battery_level',
261
247
  'alarm_category', 'show_in_app',
262
248
  )
263
249
  readonly_fields = (
264
- 'id', 'controller_uid', 'base_type', 'gateway', 'config',
250
+ 'id', 'controller_uid', 'base_type', 'info', 'gateway', 'config',
265
251
  'alive', 'error_msg', 'battery_level',
266
252
  'control', 'value', 'arm_status', 'history', 'meta'
267
253
  )
@@ -274,6 +260,9 @@ class ComponentAdmin(admin.ModelAdmin):
274
260
  list_per_page = 100
275
261
  change_list_template = 'admin/component_change_list.html'
276
262
  inlines = ComponentPermissionInline,
263
+ # standard django admin change_form.html template + adds side panel
264
+ # for displaying component controller info.
265
+ #change_form_template = 'admin/core/component_change_form.html'
277
266
 
278
267
  def get_fieldsets(self, request, obj=None):
279
268
  form = self._get_form_for_get_fields(request, obj)
@@ -346,6 +335,7 @@ class ComponentAdmin(admin.ModelAdmin):
346
335
  ctx['selected_type'] = ALL_BASE_TYPES.get(
347
336
  controller_cls.base_type, controller_cls.base_type
348
337
  )
338
+ ctx['info'] = controller_cls.info(controller_cls)
349
339
  if request.method == 'POST':
350
340
  ctx['form'] = add_form(
351
341
  request=request,
@@ -356,7 +346,6 @@ class ComponentAdmin(admin.ModelAdmin):
356
346
  pop_fields_from_form(ctx['form'])
357
347
  if ctx['form'].is_valid():
358
348
  if ctx['form'].controller.is_discoverable:
359
- print("INIT DISCOVERY!!!")
360
349
  ctx['form'].controller.init_discovery(
361
350
  ctx['form'].cleaned_data
362
351
  )
@@ -395,7 +384,6 @@ class ComponentAdmin(admin.ModelAdmin):
395
384
  if ctx['form'].is_valid():
396
385
  request.session['add_comp_type'] = \
397
386
  ctx['form'].cleaned_data['controller_type']
398
- print("Session controller type: ", request.session['add_comp_type'])
399
387
  return redirect(request.path)
400
388
 
401
389
  else:
@@ -475,6 +463,18 @@ class ComponentAdmin(admin.ModelAdmin):
475
463
  }
476
464
  )
477
465
 
466
+ def info(self, obj):
467
+ if not obj.controller:
468
+ return
469
+ info = obj.controller.info()
470
+ if not info:
471
+ return
472
+ return mark_safe(
473
+ f'<div class="markdownified-info">'
474
+ f'{markdown.markdown(info)}'
475
+ f'</div>'
476
+ )
477
+
478
478
  def history(self, obj):
479
479
  if not obj:
480
480
  return ''
@@ -483,4 +483,4 @@ class ComponentAdmin(admin.ModelAdmin):
483
483
  'value_history': obj.history.filter(type='value').order_by('-date')[:50],
484
484
  'arm_status_history': obj.history.filter(type='security').order_by('-date')[:50]
485
485
  }
486
- )
486
+ )
simo/core/api.py CHANGED
@@ -9,6 +9,7 @@ from django.utils import timezone
9
9
  from django.http import HttpResponse, Http404
10
10
  from simo.core.utils.helpers import get_self_ip, search_queryset
11
11
  from rest_framework import status
12
+ from actstream.models import Action
12
13
  from rest_framework.pagination import PageNumberPagination
13
14
  from rest_framework import viewsets
14
15
  from django_filters.rest_framework import DjangoFilterBackend
@@ -23,7 +24,8 @@ from .models import (
23
24
  )
24
25
  from .serializers import (
25
26
  IconSerializer, CategorySerializer, ZoneSerializer,
26
- ComponentSerializer, ComponentHistorySerializer
27
+ ComponentSerializer, ComponentHistorySerializer,
28
+ ActionSerializer
27
29
  )
28
30
  from .permissions import (
29
31
  IsInstanceSuperuser, InstanceSuperuserCanEdit, ComponentPermission
@@ -494,6 +496,22 @@ class ComponentHistoryViewSet(InstanceMixin, viewsets.ReadOnlyModelViewSet):
494
496
  return vectors
495
497
 
496
498
 
499
+ class ActionsViewset(InstanceMixin, viewsets.ReadOnlyModelViewSet):
500
+ url = 'core/actions'
501
+ basename = 'actions'
502
+ serializer_class = ActionSerializer
503
+ pagination_class = HistoryResultsSetPagination
504
+
505
+ def get_queryset(self):
506
+ qs = Action.objects.filter(data__instance_id=self.instance.id)
507
+ if self.request.user.is_superuser:
508
+ return qs
509
+ user_role = self.request.user.get_role(self.instance)
510
+ if user_role.is_owner:
511
+ return qs
512
+ Action.objects.none()
513
+
514
+
497
515
  class SettingsViewSet(InstanceMixin, viewsets.GenericViewSet):
498
516
  url = 'core/settings'
499
517
  basename = 'settings'
@@ -527,6 +545,7 @@ class SettingsViewSet(InstanceMixin, viewsets.GenericViewSet):
527
545
  main_alarm_group_id = main_alarm_group.id
528
546
 
529
547
  return RESTResponse({
548
+ 'hub_uid': dynamic_settings['core__hub_uid'],
530
549
  'instance_name': self.instance.name,
531
550
  'instance_uid': self.instance.uid,
532
551
  'timezone': self.instance.timezone,
@@ -626,7 +645,8 @@ class ControllerTypes(InstanceMixin, viewsets.GenericViewSet):
626
645
  'name': cls.name,
627
646
  'is_discoverable': cls.is_discoverable,
628
647
  'manual_add': cls.manual_add,
629
- 'discovery_msg': cls.discovery_msg
648
+ 'discovery_msg': cls.discovery_msg,
649
+ 'info': cls.info(cls)
630
650
  })
631
651
 
632
652
  return RESTResponse(data)
simo/core/api_meta.py CHANGED
@@ -1,8 +1,10 @@
1
1
  from collections import OrderedDict
2
+ from django.urls import reverse
2
3
  from django.utils.encoding import force_str
3
- from rest_framework import serializers
4
4
  from rest_framework.metadata import SimpleMetadata
5
+ from rest_framework import serializers
5
6
  from rest_framework.utils.field_mapping import ClassLookupDict
7
+ from simo.core.models import Icon
6
8
  from .serializers import (
7
9
  HiddenSerializerField, ComponentManyToManyRelatedField,
8
10
  TextAreaSerializerField
@@ -40,7 +42,9 @@ class SIMOAPIMetadata(SimpleMetadata):
40
42
  TextAreaSerializerField: 'textarea',
41
43
  })
42
44
 
45
+
43
46
  def get_field_info(self, field):
47
+
44
48
  """
45
49
  Given an instance of a serializer field, return a dictionary
46
50
  of metadata about it.
@@ -51,6 +55,7 @@ class SIMOAPIMetadata(SimpleMetadata):
51
55
 
52
56
  form_field = field.style.get('form_field')
53
57
  if form_field:
58
+ #TODO: Delete these completely once autocomplete fields are fully implemented
54
59
  if hasattr(form_field, 'queryset'):
55
60
  model = form_field.queryset.model
56
61
  field_info['related_object'] = ".".join(
@@ -59,6 +64,10 @@ class SIMOAPIMetadata(SimpleMetadata):
59
64
  if hasattr(form_field, 'filter_by'):
60
65
  field_info['filter_by'] = form_field.filter_by
61
66
 
67
+ if hasattr(form_field, 'forward'):
68
+ field_info['autocomplete_url'] = reverse(form_field.url)
69
+ field_info['forward'] = form_field.forward
70
+
62
71
  attrs = [
63
72
  'read_only', 'label', 'help_text',
64
73
  'min_length', 'max_length',
@@ -76,17 +85,18 @@ class SIMOAPIMetadata(SimpleMetadata):
76
85
  elif getattr(field, 'fields', None):
77
86
  field_info['children'] = self.get_serializer_info(field)
78
87
 
88
+ if form_field and hasattr(form_field, 'queryset') \
89
+ and form_field.queryset.model == Icon:
90
+ return field_info
91
+
79
92
  if not field_info.get('read_only') and hasattr(field, 'choices'):
80
- add_choices = True
81
- queryset = getattr(field, 'queryset', None)
82
- if queryset and queryset.count() > 1000:
83
- add_choices = False
84
- if add_choices:
85
- field_info['choices'] = [
86
- {
87
- 'value': choice_value,
88
- 'display_name': force_str(choice_name, strings_only=True)
89
- }
90
- for choice_value, choice_name in field.choices.items()
91
- ]
93
+ print(f"DO choices for: {field.label}")
94
+ field_info['choices'] = [
95
+ {
96
+ 'value': choice_value,
97
+ 'display_name': force_str(choice_name, strings_only=True)
98
+ }
99
+ for choice_value, choice_name in field.choices.items()
100
+ ]
101
+
92
102
  return field_info
simo/core/app_widgets.py CHANGED
@@ -23,6 +23,12 @@ class BinarySensorWidget(BaseAppWidget):
23
23
  size = [2, 1]
24
24
 
25
25
 
26
+ class ButtonWidget(BaseAppWidget):
27
+ uid = 'button'
28
+ name = _("Button")
29
+ size = [2, 1]
30
+
31
+
26
32
  class NumericSensorWidget(BaseAppWidget):
27
33
  uid = 'numeric-sensor'
28
34
  name = _("Numeric sensor")
simo/core/apps.py ADDED
@@ -0,0 +1,13 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+
5
+ class SIMOCoreAppConfig(AppConfig):
6
+ name = 'simo.core'
7
+
8
+ def ready(self):
9
+ from actstream import registry
10
+ registry.register(self.get_model('Component'))
11
+ registry.register(self.get_model('Gateway'))
12
+
13
+
simo/core/auto_urls.py CHANGED
@@ -1,9 +1,9 @@
1
1
  from django.urls import path
2
2
  from .views import (
3
- get_timestamp, setup_wizard, update, restart, reboot, set_instance
3
+ get_timestamp, update, restart, reboot, set_instance
4
4
  )
5
5
  from .autocomplete_views import (
6
- IconModelAutocomplete, #IconSlugAutocomplete,
6
+ IconModelAutocomplete,
7
7
  CategoryAutocomplete, ZoneAutocomplete,
8
8
  ComponentAutocomplete,
9
9
  )
@@ -31,7 +31,6 @@ urlpatterns = [
31
31
  ComponentAutocomplete.as_view(), name='autocomplete-component'
32
32
  ),
33
33
  path('set-instance/<slug:instance_slug>/', set_instance, name='set-instance'),
34
- path('setup/', setup_wizard, name='setup-wizard'),
35
34
  path('update/', update, name='update'),
36
35
  path('restart/', restart, name='restart'),
37
36
  path('reboot/', reboot, name='reboot')
simo/core/base_types.py CHANGED
@@ -8,6 +8,7 @@ BASE_TYPES = {
8
8
  'numeric-sensor': _("Numeric sensor"),
9
9
  'multi-sensor': _("Multi sensor"),
10
10
  'binary-sensor': _("Binary sensor"),
11
+ 'button': _("Button"),
11
12
  'dimmer': _("Dimmer"),
12
13
  'dimmer-plus': _("Dimmer Plus"),
13
14
  'rgbw-light': _('RGB(W) light'),
simo/core/controllers.py CHANGED
@@ -5,8 +5,10 @@ import statistics
5
5
  import threading
6
6
  from decimal import Decimal as D
7
7
  from abc import ABC, ABCMeta, abstractmethod
8
+ from django.utils.functional import cached_property
8
9
  from django.core.exceptions import ValidationError
9
10
  from django.utils import timezone
11
+ from django.template.loader import render_to_string
10
12
  from django.utils.translation import gettext_lazy as _
11
13
  from simo.users.middleware import introduce, get_current_user
12
14
  from simo.users.utils import get_device_user
@@ -73,6 +75,11 @@ class ControllerBase(ABC):
73
75
  :return: Default value of this base component type
74
76
  """
75
77
 
78
+ @property
79
+ def info_template_path(self) -> str:
80
+ return f"{self.__class__.__module__.split('.')[-2]}/" \
81
+ f"controllers_info/{self.__class__.__name__.lower()}.md"
82
+
76
83
  @abstractmethod
77
84
  def _validate_val(self, value, occasion=None):
78
85
  """
@@ -113,6 +120,24 @@ class ControllerBase(ABC):
113
120
  cls, '_process_discovery'
114
121
  )
115
122
 
123
+ def info(self):
124
+ '''
125
+ Override this to give users help on how to use this component type,
126
+ after you do that, include any component instance specific information
127
+ if you see it necessary.
128
+ :return: Markdown component info on how to set it up and use it
129
+ along with any other relative information,
130
+ regarding this particular component instance
131
+ '''
132
+ try:
133
+ return render_to_string(
134
+ self.info_template_path, {
135
+ 'component': self.component if hasattr(self, 'component') else None
136
+ }
137
+ )
138
+ except:
139
+ return
140
+
116
141
  def _aggregate_values(self, values):
117
142
  if type(values[0]) in (float, int):
118
143
  return [statistics.mean(values)]
@@ -306,6 +331,8 @@ class ControllerBase(ABC):
306
331
  return value
307
332
 
308
333
 
334
+
335
+
309
336
  class TimerMixin:
310
337
 
311
338
  def __init__(self, *args, **kwargs):
@@ -459,6 +486,36 @@ class BinarySensor(ControllerBase):
459
486
  return value
460
487
 
461
488
 
489
+ class Button(ControllerBase):
490
+ name = _("Button")
491
+ base_type = 'button'
492
+ app_widget = ButtonWidget
493
+ admin_widget_template = 'admin/controller_widgets/button.html'
494
+ default_value = 'up'
495
+
496
+ def _validate_val(self, value, occasion=None):
497
+ if value not in ('down', 'up', 'hold', 'click', 'double-click'):
498
+ raise ValidationError("Bad button value!")
499
+ return value
500
+
501
+ def is_down(self):
502
+ return self.component.value in ('down', 'hold')
503
+
504
+ def is_held(self):
505
+ return self.component.value == 'hold'
506
+
507
+ @cached_property
508
+ def bonded_gear(self):
509
+ from simo.core.models import Component
510
+ gear = []
511
+ for comp in Component.objects.filter(config__has_key='controls'):
512
+ for ctrl in comp.config['controls']:
513
+ if ctrl.get('button') == self.component.id:
514
+ gear.append(comp)
515
+ break
516
+ return gear
517
+
518
+
462
519
  class OnOffPokerMixin:
463
520
  _poke_toggle = False
464
521
 
@@ -43,14 +43,6 @@ class RemoteConnectionVersion(IntegerPreference):
43
43
  "when hub get's synced up to the simo.io."
44
44
 
45
45
 
46
- @global_preferences_registry.register
47
- class UnitsOfMeasure(ChoicePreference):
48
- section = core
49
- name = 'units_of_measure'
50
- choices = (('metric', "Metric"), ('imperial', "Imperial"))
51
- default = 'metric'
52
- required = True
53
-
54
46
 
55
47
  @global_preferences_registry.register
56
48
  class LatestHubOSVersionAvailable(StringPreference):
@@ -0,0 +1,93 @@
1
+ import copy
2
+ from django import forms
3
+ from dal import autocomplete
4
+
5
+
6
+ class LazyChoicesMixin:
7
+
8
+ _choices = ()
9
+
10
+ @property
11
+ def choices(self):
12
+ if callable(self._choices):
13
+ return self._choices()
14
+ return self._choices
15
+
16
+ @choices.setter
17
+ def choices(self, value):
18
+ self._choices = value
19
+
20
+ def __deepcopy__(self, memo):
21
+ obj = copy.copy(self)
22
+ obj.attrs = self.attrs.copy()
23
+ if not callable(self._choices):
24
+ obj._choices = copy.copy(self._choices)
25
+ memo[id(self)] = obj
26
+ return obj
27
+
28
+ def optgroups(self, name, value, attrs=None):
29
+ for (val, display) in self.choices:
30
+ if val == value[0]:
31
+ self.choices = [(val, display)]
32
+ break
33
+ return super().optgroups(name, value, attrs)
34
+
35
+
36
+ class ListSelect2(LazyChoicesMixin, autocomplete.ListSelect2):
37
+ pass
38
+
39
+
40
+ class Select2Multiple(LazyChoicesMixin, autocomplete.Select2Multiple):
41
+ pass
42
+
43
+
44
+ class Select2ListMixin:
45
+
46
+ def __init__(self, url, forward=None, *args, **kwargs):
47
+ self.url = url
48
+ self.forward = []
49
+ if forward:
50
+ self.forward = [fw.to_dict() for fw in forward]
51
+
52
+ widget = ListSelect2(
53
+ url=url, forward=forward, attrs={'data-html': True},
54
+
55
+ )
56
+ widget.choices = kwargs.get('choices', None)
57
+
58
+ super().__init__(widget=widget, *args, **kwargs)
59
+
60
+ class Select2ModelChoiceField(Select2ListMixin, forms.ModelChoiceField):
61
+ pass
62
+
63
+
64
+ class Select2ListChoiceField(Select2ListMixin, forms.ChoiceField):
65
+ pass
66
+
67
+
68
+ class Select2MultipleMixin:
69
+
70
+ def __init__(self, url, forward=None, *args, **kwargs):
71
+ self.url = url
72
+ self.forward = []
73
+ if forward:
74
+ self.forward = [fw.to_dict() for fw in forward]
75
+
76
+ widget = Select2Multiple(
77
+ url=url, forward=forward, attrs={'data-html': True}
78
+ )
79
+ widget.choices = kwargs.pop('choices', None)
80
+
81
+ super().__init__(widget, *args, **kwargs)
82
+
83
+
84
+ class Select2ModelMultipleChoiceField(
85
+ Select2MultipleMixin, forms.ModelMultipleChoiceField
86
+ ):
87
+ pass
88
+
89
+
90
+ class Select2ListMultipleChoiceField(
91
+ Select2MultipleMixin, forms.MultipleChoiceField
92
+ ):
93
+ pass