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
simo/generic/forms.py
CHANGED
|
@@ -620,55 +620,6 @@ class GateConfigForm(BaseComponentForm):
|
|
|
620
620
|
)
|
|
621
621
|
|
|
622
622
|
|
|
623
|
-
class BlindsConfigForm(BaseComponentForm):
|
|
624
|
-
open_switch = forms.ModelChoiceField(
|
|
625
|
-
Component.objects.filter(base_type=Switch.base_type),
|
|
626
|
-
widget=autocomplete.ModelSelect2(
|
|
627
|
-
url='autocomplete-component', attrs={'data-html': True},
|
|
628
|
-
forward=(
|
|
629
|
-
forward.Const([Switch.base_type], 'base_type'),
|
|
630
|
-
)
|
|
631
|
-
)
|
|
632
|
-
)
|
|
633
|
-
close_switch = forms.ModelChoiceField(
|
|
634
|
-
Component.objects.filter(base_type=Switch.base_type),
|
|
635
|
-
widget=autocomplete.ModelSelect2(
|
|
636
|
-
url='autocomplete-component', attrs={'data-html': True},
|
|
637
|
-
forward=(
|
|
638
|
-
forward.Const([Switch.base_type], 'base_type'),
|
|
639
|
-
)
|
|
640
|
-
)
|
|
641
|
-
)
|
|
642
|
-
open_direction = forms.ChoiceField(
|
|
643
|
-
label='Closed > Open direction',
|
|
644
|
-
required=True, choices=(
|
|
645
|
-
('up', "Up"), ('down', "Down"),
|
|
646
|
-
('right', "Right"), ('left', "Left")
|
|
647
|
-
),
|
|
648
|
-
help_text="Move direction from fully closed to fully open."
|
|
649
|
-
|
|
650
|
-
)
|
|
651
|
-
open_duration = forms.FloatField(
|
|
652
|
-
label='Open duration', min_value=0.001, max_value=360000,
|
|
653
|
-
initial=30,
|
|
654
|
-
help_text="Time in seconds it takes for your blinds to go "
|
|
655
|
-
"from fully closed to fully open."
|
|
656
|
-
)
|
|
657
|
-
slats_angle_duration = forms.FloatField(
|
|
658
|
-
label='Slats angle duration', min_value=0.01, max_value=360000,
|
|
659
|
-
required=False,
|
|
660
|
-
help_text="Takes effect only with App control mode - 'Slide', "
|
|
661
|
-
"can be used with slat blinds to control slats angle. <br>"
|
|
662
|
-
"Time in seconds it takes "
|
|
663
|
-
"to go from fully closed to the start of open movement. <br>"
|
|
664
|
-
"Usually it's in between of 1 - 3 seconds."
|
|
665
|
-
)
|
|
666
|
-
control_mode = forms.ChoiceField(
|
|
667
|
-
label="App control mode", required=True, choices=(
|
|
668
|
-
('click', "Click"), ('hold', "Hold"), ('slide', "Slide")
|
|
669
|
-
),
|
|
670
|
-
)
|
|
671
|
-
|
|
672
623
|
|
|
673
624
|
class ContourForm(forms.Form):
|
|
674
625
|
uid = forms.CharField(widget=forms.HiddenInput(), required=False)
|
simo/generic/gateways.py
CHANGED
|
@@ -20,62 +20,6 @@ from simo.core.events import GatewayObjectCommand, get_event_obj
|
|
|
20
20
|
from simo.core.loggers import get_gw_logger, get_component_logger
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
class BlindsRunner(threading.Thread):
|
|
25
|
-
|
|
26
|
-
def __init__(self, blinds, *args, **kwargs):
|
|
27
|
-
self.blinds = blinds
|
|
28
|
-
self.target = self.blinds.value['target']
|
|
29
|
-
self.position = self.blinds.value['position']
|
|
30
|
-
self.open_duration = self.blinds.config.get('open_duration', 0) * 1000
|
|
31
|
-
assert self.target >= -1
|
|
32
|
-
assert self.target <= self.open_duration
|
|
33
|
-
self.exit = multiprocessing.Event()
|
|
34
|
-
super().__init__(*args, **kwargs)
|
|
35
|
-
|
|
36
|
-
def run(self):
|
|
37
|
-
try:
|
|
38
|
-
self.open_switch = Component.objects.get(
|
|
39
|
-
pk=self.blinds.config.get('open_switch')
|
|
40
|
-
)
|
|
41
|
-
self.close_switch = Component.objects.get(
|
|
42
|
-
pk=self.blinds.config.get('close_switch')
|
|
43
|
-
)
|
|
44
|
-
except:
|
|
45
|
-
self.done = True
|
|
46
|
-
return
|
|
47
|
-
self.start_position = self.blinds.value['position']
|
|
48
|
-
self.position = self.blinds.value['position']
|
|
49
|
-
self.start_time = time.time()
|
|
50
|
-
self.last_save = time.time()
|
|
51
|
-
while not self.exit.is_set():
|
|
52
|
-
change = (time.time() - self.start_time) * 1000
|
|
53
|
-
if self.target > self.start_position:
|
|
54
|
-
self.position = self.start_position + change
|
|
55
|
-
if self.position >= self.target:
|
|
56
|
-
self.blinds.set(
|
|
57
|
-
{'position': self.target, 'target': -1}
|
|
58
|
-
)
|
|
59
|
-
self.open_switch.turn_off()
|
|
60
|
-
self.close_switch.turn_off()
|
|
61
|
-
return
|
|
62
|
-
else:
|
|
63
|
-
self.position = self.start_position - change
|
|
64
|
-
if self.position < self.target:
|
|
65
|
-
self.blinds.set({'position': self.target, 'target': -1})
|
|
66
|
-
self.open_switch.turn_off()
|
|
67
|
-
self.close_switch.turn_off()
|
|
68
|
-
return
|
|
69
|
-
|
|
70
|
-
if self.last_save < time.time() - 1:
|
|
71
|
-
self.blinds.set({'position': self.position})
|
|
72
|
-
self.last_save = time.time()
|
|
73
|
-
time.sleep(0.01)
|
|
74
|
-
|
|
75
|
-
def terminate(self):
|
|
76
|
-
self.exit.set()
|
|
77
|
-
|
|
78
|
-
|
|
79
23
|
class CameraWatcher(threading.Thread):
|
|
80
24
|
|
|
81
25
|
def __init__(self, component_id, exit, *args, **kwargs):
|
|
@@ -130,7 +74,10 @@ class ScriptRunHandler(multiprocessing.Process):
|
|
|
130
74
|
def run(self):
|
|
131
75
|
db_connection.connect()
|
|
132
76
|
self.component = Component.objects.get(id=self.component_id)
|
|
133
|
-
|
|
77
|
+
try:
|
|
78
|
+
tz = pytz.timezone(self.component.zone.instance.timezone)
|
|
79
|
+
except:
|
|
80
|
+
tz = pytz.timezone('UTC')
|
|
134
81
|
timezone.activate(tz)
|
|
135
82
|
introduce_instance(self.component.zone.instance)
|
|
136
83
|
self.logger = get_component_logger(self.component)
|
|
@@ -174,7 +121,6 @@ class GenericGatewayHandler(BaseObjectCommandsGatewayHandler):
|
|
|
174
121
|
config_form = BaseGatewayForm
|
|
175
122
|
|
|
176
123
|
running_scripts = {}
|
|
177
|
-
blinds_runners = {}
|
|
178
124
|
periodic_tasks = (
|
|
179
125
|
('watch_thermostats', 60),
|
|
180
126
|
('watch_alarm_clocks', 30),
|
|
@@ -207,6 +153,9 @@ class GenericGatewayHandler(BaseObjectCommandsGatewayHandler):
|
|
|
207
153
|
dead_processes = []
|
|
208
154
|
for id, process in self.running_scripts.items():
|
|
209
155
|
if process.is_alive():
|
|
156
|
+
if not Component.objects.filter(id=id).count():
|
|
157
|
+
# script is deleted, or instance deactivated
|
|
158
|
+
process.terminate()
|
|
210
159
|
continue
|
|
211
160
|
component = Component.objects.filter(id=id).exclude(
|
|
212
161
|
value__in=('error', 'finished')
|
|
@@ -270,7 +219,7 @@ class GenericGatewayHandler(BaseObjectCommandsGatewayHandler):
|
|
|
270
219
|
component.save()
|
|
271
220
|
|
|
272
221
|
# Start scripts that are designed to be autostarted
|
|
273
|
-
# as well as those
|
|
222
|
+
# as well as those that are designed to be kept alive, but
|
|
274
223
|
# got terminated unexpectedly
|
|
275
224
|
for script in Component.objects.filter(
|
|
276
225
|
base_type='script',
|
|
@@ -291,9 +240,6 @@ class GenericGatewayHandler(BaseObjectCommandsGatewayHandler):
|
|
|
291
240
|
mqtt_client.loop()
|
|
292
241
|
mqtt_client.disconnect()
|
|
293
242
|
|
|
294
|
-
for id, runner in self.blinds_runners.items():
|
|
295
|
-
runner.terminate()
|
|
296
|
-
|
|
297
243
|
script_ids = [id for id in self.running_scripts.keys()]
|
|
298
244
|
for id in script_ids:
|
|
299
245
|
self.stop_script(
|
|
@@ -310,7 +256,7 @@ class GenericGatewayHandler(BaseObjectCommandsGatewayHandler):
|
|
|
310
256
|
def on_mqtt_message(self, client, userdata, msg):
|
|
311
257
|
print("Mqtt message: ", msg.payload)
|
|
312
258
|
from simo.generic.controllers import (
|
|
313
|
-
Script,
|
|
259
|
+
Script, AlarmGroup
|
|
314
260
|
)
|
|
315
261
|
payload = json.loads(msg.payload)
|
|
316
262
|
component = get_event_obj(payload, Component)
|
|
@@ -323,12 +269,8 @@ class GenericGatewayHandler(BaseObjectCommandsGatewayHandler):
|
|
|
323
269
|
elif payload.get('set_val') == 'stop':
|
|
324
270
|
self.stop_script(component)
|
|
325
271
|
return
|
|
326
|
-
elif component.controller_uid == Blinds.uid:
|
|
327
|
-
self.control_blinds(component, payload.get('set_val'))
|
|
328
272
|
elif component.controller_uid == AlarmGroup.uid:
|
|
329
273
|
self.control_alarm_group(component, payload.get('set_val'))
|
|
330
|
-
elif component.controller_uid == Gate:
|
|
331
|
-
self.control_gate(component, payload.get('set_val'))
|
|
332
274
|
else:
|
|
333
275
|
component.controller.set(payload.get('set_val'))
|
|
334
276
|
except Exception:
|
|
@@ -385,43 +327,6 @@ class GenericGatewayHandler(BaseObjectCommandsGatewayHandler):
|
|
|
385
327
|
|
|
386
328
|
threading.Thread(target=kill, daemon=True).start()
|
|
387
329
|
|
|
388
|
-
def control_blinds(self, blinds, target):
|
|
389
|
-
try:
|
|
390
|
-
open_switch = Component.objects.get(
|
|
391
|
-
pk=blinds.config['open_switch']
|
|
392
|
-
)
|
|
393
|
-
close_switch = Component.objects.get(
|
|
394
|
-
pk=blinds.config['close_switch']
|
|
395
|
-
)
|
|
396
|
-
except:
|
|
397
|
-
return
|
|
398
|
-
|
|
399
|
-
blinds.set({'target': target})
|
|
400
|
-
|
|
401
|
-
blinds_runner = self.blinds_runners.get(blinds.id)
|
|
402
|
-
if blinds_runner:
|
|
403
|
-
blinds_runner.terminate()
|
|
404
|
-
|
|
405
|
-
if target == -1:
|
|
406
|
-
open_switch.turn_off()
|
|
407
|
-
close_switch.turn_off()
|
|
408
|
-
|
|
409
|
-
elif target != blinds.value['position']:
|
|
410
|
-
try:
|
|
411
|
-
self.blinds_runners[blinds.id] = BlindsRunner(blinds)
|
|
412
|
-
self.blinds_runners[blinds.id].daemon = True
|
|
413
|
-
except:
|
|
414
|
-
pass
|
|
415
|
-
else:
|
|
416
|
-
if target > blinds.value['position']:
|
|
417
|
-
close_switch.turn_off()
|
|
418
|
-
open_switch.turn_on()
|
|
419
|
-
else:
|
|
420
|
-
open_switch.turn_off()
|
|
421
|
-
close_switch.turn_on()
|
|
422
|
-
|
|
423
|
-
self.blinds_runners[blinds.id].start()
|
|
424
|
-
|
|
425
330
|
def control_alarm_group(self, alarm_group, value):
|
|
426
331
|
from simo.generic.controllers import AlarmGroup
|
|
427
332
|
|
|
@@ -464,21 +369,6 @@ class GenericGatewayHandler(BaseObjectCommandsGatewayHandler):
|
|
|
464
369
|
other_group.refresh_status()
|
|
465
370
|
|
|
466
371
|
|
|
467
|
-
def control_gate(self, gate, value):
|
|
468
|
-
switch = Component.objects.filter(
|
|
469
|
-
pk=gate.config.get('action_switch')
|
|
470
|
-
).first()
|
|
471
|
-
if not switch:
|
|
472
|
-
return
|
|
473
|
-
|
|
474
|
-
if gate.config.get('action_method') == 'click':
|
|
475
|
-
switch.click()
|
|
476
|
-
else:
|
|
477
|
-
if value == 'open':
|
|
478
|
-
switch.turn_on()
|
|
479
|
-
else:
|
|
480
|
-
switch.turn_off()
|
|
481
|
-
|
|
482
372
|
def watch_alarm_events(self):
|
|
483
373
|
from .controllers import AlarmGroup
|
|
484
374
|
for alarm in Component.objects.filter(
|
simo/generic/models.py
CHANGED
|
Binary file
|
|
@@ -0,0 +1,66 @@
|
|
|
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
|
+
REZIMAS_COMPONENT_ID = 130
|
|
12
|
+
|
|
13
|
+
def __init__(self):
|
|
14
|
+
self.instance = get_current_instance()
|
|
15
|
+
self.rezimas = Component.objects.get(id=self.REZIMAS_COMPONENT_ID)
|
|
16
|
+
self.sun = LocalSun(self.instance.location)
|
|
17
|
+
self.night_is_on = False
|
|
18
|
+
|
|
19
|
+
def check_owner_phones(self, rezimas, 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_rezimas diena only if there are still users
|
|
35
|
+
# at home, none of them have their phones on charge
|
|
36
|
+
# and current rezimas 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 rezimas.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.rezimas.refresh_from_db()
|
|
55
|
+
new_rezimas = self.check_owner_phones(
|
|
56
|
+
self.rezimas, instance_users, timezone.localtime()
|
|
57
|
+
)
|
|
58
|
+
if new_rezimas:
|
|
59
|
+
self.rezimas.send(new_rezimas)
|
|
60
|
+
|
|
61
|
+
# randomize script load
|
|
62
|
+
time.sleep(random.randint(20, 40))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test(self):
|
|
66
|
+
pass
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import pytz
|
|
2
|
+
import math
|
|
1
3
|
from django.utils import timezone
|
|
2
4
|
from suntime import Sun
|
|
3
5
|
from simo.core.models import Instance
|
|
@@ -5,10 +7,12 @@ from simo.core.models import Instance
|
|
|
5
7
|
|
|
6
8
|
class LocalSun(Sun):
|
|
7
9
|
|
|
8
|
-
def __init__(self,
|
|
9
|
-
if not
|
|
10
|
+
def __init__(self, location=None):
|
|
11
|
+
if not location:
|
|
10
12
|
instance = Instance.objects.all().first()
|
|
11
|
-
|
|
13
|
+
coordinates = instance.location.split(',')
|
|
14
|
+
else:
|
|
15
|
+
coordinates = location.split(',')
|
|
12
16
|
try:
|
|
13
17
|
lat = float(coordinates[0])
|
|
14
18
|
except:
|
|
@@ -19,17 +23,69 @@ class LocalSun(Sun):
|
|
|
19
23
|
lon = 0
|
|
20
24
|
super().__init__(lat, lon)
|
|
21
25
|
|
|
22
|
-
def
|
|
23
|
-
|
|
26
|
+
def get_sunrise_time(self, localdatetime=None):
|
|
27
|
+
sunrise = super().get_sunrise_time(date=localdatetime)
|
|
28
|
+
if not localdatetime or not localdatetime.tzinfo:
|
|
29
|
+
return sunrise
|
|
30
|
+
return sunrise.astimezone(localdatetime.tzinfo)
|
|
31
|
+
|
|
32
|
+
def get_sunset_time(self, localdatetime=None):
|
|
33
|
+
sunset = super().get_sunset_time(date=localdatetime)
|
|
34
|
+
if not localdatetime or not localdatetime.tzinfo:
|
|
35
|
+
return sunset
|
|
36
|
+
return sunset.astimezone(localdatetime.tzinfo)
|
|
37
|
+
|
|
38
|
+
def _get_utc_datetime(self, localdatetime=None):
|
|
39
|
+
if not localdatetime:
|
|
40
|
+
utc_datetime = timezone.now()
|
|
41
|
+
else:
|
|
42
|
+
utc_datetime = localdatetime.astimezone(pytz.utc)
|
|
43
|
+
return utc_datetime
|
|
44
|
+
|
|
45
|
+
def is_night(self, localdatetime=None):
|
|
46
|
+
utc_datetime = self._get_utc_datetime(localdatetime)
|
|
47
|
+
if utc_datetime > self.get_sunset_time(utc_datetime):
|
|
24
48
|
return True
|
|
25
|
-
if
|
|
49
|
+
if utc_datetime < self.get_sunrise_time(utc_datetime):
|
|
26
50
|
return True
|
|
27
51
|
return False
|
|
28
52
|
|
|
29
|
-
def seconds_to_sunset(self):
|
|
30
|
-
|
|
53
|
+
def seconds_to_sunset(self, localdatetime=None):
|
|
54
|
+
utc_datetime = self._get_utc_datetime(localdatetime)
|
|
55
|
+
return (self.get_sunset_time(utc_datetime) - utc_datetime).total_seconds()
|
|
56
|
+
|
|
57
|
+
def seconds_to_sunrise(self, localdatetime=None):
|
|
58
|
+
utc_datetime = self._get_utc_datetime(localdatetime)
|
|
59
|
+
return (self.get_sunrise_time(utc_datetime) - utc_datetime).total_seconds()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def haversine_distance(location1, location2, units_of_measure='metric'):
|
|
63
|
+
# Radius of Earth in meters
|
|
64
|
+
R = 6371000
|
|
65
|
+
|
|
66
|
+
# Unpack coordinates
|
|
67
|
+
lat1, lon1 = location1.split(',')
|
|
68
|
+
lat2, lon2 = location2.split(',')
|
|
69
|
+
lat1, lon1, lat2, lon2 = float(lat1), float(lon1), float(lat2), float(lon2)
|
|
70
|
+
|
|
71
|
+
# Convert latitude and longitude from degrees to radians
|
|
72
|
+
phi1 = math.radians(lat1)
|
|
73
|
+
phi2 = math.radians(lat2)
|
|
74
|
+
delta_phi = math.radians(lat2 - lat1)
|
|
75
|
+
delta_lambda = math.radians(lon2 - lon1)
|
|
76
|
+
|
|
77
|
+
# Haversine formula
|
|
78
|
+
a = math.sin(delta_phi / 2) ** 2 + math.cos(phi1) * math.cos(
|
|
79
|
+
phi2) * math.sin(delta_lambda / 2) ** 2
|
|
80
|
+
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
|
|
31
81
|
|
|
32
|
-
|
|
33
|
-
|
|
82
|
+
# Distance in meters
|
|
83
|
+
distance_meters = R * c
|
|
34
84
|
|
|
85
|
+
# Convert to feet if 'imperial' is chosen
|
|
86
|
+
if units_of_measure == 'imperial':
|
|
87
|
+
distance = distance_meters * 3.28084 # Convert meters to feet
|
|
88
|
+
else:
|
|
89
|
+
distance = distance_meters # Keep in meters for 'metric'
|
|
35
90
|
|
|
91
|
+
return distance
|
|
Binary file
|
|
Binary file
|
simo/notifications/admin.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from django.contrib import admin
|
|
2
|
+
from simo.core.middleware import get_current_instance
|
|
2
3
|
from .models import Notification, UserNotification
|
|
3
4
|
|
|
4
5
|
|
|
@@ -15,9 +16,12 @@ class NotificationAdmin(admin.ModelAdmin):
|
|
|
15
16
|
actions = 'dispatch',
|
|
16
17
|
|
|
17
18
|
def get_queryset(self, request):
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
qs = super().get_queryset(request)
|
|
20
|
+
instance = get_current_instance()
|
|
21
|
+
if instance:
|
|
22
|
+
qs = qs.filter(instance=instance)
|
|
23
|
+
return qs.prefetch_related('to_users')
|
|
24
|
+
|
|
21
25
|
|
|
22
26
|
def to(self, obj):
|
|
23
27
|
return ', '.join([str(u) for u in obj.to_users.all()])
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
simo/users/admin.py
CHANGED
|
@@ -50,6 +50,20 @@ class PermissionsRoleAdmin(admin.ModelAdmin):
|
|
|
50
50
|
return fields
|
|
51
51
|
|
|
52
52
|
|
|
53
|
+
@admin.register(InstanceUser)
|
|
54
|
+
class InstanceUserAdmin(admin.ModelAdmin):
|
|
55
|
+
list_display = (
|
|
56
|
+
'user', 'role', 'is_active', 'at_home', 'last_seen', 'phone_on_charge'
|
|
57
|
+
)
|
|
58
|
+
fields = (
|
|
59
|
+
'user', 'role', 'is_active', 'at_home', 'last_seen',
|
|
60
|
+
'last_seen_location', 'last_seen_speed_kmh', 'phone_on_charge'
|
|
61
|
+
)
|
|
62
|
+
readonly_fields = (
|
|
63
|
+
'at_home', 'last_seen', 'last_seen_speed_kmh', 'phone_on_charge',
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
53
67
|
class InstanceUserInline(admin.TabularInline):
|
|
54
68
|
model = InstanceUser
|
|
55
69
|
extra = 0
|
|
@@ -69,7 +83,6 @@ class UserAdmin(OrgUserAdmin):
|
|
|
69
83
|
fields = (
|
|
70
84
|
'name', 'email', 'is_active', 'is_master',
|
|
71
85
|
'ssh_key', 'secret_key',
|
|
72
|
-
'last_seen_location',
|
|
73
86
|
)
|
|
74
87
|
readonly_fields = (
|
|
75
88
|
'name', 'email', 'avatar', 'last_action', 'is_active'
|
|
@@ -114,11 +127,18 @@ admin.site.unregister(Group)
|
|
|
114
127
|
|
|
115
128
|
|
|
116
129
|
@admin.register(UserDeviceReportLog)
|
|
117
|
-
class
|
|
130
|
+
class UserDeviceLog(admin.ModelAdmin):
|
|
118
131
|
model = UserDeviceReportLog
|
|
119
|
-
readonly_fields =
|
|
120
|
-
|
|
132
|
+
readonly_fields = (
|
|
133
|
+
'datetime', 'app_open', 'location', 'relay', 'users',
|
|
134
|
+
'speed_kmh', 'phone_on_charge'
|
|
135
|
+
)
|
|
136
|
+
list_display = (
|
|
137
|
+
'datetime', 'app_open', 'location', 'relay', 'speed_kmh',
|
|
138
|
+
'phone_on_charge', 'users'
|
|
139
|
+
)
|
|
121
140
|
fields = readonly_fields
|
|
141
|
+
list_filter = 'user_device__users',
|
|
122
142
|
|
|
123
143
|
def has_add_permission(self, request, obj=None):
|
|
124
144
|
return False
|
|
@@ -142,7 +162,7 @@ class UserDeviceAdmin(admin.ModelAdmin):
|
|
|
142
162
|
readonly_fields = (
|
|
143
163
|
'users_display', 'token', 'os', 'last_seen',
|
|
144
164
|
)
|
|
145
|
-
fields = readonly_fields + ('
|
|
165
|
+
fields = readonly_fields + ('is_primary', )
|
|
146
166
|
|
|
147
167
|
def get_queryset(self, request):
|
|
148
168
|
qs = super().get_queryset(request)
|
simo/users/api.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import sys
|
|
2
|
+
import datetime
|
|
2
3
|
from django.db.models import Q
|
|
3
4
|
from rest_framework import viewsets, mixins, status
|
|
4
5
|
from rest_framework.serializers import Serializer
|
|
@@ -8,6 +9,7 @@ from rest_framework.exceptions import ValidationError, PermissionDenied
|
|
|
8
9
|
from django.contrib.gis.geos import Point
|
|
9
10
|
from django.utils import timezone
|
|
10
11
|
from django_filters.rest_framework import DjangoFilterBackend
|
|
12
|
+
from simo.conf import dynamic_settings
|
|
11
13
|
from simo.core.api import InstanceMixin
|
|
12
14
|
from .models import (
|
|
13
15
|
User, UserDevice, UserDeviceReportLog, PermissionsRole, InstanceInvitation,
|
|
@@ -172,7 +174,7 @@ class UserDeviceReport(InstanceMixin, viewsets.GenericViewSet):
|
|
|
172
174
|
|
|
173
175
|
@action(url_path='device-report', detail=False, methods=['post'])
|
|
174
176
|
def report(self, request, *args, **kwargs):
|
|
175
|
-
|
|
177
|
+
from simo.generic.scripting.helpers import haversine_distance
|
|
176
178
|
if not request.data.get('device_token'):
|
|
177
179
|
return RESTResponse(
|
|
178
180
|
{'status': 'error', 'msg': 'device_token - not provided'},
|
|
@@ -199,6 +201,11 @@ class UserDeviceReport(InstanceMixin, viewsets.GenericViewSet):
|
|
|
199
201
|
except:
|
|
200
202
|
location = None
|
|
201
203
|
|
|
204
|
+
relay = None
|
|
205
|
+
if request.META.get('HTTP_HOST', '').endswith('.simo.io'):
|
|
206
|
+
relay = request.META.get('HTTP_HOST')
|
|
207
|
+
|
|
208
|
+
|
|
202
209
|
user_device.last_seen = timezone.now()
|
|
203
210
|
if location:
|
|
204
211
|
user_device.last_seen_location = ','.join(
|
|
@@ -213,23 +220,39 @@ class UserDeviceReport(InstanceMixin, viewsets.GenericViewSet):
|
|
|
213
220
|
user_device.save()
|
|
214
221
|
|
|
215
222
|
for iu in request.user.instance_roles.filter(is_active=True):
|
|
223
|
+
if location:
|
|
224
|
+
iu.at_home = haversine_distance(
|
|
225
|
+
iu.instance.location, user_device.last_seen_location
|
|
226
|
+
) < dynamic_settings['users__at_home_radius']
|
|
227
|
+
elif not relay:
|
|
228
|
+
iu.at_home = True
|
|
229
|
+
speed_kmh = 0
|
|
230
|
+
if user_device.last_seen_location and iu.last_seen_location \
|
|
231
|
+
and iu.last_seen > timezone.now() - datetime.timedelta(seconds=30):
|
|
232
|
+
if user_device.last_seen_location == iu.last_seen_location:
|
|
233
|
+
speed_kmh = iu.last_seen_speed_kmh
|
|
234
|
+
else:
|
|
235
|
+
seconds_passed = (timezone.now() - user_device.last_seen).seconds
|
|
236
|
+
if not seconds_passed:
|
|
237
|
+
speed_kmh = 0
|
|
238
|
+
else:
|
|
239
|
+
moved_distance = haversine_distance(
|
|
240
|
+
iu.last_seen_location, user_device.last_seen_location
|
|
241
|
+
)
|
|
242
|
+
speed_mps = moved_distance / seconds_passed
|
|
243
|
+
speed_kmh = speed_mps * 3.6
|
|
244
|
+
iu.last_seen = user_device.last_seen
|
|
216
245
|
iu.last_seen_location = user_device.last_seen_location
|
|
217
|
-
iu.
|
|
246
|
+
iu.last_seen_speed_kmh = speed_kmh
|
|
247
|
+
iu.phone_on_charge = request.data.get('is_charging', False)
|
|
218
248
|
iu.save()
|
|
219
249
|
|
|
220
|
-
request.user.last_seen_location = user_device.last_seen_location
|
|
221
|
-
request.user.last_seen_location_datetime = user_device.last_seen
|
|
222
|
-
request.user.save()
|
|
223
|
-
|
|
224
|
-
relay = None
|
|
225
|
-
if request.META.get('HTTP_HOST', '').endswith('.simo.io'):
|
|
226
|
-
relay = request.META.get('HTTP_HOST')
|
|
227
|
-
|
|
228
250
|
UserDeviceReportLog.objects.create(
|
|
229
251
|
user_device=user_device, instance=self.instance,
|
|
230
252
|
app_open=request.data.get('app_open', False),
|
|
231
253
|
location=','.join([str(i) for i in location]) if location else None,
|
|
232
|
-
relay=relay
|
|
254
|
+
relay=relay, speed_kmh=speed_kmh,
|
|
255
|
+
phone_on_charge=request.data.get('is_charging', False)
|
|
233
256
|
)
|
|
234
257
|
|
|
235
258
|
return RESTResponse({'status': 'success'})
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Generated by Django 4.2.10 on 2024-10-29 07:38
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
('users', '0034_instanceuser_last_seen_location_and_more'),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.AddField(
|
|
14
|
+
model_name='instanceuser',
|
|
15
|
+
name='last_seen_speed_kmh',
|
|
16
|
+
field=models.FloatField(default=0),
|
|
17
|
+
),
|
|
18
|
+
migrations.AddField(
|
|
19
|
+
model_name='user',
|
|
20
|
+
name='last_seen_speed_kmh',
|
|
21
|
+
field=models.FloatField(default=0),
|
|
22
|
+
),
|
|
23
|
+
]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Generated by Django 4.2.10 on 2024-10-29 12:58
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
('users', '0035_instanceuser_last_seen_speed_kmh_and_more'),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.AddField(
|
|
14
|
+
model_name='instanceuser',
|
|
15
|
+
name='phone_on_charge',
|
|
16
|
+
field=models.BooleanField(db_index=True, default=False),
|
|
17
|
+
),
|
|
18
|
+
migrations.AddField(
|
|
19
|
+
model_name='user',
|
|
20
|
+
name='phone_on_charge',
|
|
21
|
+
field=models.BooleanField(db_index=True, default=False),
|
|
22
|
+
),
|
|
23
|
+
]
|