simo 2.5.2__py3-none-any.whl → 2.5.4__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 (48) 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__/signal_receivers.cpython-38.pyc +0 -0
  5. simo/core/__pycache__/tasks.cpython-38.pyc +0 -0
  6. simo/core/app_widgets.py +19 -1
  7. simo/core/base_types.py +2 -0
  8. simo/core/controllers.py +157 -6
  9. simo/core/management/_hub_template/hub/supervisor.conf +4 -0
  10. simo/core/management/update.py +5 -3
  11. simo/core/signal_receivers.py +8 -5
  12. simo/core/tasks.py +1 -1
  13. simo/fleet/__pycache__/controllers.cpython-38.pyc +0 -0
  14. simo/fleet/__pycache__/forms.cpython-38.pyc +0 -0
  15. simo/fleet/controllers.py +20 -119
  16. simo/fleet/forms.py +101 -0
  17. simo/generic/__pycache__/app_widgets.cpython-38.pyc +0 -0
  18. simo/generic/__pycache__/base_types.cpython-38.pyc +0 -0
  19. simo/generic/__pycache__/controllers.cpython-38.pyc +0 -0
  20. simo/generic/__pycache__/forms.cpython-38.pyc +0 -0
  21. simo/generic/__pycache__/gateways.cpython-38.pyc +0 -0
  22. simo/generic/app_widgets.py +0 -18
  23. simo/generic/base_types.py +0 -2
  24. simo/generic/controllers.py +2 -262
  25. simo/generic/forms.py +0 -49
  26. simo/generic/gateways.py +8 -118
  27. simo/generic/scripting/__pycache__/helpers.cpython-38.pyc +0 -0
  28. simo/generic/scripting/example.py +67 -0
  29. simo/generic/scripting/helpers.py +66 -10
  30. simo/users/__pycache__/admin.cpython-38.pyc +0 -0
  31. simo/users/__pycache__/api.cpython-38.pyc +0 -0
  32. simo/users/__pycache__/models.cpython-38.pyc +0 -0
  33. simo/users/admin.py +25 -5
  34. simo/users/api.py +25 -11
  35. simo/users/migrations/0035_instanceuser_last_seen_speed_kmh_and_more.py +23 -0
  36. simo/users/migrations/0036_instanceuser_phone_on_charge_user_phone_on_charge.py +23 -0
  37. simo/users/migrations/0037_rename_last_seen_location_datetime_instanceuser_last_seen_and_more.py +53 -0
  38. simo/users/migrations/__pycache__/0035_instanceuser_last_seen_speed_kmh_and_more.cpython-38.pyc +0 -0
  39. simo/users/migrations/__pycache__/0036_instanceuser_phone_on_charge_user_phone_on_charge.cpython-38.pyc +0 -0
  40. simo/users/migrations/__pycache__/0037_rename_last_seen_location_datetime_instanceuser_last_seen_and_more.cpython-38.pyc +0 -0
  41. simo/users/models.py +12 -55
  42. {simo-2.5.2.dist-info → simo-2.5.4.dist-info}/METADATA +1 -1
  43. {simo-2.5.2.dist-info → simo-2.5.4.dist-info}/RECORD +47 -41
  44. {simo-2.5.2.dist-info → simo-2.5.4.dist-info}/WHEEL +1 -1
  45. simo/scripting.py +0 -39
  46. {simo-2.5.2.dist-info → simo-2.5.4.dist-info}/LICENSE.md +0 -0
  47. {simo-2.5.2.dist-info → simo-2.5.4.dist-info}/entry_points.txt +0 -0
  48. {simo-2.5.2.dist-info → simo-2.5.4.dist-info}/top_level.txt +0 -0
@@ -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
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
@@ -8,6 +8,7 @@ from rest_framework.exceptions import ValidationError, PermissionDenied
8
8
  from django.contrib.gis.geos import Point
9
9
  from django.utils import timezone
10
10
  from django_filters.rest_framework import DjangoFilterBackend
11
+ from simo.conf import dynamic_settings
11
12
  from simo.core.api import InstanceMixin
12
13
  from .models import (
13
14
  User, UserDevice, UserDeviceReportLog, PermissionsRole, InstanceInvitation,
@@ -172,7 +173,7 @@ class UserDeviceReport(InstanceMixin, viewsets.GenericViewSet):
172
173
 
173
174
  @action(url_path='device-report', detail=False, methods=['post'])
174
175
  def report(self, request, *args, **kwargs):
175
-
176
+ from simo.generic.scripting.helpers import haversine_distance
176
177
  if not request.data.get('device_token'):
177
178
  return RESTResponse(
178
179
  {'status': 'error', 'msg': 'device_token - not provided'},
@@ -199,12 +200,23 @@ class UserDeviceReport(InstanceMixin, viewsets.GenericViewSet):
199
200
  except:
200
201
  location = None
201
202
 
203
+ relay = None
204
+ if request.META.get('HTTP_HOST', '').endswith('.simo.io'):
205
+ relay = request.META.get('HTTP_HOST')
206
+
207
+
202
208
  user_device.last_seen = timezone.now()
203
209
  if location:
204
210
  user_device.last_seen_location = ','.join(
205
211
  [str(i) for i in location]
206
212
  ) if location else None
207
213
 
214
+ speed_mps = float(request.data.get('speed', 0))
215
+ if speed_mps < 0:
216
+ speed_mps = 0
217
+ speed_kmh = speed_mps * 3.6
218
+
219
+
208
220
  if request.data.get('app_open', False):
209
221
  user_device.is_primary = True
210
222
  UserDevice.objects.filter(
@@ -213,23 +225,25 @@ class UserDeviceReport(InstanceMixin, viewsets.GenericViewSet):
213
225
  user_device.save()
214
226
 
215
227
  for iu in request.user.instance_roles.filter(is_active=True):
228
+ if location:
229
+ iu.at_home = haversine_distance(
230
+ iu.instance.location, user_device.last_seen_location
231
+ ) < dynamic_settings['users__at_home_radius']
232
+ elif not relay:
233
+ iu.at_home = True
234
+
235
+ iu.last_seen = user_device.last_seen
216
236
  iu.last_seen_location = user_device.last_seen_location
217
- iu.last_seen_location_datetime = user_device.last_seen
237
+ iu.last_seen_speed_kmh = speed_kmh
238
+ iu.phone_on_charge = request.data.get('is_charging', False)
218
239
  iu.save()
219
240
 
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
241
  UserDeviceReportLog.objects.create(
229
242
  user_device=user_device, instance=self.instance,
230
243
  app_open=request.data.get('app_open', False),
231
244
  location=','.join([str(i) for i in location]) if location else None,
232
- relay=relay
245
+ relay=relay, speed_kmh=speed_kmh,
246
+ phone_on_charge=request.data.get('is_charging', False)
233
247
  )
234
248
 
235
249
  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
+ ]
@@ -0,0 +1,53 @@
1
+ # Generated by Django 4.2.10 on 2024-10-29 13:23
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('users', '0036_instanceuser_phone_on_charge_user_phone_on_charge'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.RenameField(
14
+ model_name='instanceuser',
15
+ old_name='last_seen_location_datetime',
16
+ new_name='last_seen',
17
+ ),
18
+ migrations.RemoveField(
19
+ model_name='user',
20
+ name='last_seen_location',
21
+ ),
22
+ migrations.RemoveField(
23
+ model_name='user',
24
+ name='last_seen_location_datetime',
25
+ ),
26
+ migrations.RemoveField(
27
+ model_name='user',
28
+ name='last_seen_speed_kmh',
29
+ ),
30
+ migrations.RemoveField(
31
+ model_name='user',
32
+ name='phone_on_charge',
33
+ ),
34
+ migrations.RemoveField(
35
+ model_name='userdevice',
36
+ name='last_seen_location',
37
+ ),
38
+ migrations.AddField(
39
+ model_name='userdevicereportlog',
40
+ name='phone_on_charge',
41
+ field=models.BooleanField(db_index=True, default=False),
42
+ ),
43
+ migrations.AddField(
44
+ model_name='userdevicereportlog',
45
+ name='speed_kmh',
46
+ field=models.FloatField(default=0),
47
+ ),
48
+ migrations.AlterField(
49
+ model_name='userdevice',
50
+ name='last_seen',
51
+ field=models.DateTimeField(auto_now=True, db_index=True),
52
+ ),
53
+ ]
simo/users/models.py CHANGED
@@ -97,17 +97,22 @@ class InstanceUser(DirtyFieldsMixin, models.Model, OnChangeMixin):
97
97
  related_name='instance_users',
98
98
  )
99
99
  role = models.ForeignKey(PermissionsRole, on_delete=models.CASCADE)
100
- at_home = models.BooleanField(default=False, db_index=True)
101
100
  is_active = models.BooleanField(default=True, db_index=True)
101
+
102
+ at_home = models.BooleanField(default=False, db_index=True)
103
+ last_seen = models.DateTimeField(
104
+ null=True, blank=True,
105
+ )
102
106
  last_seen_location = PlainLocationField(
103
107
  zoom=7, null=True, blank=True, help_text="Sent by user mobile app"
104
108
  )
105
- last_seen_location_datetime = models.DateTimeField(
106
- null=True, blank=True,
107
- )
109
+ last_seen_speed_kmh = models.FloatField(default=0)
110
+ phone_on_charge = models.BooleanField(default=False, db_index=True)
108
111
 
109
112
  objects = ActiveInstanceManager()
110
113
 
114
+ on_change_fields = ('last_seen',)
115
+
111
116
  class Meta:
112
117
  unique_together = 'user', 'instance'
113
118
 
@@ -175,12 +180,6 @@ class User(AbstractBaseUser, SimoAdminMixin):
175
180
  help_text="Will be placed in /root/.ssh/authorized_keys "
176
181
  "if user is active and is master of a hub."
177
182
  )
178
- last_seen_location = PlainLocationField(
179
- zoom=7, null=True, blank=True, help_text="Sent by user mobile app"
180
- )
181
- last_seen_location_datetime = models.DateTimeField(
182
- null=True, blank=True,
183
- )
184
183
  secret_key = models.CharField(
185
184
  max_length=20, db_index=True, default=get_random_string
186
185
  )
@@ -422,8 +421,7 @@ class UserDevice(models.Model, SimoAdminMixin):
422
421
  os = models.CharField(max_length=100, db_index=True)
423
422
  token = models.CharField(max_length=1000, db_index=True, unique=True)
424
423
  is_primary = models.BooleanField(default=True, db_index=True)
425
- last_seen = models.DateTimeField(auto_now_add=True, db_index=True)
426
- last_seen_location = PlainLocationField(zoom=7, null=True, blank=True)
424
+ last_seen = models.DateTimeField(auto_now=True, db_index=True)
427
425
 
428
426
  class Meta:
429
427
  ordering = '-last_seen',
@@ -445,54 +443,13 @@ class UserDeviceReportLog(models.Model):
445
443
  help_text="Sent via remote relay if specified, otherwise it's from LAN."
446
444
  )
447
445
  location = PlainLocationField(zoom=7, null=True, blank=True)
446
+ speed_kmh = models.FloatField(default=0)
447
+ phone_on_charge = models.BooleanField(default=False, db_index=True)
448
448
 
449
449
  class Meta:
450
450
  ordering = '-datetime',
451
451
 
452
452
 
453
- @receiver(post_save, sender=UserDeviceReportLog)
454
- def set_user_at_home(sender, instance, created, **kwargs):
455
- from simo.core.models import Instance
456
- if not created:
457
- return
458
-
459
- if not instance.location and not instance.relay:
460
- for item in InstanceUser.objects.filter(
461
- user__in=instance.user_device.users.all()
462
- ):
463
- item.at_home = True
464
- item.save()
465
- return
466
-
467
- for hub_instance in Instance.objects.filter(is_active=True):
468
- try:
469
- instance_location = Point(
470
- [float(hub_instance.location.split(',')[0]),
471
- float(hub_instance.location.split(',')[1])],
472
- srid=4326
473
- )
474
- except:
475
- return
476
- try:
477
- log_location = Point(
478
- [
479
- float(instance.location.split(',')[0]),
480
- float(instance.location.split(',')[1])
481
- ], srid=4326
482
- )
483
- except:
484
- return
485
- else:
486
- for item in InstanceUser.objects.filter(
487
- user__in=instance.user_device.users.all(),
488
- instance=hub_instance
489
- ):
490
- item.at_home = distance(
491
- instance_location, log_location
492
- ).meters < dynamic_settings['users__at_home_radius']
493
- item.save()
494
-
495
-
496
453
  class ComponentPermission(models.Model):
497
454
  role = models.ForeignKey(
498
455
  PermissionsRole, on_delete=models.CASCADE,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: simo
3
- Version: 2.5.2
3
+ Version: 2.5.4
4
4
  Summary: Smart Home on Steroids!
5
5
  Author-email: Simanas Venčkauskas <simanas@simo.io>
6
6
  Project-URL: Homepage, https://simo.io