simo 2.5.41__py3-none-any.whl → 2.6.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.
- simo/__pycache__/settings.cpython-38.pyc +0 -0
- simo/automation/__pycache__/__init__.cpython-38.pyc +0 -0
- simo/automation/__pycache__/app_widgets.cpython-38.pyc +0 -0
- simo/automation/__pycache__/controllers.cpython-38.pyc +0 -0
- simo/automation/__pycache__/forms.cpython-38.pyc +0 -0
- simo/automation/__pycache__/gateways.cpython-38.pyc +0 -0
- simo/automation/__pycache__/helpers.cpython-38.pyc +0 -0
- simo/{generic/scripting → automation}/__pycache__/serializers.cpython-38.pyc +0 -0
- simo/{generic/scripting/__pycache__/__init__.cpython-38.pyc → automation/__pycache__/state.cpython-38.pyc} +0 -0
- simo/automation/app_widgets.py +8 -0
- simo/automation/controllers.py +273 -0
- simo/automation/forms.py +290 -0
- simo/automation/gateways.py +257 -0
- simo/automation/migrations/0001_initial.py +39 -0
- simo/automation/migrations/0002_update_helpers_in_scripts.py +29 -0
- simo/automation/migrations/__init__.py +0 -0
- simo/automation/migrations/__pycache__/0001_initial.cpython-38.pyc +0 -0
- simo/automation/migrations/__pycache__/0002_update_helpers_in_scripts.cpython-38.pyc +0 -0
- simo/automation/migrations/__pycache__/__init__.cpython-38.pyc +0 -0
- simo/automation/templates/automations/auto_away.py +55 -0
- simo/automation/templates/automations/auto_state_script.py +31 -0
- simo/{core/templates/core/auto_night_day_script.py → automation/templates/automations/phones_sleep_script.py} +25 -13
- simo/core/__pycache__/admin.cpython-38.pyc +0 -0
- simo/core/__pycache__/api.cpython-38.pyc +0 -0
- simo/core/__pycache__/filters.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/admin.py +7 -4
- simo/core/api.py +8 -1
- simo/core/filters.py +61 -0
- simo/core/management/_hub_template/hub/supervisor.conf +0 -1
- simo/core/signal_receivers.py +50 -17
- simo/core/utils/__pycache__/type_constants.cpython-38.pyc +0 -0
- simo/core/utils/type_constants.py +1 -1
- simo/fleet/__pycache__/api.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/controllers.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/forms.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/serializers.cpython-38.pyc +0 -0
- simo/fleet/api.py +6 -0
- simo/fleet/controllers.py +1 -0
- simo/fleet/forms.py +22 -3
- simo/fleet/serializers.py +9 -1
- simo/generic/__pycache__/app_widgets.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/app_widgets.py +0 -6
- simo/generic/controllers.py +4 -260
- simo/generic/forms.py +2 -269
- simo/generic/gateways.py +4 -193
- simo/generic/migrations/0002_auto_20241126_0726.py +34 -0
- simo/generic/migrations/__pycache__/0002_auto_20241126_0726.cpython-38.pyc +0 -0
- simo/notifications/__pycache__/api.cpython-38.pyc +0 -0
- simo/notifications/api.py +1 -1
- simo/settings.py +1 -0
- simo/users/__pycache__/api.cpython-38.pyc +0 -0
- simo/users/api.py +1 -2
- {simo-2.5.41.dist-info → simo-2.6.2.dist-info}/METADATA +1 -1
- {simo-2.5.41.dist-info → simo-2.6.2.dist-info}/RECORD +69 -51
- simo/core/templates/core/auto_state_script.py +0 -78
- simo/generic/scripting/__pycache__/helpers.cpython-38.pyc +0 -0
- /simo/{generic/scripting/example.py → automation/__init__.py} +0 -0
- /simo/{generic/scripting → automation}/helpers.py +0 -0
- /simo/{generic/scripting → automation}/serializers.py +0 -0
- /simo/{generic/scripting/__init__.py → automation/state.py} +0 -0
- /simo/{generic → automation}/templates/admin/controller_widgets/script.html +0 -0
- {simo-2.5.41.dist-info → simo-2.6.2.dist-info}/LICENSE.md +0 -0
- {simo-2.5.41.dist-info → simo-2.6.2.dist-info}/WHEEL +0 -0
- {simo-2.5.41.dist-info → simo-2.6.2.dist-info}/entry_points.txt +0 -0
- {simo-2.5.41.dist-info → simo-2.6.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import logging
|
|
3
|
+
import pytz
|
|
4
|
+
import json
|
|
5
|
+
import time
|
|
6
|
+
import multiprocessing
|
|
7
|
+
import threading
|
|
8
|
+
import traceback
|
|
9
|
+
from django.conf import settings
|
|
10
|
+
from django.utils import timezone
|
|
11
|
+
from django.db import connection as db_connection
|
|
12
|
+
from django.db.models import Q
|
|
13
|
+
import paho.mqtt.client as mqtt
|
|
14
|
+
from simo.core.models import Component
|
|
15
|
+
from simo.core.middleware import introduce_instance, drop_current_instance
|
|
16
|
+
from simo.core.gateways import BaseObjectCommandsGatewayHandler
|
|
17
|
+
from simo.core.forms import BaseGatewayForm
|
|
18
|
+
from simo.core.utils.logs import StreamToLogger
|
|
19
|
+
from simo.core.events import GatewayObjectCommand, get_event_obj
|
|
20
|
+
from simo.core.loggers import get_gw_logger, get_component_logger
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ScriptRunHandler(multiprocessing.Process):
|
|
24
|
+
'''
|
|
25
|
+
Threading offers better overall stability, but we use
|
|
26
|
+
multiprocessing for Scripts so that they are better isolated and
|
|
27
|
+
we are able to kill them whenever we need.
|
|
28
|
+
'''
|
|
29
|
+
component = None
|
|
30
|
+
logger = None
|
|
31
|
+
|
|
32
|
+
def __init__(self, component_id, *args, **kwargs):
|
|
33
|
+
super().__init__(*args, **kwargs)
|
|
34
|
+
self.component_id = component_id
|
|
35
|
+
|
|
36
|
+
def run(self):
|
|
37
|
+
db_connection.connect()
|
|
38
|
+
self.component = Component.objects.get(id=self.component_id)
|
|
39
|
+
try:
|
|
40
|
+
tz = pytz.timezone(self.component.zone.instance.timezone)
|
|
41
|
+
except:
|
|
42
|
+
tz = pytz.timezone('UTC')
|
|
43
|
+
timezone.activate(tz)
|
|
44
|
+
introduce_instance(self.component.zone.instance)
|
|
45
|
+
self.logger = get_component_logger(self.component)
|
|
46
|
+
sys.stdout = StreamToLogger(self.logger, logging.INFO)
|
|
47
|
+
sys.stderr = StreamToLogger(self.logger, logging.ERROR)
|
|
48
|
+
self.component.set('running')
|
|
49
|
+
|
|
50
|
+
if hasattr(self.component.controller, '_run'):
|
|
51
|
+
def run_code():
|
|
52
|
+
self.component.controller._run()
|
|
53
|
+
else:
|
|
54
|
+
code = self.component.config.get('code')
|
|
55
|
+
def run_code():
|
|
56
|
+
start = time.time()
|
|
57
|
+
exec(code, globals())
|
|
58
|
+
if 'class Automation:' in code and time.time() - start < 1:
|
|
59
|
+
Automation().run()
|
|
60
|
+
|
|
61
|
+
if not code:
|
|
62
|
+
self.component.value = 'finished'
|
|
63
|
+
self.component.save(update_fields=['value'])
|
|
64
|
+
return
|
|
65
|
+
print("------START-------")
|
|
66
|
+
try:
|
|
67
|
+
run_code()
|
|
68
|
+
except:
|
|
69
|
+
print("------ERROR------")
|
|
70
|
+
self.component.set('error')
|
|
71
|
+
raise
|
|
72
|
+
else:
|
|
73
|
+
print("------FINISH-----")
|
|
74
|
+
self.component.set('finished')
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class AutomationsGatewayHandler(BaseObjectCommandsGatewayHandler):
|
|
79
|
+
name = "Automation"
|
|
80
|
+
config_form = BaseGatewayForm
|
|
81
|
+
|
|
82
|
+
running_scripts = {}
|
|
83
|
+
periodic_tasks = (
|
|
84
|
+
('watch_scripts', 10),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
terminating_scripts = set()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def watch_scripts(self):
|
|
91
|
+
drop_current_instance()
|
|
92
|
+
# observe running scripts and drop the ones that are no longer alive
|
|
93
|
+
dead_scripts = False
|
|
94
|
+
for id, process in list(self.running_scripts.items()):
|
|
95
|
+
comp = Component.objects.filter(id=id).first()
|
|
96
|
+
if process.is_alive():
|
|
97
|
+
if not comp and id not in self.terminating_scripts:
|
|
98
|
+
# script is deleted, or instance deactivated
|
|
99
|
+
process.kill()
|
|
100
|
+
continue
|
|
101
|
+
else:
|
|
102
|
+
if id not in self.terminating_scripts:
|
|
103
|
+
dead_scripts = True
|
|
104
|
+
if comp:
|
|
105
|
+
logger = get_component_logger(comp)
|
|
106
|
+
logger.log(logging.INFO, "-------DEAD!-------")
|
|
107
|
+
self.stop_script(comp, 'error')
|
|
108
|
+
|
|
109
|
+
if dead_scripts:
|
|
110
|
+
# give 10s air before we wake these dead scripts up!
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
from .controllers import Script
|
|
114
|
+
for script in Component.objects.filter(
|
|
115
|
+
controller_uid=Script.uid,
|
|
116
|
+
config__keep_alive=True
|
|
117
|
+
).exclude(value__in=('running', 'stopped', 'finished')):
|
|
118
|
+
self.start_script(script)
|
|
119
|
+
|
|
120
|
+
def run(self, exit):
|
|
121
|
+
drop_current_instance()
|
|
122
|
+
self.exit = exit
|
|
123
|
+
self.logger = get_gw_logger(self.gateway_instance.id)
|
|
124
|
+
for task, period in self.periodic_tasks:
|
|
125
|
+
threading.Thread(
|
|
126
|
+
target=self._run_periodic_task, args=(exit, task, period), daemon=True
|
|
127
|
+
).start()
|
|
128
|
+
|
|
129
|
+
from .controllers import Script
|
|
130
|
+
|
|
131
|
+
mqtt_client = mqtt.Client()
|
|
132
|
+
mqtt_client.username_pw_set('root', settings.SECRET_KEY)
|
|
133
|
+
mqtt_client.on_connect = self.on_mqtt_connect
|
|
134
|
+
mqtt_client.on_message = self.on_mqtt_message
|
|
135
|
+
mqtt_client.connect(host=settings.MQTT_HOST, port=settings.MQTT_PORT)
|
|
136
|
+
|
|
137
|
+
# We presume that this is the only running gateway, therefore
|
|
138
|
+
# if there are any running scripts, that is not true.
|
|
139
|
+
for component in Component.objects.filter(
|
|
140
|
+
controller_uid=Script.uid, value='running'
|
|
141
|
+
):
|
|
142
|
+
component.value = 'error'
|
|
143
|
+
component.save()
|
|
144
|
+
|
|
145
|
+
# Start scripts that are designed to be autostarted
|
|
146
|
+
# as well as those that are designed to be kept alive, but
|
|
147
|
+
# got terminated unexpectedly
|
|
148
|
+
for script in Component.objects.filter(
|
|
149
|
+
base_type='script',
|
|
150
|
+
).filter(
|
|
151
|
+
Q(config__autostart=True) |
|
|
152
|
+
Q(value='error', config__keep_alive=True)
|
|
153
|
+
).distinct():
|
|
154
|
+
self.start_script(script)
|
|
155
|
+
|
|
156
|
+
print("GATEWAY STARTED!")
|
|
157
|
+
while not exit.is_set():
|
|
158
|
+
mqtt_client.loop()
|
|
159
|
+
mqtt_client.disconnect()
|
|
160
|
+
|
|
161
|
+
script_ids = [id for id in self.running_scripts.keys()]
|
|
162
|
+
for id in script_ids:
|
|
163
|
+
self.stop_script(
|
|
164
|
+
Component.objects.get(id=id), 'error'
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
time.sleep(0.5)
|
|
168
|
+
while len(self.running_scripts.keys()):
|
|
169
|
+
print("Still running scripts: ", self.running_scripts.keys())
|
|
170
|
+
time.sleep(0.5)
|
|
171
|
+
|
|
172
|
+
def on_mqtt_connect(self, mqtt_client, userdata, flags, rc):
|
|
173
|
+
command = GatewayObjectCommand(self.gateway_instance)
|
|
174
|
+
mqtt_client.subscribe(command.get_topic())
|
|
175
|
+
|
|
176
|
+
def on_mqtt_message(self, client, userdata, msg):
|
|
177
|
+
print("Mqtt message: ", msg.payload)
|
|
178
|
+
from .controllers import Script
|
|
179
|
+
payload = json.loads(msg.payload)
|
|
180
|
+
drop_current_instance()
|
|
181
|
+
component = get_event_obj(payload, Component)
|
|
182
|
+
if not component:
|
|
183
|
+
return
|
|
184
|
+
introduce_instance(component.zone.instance)
|
|
185
|
+
if not isinstance(component.controller, Script):
|
|
186
|
+
return
|
|
187
|
+
|
|
188
|
+
if payload.get('set_val') == 'start':
|
|
189
|
+
self.start_script(component)
|
|
190
|
+
elif payload.get('set_val') == 'stop':
|
|
191
|
+
self.stop_script(component)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def start_script(self, component):
|
|
195
|
+
print("START SCRIPT %s" % str(component))
|
|
196
|
+
if component.id in self.running_scripts:
|
|
197
|
+
if component.id not in self.terminating_scripts:
|
|
198
|
+
if component.value != 'running':
|
|
199
|
+
component.value = 'running'
|
|
200
|
+
component.save()
|
|
201
|
+
return
|
|
202
|
+
else:
|
|
203
|
+
good_to_go = False
|
|
204
|
+
for i in range(12): # wait for 3s
|
|
205
|
+
time.sleep(0.2)
|
|
206
|
+
component.refresh_from_db()
|
|
207
|
+
if component.id not in self.running_scripts:
|
|
208
|
+
good_to_go = True
|
|
209
|
+
break
|
|
210
|
+
if not good_to_go:
|
|
211
|
+
return self.stop_script(component, 'error')
|
|
212
|
+
|
|
213
|
+
self.running_scripts[component.id] = ScriptRunHandler(
|
|
214
|
+
component.id, daemon=True
|
|
215
|
+
)
|
|
216
|
+
self.running_scripts[component.id].start()
|
|
217
|
+
|
|
218
|
+
def stop_script(self, component, stop_status='stopped'):
|
|
219
|
+
self.terminating_scripts.add(component.id)
|
|
220
|
+
if component.id not in self.running_scripts:
|
|
221
|
+
if component.value == 'running':
|
|
222
|
+
component.value = stop_status
|
|
223
|
+
component.save(update_fields=['value'])
|
|
224
|
+
return
|
|
225
|
+
|
|
226
|
+
tz = pytz.timezone(component.zone.instance.timezone)
|
|
227
|
+
timezone.activate(tz)
|
|
228
|
+
logger = get_component_logger(component)
|
|
229
|
+
if stop_status == 'error':
|
|
230
|
+
logger.log(logging.INFO, "-------GATEWAY STOP-------")
|
|
231
|
+
else:
|
|
232
|
+
logger.log(logging.INFO, "-------STOP-------")
|
|
233
|
+
self.running_scripts[component.id].terminate()
|
|
234
|
+
|
|
235
|
+
def kill():
|
|
236
|
+
start = time.time()
|
|
237
|
+
terminated = False
|
|
238
|
+
while start > time.time() - 2:
|
|
239
|
+
if not self.running_scripts[component.id].is_alive():
|
|
240
|
+
terminated = True
|
|
241
|
+
break
|
|
242
|
+
time.sleep(0.1)
|
|
243
|
+
if not terminated:
|
|
244
|
+
if stop_status == 'error':
|
|
245
|
+
logger.log(logging.INFO, "-------GATEWAY KILL-------")
|
|
246
|
+
else:
|
|
247
|
+
logger.log(logging.INFO, "-------KILL!-------")
|
|
248
|
+
self.running_scripts[component.id].kill()
|
|
249
|
+
|
|
250
|
+
component.set(stop_status)
|
|
251
|
+
self.terminating_scripts.remove(component.id)
|
|
252
|
+
# making sure it's fully killed along with it's child processes
|
|
253
|
+
self.running_scripts[component.id].kill()
|
|
254
|
+
self.running_scripts.pop(component.id, None)
|
|
255
|
+
logger.handlers = []
|
|
256
|
+
|
|
257
|
+
threading.Thread(target=kill, daemon=True).start()
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Generated by Django 4.2.10 on 2024-11-27 07:40
|
|
2
|
+
|
|
3
|
+
from django.db import migrations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def forwards_func(apps, schema_editor):
|
|
7
|
+
Component = apps.get_model("core", "Component")
|
|
8
|
+
Gateway = apps.get_model('core', "Gateway")
|
|
9
|
+
|
|
10
|
+
automation, new = Gateway.objects.get_or_create(
|
|
11
|
+
type='simo.automation.gateways.AutomationsGatewayHandler'
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
for script in Component.objects.filter(
|
|
15
|
+
controller_uid__in=(
|
|
16
|
+
'simo.generic.controllers.PresenceLighting',
|
|
17
|
+
'simo.generic.controllers.Script',
|
|
18
|
+
)
|
|
19
|
+
):
|
|
20
|
+
script.controller_uid = script.controller_uid.replace(
|
|
21
|
+
'simo.generic', 'simo.automation'
|
|
22
|
+
)
|
|
23
|
+
script.gateway = automation
|
|
24
|
+
script.save()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def reverse_func(apps, schema_editor):
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Migration(migrations.Migration):
|
|
32
|
+
|
|
33
|
+
dependencies = [
|
|
34
|
+
('generic', '0002_auto_20241126_0726'),
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
operations = [
|
|
38
|
+
migrations.RunPython(forwards_func, reverse_func, elidable=True),
|
|
39
|
+
]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Generated by Django 4.2.10 on 2024-11-27 08:23
|
|
2
|
+
|
|
3
|
+
from django.db import migrations
|
|
4
|
+
|
|
5
|
+
def forwards_func(apps, schema_editor):
|
|
6
|
+
Component = apps.get_model("core", "Component")
|
|
7
|
+
|
|
8
|
+
for script in Component.objects.filter(
|
|
9
|
+
controller_uid='simo.automation.controllers.Script'
|
|
10
|
+
):
|
|
11
|
+
script.config['code'] = script.config['code'].replace(
|
|
12
|
+
'simo.generic.scripting.helpers', 'simo.automation.helpers'
|
|
13
|
+
)
|
|
14
|
+
script.save()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def reverse_func(apps, schema_editor):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Migration(migrations.Migration):
|
|
22
|
+
|
|
23
|
+
dependencies = [
|
|
24
|
+
('automation', '0001_initial'),
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
operations = [
|
|
28
|
+
migrations.RunPython(forwards_func, reverse_func, elidable=True),
|
|
29
|
+
]
|
|
File without changes
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,55 @@
|
|
|
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.automation.helpers import (
|
|
7
|
+
get_day_evening_night_morning, LocalSun
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Automation:
|
|
12
|
+
STATE_COMP_ID = {{ state_comp_id }}
|
|
13
|
+
INACTIVITY_MINUTES = 30
|
|
14
|
+
|
|
15
|
+
def __init__(self):
|
|
16
|
+
self.instance = get_current_instance()
|
|
17
|
+
self.state = Component.objects.get(id=self.STATE_COMP_ID)
|
|
18
|
+
self.sensors_on_watch = set()
|
|
19
|
+
self.last_sensor_action = time.time()
|
|
20
|
+
self.sun = LocalSun(self.instance.location)
|
|
21
|
+
|
|
22
|
+
def sensor_change(self, sensor=None):
|
|
23
|
+
self.last_sensor_action = time.time()
|
|
24
|
+
|
|
25
|
+
def check_away(self):
|
|
26
|
+
if InstanceUser.objects.filter(
|
|
27
|
+
is_active=True, at_home=True
|
|
28
|
+
).count():
|
|
29
|
+
return False
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
time.time() - self.last_sensor_action
|
|
33
|
+
) // 60 >= self.INACTIVITY_MINUTES
|
|
34
|
+
|
|
35
|
+
def run(self):
|
|
36
|
+
while True:
|
|
37
|
+
for sensor in Component.objects.filter(
|
|
38
|
+
base_type='binary-sensor',
|
|
39
|
+
alarm_category='security'
|
|
40
|
+
):
|
|
41
|
+
if sensor.id not in self.sensors_on_watch:
|
|
42
|
+
sensor.on_change(self.sensor_change)
|
|
43
|
+
self.sensors_on_watch.add(sensor.id)
|
|
44
|
+
|
|
45
|
+
self.state.refresh_from_db()
|
|
46
|
+
if self.check_away():
|
|
47
|
+
if self.state.value != 'away':
|
|
48
|
+
print("AWAY!")
|
|
49
|
+
self.state.send('away')
|
|
50
|
+
else:
|
|
51
|
+
if self.state.value == 'away':
|
|
52
|
+
new_state = get_day_evening_night_morning(
|
|
53
|
+
self.sun, timezone.localtime()
|
|
54
|
+
)
|
|
55
|
+
print(f"{new_state.upper()}!")
|
|
@@ -0,0 +1,31 @@
|
|
|
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.automation.helpers import (
|
|
7
|
+
LocalSun, get_day_evening_night_morning
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Automation:
|
|
12
|
+
STATE_COMPONENT_ID = {{ state_comp_id }}
|
|
13
|
+
|
|
14
|
+
def __init__(self):
|
|
15
|
+
self.state = Component.objects.get(id=self.STATE_COMPONENT_ID)
|
|
16
|
+
self.sun = LocalSun(get_current_instance().location)
|
|
17
|
+
|
|
18
|
+
def run(self):
|
|
19
|
+
while True:
|
|
20
|
+
self.state.refresh_from_db()
|
|
21
|
+
if self.state.value in ('day', 'night', 'evening', 'morning'):
|
|
22
|
+
|
|
23
|
+
current_state = get_day_evening_night_morning(
|
|
24
|
+
self.sun, timezone.localtime()
|
|
25
|
+
)
|
|
26
|
+
if current_state != self.state.value:
|
|
27
|
+
print(f"New state - {current_state}")
|
|
28
|
+
self.state.send(current_state)
|
|
29
|
+
|
|
30
|
+
# randomize script check times
|
|
31
|
+
time.sleep(random.randint(20, 40))
|
|
@@ -4,7 +4,9 @@ from django.utils import timezone
|
|
|
4
4
|
from simo.core.middleware import get_current_instance
|
|
5
5
|
from simo.core.models import Component
|
|
6
6
|
from simo.users.models import InstanceUser
|
|
7
|
-
from simo.
|
|
7
|
+
from simo.automation.helpers import (
|
|
8
|
+
LocalSun, get_day_evening_night_morning
|
|
9
|
+
)
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
class Automation:
|
|
@@ -14,23 +16,26 @@ class Automation:
|
|
|
14
16
|
self.instance = get_current_instance()
|
|
15
17
|
self.state = Component.objects.get(id=self.STATE_COMPONENT_ID)
|
|
16
18
|
self.sun = LocalSun(self.instance.location)
|
|
17
|
-
self.
|
|
19
|
+
self.sleep_is_on = False
|
|
18
20
|
|
|
19
21
|
def check_owner_phones(self, state, instance_users, datetime):
|
|
20
|
-
if not self.
|
|
21
|
-
if not (datetime.hour >=
|
|
22
|
+
if not self.sleep_is_on:
|
|
23
|
+
if not (datetime.hour >= 21 or datetime.hour < 6):
|
|
22
24
|
return
|
|
23
25
|
|
|
24
26
|
for iuser in instance_users:
|
|
27
|
+
# ignoring inactive and non owner users
|
|
28
|
+
if not iuser.is_active or not iuser.role.is_owner:
|
|
29
|
+
continue
|
|
25
30
|
# skipping users that are not at home
|
|
26
31
|
if not iuser.at_home:
|
|
27
32
|
continue
|
|
28
33
|
if not iuser.phone_on_charge:
|
|
29
34
|
# at least one user's phone is not yet on charge
|
|
30
35
|
return
|
|
31
|
-
self.
|
|
32
|
-
print("
|
|
33
|
-
return '
|
|
36
|
+
self.sleep_is_on = True
|
|
37
|
+
print("Let's turn on the sleep mode!")
|
|
38
|
+
return 'sleep'
|
|
34
39
|
else:
|
|
35
40
|
if datetime.hour >= 22 or datetime.hour < 6:
|
|
36
41
|
return
|
|
@@ -38,6 +43,9 @@ class Automation:
|
|
|
38
43
|
# at home, none of them have their phones on charge
|
|
39
44
|
# and current state is still night
|
|
40
45
|
for iuser in instance_users:
|
|
46
|
+
# ignoring inactive and non owner users
|
|
47
|
+
if not iuser.is_active or not iuser.role.is_owner:
|
|
48
|
+
continue
|
|
41
49
|
# skipping users that are not at home
|
|
42
50
|
if not iuser.at_home:
|
|
43
51
|
continue
|
|
@@ -45,16 +53,20 @@ class Automation:
|
|
|
45
53
|
# at least one user's phone is still on charge
|
|
46
54
|
return
|
|
47
55
|
|
|
48
|
-
self.
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
56
|
+
self.sleep_is_on = False
|
|
57
|
+
|
|
58
|
+
if not self.sleep_is_on and state.value == 'sleep':
|
|
59
|
+
new_state = get_day_evening_night_morning(
|
|
60
|
+
self.sun, timezone.localtime()
|
|
61
|
+
)
|
|
62
|
+
print(f"Switch state back to {new_state}!")
|
|
63
|
+
return new_state
|
|
52
64
|
|
|
53
65
|
def run(self):
|
|
54
66
|
while True:
|
|
55
67
|
instance_users = InstanceUser.objects.filter(
|
|
56
68
|
is_active=True, role__is_owner=True
|
|
57
|
-
)
|
|
69
|
+
).prefetch_related('role')
|
|
58
70
|
self.state.refresh_from_db()
|
|
59
71
|
new_state = self.check_owner_phones(
|
|
60
72
|
self.state, instance_users, timezone.localtime()
|
|
@@ -62,5 +74,5 @@ class Automation:
|
|
|
62
74
|
if new_state:
|
|
63
75
|
self.state.send(new_state)
|
|
64
76
|
|
|
65
|
-
# randomize script
|
|
77
|
+
# randomize script check times
|
|
66
78
|
time.sleep(random.randint(20, 40))
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
simo/core/admin.py
CHANGED
|
@@ -22,7 +22,7 @@ from .forms import (
|
|
|
22
22
|
CompTypeSelectForm,
|
|
23
23
|
BaseComponentForm
|
|
24
24
|
)
|
|
25
|
-
from .filters import ZonesFilter
|
|
25
|
+
from .filters import ZonesFilter, AvailableChoicesFilter
|
|
26
26
|
from .widgets import AdminImageWidget
|
|
27
27
|
from simo.conf import dynamic_settings
|
|
28
28
|
|
|
@@ -255,7 +255,8 @@ class ComponentPermissionInline(admin.TabularInline):
|
|
|
255
255
|
class ComponentAdmin(EasyObjectsDeleteMixin, admin.ModelAdmin):
|
|
256
256
|
form = BaseComponentForm
|
|
257
257
|
list_display = (
|
|
258
|
-
'id', 'name_display', 'value_display', 'base_type', '
|
|
258
|
+
'id', 'name_display', 'value_display', 'base_type', 'controller_uid',
|
|
259
|
+
'alive', 'battery_level',
|
|
259
260
|
'alarm_category', 'show_in_app',
|
|
260
261
|
)
|
|
261
262
|
readonly_fields = (
|
|
@@ -264,8 +265,10 @@ class ComponentAdmin(EasyObjectsDeleteMixin, admin.ModelAdmin):
|
|
|
264
265
|
'control', 'value', 'arm_status', 'history', 'meta'
|
|
265
266
|
)
|
|
266
267
|
list_filter = (
|
|
267
|
-
'gateway', 'base_type', ('zone', ZonesFilter), 'category',
|
|
268
|
-
'
|
|
268
|
+
'gateway', 'base_type', ('zone', ZonesFilter), 'category',
|
|
269
|
+
#'controller_uid',
|
|
270
|
+
('controller_uid', AvailableChoicesFilter),
|
|
271
|
+
'alive', 'alarm_category', 'arm_status',
|
|
269
272
|
)
|
|
270
273
|
|
|
271
274
|
search_fields = 'id', 'name', 'value', 'config', 'meta', 'notes'
|
simo/core/api.py
CHANGED
|
@@ -211,7 +211,14 @@ class ComponentViewSet(
|
|
|
211
211
|
return permissions
|
|
212
212
|
|
|
213
213
|
def get_queryset(self):
|
|
214
|
-
|
|
214
|
+
qs = get_components_queryset(self.instance, self.request.user)
|
|
215
|
+
if self.request.GET.get('id'):
|
|
216
|
+
try:
|
|
217
|
+
ids = [int(id) for id in self.request.GET.get('id').split(',')]
|
|
218
|
+
return qs.filter(id__in=ids)
|
|
219
|
+
except:
|
|
220
|
+
return qs
|
|
221
|
+
return qs
|
|
215
222
|
|
|
216
223
|
def get_view_name(self):
|
|
217
224
|
singular = "Component"
|
simo/core/filters.py
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
from django.contrib import admin
|
|
2
|
+
from django.contrib.admin.utils import (
|
|
3
|
+
get_model_from_relation,
|
|
4
|
+
prepare_lookup_value,
|
|
5
|
+
reverse_field_path,
|
|
6
|
+
)
|
|
7
|
+
from django.utils.translation import gettext_lazy as _
|
|
2
8
|
|
|
3
9
|
|
|
4
10
|
class ZonesFilter(admin.RelatedFieldListFilter):
|
|
@@ -10,3 +16,58 @@ class ZonesFilter(admin.RelatedFieldListFilter):
|
|
|
10
16
|
include_blank=False, ordering=ordering,
|
|
11
17
|
limit_choices_to=limit_to
|
|
12
18
|
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AvailableChoicesFilter(admin.ChoicesFieldListFilter):
|
|
22
|
+
"""
|
|
23
|
+
presents as choices to filter only those choices that are present in a queryset
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, field, request, params, model, model_admin, field_path):
|
|
27
|
+
super().__init__(field, request, params, model, model_admin, field_path)
|
|
28
|
+
parent_model, reverse_path = reverse_field_path(model, field_path)
|
|
29
|
+
# Obey parent ModelAdmin queryset when deciding which options to show
|
|
30
|
+
if model == parent_model:
|
|
31
|
+
queryset = model_admin.get_queryset(request)
|
|
32
|
+
else:
|
|
33
|
+
queryset = parent_model._default_manager.all()
|
|
34
|
+
self.lookup_choices = (
|
|
35
|
+
queryset.distinct().order_by(field.name).values_list(
|
|
36
|
+
field.name, flat=True
|
|
37
|
+
)
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def choices(self, changelist):
|
|
42
|
+
yield {
|
|
43
|
+
"selected": self.lookup_val is None,
|
|
44
|
+
"query_string": changelist.get_query_string(
|
|
45
|
+
remove=[self.lookup_kwarg, self.lookup_kwarg_isnull]
|
|
46
|
+
),
|
|
47
|
+
"display": _("All"),
|
|
48
|
+
}
|
|
49
|
+
none_title = ""
|
|
50
|
+
|
|
51
|
+
titles_map = {l:t for l, t in self.field.flatchoices}
|
|
52
|
+
|
|
53
|
+
for val in self.lookup_choices:
|
|
54
|
+
if val is None:
|
|
55
|
+
none_title = titles_map.get(val, val)
|
|
56
|
+
continue
|
|
57
|
+
val = str(val)
|
|
58
|
+
yield {
|
|
59
|
+
"selected": self.lookup_val == val,
|
|
60
|
+
"query_string": changelist.get_query_string(
|
|
61
|
+
{self.lookup_kwarg: val}, [self.lookup_kwarg_isnull]
|
|
62
|
+
),
|
|
63
|
+
"display": titles_map.get(val, val),
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if none_title:
|
|
67
|
+
yield {
|
|
68
|
+
"selected": bool(self.lookup_val_isnull),
|
|
69
|
+
"query_string": changelist.get_query_string(
|
|
70
|
+
{self.lookup_kwarg_isnull: "True"}, [self.lookup_kwarg]
|
|
71
|
+
),
|
|
72
|
+
"display": none_title,
|
|
73
|
+
}
|