simo 2.6.8__py3-none-any.whl → 2.7.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of simo might be problematic. Click here for more details.
- simo/__pycache__/settings.cpython-38.pyc +0 -0
- simo/automation/__pycache__/controllers.cpython-38.pyc +0 -0
- simo/automation/__pycache__/gateways.cpython-38.pyc +0 -0
- simo/automation/controllers.py +21 -2
- simo/automation/gateways.py +130 -1
- simo/core/__pycache__/api.cpython-38.pyc +0 -0
- simo/core/__pycache__/api_meta.cpython-38.pyc +0 -0
- simo/core/__pycache__/autocomplete_views.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__/middleware.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/api.py +4 -1
- simo/core/api_meta.py +6 -2
- simo/core/autocomplete_views.py +4 -3
- simo/core/controllers.py +13 -4
- simo/core/form_fields.py +92 -1
- simo/core/forms.py +10 -4
- simo/core/gateways.py +11 -1
- simo/core/serializers.py +8 -1
- simo/core/signal_receivers.py +7 -83
- simo/core/utils/__pycache__/converters.cpython-38.pyc +0 -0
- simo/core/utils/converters.py +59 -0
- 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__/models.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/views.cpython-38.pyc +0 -0
- simo/fleet/auto_urls.py +5 -0
- simo/fleet/controllers.py +4 -26
- simo/fleet/forms.py +52 -4
- simo/fleet/migrations/__pycache__/0043_auto_20241203_0930.cpython-38.pyc +0 -0
- simo/fleet/views.py +37 -6
- 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 +120 -1
- simo/generic/forms.py +77 -9
- simo/generic/gateways.py +81 -2
- simo/users/__pycache__/admin.cpython-38.pyc +0 -0
- simo/users/__pycache__/api.cpython-38.pyc +0 -0
- simo/users/__pycache__/auto_urls.cpython-38.pyc +0 -0
- simo/users/__pycache__/views.cpython-38.pyc +0 -0
- simo/users/admin.py +9 -0
- simo/users/api.py +2 -0
- simo/users/auto_urls.py +6 -3
- simo/users/migrations/0039_auto_20241117_1039.py +25 -24
- simo/users/migrations/__pycache__/0039_auto_20241117_1039.cpython-38.pyc +0 -0
- simo/users/views.py +20 -1
- {simo-2.6.8.dist-info → simo-2.7.1.dist-info}/METADATA +1 -1
- {simo-2.6.8.dist-info → simo-2.7.1.dist-info}/RECORD +58 -55
- {simo-2.6.8.dist-info → simo-2.7.1.dist-info}/LICENSE.md +0 -0
- {simo-2.6.8.dist-info → simo-2.7.1.dist-info}/WHEEL +0 -0
- {simo-2.6.8.dist-info → simo-2.7.1.dist-info}/entry_points.txt +0 -0
- {simo-2.6.8.dist-info → simo-2.7.1.dist-info}/top_level.txt +0 -0
simo/generic/forms.py
CHANGED
|
@@ -425,21 +425,89 @@ class StateSelectForm(BaseComponentForm):
|
|
|
425
425
|
states = FormsetField(
|
|
426
426
|
formset_factory(StateForm, can_delete=True, can_order=True, extra=0)
|
|
427
427
|
)
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
class MainStateSelectForm(BaseComponentForm):
|
|
431
|
+
weekdays_morning_hour = forms.IntegerField(
|
|
432
|
+
initial=6, min_value=3, max_value=12
|
|
433
|
+
)
|
|
434
|
+
weekends_morning_hour = forms.IntegerField(
|
|
435
|
+
initial=6, min_value=3, max_value=12
|
|
436
|
+
)
|
|
437
|
+
away_on_no_action = forms.IntegerField(
|
|
438
|
+
required=False, initial=40, min_value=1, max_value=360,
|
|
439
|
+
help_text="Set state to Away if nobody is at home "
|
|
440
|
+
"(requires location and fitness permissions on mobile app) "
|
|
441
|
+
"and there were "
|
|
442
|
+
"no action for more than given amount of minutes from any "
|
|
443
|
+
"security alarm acategory sensors (motion sensors, door sensors etc..).<br>"
|
|
444
|
+
"No value disables this behavior."
|
|
445
|
+
)
|
|
446
|
+
sleeping_phones_hour = forms.IntegerField(
|
|
447
|
+
initial=True, required=False, min_value=18, max_value=24,
|
|
448
|
+
help_text='Set mode to "Sleep" if it is later than given hour '
|
|
449
|
+
'and all home owners phones who are at home are put on charge '
|
|
450
|
+
'(requires location and fitness permissions on mobile app). <br>'
|
|
451
|
+
'Set back to regular state as soon as none of the home owners phones '
|
|
452
|
+
'are on charge and it is morning hour or past it.'
|
|
432
453
|
)
|
|
454
|
+
states = FormsetField(
|
|
455
|
+
formset_factory(StateForm, can_delete=True, can_order=True, extra=0)
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
def clean(self):
|
|
459
|
+
if not self.instance.id:
|
|
460
|
+
if Component.objects.filter(
|
|
461
|
+
controller_uid='simo.generic.controllers.MainState'
|
|
462
|
+
).count():
|
|
463
|
+
raise forms.ValidationError("Main state already exists!")
|
|
464
|
+
|
|
465
|
+
formset_errors = {}
|
|
466
|
+
required_states = {
|
|
467
|
+
'morning', 'day', 'evening', 'night', 'sleep', 'away', 'vacation'
|
|
468
|
+
}
|
|
469
|
+
found_states = set()
|
|
470
|
+
for i, state in enumerate(self.cleaned_data.get('states', [])):
|
|
471
|
+
if state.get('slug') in found_states:
|
|
472
|
+
formset_errors['i'] = {'slug': "Duplicate!"}
|
|
473
|
+
continue
|
|
474
|
+
found_states.add(state.get('slug'))
|
|
475
|
+
|
|
476
|
+
errors_list = []
|
|
477
|
+
if formset_errors:
|
|
478
|
+
for i, control in enumerate(self.cleaned_data['states']):
|
|
479
|
+
errors_list.append(formset_errors.get(i, {}))
|
|
480
|
+
if errors_list:
|
|
481
|
+
self._errors['states'] = errors_list
|
|
482
|
+
if 'states' in self.cleaned_data:
|
|
483
|
+
del self.cleaned_data['states']
|
|
484
|
+
|
|
485
|
+
else:
|
|
486
|
+
missing_states = required_states - found_states
|
|
487
|
+
if missing_states:
|
|
488
|
+
if len(missing_states) == 1:
|
|
489
|
+
self.add_error(
|
|
490
|
+
'states',
|
|
491
|
+
f'"{list(missing_states)[0]}" state is missing!'
|
|
492
|
+
)
|
|
493
|
+
self.add_error(
|
|
494
|
+
'states',
|
|
495
|
+
f"Required states are missing: {list(missing_states)}!"
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
return self.cleaned_data
|
|
433
499
|
|
|
434
500
|
def save(self, commit=True):
|
|
435
|
-
if commit
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
501
|
+
if commit:
|
|
502
|
+
for s in Component.objects.filter(
|
|
503
|
+
base_type='state-select', config__is_main=True
|
|
504
|
+
).exclude(id=self.instance.id):
|
|
505
|
+
s.config['is_main'] = False
|
|
506
|
+
s.save()
|
|
440
507
|
return super().save(commit)
|
|
441
508
|
|
|
442
509
|
|
|
510
|
+
|
|
443
511
|
class AlarmClockEventForm(forms.Form):
|
|
444
512
|
uid = HiddenField(required=False)
|
|
445
513
|
enabled = forms.BooleanField(initial=True)
|
simo/generic/gateways.py
CHANGED
|
@@ -72,10 +72,17 @@ class GenericGatewayHandler(BaseObjectCommandsGatewayHandler):
|
|
|
72
72
|
('watch_alarm_clocks', 30),
|
|
73
73
|
('watch_watering', 60),
|
|
74
74
|
('watch_alarm_events', 1),
|
|
75
|
-
('watch_timers', 1)
|
|
75
|
+
('watch_timers', 1),
|
|
76
|
+
('watch_main_states', 60)
|
|
76
77
|
)
|
|
77
78
|
|
|
78
|
-
|
|
79
|
+
def __init__(self, *args, **kwargs):
|
|
80
|
+
super().__init__(*args, **kwargs)
|
|
81
|
+
self.last_sensor_actions = {}
|
|
82
|
+
self.sensors_on_watch = {}
|
|
83
|
+
self.sleep_is_on = {}
|
|
84
|
+
|
|
85
|
+
|
|
79
86
|
|
|
80
87
|
def watch_thermostats(self):
|
|
81
88
|
from .controllers import Thermostat
|
|
@@ -242,6 +249,78 @@ class GenericGatewayHandler(BaseObjectCommandsGatewayHandler):
|
|
|
242
249
|
print(traceback.format_exc(), file=sys.stderr)
|
|
243
250
|
|
|
244
251
|
|
|
252
|
+
def watch_main_state(self, state):
|
|
253
|
+
i_id = state.zone.instance.id
|
|
254
|
+
if state.value in ('day', 'night', 'evening', 'morning'):
|
|
255
|
+
new_state = state.get_day_evening_night_morning()
|
|
256
|
+
if new_state != state.value:
|
|
257
|
+
print(f"New main state of {state.zone.instance} - {new_state}")
|
|
258
|
+
state.send(new_state)
|
|
259
|
+
|
|
260
|
+
if state.config.get('away_on_no_action'):
|
|
261
|
+
for sensor in Component.objects.filter(
|
|
262
|
+
zone__instance=state.zone.instance,
|
|
263
|
+
base_type='binary-sensor', alarm_category='security'
|
|
264
|
+
):
|
|
265
|
+
if state.id not in self.sensors_on_watch:
|
|
266
|
+
self.sensors_on_watch[state.id] = {}
|
|
267
|
+
|
|
268
|
+
if sensor.id not in self.sensors_on_watch[state.id]:
|
|
269
|
+
self.sensors_on_watch[state.id][sensor.id] = i_id
|
|
270
|
+
self.last_sensor_actions[i_id] = time.time()
|
|
271
|
+
sensor.on_change(self.security_sensor_change)
|
|
272
|
+
|
|
273
|
+
last_action = self.last_sensor_actions.get(i_id, time.time())
|
|
274
|
+
if state.check_is_away(last_action):
|
|
275
|
+
if state.value != 'away':
|
|
276
|
+
print(f"New main state of "
|
|
277
|
+
f"{state.zone.instance} - away")
|
|
278
|
+
state.send('away')
|
|
279
|
+
else:
|
|
280
|
+
if state.value == 'away':
|
|
281
|
+
try:
|
|
282
|
+
new_state = state.get_day_evening_night_morning()
|
|
283
|
+
except:
|
|
284
|
+
new_state = 'day'
|
|
285
|
+
print(f"New main state of "
|
|
286
|
+
f"{state.zone.instance} - {new_state}")
|
|
287
|
+
state.send(new_state)
|
|
288
|
+
|
|
289
|
+
if state.config.get('sleeping_phones_hour') is not None:
|
|
290
|
+
sleep_time = state.owner_phones_on_sleep()
|
|
291
|
+
if sleep_time and state.value != 'sleep':
|
|
292
|
+
print(f"New main state of {state.zone.instance} - sleep")
|
|
293
|
+
state.send('sleep')
|
|
294
|
+
elif state.value == 'sleep':
|
|
295
|
+
try:
|
|
296
|
+
new_state = state.get_day_evening_night_morning()
|
|
297
|
+
except:
|
|
298
|
+
new_state = 'day'
|
|
299
|
+
print(f"New main state of "
|
|
300
|
+
f"{state.zone.instance} - {new_state}")
|
|
301
|
+
state.send(new_state)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def watch_main_states(self):
|
|
305
|
+
drop_current_instance()
|
|
306
|
+
from .controllers import MainState
|
|
307
|
+
for state in Component.objects.filter(
|
|
308
|
+
controller_uid=MainState.uid
|
|
309
|
+
).select_related('zone', 'zone__instance'):
|
|
310
|
+
try:
|
|
311
|
+
self.watch_main_state(state)
|
|
312
|
+
except Exception as e:
|
|
313
|
+
print(traceback.format_exc(), file=sys.stderr)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def security_sensor_change(self, sensor):
|
|
318
|
+
self.last_sensor_actions[
|
|
319
|
+
self.sensors_on_watch[sensor.id]
|
|
320
|
+
] = time.time()
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
|
|
245
324
|
class DummyGatewayHandler(BaseObjectCommandsGatewayHandler):
|
|
246
325
|
name = "Dummy"
|
|
247
326
|
config_form = BaseGatewayForm
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
simo/users/admin.py
CHANGED
|
@@ -32,6 +32,15 @@ class PermissionsRoleAdmin(admin.ModelAdmin):
|
|
|
32
32
|
inlines = ComponentPermissionInline,
|
|
33
33
|
list_filter = 'is_superuser', 'is_owner', 'can_manage_users', 'is_default'
|
|
34
34
|
|
|
35
|
+
|
|
36
|
+
def get_queryset(self, request):
|
|
37
|
+
qs = super().get_queryset(request)
|
|
38
|
+
instance = get_current_instance()
|
|
39
|
+
if instance:
|
|
40
|
+
return qs.filter(instance=instance)
|
|
41
|
+
return qs
|
|
42
|
+
|
|
43
|
+
|
|
35
44
|
def save_model(self, request, obj, form, change):
|
|
36
45
|
if not obj.id:
|
|
37
46
|
obj.instance = request.instance
|
simo/users/api.py
CHANGED
|
@@ -11,6 +11,7 @@ from django.utils import timezone
|
|
|
11
11
|
from django_filters.rest_framework import DjangoFilterBackend
|
|
12
12
|
from simo.conf import dynamic_settings
|
|
13
13
|
from simo.core.api import InstanceMixin
|
|
14
|
+
from simo.core.middleware import drop_current_instance
|
|
14
15
|
from .models import (
|
|
15
16
|
User, UserDevice, UserDeviceReportLog, PermissionsRole, InstanceInvitation,
|
|
16
17
|
Fingerprint, ComponentPermission, InstanceUser
|
|
@@ -247,6 +248,7 @@ class UserDeviceReport(InstanceMixin, viewsets.GenericViewSet):
|
|
|
247
248
|
self.instance.location, location
|
|
248
249
|
) < dynamic_settings['users__at_home_radius']
|
|
249
250
|
|
|
251
|
+
drop_current_instance()
|
|
250
252
|
for iu in request.user.instance_roles.filter(is_active=True):
|
|
251
253
|
if not relay:
|
|
252
254
|
iu.at_home = True
|
simo/users/auto_urls.py
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
from django.urls import
|
|
2
|
-
from
|
|
3
|
-
from .views import accept_invitation
|
|
1
|
+
from django.urls import re_path, path
|
|
2
|
+
from .views import accept_invitation, RolesAutocomplete
|
|
4
3
|
|
|
5
4
|
urlpatterns = [
|
|
6
5
|
re_path(
|
|
7
6
|
r"^accept-invitation/(?P<token>[a-zA-Z0-9]+)/$",
|
|
8
7
|
accept_invitation, name='accept_invitation'
|
|
9
8
|
),
|
|
9
|
+
path(
|
|
10
|
+
'autocomplete-roles',
|
|
11
|
+
RolesAutocomplete.as_view(), name='autocomplete-user-roles'
|
|
12
|
+
),
|
|
10
13
|
]
|
|
@@ -4,30 +4,31 @@ from django.db import migrations
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
def forwards_func(apps, schema_editor):
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
7
|
+
pass
|
|
8
|
+
# from simo.automation.helpers import haversine_distance
|
|
9
|
+
# UserDeviceReportLog = apps.get_model("users", "UserDeviceReportLog")
|
|
10
|
+
#
|
|
11
|
+
# logs = UserDeviceReportLog.objects.filter(
|
|
12
|
+
# instance__isnull=False
|
|
13
|
+
# ).select_related('instance')
|
|
14
|
+
#
|
|
15
|
+
# print("Calculate at_home on UserDeviceReportLog's!")
|
|
16
|
+
#
|
|
17
|
+
# bulk_update = []
|
|
18
|
+
# for log in tqdm(logs, total=logs.count()):
|
|
19
|
+
# log.at_home = False
|
|
20
|
+
# if not log.relay:
|
|
21
|
+
# log.at_home = True
|
|
22
|
+
# elif log.location:
|
|
23
|
+
# log.at_home = haversine_distance(
|
|
24
|
+
# log.instance.location, log.location
|
|
25
|
+
# ) < 250
|
|
26
|
+
# if log.at_home:
|
|
27
|
+
# bulk_update.append(log)
|
|
28
|
+
# if len(bulk_update) > 1000:
|
|
29
|
+
# UserDeviceReportLog.objects.bulk_update(bulk_update, ["at_home"])
|
|
30
|
+
# bulk_update = []
|
|
31
|
+
# UserDeviceReportLog.objects.bulk_update(bulk_update, ["at_home"])
|
|
31
32
|
|
|
32
33
|
|
|
33
34
|
def reverse_func(apps, schema_editor):
|
|
Binary file
|
simo/users/views.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import re
|
|
2
2
|
from django.contrib.auth.decorators import login_required
|
|
3
|
+
from dal import autocomplete
|
|
3
4
|
from django.contrib.auth import logout
|
|
4
5
|
from django.urls import re_path
|
|
5
6
|
from django.shortcuts import get_object_or_404, render
|
|
@@ -11,8 +12,10 @@ from django.template.loader import render_to_string
|
|
|
11
12
|
from django.http import (
|
|
12
13
|
JsonResponse, HttpResponseRedirect, HttpResponse, Http404
|
|
13
14
|
)
|
|
15
|
+
from simo.core.middleware import get_current_instance
|
|
16
|
+
from simo.core.utils.helpers import search_queryset
|
|
14
17
|
from simo.conf import dynamic_settings
|
|
15
|
-
from .models import InstanceInvitation
|
|
18
|
+
from .models import InstanceInvitation, PermissionsRole, InstanceUser
|
|
16
19
|
|
|
17
20
|
|
|
18
21
|
@atomic
|
|
@@ -104,3 +107,19 @@ def protected_static(prefix, **kwargs):
|
|
|
104
107
|
r'^%s(?P<path>.*)$' % re.escape(prefix.lstrip('/')),
|
|
105
108
|
serve_protected, kwargs={'prefix': prefix}
|
|
106
109
|
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class RolesAutocomplete(autocomplete.Select2QuerySetView):
|
|
113
|
+
|
|
114
|
+
def get_queryset(self):
|
|
115
|
+
if not self.request.user.is_authenticated:
|
|
116
|
+
raise Http404()
|
|
117
|
+
|
|
118
|
+
qs = PermissionsRole.objects.filter(instance=get_current_instance(self.request))
|
|
119
|
+
|
|
120
|
+
if self.request.GET.get('value'):
|
|
121
|
+
qs = qs.filter(pk__in=self.request.GET['value'].split(','))
|
|
122
|
+
elif self.q:
|
|
123
|
+
qs = search_queryset(qs, self.q, ('name',))
|
|
124
|
+
|
|
125
|
+
return qs.distinct()
|