simo 2.6.9__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 (51) 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 +13 -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__/forms.cpython-38.pyc +0 -0
  27. simo/fleet/__pycache__/models.cpython-38.pyc +0 -0
  28. simo/fleet/__pycache__/views.cpython-38.pyc +0 -0
  29. simo/fleet/auto_urls.py +5 -0
  30. simo/fleet/forms.py +52 -4
  31. simo/fleet/views.py +37 -6
  32. simo/generic/__pycache__/controllers.cpython-38.pyc +0 -0
  33. simo/generic/__pycache__/forms.cpython-38.pyc +0 -0
  34. simo/generic/__pycache__/gateways.cpython-38.pyc +0 -0
  35. simo/generic/controllers.py +120 -1
  36. simo/generic/forms.py +77 -9
  37. simo/generic/gateways.py +81 -2
  38. simo/users/__pycache__/admin.cpython-38.pyc +0 -0
  39. simo/users/__pycache__/api.cpython-38.pyc +0 -0
  40. simo/users/__pycache__/auto_urls.cpython-38.pyc +0 -0
  41. simo/users/__pycache__/views.cpython-38.pyc +0 -0
  42. simo/users/admin.py +9 -0
  43. simo/users/api.py +2 -0
  44. simo/users/auto_urls.py +6 -3
  45. simo/users/views.py +20 -1
  46. {simo-2.6.9.dist-info → simo-2.7.1.dist-info}/METADATA +1 -1
  47. {simo-2.6.9.dist-info → simo-2.7.1.dist-info}/RECORD +51 -49
  48. {simo-2.6.9.dist-info → simo-2.7.1.dist-info}/LICENSE.md +0 -0
  49. {simo-2.6.9.dist-info → simo-2.7.1.dist-info}/WHEEL +0 -0
  50. {simo-2.6.9.dist-info → simo-2.7.1.dist-info}/entry_points.txt +0 -0
  51. {simo-2.6.9.dist-info → simo-2.7.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,59 @@
1
+ import re
2
+
3
+
4
+ def input_to_meters(distance: str) -> float:
5
+ """
6
+ Converts a distance string to meters based on the units provided.
7
+
8
+ Args:
9
+ distance (str): A string containing a numerical value and a unit (e.g., "10 meters", "32ft.").
10
+
11
+ Returns:
12
+ float: The distance converted to meters.
13
+
14
+ Raises:
15
+ ValueError: If the unit of measurement is not specified or is invalid.
16
+ """
17
+ # Dictionary to map units to conversion factors (to meters)
18
+ unit_conversions = {
19
+ "meters": 1.0,
20
+ "meter": 1.0,
21
+ "m": 1.0,
22
+ "feet": 0.3048,
23
+ "foot": 0.3048,
24
+ "ft": 0.3048,
25
+ "kilometers": 1000.0,
26
+ "kilometer": 1000.0,
27
+ "km": 1000.0,
28
+ "inches": 0.0254,
29
+ "inch": 0.0254,
30
+ "in": 0.0254
31
+ }
32
+
33
+ # Normalize the input: remove trailing dots and extra spaces, and lowercase
34
+ distance = distance.strip().rstrip('.').lower()
35
+
36
+ # Regular expression to handle both spaced and unspaced inputs
37
+ match = re.match(r"^([\d\.]+)\s*([a-z]+)$", distance)
38
+
39
+ if not match:
40
+ raise ValueError(
41
+ "Please specify a numerical value followed by a valid unit of measure (e.g., '10 meters', '32ft').")
42
+
43
+ # Extract the numerical part and the unit part
44
+ value_str, unit = match.groups()
45
+
46
+ try:
47
+ value = float(value_str) # Convert the numerical part to a float
48
+ except ValueError:
49
+ raise ValueError("The distance value must be a valid number.")
50
+
51
+ # Check if the unit is valid
52
+ if unit not in unit_conversions:
53
+ raise ValueError(
54
+ f"Invalid unit of measure '{unit}'. Valid units are: {', '.join(unit_conversions.keys())}.")
55
+
56
+ # Convert the value to meters
57
+ return value * unit_conversions[unit]
58
+
59
+
Binary file
Binary file
simo/fleet/auto_urls.py CHANGED
@@ -1,6 +1,7 @@
1
1
  from django.urls import path, re_path
2
2
  from .views import (
3
3
  colonels_ping,
4
+ ColonelsAutocomplete,
4
5
  PinsSelectAutocomplete,
5
6
  InterfaceSelectAutocomplete,
6
7
  ControlInputSelectAutocomplete
@@ -10,6 +11,10 @@ urlpatterns = [
10
11
  re_path(
11
12
  r"^colonels-ping/$", colonels_ping, name='colonels-ping'
12
13
  ),
14
+ path(
15
+ 'autocomplete-colonels',
16
+ ColonelsAutocomplete.as_view(), name='autocomplete-colonels'
17
+ ),
13
18
  path(
14
19
  'autocomplete-colonel-pins',
15
20
  PinsSelectAutocomplete.as_view(), name='autocomplete-colonel-pins'
simo/fleet/forms.py CHANGED
@@ -11,15 +11,19 @@ from simo.core.forms import (
11
11
  BaseComponentForm, ValueLimitForm, NumericSensorForm
12
12
  )
13
13
  from simo.core.utils.formsets import FormsetField
14
+ from simo.core.utils.converters import input_to_meters
14
15
  from simo.core.widgets import LogOutputWidget
15
16
  from simo.core.utils.easing import EASING_CHOICES
16
17
  from simo.core.utils.validators import validate_slaves
17
18
  from simo.core.utils.admin import AdminFormActionForm
18
19
  from simo.core.events import GatewayObjectCommand
20
+ from simo.core.middleware import get_current_instance
19
21
  from simo.core.form_fields import (
20
22
  Select2ModelChoiceField, Select2ListChoiceField,
21
23
  Select2ModelMultipleChoiceField
22
24
  )
25
+ from simo.core.form_fields import PlainLocationField
26
+ from simo.users.models import PermissionsRole
23
27
  from .models import Colonel, ColonelPin, Interface
24
28
  from .utils import INTERFACES_PINS_MAP, get_all_control_input_choices
25
29
 
@@ -88,8 +92,9 @@ class InterfaceAdminForm(forms.ModelForm):
88
92
 
89
93
 
90
94
  class ColonelComponentForm(BaseComponentForm):
91
- colonel = forms.ModelChoiceField(
92
- label="Colonel", queryset=Colonel.objects.all()
95
+ colonel = Select2ModelChoiceField(
96
+ label="Colonel", queryset=Colonel.objects.all(),
97
+ url='autocomplete-colonels',
93
98
  )
94
99
 
95
100
  def clean_colonel(self):
@@ -1316,7 +1321,6 @@ class GateConfigForm(ColonelComponentForm):
1316
1321
  "when your gate is in closed position?"
1317
1322
  )
1318
1323
 
1319
-
1320
1324
  open_duration = forms.FloatField(
1321
1325
  initial=30, min_value=1, max_value=600,
1322
1326
  help_text="How much time in seconds does it take for your gate "
@@ -1329,9 +1333,53 @@ class GateConfigForm(ColonelComponentForm):
1329
1333
  )
1330
1334
  )
1331
1335
 
1336
+ auto_open_distance = forms.CharField(
1337
+ initial='100 m', required=False,
1338
+ help_text="Open the gate automatically whenever somebody is coming home"
1339
+ "and comes closer than this distance. Clear this value out, "
1340
+ "to disable auto opening."
1341
+ )
1342
+ auto_open_for = Select2ModelMultipleChoiceField(
1343
+ queryset=PermissionsRole.objects.all(),
1344
+ url='autocomplete-user-roles', required=False,
1345
+ help_text="Open the gates automatically only for these user roles. "
1346
+ "Leaving this field blank opens the gate for all system users."
1347
+ )
1348
+ location = PlainLocationField(
1349
+ zoom=18,
1350
+ help_text="Location of your gate. Required only for automatic opening. "
1351
+ "Adjust this if this gate is significantly distanced from "
1352
+ "your actual home location."
1353
+ )
1354
+
1355
+ def __init__(self, *args, **kwargs):
1356
+ super().__init__(*args, **kwargs)
1357
+ if not self.fields['location'].initial:
1358
+ self.fields['location'].initial = get_current_instance().location
1359
+
1360
+ def clean_distance(self):
1361
+ distance = self.cleaned_data.get('auto_open_distance')
1362
+ if not distance:
1363
+ return distance
1364
+ try:
1365
+ distance = input_to_meters(distance)
1366
+ except Exception as e:
1367
+ raise forms.ValidationError(str(e))
1368
+
1369
+ if distance < 20:
1370
+ raise forms.ValidationError(
1371
+ "That is to little of a distance. At least 20 meters is required."
1372
+ )
1373
+ if distance > 2000:
1374
+ raise forms.ValidationError(
1375
+ "This is to high of a distance. Max 2 km is allowed."
1376
+ )
1377
+
1378
+ return distance
1379
+
1380
+
1332
1381
  def clean(self):
1333
1382
  super().clean()
1334
-
1335
1383
  check_pins = ('open_pin', 'close_pin', 'sensor_pin')
1336
1384
  for pin in check_pins:
1337
1385
  if not self.cleaned_data.get(pin):
simo/fleet/views.py CHANGED
@@ -11,11 +11,33 @@ def colonels_ping(request):
11
11
  return HttpResponse('pong')
12
12
 
13
13
 
14
+ class ColonelsAutocomplete(autocomplete.Select2QuerySetView):
15
+
16
+ def get_queryset(self):
17
+ if not self.request.user.is_authenticated:
18
+ raise Http404()
19
+
20
+ instance = get_current_instance(self.request)
21
+ if not instance:
22
+ return Colonel.objects.none()
23
+
24
+ qs = Colonel.objects.filter(instance=instance)
25
+
26
+ if self.request.GET.get('value'):
27
+ qs = qs.filter(pk__in=self.request.GET['value'].split(','))
28
+ elif self.q:
29
+ qs = search_queryset(qs, self.q, ('name', ))
30
+ return qs
31
+
32
+
14
33
  class PinsSelectAutocomplete(autocomplete.Select2QuerySetView):
15
34
 
16
35
  def get_queryset(self):
17
36
 
18
- instance = get_current_instance()
37
+ if not self.request.user.is_authenticated:
38
+ raise Http404()
39
+
40
+ instance = get_current_instance(self.request)
19
41
  if not instance:
20
42
  return ColonelPin.objects.none()
21
43
 
@@ -52,10 +74,13 @@ class PinsSelectAutocomplete(autocomplete.Select2QuerySetView):
52
74
  class InterfaceSelectAutocomplete(autocomplete.Select2QuerySetView):
53
75
 
54
76
  def get_queryset(self):
77
+ if not self.request.user.is_authenticated:
78
+ raise Http404()
55
79
 
56
80
  try:
57
81
  colonel = Colonel.objects.get(
58
- pk=self.forwarded.get("colonel")
82
+ pk=self.forwarded.get("colonel"),
83
+ instance=get_current_instance(self.request)
59
84
  )
60
85
  except:
61
86
  return Interface.objects.none()
@@ -73,14 +98,19 @@ class InterfaceSelectAutocomplete(autocomplete.Select2QuerySetView):
73
98
  class ControlInputSelectAutocomplete(autocomplete.Select2ListView):
74
99
 
75
100
  def get_list(self):
101
+ if not self.request.user.is_authenticated:
102
+ raise Http404()
76
103
 
77
104
  try:
78
105
  colonel = Colonel.objects.get(
79
- pk=self.forwarded.get("colonel")
106
+ pk=self.forwarded.get("colonel"),
107
+ instance=get_current_instance(self.request)
80
108
  )
81
109
  pins_qs = ColonelPin.objects.filter(colonel=colonel)
82
110
  except:
83
- pins_qs = ColonelPin.objects.all()
111
+ pins_qs = ColonelPin.objects.filter(
112
+ colonel__instance=get_current_instance(self.request)
113
+ )
84
114
 
85
115
  if self.forwarded.get('self') and self.forwarded['self'].startswith('pin-'):
86
116
  pins_qs = pins_qs.filter(
@@ -93,7 +123,7 @@ class ControlInputSelectAutocomplete(autocomplete.Select2ListView):
93
123
  pins_qs = pins_qs.filter(**self.forwarded.get('pin_filters'))
94
124
 
95
125
  buttons_qs = Component.objects.filter(
96
- base_type='button'
126
+ base_type='button', zone__instance=get_current_instance(self.request)
97
127
  ).select_related('zone')
98
128
 
99
129
  if self.forwarded.get('button_filters'):
@@ -126,4 +156,5 @@ class ControlInputSelectAutocomplete(autocomplete.Select2ListView):
126
156
  [(f'button-{button.id}',
127
157
  f"{button.zone.name} | {button.name}"
128
158
  if button.zone else button.name)
129
- for button in buttons_qs]
159
+ for button in buttons_qs]
160
+
@@ -1,6 +1,7 @@
1
1
  import pytz
2
2
  import datetime
3
3
  import json
4
+ import time
4
5
  from django.core.exceptions import ValidationError
5
6
  from django.utils import timezone
6
7
  from django.utils.functional import cached_property
@@ -34,12 +35,15 @@ from .app_widgets import (
34
35
  from .forms import (
35
36
  ThermostatConfigForm, AlarmGroupConfigForm,
36
37
  IPCameraConfigForm, WeatherForm,
37
- WateringConfigForm, StateSelectForm,
38
+ WateringConfigForm, StateSelectForm, MainStateSelectForm,
38
39
  AlarmClockConfigForm
39
40
  )
40
41
 
41
42
  # ----------- Generic controllers -----------------------------
42
43
 
44
+
45
+
46
+
43
47
  class Thermostat(ControllerBase):
44
48
  name = _("Thermostat")
45
49
  base_type = 'thermostat'
@@ -319,6 +323,7 @@ class Weather(ControllerBase):
319
323
  admin_widget_template = 'admin/controller_widgets/weather.html'
320
324
  default_config = {}
321
325
  default_value = {}
326
+ manual_add = False
322
327
 
323
328
  def _validate_val(self, value, occasion=None):
324
329
  return value
@@ -1054,6 +1059,120 @@ class StateSelect(ControllerBase):
1054
1059
  return value
1055
1060
 
1056
1061
 
1062
+ class MainState(StateSelect):
1063
+ name = _("Main State")
1064
+ config_form = MainStateSelectForm
1065
+ default_value = 'day'
1066
+
1067
+ default_config = {
1068
+ 'is_main': True,
1069
+ 'weekdays_morning_hour': 6,
1070
+ 'weekends_morning_hour': 7,
1071
+ 'away_on_no_action': 30,
1072
+ 'sleeping_phones_hour': 21,
1073
+ 'states': [
1074
+ {
1075
+ "icon": "sunrise", "name": "Morning", "slug": "morning",
1076
+ 'help_text': "Morning hour to sunrise. Activates in dark time of a year."
1077
+ },
1078
+ {
1079
+ "icon": "house-day", "name": "Day", "slug": "day",
1080
+ 'help_text': "From sunrise to sunset."
1081
+ },
1082
+ {
1083
+ "icon": "house-night", "name": "Evening", "slug": "evening",
1084
+ 'help_text': "From sunrise to midnight"
1085
+ },
1086
+ {
1087
+ "icon": "moon-cloud", "name": "Night", "slug": "night",
1088
+ 'help_text': "From midnight to sunrise or static morning hour."
1089
+ },
1090
+ {"icon": "snooze", "name": "Sleep time", "slug": "sleep"},
1091
+ {"icon": "house-person-leave", "name": "Away", "slug": "away"},
1092
+ {"icon": "island-tropical", "name": "Vacation", "slug": "vacation"}
1093
+ ]
1094
+ }
1095
+
1096
+ def get_day_evening_night_morning(self):
1097
+ from simo.automation.helpers import LocalSun
1098
+ sun = LocalSun(self.component.zone.instance.location)
1099
+ timezone.activate(self.component.zone.instance.timezone)
1100
+ localtime = timezone.localtime()
1101
+
1102
+ # It is daytime if the sun is up!
1103
+ if not sun.is_night():
1104
+ return 'day'
1105
+
1106
+ # it is evening if the sun is down at the evening
1107
+ if sun.get_sunset_time(localtime) < localtime:
1108
+ return 'evening'
1109
+
1110
+ if localtime.weekday() < 5:
1111
+ if localtime.hour >= self.component.config['weekdays_morning_hour']:
1112
+ return 'morning'
1113
+ else:
1114
+ if localtime.hour >= self.component.config['weekends_morning_hour']:
1115
+ return 'morning'
1116
+
1117
+ # 0 - 6AM and still dark
1118
+ return 'night'
1119
+
1120
+
1121
+ def check_is_away(self, last_sensor_action):
1122
+ away_on_no_action = not self.component.config.get('away_on_no_action')
1123
+ if not away_on_no_action:
1124
+ return False
1125
+ from simo.users.models import InstanceUser
1126
+ if InstanceUser.objects.filter(
1127
+ is_active=True, at_home=True, instance=self.component.zone.instance
1128
+ ).count():
1129
+ return False
1130
+
1131
+ return (time.time() - last_sensor_action) // 60 >= away_on_no_action
1132
+
1133
+
1134
+ def is_sleep_time(self):
1135
+ timezone.activate(self.component.zone.instance.timezone)
1136
+ localtime = timezone.localtime()
1137
+ if localtime.weekday() < 5:
1138
+ if localtime.hour < self.component.config['weekdays_morning_hour']:
1139
+ return True
1140
+ else:
1141
+ if localtime.hour < self.component.config['weekends_morning_hour']:
1142
+ return True
1143
+ sleeping_phones_hour = self.component.config.get(
1144
+ 'sleeping_phones_hour'
1145
+ )
1146
+ if localtime.hour >= sleeping_phones_hour:
1147
+ return True
1148
+
1149
+ return False
1150
+
1151
+
1152
+ def owner_phones_on_sleep(self):
1153
+ sleeping_phones_hour = self.component.config.get('sleeping_phones_hour')
1154
+ if sleeping_phones_hour is not None:
1155
+ return False
1156
+
1157
+ if not self.is_sleep_time():
1158
+ return False
1159
+
1160
+ from simo.users.models import InstanceUser
1161
+
1162
+ for iuser in InstanceUser.objects.filter(
1163
+ is_active=True, role__is_owner=True,
1164
+ instance=self.component.zone.instance
1165
+ ):
1166
+ # skipping users that are not at home
1167
+ if not iuser.at_home:
1168
+ continue
1169
+ if not iuser.phone_on_charge:
1170
+ # at least one user's phone is not yet on charge
1171
+ return False
1172
+
1173
+ return True
1174
+
1175
+
1057
1176
  # ----------- Dummy controllers -----------------------------
1058
1177
 
1059
1178
  class DummyBinarySensor(BinarySensor):
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