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
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
- tz = pytz.timezone(self.component.zone.instance.timezone)
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 who are designed to be kept alive, but
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, Blinds, AlarmGroup, Gate
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
@@ -8,6 +8,7 @@ from simo.core.models import Instance, Component
8
8
  from simo.users.models import InstanceUser
9
9
 
10
10
 
11
+
11
12
  @receiver(post_save, sender=Component)
12
13
  def handle_alarm_groups(sender, instance, *args, **kwargs):
13
14
  if not instance.alarm_category:
@@ -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, instance=None):
9
- if not instance:
10
+ def __init__(self, location=None):
11
+ if not location:
10
12
  instance = Instance.objects.all().first()
11
- coordinates = instance.location.split(',')
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 is_night(self):
23
- if timezone.now() > self.get_sunset_time():
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 timezone.now() < self.get_sunrise_time():
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
- return (self.get_sunset_time() - timezone.now()).total_seconds()
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
- def seconds_to_sunrise(self):
33
- return (self.get_sunrise_time() - timezone.now()).total_seconds()
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
@@ -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
- return super().get_queryset(request).filter(
19
- instance__in=request.user.instances
20
- ).prefetch_related('to_users')
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
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 UserDeviceLogInline(admin.ModelAdmin):
130
+ class UserDeviceLog(admin.ModelAdmin):
118
131
  model = UserDeviceReportLog
119
- readonly_fields = 'datetime', 'app_open', 'location', 'relay', 'users'
120
- list_display = 'datetime', 'app_open', 'location', 'relay', 'users'
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 + ('last_seen_location', 'is_primary')
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.last_seen_location_datetime = user_device.last_seen
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
+ ]