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.
- simo/core/__pycache__/app_widgets.cpython-38.pyc +0 -0
- simo/core/__pycache__/base_types.cpython-38.pyc +0 -0
- simo/core/__pycache__/controllers.cpython-38.pyc +0 -0
- simo/core/__pycache__/middleware.cpython-38.pyc +0 -0
- simo/core/__pycache__/models.cpython-38.pyc +0 -0
- simo/core/__pycache__/signal_receivers.cpython-38.pyc +0 -0
- simo/core/__pycache__/tasks.cpython-38.pyc +0 -0
- simo/core/app_widgets.py +19 -1
- simo/core/base_types.py +2 -0
- simo/core/controllers.py +154 -4
- simo/core/management/_hub_template/hub/supervisor.conf +4 -0
- simo/core/management/commands/__pycache__/gateways_manager.cpython-38.pyc +0 -0
- simo/core/middleware.py +14 -7
- simo/core/models.py +5 -3
- simo/core/signal_receivers.py +71 -7
- simo/core/tasks.py +1 -1
- simo/core/templates/core/auto_night_day_script.py +62 -0
- simo/core/templates/core/auto_state_script.py +78 -0
- simo/fleet/__pycache__/controllers.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/forms.cpython-38.pyc +0 -0
- simo/fleet/controllers.py +20 -119
- simo/fleet/forms.py +101 -0
- simo/generic/__pycache__/app_widgets.cpython-38.pyc +0 -0
- simo/generic/__pycache__/base_types.cpython-38.pyc +0 -0
- 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/__pycache__/models.cpython-38.pyc +0 -0
- simo/generic/app_widgets.py +0 -18
- simo/generic/base_types.py +0 -2
- simo/generic/controllers.py +2 -262
- simo/generic/forms.py +0 -49
- simo/generic/gateways.py +9 -119
- simo/generic/models.py +1 -0
- simo/generic/scripting/__pycache__/helpers.cpython-38.pyc +0 -0
- simo/generic/scripting/example.py +66 -0
- simo/generic/scripting/helpers.py +66 -10
- simo/notifications/__pycache__/admin.cpython-38.pyc +0 -0
- simo/notifications/__pycache__/utils.cpython-38.pyc +0 -0
- simo/notifications/admin.py +7 -3
- simo/users/__pycache__/admin.cpython-38.pyc +0 -0
- simo/users/__pycache__/api.cpython-38.pyc +0 -0
- simo/users/__pycache__/models.cpython-38.pyc +0 -0
- simo/users/admin.py +25 -5
- simo/users/api.py +34 -11
- simo/users/migrations/0035_instanceuser_last_seen_speed_kmh_and_more.py +23 -0
- simo/users/migrations/0036_instanceuser_phone_on_charge_user_phone_on_charge.py +23 -0
- simo/users/migrations/0037_rename_last_seen_location_datetime_instanceuser_last_seen_and_more.py +53 -0
- simo/users/migrations/__pycache__/0035_instanceuser_last_seen_speed_kmh_and_more.cpython-38.pyc +0 -0
- simo/users/migrations/__pycache__/0036_instanceuser_phone_on_charge_user_phone_on_charge.cpython-38.pyc +0 -0
- simo/users/migrations/__pycache__/0037_rename_last_seen_location_datetime_instanceuser_last_seen_and_more.cpython-38.pyc +0 -0
- simo/users/models.py +14 -57
- {simo-2.5.3.dist-info → simo-2.5.5.dist-info}/METADATA +1 -1
- {simo-2.5.3.dist-info → simo-2.5.5.dist-info}/RECORD +58 -50
- {simo-2.5.3.dist-info → simo-2.5.5.dist-info}/WHEEL +1 -1
- simo/scripting.py +0 -39
- {simo-2.5.3.dist-info → simo-2.5.5.dist-info}/LICENSE.md +0 -0
- {simo-2.5.3.dist-info → simo-2.5.5.dist-info}/entry_points.txt +0 -0
- {simo-2.5.3.dist-info → simo-2.5.5.dist-info}/top_level.txt +0 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
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
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
|
-
|
|
781
|
-
|
|
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/
|
|
Binary file
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
96
|
-
|
|
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(
|
|
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
|
simo/core/signal_receivers.py
CHANGED
|
@@ -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',
|
|
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
|
-
|
|
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
|
|
Binary file
|