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
@@ -81,100 +81,24 @@ def create_instance_defaults(sender, instance, created, **kwargs):
81
81
  )
82
82
  weather_icon = Icon.objects.get(slug='cloud-bolt-sun')
83
83
 
84
+ from simo.generic.controllers import Weather, MainState
84
85
  Component.objects.create(
85
86
  name='Weather', icon=weather_icon,
86
87
  zone=other_zone,
87
88
  category=climate_category,
88
89
  gateway=generic, base_type='weather',
89
- controller_uid='simo.generic.controllers.Weather',
90
+ controller_uid=Weather.uid,
90
91
  config={'is_main': True}
91
92
  )
92
93
 
93
- state_comp = Component.objects.create(
94
- name='State', icon=Icon.objects.get(slug='home'),
94
+ Component.objects.create(
95
+ name='Main State', icon=Icon.objects.get(slug='home'),
95
96
  zone=other_zone,
96
97
  category=other_category,
97
- gateway=generic, base_type='state-select',
98
- controller_uid='simo.generic.controllers.StateSelect',
98
+ gateway=generic, base_type=MainState.base_type,
99
+ controller_uid=MainState.uid,
99
100
  value='day',
100
- config={"states": [
101
- {
102
- "icon": "sunrise", "name": "Morning", "slug": "morning",
103
- 'help_text': "6:00 AM to sunrise. Activates only in dark time of a year."
104
- },
105
- {
106
- "icon": "house-day", "name": "Day", "slug": "day",
107
- 'help_text': "From sunrise to sunset."
108
- },
109
- {
110
- "icon": "house-night", "name": "Evening", "slug": "evening",
111
- 'help_text': "From sunrise to midnight"
112
- },
113
- {
114
- "icon": "moon-cloud", "name": "Night", "slug": "night",
115
- 'help_text': "From midnight to sunrise or 6:00 AM."
116
- },
117
- {"icon": "snooze", "name": "Sleep time", "slug": "sleep"},
118
- {"icon": "house-person-leave", "name": "Away", "slug": "away"},
119
- {"icon": "island-tropical", "name": "Vacation", "slug": "vacation"}
120
- ], "is_main": True}
121
- )
122
-
123
-
124
- auto_state_code = render_to_string(
125
- 'automations/auto_state_script.py', {'state_comp_id': state_comp.id}
126
- )
127
- Component.objects.create(
128
- name='Auto state', icon=Icon.objects.get(slug='bolt'),
129
- zone=other_zone,
130
- category=other_category, show_in_app=False,
131
- gateway=automation, base_type='script',
132
- controller_uid='simo.automation.controllers.Script',
133
- config={
134
- "code": auto_state_code, 'autostart': True, 'keep_alive': True,
135
- "notes": f"""
136
- The script automatically controls the states of the "State" component (ID:{state_comp.id}) — 'morning', 'day', 'evening', 'night'.
137
-
138
- """
139
- }
140
- )
141
-
142
- code = render_to_string(
143
- 'automations/phones_sleep_script.py', {'state_comp_id': state_comp.id}
144
- )
145
- Component.objects.create(
146
- name='Sleep mode when owner phones are charge',
147
- icon=Icon.objects.get(slug='bolt'), zone=other_zone,
148
- category=other_category, show_in_app=False,
149
- gateway=automation, base_type='script',
150
- controller_uid='simo.automation.controllers.Script',
151
- config={
152
- "code": code, 'autostart': True, 'keep_alive': True,
153
- "notes": f"""
154
- Automatically sets State component (ID: {state_comp.id}) to "Sleep" if it is later than 10pm and all home owners phones who are at home are put on charge.
155
- Sets State component back to regular state as soon as none of the home owners phones are on charge and it is 6am or later.
156
-
157
- """
158
- }
159
- )
160
-
161
- code = render_to_string(
162
- 'automations/auto_away.py', {'state_comp_id': state_comp.id}
163
- )
164
- Component.objects.create(
165
- name='Auto Away State',
166
- icon=Icon.objects.get(slug='bolt'), zone=other_zone,
167
- category=other_category, show_in_app=False,
168
- gateway=automation, base_type='script',
169
- controller_uid='simo.automation.controllers.Script',
170
- config={
171
- "code": code, 'autostart': True, 'keep_alive': True,
172
- "notes": f"""
173
- Automatically set mode to "Away" there are no users at home and there was no motion for more than 30 seconds.
174
- Set it back to a regular mode as soon as somebody comes back home or motion is detected.
175
-
176
- """
177
- }
101
+ config=MainState.default_config
178
102
  )
179
103
 
180
104
  # Create default User permission roles
@@ -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):
@@ -301,6 +306,12 @@ class ColonelButtonConfigForm(ColonelComponentForm):
301
306
  ('up', "UP (On +5V delivery)")
302
307
  )
303
308
  )
309
+ btn_type = forms.ChoiceField(
310
+ label="Type", initial='momentary',
311
+ choices=(
312
+ ('momentary', "Momentary"), ('toggle', "Toggle")
313
+ )
314
+ )
304
315
 
305
316
  def __init__(self, *args, **kwargs):
306
317
  super().__init__(*args, **kwargs)
@@ -1316,7 +1327,6 @@ class GateConfigForm(ColonelComponentForm):
1316
1327
  "when your gate is in closed position?"
1317
1328
  )
1318
1329
 
1319
-
1320
1330
  open_duration = forms.FloatField(
1321
1331
  initial=30, min_value=1, max_value=600,
1322
1332
  help_text="How much time in seconds does it take for your gate "
@@ -1329,9 +1339,53 @@ class GateConfigForm(ColonelComponentForm):
1329
1339
  )
1330
1340
  )
1331
1341
 
1342
+ auto_open_distance = forms.CharField(
1343
+ initial='100 m', required=False,
1344
+ help_text="Open the gate automatically whenever somebody is coming home"
1345
+ "and comes closer than this distance. Clear this value out, "
1346
+ "to disable auto opening."
1347
+ )
1348
+ auto_open_for = Select2ModelMultipleChoiceField(
1349
+ queryset=PermissionsRole.objects.all(),
1350
+ url='autocomplete-user-roles', required=False,
1351
+ help_text="Open the gates automatically only for these user roles. "
1352
+ "Leaving this field blank opens the gate for all system users."
1353
+ )
1354
+ location = PlainLocationField(
1355
+ zoom=18,
1356
+ help_text="Location of your gate. Required only for automatic opening. "
1357
+ "Adjust this if this gate is significantly distanced from "
1358
+ "your actual home location."
1359
+ )
1360
+
1361
+ def __init__(self, *args, **kwargs):
1362
+ super().__init__(*args, **kwargs)
1363
+ if not self.fields['location'].initial:
1364
+ self.fields['location'].initial = get_current_instance().location
1365
+
1366
+ def clean_distance(self):
1367
+ distance = self.cleaned_data.get('auto_open_distance')
1368
+ if not distance:
1369
+ return distance
1370
+ try:
1371
+ distance = input_to_meters(distance)
1372
+ except Exception as e:
1373
+ raise forms.ValidationError(str(e))
1374
+
1375
+ if distance < 20:
1376
+ raise forms.ValidationError(
1377
+ "That is to little of a distance. At least 20 meters is required."
1378
+ )
1379
+ if distance > 2000:
1380
+ raise forms.ValidationError(
1381
+ "This is to high of a distance. Max 2 km is allowed."
1382
+ )
1383
+
1384
+ return distance
1385
+
1386
+
1332
1387
  def clean(self):
1333
1388
  super().clean()
1334
-
1335
1389
  check_pins = ('open_pin', 'close_pin', 'sensor_pin')
1336
1390
  for pin in check_pins:
1337
1391
  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
@@ -32,14 +33,29 @@ from .app_widgets import (
32
33
  WeatherWidget
33
34
  )
34
35
  from .forms import (
36
+ DimmableLightsGroupConfigForm, SwitchGroupConfigForm,
35
37
  ThermostatConfigForm, AlarmGroupConfigForm,
36
38
  IPCameraConfigForm, WeatherForm,
37
- WateringConfigForm, StateSelectForm,
39
+ WateringConfigForm, StateSelectForm, MainStateSelectForm,
38
40
  AlarmClockConfigForm
39
41
  )
40
42
 
41
43
  # ----------- Generic controllers -----------------------------
42
44
 
45
+
46
+
47
+ class DimmableLightsGroup(Dimmer):
48
+ name = _("Dimmable Lights Group")
49
+ gateway_class = GenericGatewayHandler
50
+ config_form = DimmableLightsGroupConfigForm
51
+
52
+
53
+ class SwitchGroup(Switch):
54
+ name = _("On/Off Group")
55
+ gateway_class = GenericGatewayHandler
56
+ config_form = SwitchGroupConfigForm
57
+
58
+
43
59
  class Thermostat(ControllerBase):
44
60
  name = _("Thermostat")
45
61
  base_type = 'thermostat'
@@ -319,6 +335,7 @@ class Weather(ControllerBase):
319
335
  admin_widget_template = 'admin/controller_widgets/weather.html'
320
336
  default_config = {}
321
337
  default_value = {}
338
+ manual_add = False
322
339
 
323
340
  def _validate_val(self, value, occasion=None):
324
341
  return value
@@ -1054,6 +1071,120 @@ class StateSelect(ControllerBase):
1054
1071
  return value
1055
1072
 
1056
1073
 
1074
+ class MainState(StateSelect):
1075
+ name = _("Main State")
1076
+ config_form = MainStateSelectForm
1077
+ default_value = 'day'
1078
+
1079
+ default_config = {
1080
+ 'is_main': True,
1081
+ 'weekdays_morning_hour': 6,
1082
+ 'weekends_morning_hour': 7,
1083
+ 'away_on_no_action': 30,
1084
+ 'sleeping_phones_hour': 21,
1085
+ 'states': [
1086
+ {
1087
+ "icon": "sunrise", "name": "Morning", "slug": "morning",
1088
+ 'help_text': "Morning hour to sunrise. Activates in dark time of a year."
1089
+ },
1090
+ {
1091
+ "icon": "house-day", "name": "Day", "slug": "day",
1092
+ 'help_text': "From sunrise to sunset."
1093
+ },
1094
+ {
1095
+ "icon": "house-night", "name": "Evening", "slug": "evening",
1096
+ 'help_text': "From sunrise to midnight"
1097
+ },
1098
+ {
1099
+ "icon": "moon-cloud", "name": "Night", "slug": "night",
1100
+ 'help_text': "From midnight to sunrise or static morning hour."
1101
+ },
1102
+ {"icon": "snooze", "name": "Sleep time", "slug": "sleep"},
1103
+ {"icon": "house-person-leave", "name": "Away", "slug": "away"},
1104
+ {"icon": "island-tropical", "name": "Vacation", "slug": "vacation"}
1105
+ ]
1106
+ }
1107
+
1108
+ def get_day_evening_night_morning(self):
1109
+ from simo.automation.helpers import LocalSun
1110
+ sun = LocalSun(self.component.zone.instance.location)
1111
+ timezone.activate(self.component.zone.instance.timezone)
1112
+ localtime = timezone.localtime()
1113
+
1114
+ # It is daytime if the sun is up!
1115
+ if not sun.is_night():
1116
+ return 'day'
1117
+
1118
+ # it is evening if the sun is down at the evening
1119
+ if sun.get_sunset_time(localtime) < localtime:
1120
+ return 'evening'
1121
+
1122
+ if localtime.weekday() < 5:
1123
+ if localtime.hour >= self.component.config['weekdays_morning_hour']:
1124
+ return 'morning'
1125
+ else:
1126
+ if localtime.hour >= self.component.config['weekends_morning_hour']:
1127
+ return 'morning'
1128
+
1129
+ # 0 - 6AM and still dark
1130
+ return 'night'
1131
+
1132
+
1133
+ def check_is_away(self, last_sensor_action):
1134
+ away_on_no_action = not self.component.config.get('away_on_no_action')
1135
+ if not away_on_no_action:
1136
+ return False
1137
+ from simo.users.models import InstanceUser
1138
+ if InstanceUser.objects.filter(
1139
+ is_active=True, at_home=True, instance=self.component.zone.instance
1140
+ ).count():
1141
+ return False
1142
+
1143
+ return (time.time() - last_sensor_action) // 60 >= away_on_no_action
1144
+
1145
+
1146
+ def is_sleep_time(self):
1147
+ timezone.activate(self.component.zone.instance.timezone)
1148
+ localtime = timezone.localtime()
1149
+ if localtime.weekday() < 5:
1150
+ if localtime.hour < self.component.config['weekdays_morning_hour']:
1151
+ return True
1152
+ else:
1153
+ if localtime.hour < self.component.config['weekends_morning_hour']:
1154
+ return True
1155
+ sleeping_phones_hour = self.component.config.get(
1156
+ 'sleeping_phones_hour'
1157
+ )
1158
+ if localtime.hour >= sleeping_phones_hour:
1159
+ return True
1160
+
1161
+ return False
1162
+
1163
+
1164
+ def owner_phones_on_sleep(self):
1165
+ sleeping_phones_hour = self.component.config.get('sleeping_phones_hour')
1166
+ if sleeping_phones_hour is not None:
1167
+ return False
1168
+
1169
+ if not self.is_sleep_time():
1170
+ return False
1171
+
1172
+ from simo.users.models import InstanceUser
1173
+
1174
+ for iuser in InstanceUser.objects.filter(
1175
+ is_active=True, role__is_owner=True,
1176
+ instance=self.component.zone.instance
1177
+ ):
1178
+ # skipping users that are not at home
1179
+ if not iuser.at_home:
1180
+ continue
1181
+ if not iuser.phone_on_charge:
1182
+ # at least one user's phone is not yet on charge
1183
+ return False
1184
+
1185
+ return True
1186
+
1187
+
1057
1188
  # ----------- Dummy controllers -----------------------------
1058
1189
 
1059
1190
  class DummyBinarySensor(BinarySensor):