simo 2.5.3__py3-none-any.whl → 2.5.5__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 (59) hide show
  1. simo/core/__pycache__/app_widgets.cpython-38.pyc +0 -0
  2. simo/core/__pycache__/base_types.cpython-38.pyc +0 -0
  3. simo/core/__pycache__/controllers.cpython-38.pyc +0 -0
  4. simo/core/__pycache__/middleware.cpython-38.pyc +0 -0
  5. simo/core/__pycache__/models.cpython-38.pyc +0 -0
  6. simo/core/__pycache__/signal_receivers.cpython-38.pyc +0 -0
  7. simo/core/__pycache__/tasks.cpython-38.pyc +0 -0
  8. simo/core/app_widgets.py +19 -1
  9. simo/core/base_types.py +2 -0
  10. simo/core/controllers.py +154 -4
  11. simo/core/management/_hub_template/hub/supervisor.conf +4 -0
  12. simo/core/management/commands/__pycache__/gateways_manager.cpython-38.pyc +0 -0
  13. simo/core/middleware.py +14 -7
  14. simo/core/models.py +5 -3
  15. simo/core/signal_receivers.py +71 -7
  16. simo/core/tasks.py +1 -1
  17. simo/core/templates/core/auto_night_day_script.py +62 -0
  18. simo/core/templates/core/auto_state_script.py +78 -0
  19. simo/fleet/__pycache__/controllers.cpython-38.pyc +0 -0
  20. simo/fleet/__pycache__/forms.cpython-38.pyc +0 -0
  21. simo/fleet/controllers.py +20 -119
  22. simo/fleet/forms.py +101 -0
  23. simo/generic/__pycache__/app_widgets.cpython-38.pyc +0 -0
  24. simo/generic/__pycache__/base_types.cpython-38.pyc +0 -0
  25. simo/generic/__pycache__/controllers.cpython-38.pyc +0 -0
  26. simo/generic/__pycache__/forms.cpython-38.pyc +0 -0
  27. simo/generic/__pycache__/gateways.cpython-38.pyc +0 -0
  28. simo/generic/__pycache__/models.cpython-38.pyc +0 -0
  29. simo/generic/app_widgets.py +0 -18
  30. simo/generic/base_types.py +0 -2
  31. simo/generic/controllers.py +2 -262
  32. simo/generic/forms.py +0 -49
  33. simo/generic/gateways.py +9 -119
  34. simo/generic/models.py +1 -0
  35. simo/generic/scripting/__pycache__/helpers.cpython-38.pyc +0 -0
  36. simo/generic/scripting/example.py +66 -0
  37. simo/generic/scripting/helpers.py +66 -10
  38. simo/notifications/__pycache__/admin.cpython-38.pyc +0 -0
  39. simo/notifications/__pycache__/utils.cpython-38.pyc +0 -0
  40. simo/notifications/admin.py +7 -3
  41. simo/users/__pycache__/admin.cpython-38.pyc +0 -0
  42. simo/users/__pycache__/api.cpython-38.pyc +0 -0
  43. simo/users/__pycache__/models.cpython-38.pyc +0 -0
  44. simo/users/admin.py +25 -5
  45. simo/users/api.py +34 -11
  46. simo/users/migrations/0035_instanceuser_last_seen_speed_kmh_and_more.py +23 -0
  47. simo/users/migrations/0036_instanceuser_phone_on_charge_user_phone_on_charge.py +23 -0
  48. simo/users/migrations/0037_rename_last_seen_location_datetime_instanceuser_last_seen_and_more.py +53 -0
  49. simo/users/migrations/__pycache__/0035_instanceuser_last_seen_speed_kmh_and_more.cpython-38.pyc +0 -0
  50. simo/users/migrations/__pycache__/0036_instanceuser_phone_on_charge_user_phone_on_charge.cpython-38.pyc +0 -0
  51. simo/users/migrations/__pycache__/0037_rename_last_seen_location_datetime_instanceuser_last_seen_and_more.cpython-38.pyc +0 -0
  52. simo/users/models.py +14 -57
  53. {simo-2.5.3.dist-info → simo-2.5.5.dist-info}/METADATA +1 -1
  54. {simo-2.5.3.dist-info → simo-2.5.5.dist-info}/RECORD +58 -50
  55. {simo-2.5.3.dist-info → simo-2.5.5.dist-info}/WHEEL +1 -1
  56. simo/scripting.py +0 -39
  57. {simo-2.5.3.dist-info → simo-2.5.5.dist-info}/LICENSE.md +0 -0
  58. {simo-2.5.3.dist-info → simo-2.5.5.dist-info}/entry_points.txt +0 -0
  59. {simo-2.5.3.dist-info → simo-2.5.5.dist-info}/top_level.txt +0 -0
Binary file
Binary file
simo/core/app_widgets.py CHANGED
@@ -104,4 +104,22 @@ class LockWidget(BaseAppWidget):
104
104
  class AirQualityWidget(BaseAppWidget):
105
105
  uid = 'air-quality'
106
106
  name = _("Air Quality")
107
- size = [2, 2]
107
+ size = [2, 2]
108
+
109
+
110
+ class GateWidget(BaseAppWidget):
111
+ uid = 'gate'
112
+ name = _('Gate')
113
+ size = [2, 1]
114
+
115
+
116
+ class BlindsWidget(BaseAppWidget):
117
+ uid = 'blinds'
118
+ name = _('Blinds')
119
+ size = [4, 1]
120
+
121
+
122
+ class SlidesWidget(BaseAppWidget):
123
+ uid = 'slides'
124
+ name = _('Slides')
125
+ size = [2, 1]
simo/core/base_types.py CHANGED
@@ -18,4 +18,6 @@ BASE_TYPES = {
18
18
  'switch-quadruple': _("Switch Quadruple"),
19
19
  'switch-quintuple': _("Switch Quintuple"),
20
20
  'lock': _("Lock"),
21
+ 'gate': _("Gate"),
22
+ 'blinds': _("Blinds"),
21
23
  }
simo/core/controllers.py CHANGED
@@ -777,10 +777,8 @@ class Switch(MultiSwitchBase, TimerMixin, OnOffPokerMixin):
777
777
  Gateway specific implementation is very welcome of this!
778
778
  '''
779
779
  self.turn_on()
780
- def toggle_back():
781
- time.sleep(0.5)
782
- self.turn_off()
783
- threading.Thread(target=toggle_back).start()
780
+ time.sleep(0.5)
781
+ self.turn_off()
784
782
 
785
783
 
786
784
  class DoubleSwitch(MultiSwitchBase):
@@ -899,3 +897,155 @@ class Lock(Switch):
899
897
  self.component.save(
900
898
  update_fields=['change_init_by', 'change_init_date']
901
899
  )
900
+
901
+
902
+ class Blinds(ControllerBase, TimerMixin):
903
+ name = _("Blind")
904
+ base_type = 'blinds'
905
+ admin_widget_template = 'admin/controller_widgets/blinds.html'
906
+ default_config = {}
907
+
908
+ @property
909
+ def app_widget(self):
910
+ if self.component.config.get('control_mode') == 'slide':
911
+ return SlidesWidget
912
+ else:
913
+ return BlindsWidget
914
+
915
+ @property
916
+ def default_value(self):
917
+ # target and current positions in milliseconds, angle in degrees (0 - 180)
918
+ return {'target': 0, 'position': 0, 'angle': 0}
919
+
920
+ def _validate_val(self, value, occasion=None):
921
+
922
+ if occasion == BEFORE_SEND:
923
+ if isinstance(value, int) or isinstance(value, float):
924
+ # legacy support
925
+ value = {'target': int(value)}
926
+ if 'target' not in value:
927
+ raise ValidationError("Target value is required!")
928
+ target = value.get('target')
929
+ if type(target) not in (float, int):
930
+ raise ValidationError(
931
+ "Bad target position for blinds to go."
932
+ )
933
+ if target > self.component.config.get('open_duration') * 1000:
934
+ raise ValidationError(
935
+ "Target value lower than %d expected, "
936
+ "%d received instead" % (
937
+ self.component.config['open_duration'] * 1000,
938
+ target
939
+ )
940
+ )
941
+ if 'angle' in value:
942
+ try:
943
+ angle = int(value['angle'])
944
+ except:
945
+ raise ValidationError(
946
+ "Integer between 0 - 180 is required for blinds angle."
947
+ )
948
+ if angle < 0 or angle > 180:
949
+ raise ValidationError(
950
+ "Integer between 0 - 180 is required for blinds angle."
951
+ )
952
+ else:
953
+ value['angle'] = self.component.value.get('angle', 0)
954
+
955
+ elif occasion == BEFORE_SET:
956
+ if not isinstance(value, dict):
957
+ raise ValidationError("Dictionary is expected")
958
+ for key, val in value.items():
959
+ if key not in ('target', 'position', 'angle'):
960
+ raise ValidationError(
961
+ "'target', 'position' or 'angle' parameters are expected."
962
+ )
963
+ if key == 'position':
964
+ if val < 0:
965
+ raise ValidationError(
966
+ "Positive integer expected for blind position"
967
+ )
968
+ if val > self.component.config.get('open_duration') * 1000:
969
+ raise ValidationError(
970
+ "Positive value is to big. Must be lower than %d, "
971
+ "but you have provided %d" % (
972
+ self.component.config.get('open_duration') * 1000, val
973
+ )
974
+ )
975
+
976
+ self.component.refresh_from_db()
977
+ if 'target' not in value:
978
+ value['target'] = self.component.value.get('target')
979
+ if 'position' not in value:
980
+ value['position'] = self.component.value.get('position')
981
+ if 'angle' not in value:
982
+ value['angle'] = self.component.value.get('angle')
983
+
984
+ return value
985
+
986
+ def open(self):
987
+ send_val = {'target': 0}
988
+ angle = self.component.value.get('angle')
989
+ if angle is not None and 0 <= angle <= 180:
990
+ send_val['angle'] = angle
991
+ self.send(send_val)
992
+
993
+ def close(self):
994
+ send_val = {'target': self.component.config['open_duration'] * 1000}
995
+ angle = self.component.value.get('angle')
996
+ if angle is not None and 0 <= angle <= 180:
997
+ send_val['angle'] = angle
998
+ self.send(send_val)
999
+
1000
+ def stop(self):
1001
+ send_val = {'target': -1}
1002
+ angle = self.component.value.get('angle')
1003
+ if angle is not None and 0 <= angle <= 180:
1004
+ send_val['angle'] = angle
1005
+ self.send(send_val)
1006
+
1007
+
1008
+ class Gate(ControllerBase, TimerMixin):
1009
+ name = _("Gate")
1010
+ base_type = 'gate'
1011
+ app_widget = GateWidget
1012
+ admin_widget_template = 'admin/controller_widgets/gate.html'
1013
+ default_config = {}
1014
+
1015
+ @property
1016
+ def default_value(self):
1017
+ return 'closed'
1018
+
1019
+ def _validate_val(self, value, occasion=None):
1020
+ if occasion == BEFORE_SEND:
1021
+ if self.component.config.get('action_method') == 'click':
1022
+ if value != 'call':
1023
+ raise ValidationError(
1024
+ 'Gate component understands only one command: '
1025
+ '"call". You have provided: "%s"' % (str(value))
1026
+ )
1027
+ else:
1028
+ if value not in ('call', 'open', 'close'):
1029
+ raise ValidationError(
1030
+ 'This gate component understands only 3 commands: '
1031
+ '"open", "close" and "call". You have provided: "%s"' %
1032
+ (str(value))
1033
+ )
1034
+ elif occasion == BEFORE_SET and value not in (
1035
+ 'closed', 'open', 'open_moving', 'closed_moving'
1036
+ ):
1037
+ raise ValidationError(
1038
+ 'Gate component can only be in 4 states: '
1039
+ '"closed", "closed", "open_moving", "closed_moving". '
1040
+ 'You have provided: "%s"' % (str(value))
1041
+ )
1042
+ return value
1043
+
1044
+ def open(self):
1045
+ self.send('open')
1046
+
1047
+ def close(self):
1048
+ self.send('close')
1049
+
1050
+ def call(self):
1051
+ self.send('call')
@@ -27,6 +27,8 @@ stdout_logfile_backups=3
27
27
  redirect_stderr=true
28
28
  autostart=true
29
29
  autorestart=true
30
+ stopasgroup=true
31
+ killasgroup=true
30
32
 
31
33
 
32
34
  [program:simo-gateways]
@@ -41,6 +43,8 @@ stdout_logfile_backups=3
41
43
  redirect_stderr=true
42
44
  autostart=true
43
45
  autorestart=true
46
+ stopasgroup=true
47
+ killasgroup=true
44
48
 
45
49
  [program:simo-celery-beat]
46
50
  directory={{ project_dir }}/hub/
simo/core/middleware.py CHANGED
@@ -37,11 +37,13 @@ def get_current_instance(request=None):
37
37
 
38
38
  instance = getattr(_thread_locals, 'instance', None)
39
39
 
40
- if not instance:
41
- from .models import Instance
42
- instance = Instance.objects.filter(is_active=True).first()
43
- if instance:
44
- introduce_instance(instance)
40
+ # NEVER FORCE THIS! IT's A very BAD IDEA!
41
+ # For example gateways run on an instance neutral environment!
42
+ # if not instance:
43
+ # from .models import Instance
44
+ # instance = Instance.objects.filter(is_active=True).first()
45
+ # if instance:
46
+ # introduce_instance(instance)
45
47
  return instance
46
48
 
47
49
 
@@ -92,8 +94,13 @@ def instance_middleware(get_response):
92
94
 
93
95
  if instance:
94
96
  introduce_instance(instance, request)
95
- tz = pytz.timezone(instance.timezone)
96
- timezone.activate(tz)
97
+ try:
98
+ # should never, but just in case
99
+ tz = pytz.timezone(instance.timezone)
100
+ timezone.activate(tz)
101
+ except:
102
+ tz = pytz.timezone('UTC')
103
+ timezone.activate(tz)
97
104
 
98
105
  response = get_response(request)
99
106
 
simo/core/models.py CHANGED
@@ -61,7 +61,7 @@ def post_icon_delete(sender, instance, *args, **kwargs):
61
61
  pass
62
62
 
63
63
 
64
- class Instance(models.Model, SimoAdminMixin):
64
+ class Instance(DirtyFieldsMixin, models.Model, SimoAdminMixin):
65
65
  # Multiple home instances can be had on a single hub computer!
66
66
  # For example separate hotel apartments
67
67
  # or something of that kind.
@@ -101,7 +101,7 @@ class Instance(models.Model, SimoAdminMixin):
101
101
  User, null=True, blank=True, on_delete=models.SET_NULL
102
102
  )
103
103
 
104
- objects = InstanceManager()
104
+ #objects = InstanceManager()
105
105
 
106
106
 
107
107
  def __str__(self):
@@ -118,7 +118,9 @@ class Instance(models.Model, SimoAdminMixin):
118
118
 
119
119
 
120
120
  class Zone(DirtyFieldsMixin, models.Model, SimoAdminMixin):
121
- instance = models.ForeignKey(Instance, on_delete=models.CASCADE)
121
+ instance = models.ForeignKey(
122
+ Instance, on_delete=models.CASCADE, related_name='zones'
123
+ )
122
124
  name = models.CharField(_('name'), max_length=40)
123
125
  order = models.PositiveIntegerField(
124
126
  default=0, blank=False, null=False, db_index=True
@@ -5,6 +5,7 @@ from django.db.models.signals import post_save, post_delete
5
5
  from django.dispatch import receiver
6
6
  from django.utils import timezone
7
7
  from django.conf import settings
8
+ from django.template.loader import render_to_string
8
9
  from actstream import action
9
10
  from simo.users.models import PermissionsRole
10
11
  from .models import Instance, Gateway, Component, Icon, Zone, Category
@@ -26,10 +27,12 @@ def create_instance_defaults(sender, instance, created, **kwargs):
26
27
  # Create default zones
27
28
 
28
29
  for zone_name in (
29
- 'Living Room', 'Kitchen', 'Bathroom', 'Porch', 'Garage', 'Yard', 'Other'
30
+ 'Living Room', 'Kitchen', 'Bathroom', 'Porch', 'Garage', 'Yard',
30
31
  ):
31
32
  Zone.objects.create(instance=instance, name=zone_name)
32
33
 
34
+ other_zone = Zone.objects.create(instance=instance, name='Other')
35
+
33
36
  core_dir_path = os.path.dirname(os.path.realpath(__file__))
34
37
  imgs_folder = os.path.join(
35
38
  core_dir_path, 'static/defaults/category_headers'
@@ -40,7 +43,8 @@ def create_instance_defaults(sender, instance, created, **kwargs):
40
43
  os.makedirs(categories_media_dir)
41
44
 
42
45
  # Create default categories
43
-
46
+ climate_category = None
47
+ other_category = None
44
48
  for i, data in enumerate([
45
49
  ("All", 'star'), ("Climate", 'temperature-half'),
46
50
  ("Lights", 'lightbulb'), ("Security", 'eye'),
@@ -52,27 +56,28 @@ def create_instance_defaults(sender, instance, created, **kwargs):
52
56
  settings.MEDIA_ROOT, 'categories', "%s.jpg" % data[0].lower()
53
57
  )
54
58
  )
55
- Category.objects.create(
59
+ cat = Category.objects.create(
56
60
  instance=instance,
57
61
  name=data[0], icon=Icon.objects.get(slug=data[1]),
58
62
  all=i == 0, header_image=os.path.join(
59
63
  'categories', "%s.jpg" % data[0].lower()
60
64
  ), order=i + 10
61
65
  )
66
+ if cat.name == 'Climate':
67
+ climate_category = cat
68
+ if cat.name == 'Other':
69
+ other_category = cat
62
70
 
63
71
  # Create generic gateway and components
64
72
 
65
73
  generic, new = Gateway.objects.get_or_create(
66
74
  type='simo.generic.gateways.GenericGatewayHandler'
67
75
  )
68
- generic.start()
69
76
  dummy, new = Gateway.objects.get_or_create(
70
77
  type='simo.generic.gateways.DummyGatewayHandler'
71
78
  )
72
- dummy.start()
73
79
  weather_icon = Icon.objects.get(slug='cloud-bolt-sun')
74
- other_zone = Zone.objects.get(name='Other', instance=instance)
75
- climate_category = Category.objects.get(name='Climate', instance=instance)
80
+
76
81
  Component.objects.create(
77
82
  name='Weather', icon=weather_icon,
78
83
  zone=other_zone,
@@ -82,6 +87,63 @@ def create_instance_defaults(sender, instance, created, **kwargs):
82
87
  config={'is_main': True}
83
88
  )
84
89
 
90
+ state_comp = Component.objects.create(
91
+ name='State', icon=Icon.objects.get(slug='home'),
92
+ zone=other_zone,
93
+ category=other_category,
94
+ gateway=generic, base_type='state-select',
95
+ controller_uid='simo.generic.controllers.StateSelect',
96
+ value='day',
97
+ config={"states": [
98
+ {"icon": "house-day", "name": "Day", "slug": "day"},
99
+ {"icon": "house-night", "name": "Evening", "slug": "evening"},
100
+ {"icon": "moon-cloud", "name": "Night", "slug": "night"},
101
+ {"icon": "house-person-leave", "name": "Away", "slug": "away"},
102
+ {"icon": "island-tropical", "name": "Vacation", "slug": "vacation"}
103
+ ], "is_main": True}
104
+ )
105
+
106
+
107
+ auto_state_code = render_to_string(
108
+ 'core/auto_state_script.py', {'state_comp_id': state_comp.id}
109
+ )
110
+ Component.objects.create(
111
+ name='Auto state', icon=Icon.objects.get(slug='bolt'),
112
+ zone=other_zone,
113
+ category=other_category,
114
+ gateway=generic, base_type='script',
115
+ controller_uid='simo.generic.controllers.Script',
116
+ config={
117
+ "code": auto_state_code, 'autostart': True, 'keep_alive': True,
118
+ "notes": f"""
119
+ The script automatically controls the states of the "State" component (ID:{state_comp.id}) — 'day,' 'evening,' 'night,' 'away.'
120
+ The 'day' state is activated on weekdays from 10 a.m., and on weekends from 11 a.m. When the sun sets, the 'evening' state is activated, and at midnight, the 'night' state is activated.
121
+ If no one is home, the 'away' state is activated.
122
+ If a different state, such as 'vacation,' is selected, the script stops running and waits until the State is switched back to one of the controlled states.
123
+ If one of the controlled states is manually selected, the script waits until that state is reached automatically and, once aligned with the manually set state, resumes its operation in normal mode.
124
+ """
125
+ }
126
+ )
127
+
128
+ code = render_to_string(
129
+ 'core/auto_night_day_script.py', {'state_comp_id': state_comp.id}
130
+ )
131
+ Component.objects.create(
132
+ name='Auto night/day by owner phones on charge',
133
+ icon=Icon.objects.get(slug='bolt'), zone=other_zone,
134
+ category=other_category, show_in_app=False,
135
+ gateway=generic, base_type='script',
136
+ controller_uid='simo.generic.controllers.Script',
137
+ config={
138
+ "code": code, 'autostart': True, 'keep_alive': True,
139
+ "notes": f"""
140
+ Automatically sets State component (ID: {state_comp.id}) to "night" if it is later than 10pm and all home owners phones who are at home are put on charge.
141
+ Sets State component to "day" state as soon as none of the home owners phones are on charge and it is 6am or later.
142
+
143
+ """
144
+ }
145
+ )
146
+
85
147
  # Create default User permission roles
86
148
 
87
149
  PermissionsRole.objects.create(
@@ -93,6 +155,8 @@ def create_instance_defaults(sender, instance, created, **kwargs):
93
155
  PermissionsRole.objects.create(
94
156
  instance=instance, name="Guest", is_owner=True
95
157
  )
158
+ generic.start()
159
+ dummy.start()
96
160
 
97
161
 
98
162
  @receiver(post_save, sender=Zone)
simo/core/tasks.py CHANGED
@@ -143,7 +143,7 @@ def sync_with_remote():
143
143
 
144
144
  for user in User.objects.filter(
145
145
  Q(roles__instance=instance) | Q(is_master=True)
146
- ).exclude(email__in=('system@simo.io', 'device@simo.io')):
146
+ ).exclude(email__in=('system@simo.io', 'device@simo.io')).distinct():
147
147
  is_superuser = False
148
148
  user_role = user.get_role(instance)
149
149
  if user_role and user_role.is_superuser:
@@ -0,0 +1,62 @@
1
+ import time
2
+ import random
3
+ from django.utils import timezone
4
+ from simo.core.middleware import get_current_instance
5
+ from simo.core.models import Component
6
+ from simo.users.models import InstanceUser
7
+ from simo.generic.scripting.helpers import LocalSun
8
+
9
+
10
+ class Automation:
11
+ STATE_COMPONENT_ID = {{ state_comp_id }}
12
+
13
+ def __init__(self):
14
+ self.instance = get_current_instance()
15
+ self.state = Component.objects.get(id=self.STATE_COMPONENT_ID)
16
+ self.sun = LocalSun(self.instance.location)
17
+ self.night_is_on = False
18
+
19
+ def check_owner_phones(self, state, instance_users, datetime):
20
+ if not self.night_is_on:
21
+ if not (datetime.hour >= 22 or datetime.hour < 6):
22
+ return
23
+
24
+ for iuser in instance_users:
25
+ # skipping users that are not at home
26
+ if not iuser.at_home:
27
+ continue
28
+ if not iuser.phone_on_charge:
29
+ # at least one user's phone is not yet on charge
30
+ return
31
+ self.night_is_on = True
32
+ return 'night'
33
+ else:
34
+ # return new_state diena only if there are still users
35
+ # at home, none of them have their phones on charge
36
+ # and current state is still night
37
+ for iuser in instance_users:
38
+ # skipping users that are not at home
39
+ if not iuser.at_home:
40
+ continue
41
+ if iuser.phone_on_charge:
42
+ # at least one user's phone is still on charge
43
+ return
44
+ else:
45
+ self.night_is_on = False
46
+ if not self.night_is_on and state.value == 'night':
47
+ return 'day'
48
+
49
+ def run(self):
50
+ while True:
51
+ instance_users = InstanceUser.objects.filter(
52
+ is_active=True, role__is_owner=True
53
+ )
54
+ self.state.refresh_from_db()
55
+ new_state = self.check_owner_phones(
56
+ self.state, instance_users, timezone.localtime()
57
+ )
58
+ if new_state:
59
+ self.state.send(new_state)
60
+
61
+ # randomize script load
62
+ time.sleep(random.randint(20, 40))
@@ -0,0 +1,78 @@
1
+ import time
2
+ from django.utils import timezone
3
+ from simo.core.middleware import get_current_instance
4
+ from simo.core.models import Component
5
+ from simo.users.models import InstanceUser
6
+ from simo.generic.scripting.helpers import LocalSun
7
+
8
+
9
+ class Automation:
10
+ STATE_COMPONENT_ID = {{ state_comp_id }}
11
+ last_state = None
12
+ weekdays_morning_hour = 10
13
+ weekends_morning_hour = 11
14
+
15
+ def __init__(self):
16
+ self.instance = get_current_instance()
17
+ self.state = Component.objects.get(id=self.STATE_COMPONENT_ID)
18
+ self.sun = LocalSun(self.instance.location)
19
+
20
+ def check_at_home(self):
21
+ return bool(InstanceUser.objects.filter(
22
+ is_active=True, at_home=True
23
+ ).count())
24
+
25
+ def calculate_appropriate_state(self, localtime, at_home):
26
+ if not at_home:
27
+ return 'away'
28
+ if self.sun.is_night(localtime) \
29
+ and self.sun.get_sunset_time(localtime) < localtime:
30
+ return 'evening'
31
+
32
+ if localtime.weekday() < 5 \
33
+ and localtime.hour < self.weekdays_morning_hour:
34
+ return 'night'
35
+
36
+ if localtime.weekday() >= 5 \
37
+ and localtime.hour < self.weekends_morning_hour:
38
+ return 'night'
39
+
40
+ return 'day'
41
+
42
+ def get_new_state(self, state, localtime, at_home):
43
+ # If state component on vacation or in some other state
44
+ # we do not interfere!
45
+ if state.value not in ('day', 'night', 'evening', 'away'):
46
+ return
47
+ should_be = self.calculate_appropriate_state(
48
+ localtime, at_home
49
+ )
50
+
51
+ if self.last_state != state.value:
52
+ # user changed something manually
53
+ # we must first wait for appropriate state to get in to
54
+ # manually selected one, only then we will transition to forward.
55
+ if should_be == state.value:
56
+ print("Consensus with system users reached!")
57
+ self.last_state = should_be
58
+ elif self.last_state != should_be:
59
+ print("New state: ", should_be)
60
+ self.last_state = should_be
61
+ return should_be
62
+
63
+ def run(self):
64
+ # do not interfere on script start,
65
+ # only later when we absolutely must
66
+ self.last_state = self.get_new_state(
67
+ self.state, timezone.localtime(),
68
+ self.check_at_home()
69
+ )
70
+ while True:
71
+ self.state.refresh_from_db()
72
+ new_state_value = self.get_new_state(
73
+ self.state, timezone.localtime(),
74
+ self.check_at_home()
75
+ )
76
+ if new_state_value:
77
+ self.state.send(new_state_value)
78
+ time.sleep(10)
Binary file