simo 2.6.9__py3-none-any.whl → 2.7.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.

Files changed (52) 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 +18 -1
  5. simo/automation/gateways.py +130 -1
  6. simo/core/__pycache__/api_meta.cpython-38.pyc +0 -0
  7. simo/core/__pycache__/autocomplete_views.cpython-38.pyc +0 -0
  8. simo/core/__pycache__/controllers.cpython-38.pyc +0 -0
  9. simo/core/__pycache__/form_fields.cpython-38.pyc +0 -0
  10. simo/core/__pycache__/forms.cpython-38.pyc +0 -0
  11. simo/core/__pycache__/gateways.cpython-38.pyc +0 -0
  12. simo/core/__pycache__/middleware.cpython-38.pyc +0 -0
  13. simo/core/__pycache__/serializers.cpython-38.pyc +0 -0
  14. simo/core/__pycache__/signal_receivers.cpython-38.pyc +0 -0
  15. simo/core/api_meta.py +6 -2
  16. simo/core/autocomplete_views.py +4 -3
  17. simo/core/controllers.py +42 -4
  18. simo/core/form_fields.py +92 -1
  19. simo/core/forms.py +10 -4
  20. simo/core/gateways.py +11 -1
  21. simo/core/serializers.py +8 -1
  22. simo/core/signal_receivers.py +7 -83
  23. simo/core/utils/__pycache__/converters.cpython-38.pyc +0 -0
  24. simo/core/utils/converters.py +59 -0
  25. simo/fleet/__pycache__/auto_urls.cpython-38.pyc +0 -0
  26. simo/fleet/__pycache__/controllers.cpython-38.pyc +0 -0
  27. simo/fleet/__pycache__/forms.cpython-38.pyc +0 -0
  28. simo/fleet/__pycache__/models.cpython-38.pyc +0 -0
  29. simo/fleet/__pycache__/views.cpython-38.pyc +0 -0
  30. simo/fleet/auto_urls.py +5 -0
  31. simo/fleet/forms.py +58 -4
  32. simo/fleet/views.py +37 -6
  33. simo/generic/__pycache__/controllers.cpython-38.pyc +0 -0
  34. simo/generic/__pycache__/forms.cpython-38.pyc +0 -0
  35. simo/generic/__pycache__/gateways.cpython-38.pyc +0 -0
  36. simo/generic/controllers.py +132 -1
  37. simo/generic/forms.py +118 -10
  38. simo/generic/gateways.py +157 -2
  39. simo/users/__pycache__/admin.cpython-38.pyc +0 -0
  40. simo/users/__pycache__/api.cpython-38.pyc +0 -0
  41. simo/users/__pycache__/auto_urls.cpython-38.pyc +0 -0
  42. simo/users/__pycache__/views.cpython-38.pyc +0 -0
  43. simo/users/admin.py +9 -0
  44. simo/users/api.py +2 -0
  45. simo/users/auto_urls.py +6 -3
  46. simo/users/views.py +20 -1
  47. {simo-2.6.9.dist-info → simo-2.7.2.dist-info}/METADATA +1 -1
  48. {simo-2.6.9.dist-info → simo-2.7.2.dist-info}/RECORD +52 -50
  49. {simo-2.6.9.dist-info → simo-2.7.2.dist-info}/LICENSE.md +0 -0
  50. {simo-2.6.9.dist-info → simo-2.7.2.dist-info}/WHEEL +0 -0
  51. {simo-2.6.9.dist-info → simo-2.7.2.dist-info}/entry_points.txt +0 -0
  52. {simo-2.6.9.dist-info → simo-2.7.2.dist-info}/top_level.txt +0 -0
simo/generic/forms.py CHANGED
@@ -16,7 +16,7 @@ from simo.core.form_fields import (
16
16
  Select2ModelChoiceField, Select2ListChoiceField,
17
17
  Select2ModelMultipleChoiceField
18
18
  )
19
-
19
+ from simo.core.forms import DimmerConfigForm, SwitchForm
20
20
 
21
21
  ACTION_METHODS = (
22
22
  ('turn_on', "Turn ON"), ('turn_off', "Turn OFF"),
@@ -26,6 +26,46 @@ ACTION_METHODS = (
26
26
  )
27
27
 
28
28
 
29
+ class ControlForm(forms.Form):
30
+ button = Select2ModelChoiceField(
31
+ queryset=Component.objects.filter(base_type='button'),
32
+ url='autocomplete-component'
33
+ )
34
+ prefix = 'controls'
35
+
36
+
37
+ class DimmableLightsGroupConfigForm(DimmerConfigForm):
38
+ slaves = Select2ModelMultipleChoiceField(
39
+ queryset=Component.objects.filter(
40
+ base_type__in=('dimmer', 'switch')
41
+ ),
42
+ url='autocomplete-component',
43
+ forward=(forward.Const(['dimmer', 'switch'], 'base_type'),),
44
+ required=False
45
+ )
46
+ controls = FormsetField(
47
+ formset_factory(
48
+ ControlForm, can_delete=True, can_order=True, extra=0, max_num=999
49
+ )
50
+ )
51
+
52
+
53
+ class SwitchGroupConfigForm(SwitchForm):
54
+ slaves = Select2ModelMultipleChoiceField(
55
+ queryset=Component.objects.filter(
56
+ base_type__in=('dimmer', 'switch')
57
+ ),
58
+ url='autocomplete-component',
59
+ forward=(forward.Const(['dimmer', 'switch'], 'base_type'),),
60
+ required=False
61
+ )
62
+ controls = FormsetField(
63
+ formset_factory(
64
+ ControlForm, can_delete=True, can_order=True, extra=0, max_num=999
65
+ )
66
+ )
67
+
68
+
29
69
  class ThermostatConfigForm(BaseComponentForm):
30
70
  temperature_sensor = Select2ModelChoiceField(
31
71
  queryset=Component.objects.filter(
@@ -425,21 +465,89 @@ class StateSelectForm(BaseComponentForm):
425
465
  states = FormsetField(
426
466
  formset_factory(StateForm, can_delete=True, can_order=True, extra=0)
427
467
  )
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."
468
+
469
+
470
+ class MainStateSelectForm(BaseComponentForm):
471
+ weekdays_morning_hour = forms.IntegerField(
472
+ initial=6, min_value=3, max_value=12
473
+ )
474
+ weekends_morning_hour = forms.IntegerField(
475
+ initial=6, min_value=3, max_value=12
476
+ )
477
+ away_on_no_action = forms.IntegerField(
478
+ required=False, initial=40, min_value=1, max_value=360,
479
+ help_text="Set state to Away if nobody is at home "
480
+ "(requires location and fitness permissions on mobile app) "
481
+ "and there were "
482
+ "no action for more than given amount of minutes from any "
483
+ "security alarm acategory sensors (motion sensors, door sensors etc..).<br>"
484
+ "No value disables this behavior."
432
485
  )
486
+ sleeping_phones_hour = forms.IntegerField(
487
+ initial=True, required=False, min_value=18, max_value=24,
488
+ help_text='Set mode to "Sleep" if it is later than given hour '
489
+ 'and all home owners phones who are at home are put on charge '
490
+ '(requires location and fitness permissions on mobile app). <br>'
491
+ 'Set back to regular state as soon as none of the home owners phones '
492
+ 'are on charge and it is morning hour or past it.'
493
+ )
494
+ states = FormsetField(
495
+ formset_factory(StateForm, can_delete=True, can_order=True, extra=0)
496
+ )
497
+
498
+ def clean(self):
499
+ if not self.instance.id:
500
+ if Component.objects.filter(
501
+ controller_uid='simo.generic.controllers.MainState'
502
+ ).count():
503
+ raise forms.ValidationError("Main state already exists!")
504
+
505
+ formset_errors = {}
506
+ required_states = {
507
+ 'morning', 'day', 'evening', 'night', 'sleep', 'away', 'vacation'
508
+ }
509
+ found_states = set()
510
+ for i, state in enumerate(self.cleaned_data.get('states', [])):
511
+ if state.get('slug') in found_states:
512
+ formset_errors['i'] = {'slug': "Duplicate!"}
513
+ continue
514
+ found_states.add(state.get('slug'))
515
+
516
+ errors_list = []
517
+ if formset_errors:
518
+ for i, control in enumerate(self.cleaned_data['states']):
519
+ errors_list.append(formset_errors.get(i, {}))
520
+ if errors_list:
521
+ self._errors['states'] = errors_list
522
+ if 'states' in self.cleaned_data:
523
+ del self.cleaned_data['states']
524
+
525
+ else:
526
+ missing_states = required_states - found_states
527
+ if missing_states:
528
+ if len(missing_states) == 1:
529
+ self.add_error(
530
+ 'states',
531
+ f'"{list(missing_states)[0]}" state is missing!'
532
+ )
533
+ self.add_error(
534
+ 'states',
535
+ f"Required states are missing: {list(missing_states)}!"
536
+ )
537
+
538
+ return self.cleaned_data
433
539
 
434
540
  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()
541
+ if commit:
542
+ for s in Component.objects.filter(
543
+ base_type='state-select', config__is_main=True
544
+ ).exclude(id=self.instance.id):
545
+ s.config['is_main'] = False
546
+ s.save()
440
547
  return super().save(commit)
441
548
 
442
549
 
550
+
443
551
  class AlarmClockEventForm(forms.Form):
444
552
  uid = HiddenField(required=False)
445
553
  enabled = forms.BooleanField(initial=True)
simo/generic/gateways.py CHANGED
@@ -59,6 +59,82 @@ class CameraWatcher(threading.Thread):
59
59
 
60
60
 
61
61
 
62
+ class GroupButtonsHandler:
63
+
64
+ def __init__(self, *args, **kwargs):
65
+ super().__init__(*args, **kwargs)
66
+ self.group_buttons = {}
67
+ self.fade_directions = {}
68
+
69
+ def watch_groups(self):
70
+ from .controllers import DimmableLightsGroup, SwitchGroup
71
+ current_group_buttons = {}
72
+ for group_comp in Component.objects.filter(
73
+ controller_uid__in=(DimmableLightsGroup.uid, SwitchGroup.uid)
74
+ ):
75
+ for ctrl in group_comp.config.get('controls', []):
76
+ if ctrl['button'] not in current_group_buttons:
77
+ current_group_buttons[ctrl['button']] = set(group_comp.id)
78
+ else:
79
+ current_group_buttons[ctrl['button']].add(group_comp.id)
80
+
81
+ if ctrl['button'] not in self.group_buttons:
82
+ self.group_buttons[ctrl['button']] = set()
83
+ if group_comp.id not in self.group_buttons[ctrl['button']]:
84
+ self.group_buttons[ctrl['button']].add(group_comp.id)
85
+ btn = Component.objects.filter(id=ctrl['button']).first()
86
+ if btn:
87
+ btn.on_change(self.watch_group_button)
88
+
89
+ # remove groups and buttons that are no longer in use
90
+ for btn_id, groups in self.group_buttons.items():
91
+ if btn_id not in current_group_buttons:
92
+ self.group_buttons[btn_id] = set()
93
+ continue
94
+ self.group_buttons[btn_id] = current_group_buttons[btn_id]
95
+
96
+
97
+ def watch_group_button(self, button):
98
+ group_ids = self.group_buttons.get(button.id)
99
+ if not group_ids:
100
+ return
101
+
102
+ btn_type = button.config.get('btn_type', 'momentary')
103
+
104
+ if btn_type == 'momentary':
105
+ if button.value not in ('click', 'hold', 'up', 'double-click'):
106
+ return
107
+ for g_id in group_ids:
108
+ group = Component.objects.filter(id=g_id).first()
109
+ if not group:
110
+ continue
111
+ if button.value == 'click':
112
+ group.toggle()
113
+ elif button.value == 'double-click':
114
+ group.send(group.config.get('max', 100))
115
+ elif button.value == 'down':
116
+ if self.fade_directions.get(group.id, 0) < 0:
117
+ self.fade_directions[group.id] = 1
118
+ group.fade_up()
119
+ else:
120
+ self.fade_directions[group.id] = -1
121
+ group.fade_down()
122
+ elif button.value == 'up':
123
+ if self.fade_directions.get(group.id):
124
+ self.fade_directions[group.id] = 0
125
+ group.fade_stop()
126
+
127
+ else: # toggle
128
+ if button.value not in ('down', 'up'):
129
+ return
130
+ for g_id in group_ids:
131
+ group = Component.objects.filter(id=g_id).first()
132
+ if not group:
133
+ continue
134
+ group.toggle()
135
+
136
+
137
+
62
138
  class GenericGatewayHandler(BaseObjectCommandsGatewayHandler):
63
139
  name = "Generic"
64
140
  config_form = BaseGatewayForm
@@ -72,10 +148,17 @@ class GenericGatewayHandler(BaseObjectCommandsGatewayHandler):
72
148
  ('watch_alarm_clocks', 30),
73
149
  ('watch_watering', 60),
74
150
  ('watch_alarm_events', 1),
75
- ('watch_timers', 1)
151
+ ('watch_timers', 1),
152
+ ('watch_main_states', 60)
76
153
  )
77
154
 
78
- terminating_scripts = set()
155
+ def __init__(self, *args, **kwargs):
156
+ super().__init__(*args, **kwargs)
157
+ self.last_sensor_actions = {}
158
+ self.sensors_on_watch = {}
159
+ self.sleep_is_on = {}
160
+
161
+
79
162
 
80
163
  def watch_thermostats(self):
81
164
  from .controllers import Thermostat
@@ -242,6 +325,78 @@ class GenericGatewayHandler(BaseObjectCommandsGatewayHandler):
242
325
  print(traceback.format_exc(), file=sys.stderr)
243
326
 
244
327
 
328
+ def watch_main_state(self, state):
329
+ i_id = state.zone.instance.id
330
+ if state.value in ('day', 'night', 'evening', 'morning'):
331
+ new_state = state.get_day_evening_night_morning()
332
+ if new_state != state.value:
333
+ print(f"New main state of {state.zone.instance} - {new_state}")
334
+ state.send(new_state)
335
+
336
+ if state.config.get('away_on_no_action'):
337
+ for sensor in Component.objects.filter(
338
+ zone__instance=state.zone.instance,
339
+ base_type='binary-sensor', alarm_category='security'
340
+ ):
341
+ if state.id not in self.sensors_on_watch:
342
+ self.sensors_on_watch[state.id] = {}
343
+
344
+ if sensor.id not in self.sensors_on_watch[state.id]:
345
+ self.sensors_on_watch[state.id][sensor.id] = i_id
346
+ self.last_sensor_actions[i_id] = time.time()
347
+ sensor.on_change(self.security_sensor_change)
348
+
349
+ last_action = self.last_sensor_actions.get(i_id, time.time())
350
+ if state.check_is_away(last_action):
351
+ if state.value != 'away':
352
+ print(f"New main state of "
353
+ f"{state.zone.instance} - away")
354
+ state.send('away')
355
+ else:
356
+ if state.value == 'away':
357
+ try:
358
+ new_state = state.get_day_evening_night_morning()
359
+ except:
360
+ new_state = 'day'
361
+ print(f"New main state of "
362
+ f"{state.zone.instance} - {new_state}")
363
+ state.send(new_state)
364
+
365
+ if state.config.get('sleeping_phones_hour') is not None:
366
+ sleep_time = state.owner_phones_on_sleep()
367
+ if sleep_time and state.value != 'sleep':
368
+ print(f"New main state of {state.zone.instance} - sleep")
369
+ state.send('sleep')
370
+ elif state.value == 'sleep':
371
+ try:
372
+ new_state = state.get_day_evening_night_morning()
373
+ except:
374
+ new_state = 'day'
375
+ print(f"New main state of "
376
+ f"{state.zone.instance} - {new_state}")
377
+ state.send(new_state)
378
+
379
+
380
+ def watch_main_states(self):
381
+ drop_current_instance()
382
+ from .controllers import MainState
383
+ for state in Component.objects.filter(
384
+ controller_uid=MainState.uid
385
+ ).select_related('zone', 'zone__instance'):
386
+ try:
387
+ self.watch_main_state(state)
388
+ except Exception as e:
389
+ print(traceback.format_exc(), file=sys.stderr)
390
+
391
+
392
+
393
+ def security_sensor_change(self, sensor):
394
+ self.last_sensor_actions[
395
+ self.sensors_on_watch[sensor.id]
396
+ ] = time.time()
397
+
398
+
399
+
245
400
  class DummyGatewayHandler(BaseObjectCommandsGatewayHandler):
246
401
  name = "Dummy"
247
402
  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
  ]
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.9
3
+ Version: 2.7.2
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