simo 2.0.41__py3-none-any.whl → 2.1.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 (94) hide show
  1. simo/__pycache__/asgi.cpython-38.pyc +0 -0
  2. simo/__pycache__/on_http_start.cpython-38.pyc +0 -0
  3. simo/__pycache__/settings.cpython-38.pyc +0 -0
  4. simo/__pycache__/wsgi.cpython-38.pyc +0 -0
  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__/base_types.cpython-38.pyc +0 -0
  13. simo/core/__pycache__/controllers.cpython-38.pyc +0 -0
  14. simo/core/__pycache__/form_fields.cpython-38.pyc +0 -0
  15. simo/core/__pycache__/forms.cpython-38.pyc +0 -0
  16. simo/core/__pycache__/gateways.cpython-38.pyc +0 -0
  17. simo/core/__pycache__/managers.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/admin.py +28 -10
  23. simo/core/api.py +24 -5
  24. simo/core/api_meta.py +23 -13
  25. simo/core/app_widgets.py +6 -0
  26. simo/core/apps.py +10 -0
  27. simo/core/base_types.py +1 -0
  28. simo/core/controllers.py +57 -0
  29. simo/core/form_fields.py +93 -0
  30. simo/core/forms.py +15 -3
  31. simo/core/gateways.py +1 -1
  32. simo/core/managers.py +14 -1
  33. simo/core/migrations/0037_auto_20240606_1057.py +33 -0
  34. simo/core/migrations/__pycache__/0037_auto_20240606_1057.cpython-38.pyc +0 -0
  35. simo/core/models.py +28 -9
  36. simo/core/permissions.py +6 -3
  37. simo/core/serializers.py +77 -5
  38. simo/core/signal_receivers.py +25 -0
  39. simo/core/static/admin/css/simo.css +14 -0
  40. simo/core/templates/admin/controller_widgets/button.html +8 -0
  41. simo/core/templates/admin/core/component_change_form.html +97 -0
  42. simo/core/templates/admin/formset_widget.html +88 -118
  43. simo/core/templates/admin/formset_widget_old.html +122 -0
  44. simo/core/templates/admin/wizard/wizard_add.html +16 -9
  45. simo/core/utils/__pycache__/admin.cpython-38.pyc +0 -0
  46. simo/core/utils/__pycache__/cache.cpython-38.pyc +0 -0
  47. simo/core/utils/__pycache__/formsets.cpython-38.pyc +0 -0
  48. simo/core/utils/admin.py +11 -0
  49. simo/core/utils/cache.py +15 -0
  50. simo/core/utils/formsets.py +11 -18
  51. simo/fleet/__pycache__/auto_urls.cpython-38.pyc +0 -0
  52. simo/fleet/__pycache__/controllers.cpython-38.pyc +0 -0
  53. simo/fleet/__pycache__/forms.cpython-38.pyc +0 -0
  54. simo/fleet/__pycache__/gateways.cpython-38.pyc +0 -0
  55. simo/fleet/__pycache__/models.cpython-38.pyc +0 -0
  56. simo/fleet/__pycache__/socket_consumers.cpython-38.pyc +0 -0
  57. simo/fleet/__pycache__/utils.cpython-38.pyc +0 -0
  58. simo/fleet/__pycache__/views.cpython-38.pyc +0 -0
  59. simo/fleet/auto_urls.py +7 -1
  60. simo/fleet/controllers.py +193 -30
  61. simo/fleet/forms.py +223 -87
  62. simo/fleet/gateways.py +53 -2
  63. simo/fleet/migrations/0036_auto_20240605_0702.py +68 -0
  64. simo/fleet/migrations/__pycache__/0036_auto_20240605_0702.cpython-38.pyc +0 -0
  65. simo/fleet/models.py +35 -6
  66. simo/fleet/socket_consumers.py +1 -1
  67. simo/fleet/templates/fleet/controllers_info/button.md +16 -0
  68. simo/fleet/utils.py +31 -1
  69. simo/fleet/views.py +45 -0
  70. simo/generic/__pycache__/controllers.cpython-38.pyc +0 -0
  71. simo/generic/__pycache__/forms.cpython-38.pyc +0 -0
  72. simo/generic/__pycache__/gateways.cpython-38.pyc +0 -0
  73. simo/generic/controllers.py +59 -14
  74. simo/generic/forms.py +4 -3
  75. simo/generic/gateways.py +2 -0
  76. simo/generic/templates/admin/controller_widgets/blinds.html +2 -1
  77. simo/generic/templates/generic/controllers_info/dummy.md +3 -0
  78. simo/generic/templates/generic/controllers_info/stateselect.md +2 -0
  79. simo/settings.py +20 -4
  80. simo/users/__init__.py +1 -0
  81. simo/users/__pycache__/__init__.cpython-38.pyc +0 -0
  82. simo/users/__pycache__/admin.cpython-38.pyc +0 -0
  83. simo/users/__pycache__/apps.cpython-38.pyc +0 -0
  84. simo/users/__pycache__/models.cpython-38.pyc +0 -0
  85. simo/users/apps.py +9 -0
  86. simo/users/migrations/__pycache__/0029_alter_instanceuser_options_instanceuser_order.cpython-38.pyc +0 -0
  87. simo/users/migrations/__pycache__/0030_alter_instanceuser_options_remove_instanceuser_order.cpython-38.pyc +0 -0
  88. simo/users/models.py +16 -3
  89. {simo-2.0.41.dist-info → simo-2.1.0.dist-info}/METADATA +5 -3
  90. {simo-2.0.41.dist-info → simo-2.1.0.dist-info}/RECORD +93 -74
  91. simo/wsgi.py +0 -7
  92. {simo-2.0.41.dist-info → simo-2.1.0.dist-info}/LICENSE.md +0 -0
  93. {simo-2.0.41.dist-info → simo-2.1.0.dist-info}/WHEEL +0 -0
  94. {simo-2.0.41.dist-info → simo-2.1.0.dist-info}/top_level.txt +0 -0
Binary file
Binary file
Binary file
simo/core/__init__.py CHANGED
@@ -0,0 +1 @@
1
+ default_app_config = 'core.apps.SIMOCoreAppConfig'
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,8 +60,9 @@ class IconAdmin(admin.ModelAdmin):
57
60
 
58
61
 
59
62
 
63
+
60
64
  @admin.register(Instance)
61
- class InstanceAdmin(admin.ModelAdmin):
65
+ class InstanceAdmin(EasyObjectsDeleteMixin, admin.ModelAdmin):
62
66
  list_display = 'name', 'timezone', 'uid'
63
67
  exclude = 'learn_fingerprints_start', 'learn_fingerprints'
64
68
 
@@ -75,7 +79,7 @@ class InstanceAdmin(admin.ModelAdmin):
75
79
 
76
80
 
77
81
  @admin.register(Zone)
78
- class ZoneAdmin(SortableAdminMixin, admin.ModelAdmin):
82
+ class ZoneAdmin(EasyObjectsDeleteMixin, SortableAdminMixin, admin.ModelAdmin):
79
83
  list_display = 'name', 'instance'
80
84
  search_fields = 'name',
81
85
  list_filter = 'instance',
@@ -91,7 +95,7 @@ class ZoneAdmin(SortableAdminMixin, admin.ModelAdmin):
91
95
 
92
96
 
93
97
  @admin.register(Category)
94
- class CategoryAdmin(SortableAdminMixin, admin.ModelAdmin):
98
+ class CategoryAdmin(EasyObjectsDeleteMixin, SortableAdminMixin, admin.ModelAdmin):
95
99
  form = CategoryAdminForm
96
100
  list_display = 'name_display', 'all'
97
101
  search_fields = 'name',
@@ -118,7 +122,7 @@ class CategoryAdmin(SortableAdminMixin, admin.ModelAdmin):
118
122
 
119
123
 
120
124
  @admin.register(Gateway)
121
- class GatewayAdmin(admin.ModelAdmin):
125
+ class GatewayAdmin(EasyObjectsDeleteMixin, admin.ModelAdmin):
122
126
  list_display = 'type', 'status'
123
127
  readonly_fields = ('type', 'control')
124
128
 
@@ -254,14 +258,14 @@ class ComponentPermissionInline(admin.TabularInline):
254
258
 
255
259
 
256
260
  @admin.register(Component)
257
- class ComponentAdmin(admin.ModelAdmin):
261
+ class ComponentAdmin(EasyObjectsDeleteMixin, admin.ModelAdmin):
258
262
  form = BaseComponentForm
259
263
  list_display = (
260
264
  'id', 'name_display', 'value_display', 'base_type', 'alive', 'battery_level',
261
265
  'alarm_category', 'show_in_app',
262
266
  )
263
267
  readonly_fields = (
264
- 'id', 'controller_uid', 'base_type', 'gateway', 'config',
268
+ 'id', 'controller_uid', 'base_type', 'info', 'gateway', 'config',
265
269
  'alive', 'error_msg', 'battery_level',
266
270
  'control', 'value', 'arm_status', 'history', 'meta'
267
271
  )
@@ -274,6 +278,9 @@ class ComponentAdmin(admin.ModelAdmin):
274
278
  list_per_page = 100
275
279
  change_list_template = 'admin/component_change_list.html'
276
280
  inlines = ComponentPermissionInline,
281
+ # standard django admin change_form.html template + adds side panel
282
+ # for displaying component controller info.
283
+ #change_form_template = 'admin/core/component_change_form.html'
277
284
 
278
285
  def get_fieldsets(self, request, obj=None):
279
286
  form = self._get_form_for_get_fields(request, obj)
@@ -346,6 +353,7 @@ class ComponentAdmin(admin.ModelAdmin):
346
353
  ctx['selected_type'] = ALL_BASE_TYPES.get(
347
354
  controller_cls.base_type, controller_cls.base_type
348
355
  )
356
+ ctx['info'] = controller_cls.info(controller_cls)
349
357
  if request.method == 'POST':
350
358
  ctx['form'] = add_form(
351
359
  request=request,
@@ -356,7 +364,6 @@ class ComponentAdmin(admin.ModelAdmin):
356
364
  pop_fields_from_form(ctx['form'])
357
365
  if ctx['form'].is_valid():
358
366
  if ctx['form'].controller.is_discoverable:
359
- print("INIT DISCOVERY!!!")
360
367
  ctx['form'].controller.init_discovery(
361
368
  ctx['form'].cleaned_data
362
369
  )
@@ -395,7 +402,6 @@ class ComponentAdmin(admin.ModelAdmin):
395
402
  if ctx['form'].is_valid():
396
403
  request.session['add_comp_type'] = \
397
404
  ctx['form'].cleaned_data['controller_type']
398
- print("Session controller type: ", request.session['add_comp_type'])
399
405
  return redirect(request.path)
400
406
 
401
407
  else:
@@ -475,6 +481,18 @@ class ComponentAdmin(admin.ModelAdmin):
475
481
  }
476
482
  )
477
483
 
484
+ def info(self, obj):
485
+ if not obj.controller:
486
+ return
487
+ info = obj.controller.info()
488
+ if not info:
489
+ return
490
+ return mark_safe(
491
+ f'<div class="markdownified-info">'
492
+ f'{markdown.markdown(info)}'
493
+ f'</div>'
494
+ )
495
+
478
496
  def history(self, obj):
479
497
  if not obj:
480
498
  return ''
@@ -483,4 +501,4 @@ class ComponentAdmin(admin.ModelAdmin):
483
501
  'value_history': obj.history.filter(type='value').order_by('-date')[:50],
484
502
  'arm_status_history': obj.history.filter(type='security').order_by('-date')[:50]
485
503
  }
486
- )
504
+ )
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
@@ -150,9 +152,9 @@ def get_components_queryset(instance, user):
150
152
  c_ids = set()
151
153
 
152
154
  from simo.generic.controllers import WeatherForecast
153
- general_components = []
155
+
154
156
  if instance.indoor_climate_sensor:
155
- general_components.append(instance.indoor_climate_sensor_id)
157
+ c_ids.add(instance.indoor_climate_sensor.id)
156
158
  wf_c = Component.objects.filter(
157
159
  zone__instance=instance,
158
160
  controller_uid=WeatherForecast.uid, config__is_main=True
@@ -170,7 +172,7 @@ def get_components_queryset(instance, user):
170
172
 
171
173
  for cp in user_role.component_permissions.all():
172
174
  if cp.read:
173
- c_ids.add(cp.id)
175
+ c_ids.add(cp.component.id)
174
176
 
175
177
  return qs.filter(id__in=c_ids)
176
178
 
@@ -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'
@@ -626,7 +644,8 @@ class ControllerTypes(InstanceMixin, viewsets.GenericViewSet):
626
644
  'name': cls.name,
627
645
  'is_discoverable': cls.is_discoverable,
628
646
  'manual_add': cls.manual_add,
629
- 'discovery_msg': cls.discovery_msg
647
+ 'discovery_msg': cls.discovery_msg,
648
+ 'info': cls.info(cls)
630
649
  })
631
650
 
632
651
  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,10 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class SIMOCoreAppConfig(AppConfig):
5
+ name = 'simo.core'
6
+
7
+ def ready(self):
8
+ from actstream import registry
9
+ registry.register(self.get_model('Component'))
10
+ registry.register(self.get_model('Gateway'))
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
 
@@ -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
simo/core/forms.py CHANGED
@@ -12,6 +12,7 @@ from django.urls.base import get_script_prefix
12
12
  from django.utils.safestring import mark_safe
13
13
  from django.utils.translation import gettext_lazy as _
14
14
  from django.contrib.contenttypes.models import ContentType
15
+ from actstream import action
15
16
  from timezone_utils.choices import ALL_TIMEZONES_CHOICES
16
17
  from dal import autocomplete
17
18
  from .models import (
@@ -209,6 +210,19 @@ class ConfigFieldsMixin:
209
210
  self.instance.config[field_name] = \
210
211
  self.cleaned_data[field_name]
211
212
 
213
+ if commit:
214
+ from simo.users.middleware import get_current_user
215
+ actor = get_current_user()
216
+ if self.instance.pk:
217
+ verb = 'modified'
218
+ else:
219
+ verb = 'created'
220
+ action.send(
221
+ actor, target=self.instance, verb=verb,
222
+ instance_id=self.instance.zone.instance.id,
223
+ action_type='management_event'
224
+ )
225
+
212
226
  return super().save(commit)
213
227
 
214
228
 
@@ -364,7 +378,7 @@ class ComponentAdminForm(forms.ModelForm):
364
378
  'alarm_category', 'arm_status',
365
379
  'notes'
366
380
  )
367
- base_fields = ['id', 'gateway', 'base_type', 'name']
381
+ base_fields = ['id', 'gateway', 'base_type', 'info', 'name']
368
382
  if cls.has_icon:
369
383
  base_fields.append('icon')
370
384
 
@@ -432,8 +446,6 @@ class ComponentAdminForm(forms.ModelForm):
432
446
  return self.cleaned_data['instance_methods']
433
447
 
434
448
 
435
-
436
-
437
449
  class BaseComponentForm(ConfigFieldsMixin, ComponentAdminForm):
438
450
  pass
439
451
 
simo/core/gateways.py CHANGED
@@ -74,7 +74,7 @@ class BaseObjectCommandsGatewayHandler(BaseGatewayHandler):
74
74
  def _run_periodic_task(self, exit, task, period):
75
75
  while not exit.is_set():
76
76
  try:
77
- print(f"Run periodic task {task}!")
77
+ #print(f"Run periodic task {task}!")
78
78
  getattr(self, task)()
79
79
  except Exception as e:
80
80
  self.logger.error(e, exc_info=True)
simo/core/managers.py CHANGED
@@ -1,10 +1,21 @@
1
1
  import sys
2
2
  import traceback
3
+ from actstream.managers import ActionManager as OrgActionManager
3
4
  from .middleware import get_current_instance
4
5
  from django.utils import timezone
5
6
  from django.db import models
6
7
 
7
8
 
9
+ class ActionManager(OrgActionManager):
10
+
11
+ def get_queryset(self):
12
+ qs = super().get_queryset()
13
+ instance = get_current_instance()
14
+ if instance:
15
+ qs = qs.filter(data__instance_id=instance.id)
16
+ return qs
17
+
18
+
8
19
  class ZonesManager(models.Manager):
9
20
 
10
21
  def get_queryset(self):
@@ -32,7 +43,7 @@ class ComponentsManager(models.Manager):
32
43
  instance = get_current_instance()
33
44
  if instance:
34
45
  qs = qs.filter(zone__instance=instance)
35
- return qs
46
+ return qs.select_related('zone', 'zone__instance', 'gateway')
36
47
 
37
48
  def bulk_send(self, data):
38
49
  """
@@ -50,6 +61,8 @@ class ComponentsManager(models.Manager):
50
61
 
51
62
  gateway_components = {}
52
63
  for comp, value in data.items():
64
+ if not comp.controller:
65
+ continue
53
66
  try:
54
67
  value = comp.controller._validate_val(value, BEFORE_SEND)
55
68
  except: