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.

Files changed (58) hide show
  1. simo/__pycache__/settings.cpython-38.pyc +0 -0
  2. simo/automation/__pycache__/controllers.cpython-38.pyc +0 -0
  3. simo/automation/__pycache__/gateways.cpython-38.pyc +0 -0
  4. simo/automation/controllers.py +21 -2
  5. simo/automation/gateways.py +130 -1
  6. simo/core/__pycache__/api.cpython-38.pyc +0 -0
  7. simo/core/__pycache__/api_meta.cpython-38.pyc +0 -0
  8. simo/core/__pycache__/autocomplete_views.cpython-38.pyc +0 -0
  9. simo/core/__pycache__/controllers.cpython-38.pyc +0 -0
  10. simo/core/__pycache__/form_fields.cpython-38.pyc +0 -0
  11. simo/core/__pycache__/forms.cpython-38.pyc +0 -0
  12. simo/core/__pycache__/gateways.cpython-38.pyc +0 -0
  13. simo/core/__pycache__/middleware.cpython-38.pyc +0 -0
  14. simo/core/__pycache__/serializers.cpython-38.pyc +0 -0
  15. simo/core/__pycache__/signal_receivers.cpython-38.pyc +0 -0
  16. simo/core/api.py +4 -1
  17. simo/core/api_meta.py +6 -2
  18. simo/core/autocomplete_views.py +4 -3
  19. simo/core/controllers.py +13 -4
  20. simo/core/form_fields.py +92 -1
  21. simo/core/forms.py +10 -4
  22. simo/core/gateways.py +11 -1
  23. simo/core/serializers.py +8 -1
  24. simo/core/signal_receivers.py +7 -83
  25. simo/core/utils/__pycache__/converters.cpython-38.pyc +0 -0
  26. simo/core/utils/converters.py +59 -0
  27. simo/fleet/__pycache__/auto_urls.cpython-38.pyc +0 -0
  28. simo/fleet/__pycache__/controllers.cpython-38.pyc +0 -0
  29. simo/fleet/__pycache__/forms.cpython-38.pyc +0 -0
  30. simo/fleet/__pycache__/models.cpython-38.pyc +0 -0
  31. simo/fleet/__pycache__/views.cpython-38.pyc +0 -0
  32. simo/fleet/auto_urls.py +5 -0
  33. simo/fleet/controllers.py +4 -26
  34. simo/fleet/forms.py +52 -4
  35. simo/fleet/migrations/__pycache__/0043_auto_20241203_0930.cpython-38.pyc +0 -0
  36. simo/fleet/views.py +37 -6
  37. simo/generic/__pycache__/controllers.cpython-38.pyc +0 -0
  38. simo/generic/__pycache__/forms.cpython-38.pyc +0 -0
  39. simo/generic/__pycache__/gateways.cpython-38.pyc +0 -0
  40. simo/generic/controllers.py +120 -1
  41. simo/generic/forms.py +77 -9
  42. simo/generic/gateways.py +81 -2
  43. simo/users/__pycache__/admin.cpython-38.pyc +0 -0
  44. simo/users/__pycache__/api.cpython-38.pyc +0 -0
  45. simo/users/__pycache__/auto_urls.cpython-38.pyc +0 -0
  46. simo/users/__pycache__/views.cpython-38.pyc +0 -0
  47. simo/users/admin.py +9 -0
  48. simo/users/api.py +2 -0
  49. simo/users/auto_urls.py +6 -3
  50. simo/users/migrations/0039_auto_20241117_1039.py +25 -24
  51. simo/users/migrations/__pycache__/0039_auto_20241117_1039.cpython-38.pyc +0 -0
  52. simo/users/views.py +20 -1
  53. {simo-2.6.8.dist-info → simo-2.7.1.dist-info}/METADATA +1 -1
  54. {simo-2.6.8.dist-info → simo-2.7.1.dist-info}/RECORD +58 -55
  55. {simo-2.6.8.dist-info → simo-2.7.1.dist-info}/LICENSE.md +0 -0
  56. {simo-2.6.8.dist-info → simo-2.7.1.dist-info}/WHEEL +0 -0
  57. {simo-2.6.8.dist-info → simo-2.7.1.dist-info}/entry_points.txt +0 -0
  58. {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
- is_main = forms.BooleanField(
429
- initial=False, required=False,
430
- help_text="Will be displayed in the app "
431
- "right top corner for quick access."
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 and self.cleaned_data['is_main']:
436
- from .controllers import StateSelect
437
- for c in Component.objects.filter(controller_uid=StateSelect.uid):
438
- c.config['is_main'] = False
439
- c.save()
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
- terminating_scripts = set()
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
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 include, re_path
2
- from django.views.generic import TemplateView
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
- from simo.generic.scripting.helpers import haversine_distance
8
- UserDeviceReportLog = apps.get_model("users", "UserDeviceReportLog")
9
-
10
- logs = UserDeviceReportLog.objects.filter(
11
- instance__isnull=False
12
- ).select_related('instance')
13
-
14
- print("Calculate at_home on UserDeviceReportLog's!")
15
-
16
- bulk_update = []
17
- for log in tqdm(logs, total=logs.count()):
18
- log.at_home = False
19
- if not log.relay:
20
- log.at_home = True
21
- elif log.location:
22
- log.at_home = haversine_distance(
23
- log.instance.location, log.location
24
- ) < 250
25
- if log.at_home:
26
- bulk_update.append(log)
27
- if len(bulk_update) > 1000:
28
- UserDeviceReportLog.objects.bulk_update(bulk_update, ["at_home"])
29
- bulk_update = []
30
- UserDeviceReportLog.objects.bulk_update(bulk_update, ["at_home"])
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):
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()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: simo
3
- Version: 2.6.8
3
+ Version: 2.7.1
4
4
  Summary: Smart Home on Steroids!
5
5
  Author-email: Simanas Venčkauskas <simanas@simo.io>
6
6
  Project-URL: Homepage, https://simo.io