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.
- 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 +18 -1
- simo/automation/gateways.py +130 -1
- 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_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__/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/forms.py +52 -4
- 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/views.py +20 -1
- {simo-2.6.9.dist-info → simo-2.7.1.dist-info}/METADATA +1 -1
- {simo-2.6.9.dist-info → simo-2.7.1.dist-info}/RECORD +51 -49
- {simo-2.6.9.dist-info → simo-2.7.1.dist-info}/LICENSE.md +0 -0
- {simo-2.6.9.dist-info → simo-2.7.1.dist-info}/WHEEL +0 -0
- {simo-2.6.9.dist-info → simo-2.7.1.dist-info}/entry_points.txt +0 -0
- {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
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
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
|
+
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
simo/generic/controllers.py
CHANGED
|
@@ -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
|
-
|
|
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
|