simo 2.0.42__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.
- simo/__pycache__/asgi.cpython-38.pyc +0 -0
- simo/__pycache__/on_http_start.cpython-38.pyc +0 -0
- simo/__pycache__/settings.cpython-38.pyc +0 -0
- simo/__pycache__/wsgi.cpython-38.pyc +0 -0
- simo/core/__init__.py +1 -0
- simo/core/__pycache__/__init__.cpython-38.pyc +0 -0
- simo/core/__pycache__/admin.cpython-38.pyc +0 -0
- simo/core/__pycache__/api.cpython-38.pyc +0 -0
- simo/core/__pycache__/api_meta.cpython-38.pyc +0 -0
- simo/core/__pycache__/app_widgets.cpython-38.pyc +0 -0
- simo/core/__pycache__/apps.cpython-38.pyc +0 -0
- simo/core/__pycache__/base_types.cpython-38.pyc +0 -0
- simo/core/__pycache__/controllers.cpython-38.pyc +0 -0
- simo/core/__pycache__/form_fields.cpython-38.pyc +0 -0
- simo/core/__pycache__/forms.cpython-38.pyc +0 -0
- simo/core/__pycache__/gateways.cpython-38.pyc +0 -0
- simo/core/__pycache__/managers.cpython-38.pyc +0 -0
- simo/core/__pycache__/models.cpython-38.pyc +0 -0
- simo/core/__pycache__/permissions.cpython-38.pyc +0 -0
- simo/core/__pycache__/serializers.cpython-38.pyc +0 -0
- simo/core/__pycache__/signal_receivers.cpython-38.pyc +0 -0
- simo/core/admin.py +28 -10
- simo/core/api.py +21 -2
- simo/core/api_meta.py +23 -13
- simo/core/app_widgets.py +6 -0
- simo/core/apps.py +10 -0
- simo/core/base_types.py +1 -0
- simo/core/controllers.py +57 -0
- simo/core/form_fields.py +93 -0
- simo/core/forms.py +15 -3
- simo/core/gateways.py +1 -1
- simo/core/managers.py +14 -1
- simo/core/migrations/0037_auto_20240606_1057.py +33 -0
- simo/core/migrations/__pycache__/0037_auto_20240606_1057.cpython-38.pyc +0 -0
- simo/core/models.py +28 -9
- simo/core/permissions.py +6 -3
- simo/core/serializers.py +77 -5
- simo/core/signal_receivers.py +25 -0
- simo/core/static/admin/css/simo.css +14 -0
- simo/core/templates/admin/controller_widgets/button.html +8 -0
- simo/core/templates/admin/core/component_change_form.html +97 -0
- simo/core/templates/admin/formset_widget.html +88 -118
- simo/core/templates/admin/formset_widget_old.html +122 -0
- simo/core/templates/admin/wizard/wizard_add.html +16 -9
- simo/core/utils/__pycache__/admin.cpython-38.pyc +0 -0
- simo/core/utils/__pycache__/cache.cpython-38.pyc +0 -0
- simo/core/utils/__pycache__/formsets.cpython-38.pyc +0 -0
- simo/core/utils/admin.py +11 -0
- simo/core/utils/cache.py +15 -0
- simo/core/utils/formsets.py +11 -18
- simo/fleet/__pycache__/auto_urls.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/controllers.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/forms.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/gateways.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/models.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/socket_consumers.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/utils.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/views.cpython-38.pyc +0 -0
- simo/fleet/auto_urls.py +7 -1
- simo/fleet/controllers.py +193 -30
- simo/fleet/forms.py +223 -87
- simo/fleet/gateways.py +53 -2
- simo/fleet/migrations/0036_auto_20240605_0702.py +68 -0
- simo/fleet/migrations/__pycache__/0036_auto_20240605_0702.cpython-38.pyc +0 -0
- simo/fleet/models.py +35 -6
- simo/fleet/socket_consumers.py +1 -1
- simo/fleet/templates/fleet/controllers_info/button.md +16 -0
- simo/fleet/utils.py +31 -1
- simo/fleet/views.py +45 -0
- simo/generic/__pycache__/controllers.cpython-38.pyc +0 -0
- simo/generic/__pycache__/gateways.cpython-38.pyc +0 -0
- simo/generic/controllers.py +59 -14
- simo/generic/gateways.py +2 -0
- simo/generic/templates/admin/controller_widgets/blinds.html +2 -1
- simo/generic/templates/generic/controllers_info/dummy.md +3 -0
- simo/generic/templates/generic/controllers_info/stateselect.md +2 -0
- simo/settings.py +20 -4
- simo/users/__init__.py +1 -0
- simo/users/__pycache__/__init__.cpython-38.pyc +0 -0
- simo/users/__pycache__/admin.cpython-38.pyc +0 -0
- simo/users/__pycache__/apps.cpython-38.pyc +0 -0
- simo/users/__pycache__/models.cpython-38.pyc +0 -0
- simo/users/apps.py +9 -0
- simo/users/migrations/__pycache__/0029_alter_instanceuser_options_instanceuser_order.cpython-38.pyc +0 -0
- simo/users/migrations/__pycache__/0030_alter_instanceuser_options_remove_instanceuser_order.cpython-38.pyc +0 -0
- simo/users/models.py +16 -3
- {simo-2.0.42.dist-info → simo-2.1.0.dist-info}/METADATA +5 -3
- {simo-2.0.42.dist-info → simo-2.1.0.dist-info}/RECORD +91 -72
- simo/wsgi.py +0 -7
- {simo-2.0.42.dist-info → simo-2.1.0.dist-info}/LICENSE.md +0 -0
- {simo-2.0.42.dist-info → simo-2.1.0.dist-info}/WHEEL +0 -0
- {simo-2.0.42.dist-info → simo-2.1.0.dist-info}/top_level.txt +0 -0
|
Binary file
|
|
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
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
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,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
|
|
@@ -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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
simo/core/base_types.py
CHANGED
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
|
|
simo/core/form_fields.py
ADDED
|
@@ -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:
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Generated by Django 4.2.10 on 2024-06-06 10:57
|
|
2
|
+
|
|
3
|
+
from django.db import migrations
|
|
4
|
+
|
|
5
|
+
def forwards_func(apps, schema_editor):
|
|
6
|
+
|
|
7
|
+
Component = apps.get_model("core", "Component")
|
|
8
|
+
Gateway = apps.get_model('core', "Gateway")
|
|
9
|
+
|
|
10
|
+
generic_gateway = Gateway.objects.filter(
|
|
11
|
+
type='simo.generic.gateways.GenericGatewayHandler'
|
|
12
|
+
).first()
|
|
13
|
+
if not generic_gateway:
|
|
14
|
+
return
|
|
15
|
+
|
|
16
|
+
Component.objects.filter(
|
|
17
|
+
controller_uid='simo.generic.controllers.StateSelect'
|
|
18
|
+
).update(gateway=generic_gateway)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def reverse_func(apps, schema_editor):
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Migration(migrations.Migration):
|
|
26
|
+
|
|
27
|
+
dependencies = [
|
|
28
|
+
('core', '0036_auto_20240521_0823'),
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
operations = [
|
|
32
|
+
migrations.RunPython(forwards_func, reverse_func, elidable=True),
|
|
33
|
+
]
|
|
Binary file
|