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.
- simo/__pycache__/asgi.cpython-38.pyc +0 -0
- simo/__pycache__/settings.cpython-38.pyc +0 -0
- simo/__pycache__/wsgi.cpython-38.pyc +0 -0
- simo/asgi.py +1 -1
- 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__/auto_urls.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__/dynamic_settings.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/__pycache__/tasks.cpython-38.pyc +0 -0
- simo/core/__pycache__/views.cpython-38.pyc +0 -0
- simo/core/admin.py +26 -26
- simo/core/api.py +22 -2
- simo/core/api_meta.py +23 -13
- simo/core/app_widgets.py +6 -0
- simo/core/apps.py +13 -0
- simo/core/auto_urls.py +2 -3
- simo/core/base_types.py +1 -0
- simo/core/controllers.py +57 -0
- simo/core/dynamic_settings.py +0 -8
- simo/core/form_fields.py +93 -0
- simo/core/forms.py +16 -101
- 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/0038_remove_instance_cover_image_and_more.py +30 -0
- simo/core/migrations/__pycache__/0037_auto_20240606_1057.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0038_remove_instance_cover_image_and_more.cpython-38.pyc +0 -0
- simo/core/models.py +30 -16
- 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/tasks.py +82 -49
- 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/user_tools.html +0 -3
- 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/core/views.py +2 -85
- 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 +194 -31
- 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/0037_alter_colonelpin_options_alter_colonelpin_no_and_more.py +27 -0
- simo/fleet/migrations/__pycache__/0036_auto_20240605_0702.cpython-38.pyc +0 -0
- simo/fleet/migrations/__pycache__/0037_alter_colonelpin_options_alter_colonelpin_no_and_more.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__/forms.cpython-38.pyc +0 -0
- simo/generic/__pycache__/gateways.cpython-38.pyc +0 -0
- simo/generic/controllers.py +61 -16
- simo/generic/forms.py +0 -3
- simo/generic/gateways.py +2 -0
- simo/generic/templates/admin/controller_widgets/blinds.html +2 -1
- simo/generic/templates/admin/controller_widgets/weather_forecast.html +1 -1
- simo/generic/templates/generic/controllers_info/dummy.md +3 -0
- simo/generic/templates/generic/controllers_info/stateselect.md +2 -0
- simo/management/__init__.py +0 -0
- simo/management/__pycache__/__init__.cpython-38.pyc +0 -0
- simo/management/__pycache__/on_http_start.cpython-38.pyc +0 -0
- simo/{_hub_template → management/_hub_template}/hub/nginx.conf +2 -2
- simo/{auto_update.py → management/auto_update.py} +3 -0
- simo/{cli.py → management/copy_template.py} +3 -16
- simo/management/install.py +258 -0
- simo/{on_http_start.py → management/on_http_start.py} +22 -2
- 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.2.dist-info}/METADATA +5 -3
- {simo-2.0.42.dist-info → simo-2.1.2.dist-info}/RECORD +122 -95
- simo-2.1.2.dist-info/entry_points.txt +2 -0
- simo/__pycache__/on_http_start.cpython-38.pyc +0 -0
- simo/wsgi.py +0 -7
- /simo/{_hub_template → management/_hub_template}/hub/asgi.py +0 -0
- /simo/{_hub_template → management/_hub_template}/hub/celeryc.py +0 -0
- /simo/{_hub_template → management/_hub_template}/hub/manage.py +0 -0
- /simo/{_hub_template → management/_hub_template}/hub/settings.py +0 -0
- /simo/{_hub_template → management/_hub_template}/hub/supervisor.conf +0 -0
- /simo/{_hub_template → management/_hub_template}/hub/urls.py +0 -0
- {simo-2.0.42.dist-info → simo-2.1.2.dist-info}/LICENSE.md +0 -0
- {simo-2.0.42.dist-info → simo-2.1.2.dist-info}/WHEEL +0 -0
- {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
|
|
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,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
|
-
|
|
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
|
@@ -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,
|
|
3
|
+
get_timestamp, update, restart, reboot, set_instance
|
|
4
4
|
)
|
|
5
5
|
from .autocomplete_views import (
|
|
6
|
-
IconModelAutocomplete,
|
|
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
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/dynamic_settings.py
CHANGED
|
@@ -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):
|
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
|