simo 2.9.1__py3-none-any.whl → 2.10.1__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.

@@ -165,7 +165,7 @@ class GatesHandler:
165
165
  if iu_id != iuser.id:
166
166
  continue
167
167
  gate = Component.objects.get(id=gate_id)
168
- if is_out:
168
+ if is_out > 4:
169
169
  print(
170
170
  f"{iuser.user.name} is out, "
171
171
  f"let's see if we must open the gates for him"
@@ -174,17 +174,17 @@ class GatesHandler:
174
174
  # he is now coming back and open the gate for him
175
175
  if self._is_in_geofence(gate, iuser.last_seen_location):
176
176
  print("Yes he is back in a geofence! Open THE GATEEE!!")
177
- self.gate_iusers[gate_id][iuser.id] = False
178
- gate.open()
177
+ self.gate_iusers[gate_id][iuser.id] = 0
178
+ if iuser.last_seen_speed_kmh > 10:
179
+ gate.open()
179
180
  else:
180
181
  print("No he is not back yet.")
181
182
  else:
182
183
  print(f"Check if {iuser.user.name} is out.")
183
- self.gate_iusers[gate_id][iuser.id] = self._is_out_of_geofence(
184
- gate, iuser.last_seen_location
185
- )
186
- if self.gate_iusers[gate_id][iuser.id]:
187
- print(f"YES {iuser.user.name} is out!")
184
+ if self._is_out_of_geofence(gate, iuser.last_seen_location):
185
+ self.gate_iusers[gate_id][iuser.id] += 1
186
+ if self.gate_iusers[gate_id][iuser.id] > 4:
187
+ print(f"YES {iuser.user.name} is truly out!")
188
188
 
189
189
  def watch_gates(self):
190
190
  drop_current_instance()
@@ -204,9 +204,11 @@ class GatesHandler:
204
204
  self.gate_iusers[gate.id] = {}
205
205
  if iuser.id not in self.gate_iusers[gate.id]:
206
206
  if iuser.last_seen_location:
207
- self.gate_iusers[gate.id][iuser.id] = self._is_out_of_geofence(
207
+ self.gate_iusers[gate.id][iuser.id] = 0
208
+ if self._is_out_of_geofence(
208
209
  gate, iuser.last_seen_location
209
- )
210
+ ):
211
+ self.gate_iusers[gate.id][iuser.id] += 1
210
212
  iuser.on_change(self.check_gates)
211
213
 
212
214
 
Binary file
simo/core/controllers.py CHANGED
@@ -631,6 +631,15 @@ class Dimmer(ControllerBase, TimerMixin, OnOffPokerMixin):
631
631
  else:
632
632
  self.send(self.component.config.get('max', 90))
633
633
 
634
+ def max_out(self):
635
+ self.send(self.component.config.get('max', 90))
636
+
637
+ def output_percent(self, value):
638
+ min = self.component.config.get('min', 0)
639
+ max = self.component.config.get('max', 100)
640
+ delta = max - min
641
+ self.send(min + delta * value / 100)
642
+
634
643
  def toggle(self):
635
644
  self.component.refresh_from_db()
636
645
  if self.component.value:
@@ -845,21 +854,51 @@ class Switch(MultiSwitchBase, TimerMixin, OnOffPokerMixin):
845
854
  default_value = False
846
855
 
847
856
  def turn_on(self):
857
+ if self.component.meta.get('pulse'):
858
+ self.component.meta.pop('pulse')
859
+ self.component.save()
848
860
  self.send(True)
849
861
 
850
862
  def turn_off(self):
863
+ if self.component.meta.get('pulse'):
864
+ self.component.meta.pop('pulse')
865
+ self.component.save()
851
866
  self.send(False)
852
867
 
853
868
  def toggle(self):
869
+ if self.component.meta.get('pulse'):
870
+ self.component.meta.pop('pulse')
871
+ self.component.save()
854
872
  self.send(not self.component.value)
855
873
 
856
874
  def click(self):
857
875
  '''
858
876
  Gateway specific implementation is very welcome of this!
859
877
  '''
878
+ if self.component.meta.get('pulse'):
879
+ self.component.meta.pop('pulse')
880
+ self.component.save()
860
881
  self.turn_on()
861
- time.sleep(0.5)
862
- self.turn_off()
882
+ from .tasks import component_action
883
+ component_action.s(
884
+ self.component.id, 'turn_off'
885
+ ).apply_async(countdown=1)
886
+
887
+ def pulse(self, frame_length_s, on_percentage):
888
+ self.component.meta['pulse'] = {
889
+ 'frame': frame_length_s, 'duty': on_percentage / 100
890
+ }
891
+ self.component.save()
892
+ from simo.generic.gateways import GenericGatewayHandler
893
+ from .models import Gateway
894
+ generic_gateway = Gateway.objects.filter(
895
+ type=GenericGatewayHandler.uid
896
+ ).first()
897
+ if generic_gateway:
898
+ GatewayObjectCommand(
899
+ generic_gateway, self.component,
900
+ pulse=self.component.meta['pulse']
901
+ ).publish()
863
902
 
864
903
 
865
904
  class DoubleSwitch(MultiSwitchBase):
simo/core/tasks.py CHANGED
@@ -16,11 +16,18 @@ from django.utils import timezone
16
16
  from actstream.models import Action
17
17
  from simo.conf import dynamic_settings
18
18
  from simo.core.utils.helpers import get_self_ip
19
- from simo.core.middleware import introduce_instance
19
+ from simo.core.middleware import introduce_instance, drop_current_instance
20
20
  from simo.users.models import PermissionsRole, InstanceUser
21
21
  from .models import Instance, Component, ComponentHistory, HistoryAggregate
22
22
 
23
23
 
24
+ @celery_app.task
25
+ def component_action(comp_id, method, args=None, kwargs=None):
26
+ drop_current_instance()
27
+ component = Component.objects.get(id=comp_id)
28
+ getattr(component, method)(*args, **kwargs)
29
+
30
+
24
31
  @celery_app.task
25
32
  def supervisor_restart():
26
33
  time.sleep(2)
@@ -69,11 +69,16 @@ class Thermostat(ControllerBase):
69
69
 
70
70
  @property
71
71
  def default_config(self):
72
- min = 3
73
- max = 100
72
+ instance = get_current_instance()
73
+ min = 4
74
+ max = 36
75
+ if instance and instance.units_of_measure == 'imperial':
76
+ min = 40
77
+ max = 95
74
78
  return {
75
79
  'temperature_sensor': 0, 'heater': 0, 'cooler': 0,
76
- 'reaction_difference': 0, 'min': min, 'max': max,
80
+ 'engagement': 'dynamic','reaction_difference': 2,
81
+ 'min': min, 'max': max,
77
82
  'has_real_feel': False,
78
83
  'user_config': config_to_dict(self._get_default_user_config())
79
84
  }
@@ -161,12 +166,12 @@ class Thermostat(ControllerBase):
161
166
  temperature_sensor = Component.objects.filter(
162
167
  pk=self.component.config.get('temperature_sensor')
163
168
  ).first()
164
- heater = Component.objects.filter(
165
- pk=self.component.config.get('heater')
166
- ).first()
167
- cooler = Component.objects.filter(
168
- pk=self.component.config.get('cooler')
169
- ).first()
169
+ heaters = Component.objects.filter(
170
+ pk__in=self.component.config.get('heater')
171
+ )
172
+ coolers = Component.objects.filter(
173
+ pk__in=self.component.config.get('cooler')
174
+ )
170
175
 
171
176
  if not temperature_sensor or not temperature_sensor.alive:
172
177
  self.component.error_msg = "No temperature sensor"
@@ -187,50 +192,100 @@ class Thermostat(ControllerBase):
187
192
  target_temp = self.get_current_target_temperature()
188
193
  mode = self.component.config['user_config'].get('mode', 'auto')
189
194
 
195
+ low = target_temp - self.component.config['reaction_difference']
196
+ high = target_temp + self.component.config['reaction_difference']
197
+
198
+ heating = False
199
+ cooling = False
200
+
201
+ if self.component.config.get('engagement', 'static'):
202
+ if heaters:
203
+ for heater in heaters:
204
+ if current_temp < low:
205
+ if heater.base_type == 'dimmer':
206
+ heater.max_out()
207
+ else:
208
+ heater.turn_on()
209
+ heating = True
210
+ elif current_temp > high:
211
+ heater.turn_off()
212
+ heating = False
213
+ else:
214
+ if heater.value:
215
+ heating = True
216
+ break
217
+
218
+ if coolers:
219
+ for cooler in coolers:
220
+ if heating: # Do not cool if heating!
221
+ cooler.turn_off()
222
+ else:
223
+ if current_temp > high:
224
+ if heater.base_type == 'dimmer':
225
+ cooler.max_out()
226
+ else:
227
+ cooler.turn_on()
228
+ cooling = True
229
+ elif current_temp < low:
230
+ if cooler.value:
231
+ cooler.turn_off()
232
+ cooling = False
233
+ else:
234
+ if cooler.value:
235
+ cooling = True
236
+ break
237
+
238
+ else:
239
+ window = high - low
240
+ if heaters:
241
+ reach = high - current_temp
242
+ reaction_force = self._get_reaction_force(window, reach)
243
+ if reaction_force:
244
+ heating = True
245
+ self._engage_devices(heaters, reaction_force)
246
+ if coolers:
247
+ if heating: # Do not cool if heating!
248
+ reaction_force = 0
249
+ else:
250
+ reach = current_temp - low
251
+ reaction_force = self._get_reaction_force(window, reach)
252
+ self._engage_devices(coolers, reaction_force)
253
+
190
254
  self.component.set({
191
255
  'mode': mode,
192
256
  'current_temp': current_temp,
193
257
  'target_temp': target_temp,
194
- 'heating': False, 'cooling': False
258
+ 'heating': heating, 'cooling': cooling
195
259
  }, actor=get_system_user())
196
260
 
197
- low = target_temp - self.component.config['reaction_difference'] / 2
198
- high = target_temp + self.component.config['reaction_difference'] / 2
199
-
200
- if mode in ('auto', 'heater'):
201
- if (not heater or not heater.alive) and mode == 'heater':
202
- self.component.error_msg = "No heater"
203
- self.component.alive = False
204
- self.component.save()
205
- return
206
- if current_temp < low:
207
- if not heater.value:
208
- heater.turn_on()
209
- self.component.value['heating'] = True
210
- elif current_temp > high:
211
- if heater.value:
212
- heater.turn_off()
213
- self.component.value['heating'] = False
214
- if mode in ('auto', 'cooler') and cooler:
215
- if not cooler or not cooler.alive:
216
- if mode == 'cooler' or (not heater or not heater.alive):
217
- print(f"No cooler or heater on {self.component}!")
218
- self.component.alive = False
219
- self.component.save()
220
- return
221
- if current_temp > high:
222
- if not cooler.value:
223
- cooler.turn_on()
224
- self.component.value['cooling'] = True
225
- elif current_temp < low:
226
- if cooler.value:
227
- cooler.turn_off()
228
- self.component.value['cooling'] = False
229
-
230
261
  self.component.error_msg = None
231
262
  self.component.alive = True
232
263
  self.component.save()
233
264
 
265
+
266
+ def _get_reaction_force(self, window, reach):
267
+ if reach > window:
268
+ reaction_force = 100
269
+ elif reach <= 0:
270
+ reaction_force = 0
271
+ else:
272
+ reaction_force = reach / window * 100
273
+ return reaction_force
274
+
275
+
276
+ def _engage_devices(self, devices, reaction_force):
277
+ for device in devices:
278
+ if device.base_type == 'dimmer':
279
+ device.output_percent(reaction_force)
280
+ elif device.base_type == 'switch':
281
+ if reaction_force == 100:
282
+ device.turn_on()
283
+ elif reaction_force == 0:
284
+ device.turn_off()
285
+ else:
286
+ device.pulse(30, reaction_force)
287
+
288
+
234
289
  def update_user_conf(self, new_conf):
235
290
  self.component.refresh_from_db()
236
291
  self.component.config['user_config'] = validate_new_conf(
@@ -241,6 +296,7 @@ class Thermostat(ControllerBase):
241
296
  self.component.save()
242
297
  self.evaluate()
243
298
 
299
+
244
300
  def hold(self, temperature=None):
245
301
  if temperature != None:
246
302
  self.component.config['user_config']['hard'] = {
simo/generic/forms.py CHANGED
@@ -90,33 +90,38 @@ class ThermostatConfigForm(BaseComponentForm):
90
90
  )
91
91
  )
92
92
  # TODO: support for multiple heaters
93
- heater = Select2ModelChoiceField(
93
+ heaters = Select2ModelMultipleChoiceField(
94
94
  queryset=Component.objects.filter(base_type=Switch.base_type),
95
95
  required=False,
96
96
  url='autocomplete-component',
97
97
  forward=(
98
98
  forward.Const([
99
- Switch.base_type,
99
+ Switch.base_type, Dimmer.base_type
100
100
  ], 'base_type'),
101
101
  )
102
102
  )
103
103
  # TODO: support for multiple coolers
104
- cooler = Select2ModelChoiceField(
104
+ coolers = Select2ModelMultipleChoiceField(
105
105
  queryset=Component.objects.filter(base_type=Switch.base_type),
106
106
  required=False,
107
107
  url='autocomplete-component',
108
108
  forward=(
109
109
  forward.Const([
110
- Switch.base_type,
110
+ Switch.base_type, Dimmer.base_type
111
111
  ], 'base_type'),
112
112
  )
113
113
 
114
114
  )
115
- mode = forms.ChoiceField(
116
- choices=(('heater', "Heater"), ('cooler', "Cooler"), ('auto', "Auto"),),
117
- initial='heater'
115
+ engagement = forms.ChoiceField(
116
+ choices=(('dynamic', "Dynamic"), ('static', "Static")),
117
+ initial='dynamic',
118
+ help_text="Dynamic - scales engagement intensity within reaction window <br>"
119
+ "Static - engages/disengages fully within reaction window <br>"
120
+ )
121
+ reaction_difference = forms.FloatField(
122
+ initial=2, min_value=0, max_value=50,
123
+ help_text="Reaction window = target temp +- reaction difference."
118
124
  )
119
- reaction_difference = forms.FloatField(initial=0.5)
120
125
  min = forms.IntegerField(initial=3)
121
126
  max = forms.IntegerField(initial=36)
122
127
  use_real_feel = forms.BooleanField(
@@ -126,8 +131,6 @@ class ThermostatConfigForm(BaseComponentForm):
126
131
  def __init__(self, *args, **kwargs):
127
132
  super().__init__(*args, **kwargs)
128
133
  if self.instance.pk:
129
- self.fields['mode'].initial = \
130
- self.instance.config['user_config']['mode']
131
134
  temperature_sensor = Component.objects.filter(
132
135
  pk=self.instance.config.get('temperature_sensor', 0)
133
136
  ).first()
simo/generic/gateways.py CHANGED
@@ -195,7 +195,7 @@ class GenericGatewayHandler(
195
195
  self.sensors_on_watch = {}
196
196
  self.sleep_is_on = {}
197
197
  self.last_set_state = None
198
-
198
+ self.pulsing_switches = {}
199
199
 
200
200
 
201
201
  def watch_thermostats(self):
@@ -208,6 +208,7 @@ class GenericGatewayHandler(
208
208
  timezone.activate(tz)
209
209
  thermostat.evaluate()
210
210
 
211
+
211
212
  def watch_alarm_clocks(self):
212
213
  from .controllers import AlarmClock
213
214
  drop_current_instance()
@@ -218,6 +219,7 @@ class GenericGatewayHandler(
218
219
  timezone.activate(tz)
219
220
  alarm_clock.tick()
220
221
 
222
+
221
223
  def watch_watering(self):
222
224
  drop_current_instance()
223
225
  from .controllers import Watering
@@ -231,13 +233,15 @@ class GenericGatewayHandler(
231
233
  else:
232
234
  watering.controller._perform_schedule()
233
235
 
236
+
234
237
  def run(self, exit):
235
238
  drop_current_instance()
236
239
  self.exit = exit
237
240
  self.logger = get_gw_logger(self.gateway_instance.id)
238
241
  for task, period in self.periodic_tasks:
239
242
  threading.Thread(
240
- target=self._run_periodic_task, args=(exit, task, period), daemon=True
243
+ target=self._run_periodic_task, args=(exit, task, period),
244
+ daemon=True
241
245
  ).start()
242
246
 
243
247
  from simo.generic.controllers import IPCamera
@@ -254,6 +258,11 @@ class GenericGatewayHandler(
254
258
  cam_watch = CameraWatcher(cam.id, exit)
255
259
  cam_watch.start()
256
260
 
261
+ threading.Thread(
262
+ target=self.watch_switch_pulses, args=(exit,),
263
+ daemon=True
264
+ ).start()
265
+
257
266
  print("GATEWAY STARTED!")
258
267
  while not exit.is_set():
259
268
  mqtt_client.loop()
@@ -278,6 +287,8 @@ class GenericGatewayHandler(
278
287
  self.control_alarm_group(component, payload.get('set_val'))
279
288
  # elif component.controller_uid == AudioAlert.uid:
280
289
  # self.control_audio_alert(component, payload.get('set_val'))
290
+ elif payload.get('pulse'):
291
+ self.start_pulse(component, payload['pulse'])
281
292
  else:
282
293
  component.controller.set(payload.get('set_val'))
283
294
  except Exception:
@@ -416,6 +427,84 @@ class GenericGatewayHandler(
416
427
  ] = time.time()
417
428
 
418
429
 
430
+ def watch_switch_pulses(self, exit):
431
+ for comp in Component.objects.filter(
432
+ base_type='switch', meta__has_key='pulse'
433
+ ):
434
+ comp.send(True)
435
+ self.pulsing_switches[comp.id] = {
436
+ 'comp': comp, 'last_toggle': time.time(), 'value': True,
437
+ 'pulse': comp.meta['pulse']
438
+ }
439
+
440
+ step = 0
441
+ while not exit.is_set():
442
+ time.sleep(0.1)
443
+ step += 1
444
+ remove_switches = []
445
+ for id, data in self.pulsing_switches.items():
446
+ on_interval = data['pulse']['frame'] * data['pulse']['duty']
447
+ off_interval = data['pulse']['frame'] - on_interval
448
+
449
+ if (
450
+ data['value'] and
451
+ time.time() - data['last_toggle'] > on_interval
452
+ ) or (
453
+ not data['value'] and
454
+ time.time() - data['last_toggle'] > off_interval
455
+ ):
456
+ data['comp'].refresh_from_db()
457
+ if not data['comp'].meta.get('pulse'):
458
+ remove_switches.append(id)
459
+ continue
460
+ if data['pulse'] != data['comp'].meta['pulse']:
461
+ self.pulsing_switches[id]['pulse'] = data['comp'].meta['pulse']
462
+ continue
463
+
464
+ if data['value']:
465
+ if time.time() - data['last_toggle'] > on_interval:
466
+ data['comp'].send(False)
467
+ self.pulsing_switches[id]['last_toggle'] = time.time()
468
+ self.pulsing_switches[id]['value'] = False
469
+ else:
470
+ if time.time() - data['last_toggle'] > off_interval:
471
+ data['comp'].send(True)
472
+ self.pulsing_switches[id]['last_toggle'] = time.time()
473
+ self.pulsing_switches[id]['value'] = True
474
+
475
+ for id in remove_switches:
476
+ del self.pulsing_switches[id]
477
+
478
+ # Update with db every 5s just in case something is missed.
479
+ if step < 50:
480
+ continue
481
+ step = 0
482
+
483
+ remove_switches = set(self.pulsing_switches.keys())
484
+ for comp in Component.objects.filter(
485
+ base_type='switch', meta__has_key='pulse'
486
+ ):
487
+ if comp.id in remove_switches:
488
+ remove_switches.remove(comp.id)
489
+ self.pulsing_switches[comp.id]['pulse'] = comp.meta['pulse']
490
+ continue
491
+ comp.send(True)
492
+ self.pulsing_switches[comp.id] = {
493
+ 'comp': comp, 'last_toggle': time.time(), 'value': True,
494
+ 'pulse': comp.meta['pulse']
495
+ }
496
+ for id in remove_switches:
497
+ del self.pulsing_switches[id]
498
+
499
+
500
+ def start_pulse(self, comp, pulse):
501
+ comp.send(True)
502
+ self.pulsing_switches[comp.id] = {
503
+ 'comp': comp, 'last_toggle': time.time(), 'value': True,
504
+ 'pulse': pulse
505
+ }
506
+
507
+
419
508
 
420
509
  class DummyGatewayHandler(BaseObjectCommandsGatewayHandler):
421
510
  name = "Dummy"
@@ -0,0 +1,33 @@
1
+ # Generated by Django 4.2.10 on 2025-04-09 14:04
2
+
3
+ from django.db import migrations
4
+
5
+ def forwards_func(apps, schema_editor):
6
+
7
+ Component = apps.get_model("core", "Component")
8
+
9
+ for thermostat in Component.objects.filter(base_type='thermostat'):
10
+ thermostat.config['heaters'] = []
11
+ heater = thermostat.config.pop('heater')
12
+ if heater:
13
+ thermostat.config['heaters'].append(heater)
14
+ thermostat.config['coolers'] = []
15
+ cooler = thermostat.config.pop('cooler')
16
+ if cooler:
17
+ thermostat.config['coolers'].append(cooler)
18
+ thermostat.save()
19
+
20
+
21
+ def reverse_func(apps, schema_editor):
22
+ pass
23
+
24
+
25
+ class Migration(migrations.Migration):
26
+
27
+ dependencies = [
28
+ ('generic', '0002_auto_20241126_0726'),
29
+ ]
30
+
31
+ operations = [
32
+ migrations.RunPython(forwards_func, reverse_func, elidable=True),
33
+ ]
Binary file
simo/users/api.py CHANGED
@@ -194,10 +194,6 @@ class UserDeviceReport(InstanceMixin, viewsets.GenericViewSet):
194
194
  user_device.users.add(request.user)
195
195
 
196
196
  log_datetime = timezone.now()
197
- # if request.data.get('timestamp'):
198
- # log_datetime = datetime.datetime.fromtimestamp(
199
- # int(float(request.GET['timestamp'])), pytz.utc
200
- # )
201
197
 
202
198
  relay = None
203
199
  if request.META.get('HTTP_HOST', '').endswith('.simo.io'):
@@ -208,22 +204,26 @@ class UserDeviceReport(InstanceMixin, viewsets.GenericViewSet):
208
204
  except:
209
205
  speed_kmh = 0
210
206
 
207
+ if speed_kmh < 0:
208
+ speed_kmh = 0
209
+
211
210
  avg_speed_kmh = 0
212
211
 
213
212
  if relay:
214
213
  location = request.data.get('location')
215
214
  if 'null' in location:
216
215
  location = None
217
- prev_log = UserDeviceReportLog.objects.filter(
216
+ sum = 0
217
+ no_of_points = 0
218
+ for speed in UserDeviceReportLog.objects.filter(
218
219
  user_device=user_device, instance=self.instance,
219
220
  datetime__lt=log_datetime - datetime.timedelta(seconds=3),
220
- datetime__gt=log_datetime - datetime.timedelta(seconds=10),
221
+ datetime__gt=log_datetime - datetime.timedelta(seconds=30),
221
222
  at_home=False, location__isnull=False
222
- ).last()
223
- if prev_log:
224
- meters_traveled = haversine_distance(location, prev_log.location)
225
- seconds_passed = (log_datetime - prev_log.datetime).total_seconds()
226
- avg_speed_kmh = round(meters_traveled / seconds_passed * 3.6, 0)
223
+ ).values('speed_kmh',):
224
+ sum += speed[0]
225
+ sum += speed_kmh
226
+ avg_speed_kmh = round(sum / (no_of_points + 1))
227
227
  else:
228
228
  location = self.instance.location
229
229
 
@@ -248,7 +248,30 @@ class UserDeviceReport(InstanceMixin, viewsets.GenericViewSet):
248
248
  self.instance.location, location
249
249
  ) < dynamic_settings['users__at_home_radius']
250
250
 
251
+ app_open = request.data.get('app_open', False)
252
+
253
+ if self.reject_location_report(
254
+ log_datetime, user_device, location, app_open, relay,
255
+ phone_on_charge, at_home, speed_kmh
256
+ ):
257
+ # We respond with success status, so that the device dos not try to
258
+ # report this data point again.
259
+ return RESTResponse({'status': 'success'})
260
+ # return RESTResponse(
261
+ # {'status': 'error', 'msg': 'Duplicate or Bad report!'},
262
+ # status=status.HTTP_400_BAD_REQUEST
263
+ # )
264
+
265
+ UserDeviceReportLog.objects.create(
266
+ user_device=user_device, instance=self.instance,
267
+ app_open=app_open,
268
+ location=location, datetime=log_datetime,
269
+ relay=relay, speed_kmh=speed_kmh, avg_speed_kmh=avg_speed_kmh,
270
+ phone_on_charge=phone_on_charge, at_home=at_home
271
+ )
272
+
251
273
  drop_current_instance()
274
+
252
275
  for iu in request.user.instance_roles.filter(is_active=True):
253
276
  if not relay:
254
277
  iu.at_home = True
@@ -260,19 +283,49 @@ class UserDeviceReport(InstanceMixin, viewsets.GenericViewSet):
260
283
  iu.last_seen = user_device.last_seen
261
284
  if location:
262
285
  iu.last_seen_location = location
263
- iu.last_seen_speed_kmh = speed_kmh
286
+ iu.last_seen_speed_kmh = avg_speed_kmh
264
287
  iu.phone_on_charge = phone_on_charge
265
288
  iu.save()
266
289
 
267
- UserDeviceReportLog.objects.create(
290
+ return RESTResponse({'status': 'success'})
291
+
292
+
293
+ def reject_location_report(
294
+ self, log_datetime, user_device, location, app_open, relay,
295
+ phone_on_charge, at_home, speed_kmh
296
+ ):
297
+ # Phone's App location repoorting is not always as reliable as we would like to
298
+ # therefore we filter out duplicate reports as they happen quiet often
299
+ # It has been observer that sometimes an app reports locations that are
300
+ # way from past, therefore locations might jump out of the usual pattern,
301
+ # so we try to filter out these anomalies to.
302
+ q = UserDeviceReportLog.objects.filter(
268
303
  user_device=user_device, instance=self.instance,
269
- app_open=request.data.get('app_open', False),
270
- location=location, datetime=log_datetime,
271
- relay=relay, speed_kmh=speed_kmh, avg_speed_kmh=avg_speed_kmh,
272
- phone_on_charge=phone_on_charge, at_home=at_home
304
+ app_open=app_open,
305
+ datetime__gt=log_datetime - datetime.timedelta(seconds=20),
306
+ relay=relay, phone_on_charge=phone_on_charge, at_home=at_home
273
307
  )
308
+ if location:
309
+ q = q.filter(location__isnull=False)
310
+ last_similar_report = q.last()
311
+
312
+ if not last_similar_report:
313
+ return False
314
+
315
+ if location == last_similar_report.location:
316
+ # This looks like 100% duplicate
317
+ return True
318
+
319
+ from simo.automation.helpers import haversine_distance
320
+ distance = haversine_distance(location, last_similar_report.location)
321
+ seconds_passed = (
322
+ log_datetime - last_similar_report.datetime
323
+ ).total_seconds()
324
+ if speed_kmh < 100 and distance / seconds_passed * 3.6 > 300:
325
+ return True
326
+
327
+ return False
274
328
 
275
- return RESTResponse({'status': 'success'})
276
329
 
277
330
 
278
331
  class InvitationsViewSet(InstanceMixin, viewsets.ModelViewSet):
@@ -12,7 +12,7 @@ users = Section('users')
12
12
  class AtHomeRadius(IntegerPreference):
13
13
  section = users
14
14
  name = 'at_home_radius'
15
- default = 250
15
+ default = 50
16
16
  required = True
17
17
  help_text = 'Distance in meters around hub location point that is ' \
18
18
  'considered as At Home.'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simo
3
- Version: 2.9.1
3
+ Version: 2.10.1
4
4
  Summary: Smart Home Supremacy
5
5
  Author-email: Simanas Venčkauskas <simanas@simo.io>
6
6
  Project-URL: Homepage, https://simo.io
@@ -21,7 +21,7 @@ simo/automation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
21
  simo/automation/app_widgets.py,sha256=gaqImMZjuMHm7nIb9a4D-Y3qipz_WhSPAHXcwGx4Uzs,199
22
22
  simo/automation/controllers.py,sha256=66aGjlfgigcePXiLmDkbVrxbm-Z26klMKIiOvaMH71k,11545
23
23
  simo/automation/forms.py,sha256=jcmiq7A-S5WwoIY3YD7GmWomHXXJAipTawWcgOavuRM,10239
24
- simo/automation/gateways.py,sha256=8qgBDhkM_xYGmeEbnH5WXs5m3ihMp8Yzx7yj6XG8DNM,16382
24
+ simo/automation/gateways.py,sha256=GH8StZslRvZvWGz6_q8IjQzQg5zSmIZ7_mALbbKm5OY,16539
25
25
  simo/automation/helpers.py,sha256=iP-fxxB8HsFQy3k2CjFubu86aMqvWgmh-p24DiyOrek,4330
26
26
  simo/automation/models.py,sha256=zt-jkzyq5ddqGT864OkJzCsvov2vZ0nO4ez3hAeZkXg,934
27
27
  simo/automation/serializers.py,sha256=Pg-hMaASQPB5_BTAMkfqM6z4jdHWH8xMYWOvDxIvmx8,2126
@@ -34,7 +34,7 @@ simo/automation/__pycache__/controllers.cpython-312.pyc,sha256=QV63g3UlpMAA-yCaZ
34
34
  simo/automation/__pycache__/controllers.cpython-38.pyc,sha256=CL-0Tq9B4-E36fYfWT1XEBTq1dkq1W8003f6MrBnQU0,8391
35
35
  simo/automation/__pycache__/forms.cpython-312.pyc,sha256=kmw04yujs7dVHNHvzr9Q8gXc0e6BAZHg_URPXjLbrfU,12419
36
36
  simo/automation/__pycache__/forms.cpython-38.pyc,sha256=cpA5hA2Iz3JsPC0Dq01ki1I7S9c5DKRcXveHApI1dJo,7772
37
- simo/automation/__pycache__/gateways.cpython-312.pyc,sha256=mQWn7s8qqF-CIw9Zn80RbbaFJjTiCdaN_grMsfwa-nY,22405
37
+ simo/automation/__pycache__/gateways.cpython-312.pyc,sha256=SvCnrrJGw727sredFOl3JEeljBcNrno3e5Yqdai6xI8,22686
38
38
  simo/automation/__pycache__/gateways.cpython-38.pyc,sha256=nHujqChMCqqxHbZezP3MisavjKDhczqzFGurO10h-lc,11113
39
39
  simo/automation/__pycache__/helpers.cpython-312.pyc,sha256=v6abo_h8qfWD3isbJvTO3X9sfUREJxTlc_P1ZhN8ZCs,5853
40
40
  simo/automation/__pycache__/helpers.cpython-38.pyc,sha256=fNjSyn4Mfq7-JQx-bdsnj-rSxgu20dVJ9-5ZEMT6yiM,3627
@@ -98,7 +98,7 @@ simo/core/auto_urls.py,sha256=fM9Tqzt0OfJ2FNnePGp7LcbJAWzgEwaNAJy7FNXHY-o,1299
98
98
  simo/core/autocomplete_views.py,sha256=x3MKOZvXYS3xVQ-V1S7Liv_U5bxr-uc0gePa85wv5nA,4561
99
99
  simo/core/base_types.py,sha256=WypW8hTfzveuTQtruGjLYAGQZIuczxTlW-SdRk3iQug,666
100
100
  simo/core/context.py,sha256=LKw1I4iIRnlnzoTCuSLLqDX7crHdBnMo3hjqYvVmzFc,1557
101
- simo/core/controllers.py,sha256=aaUySkaZoY0tX0zAQfeZVoGRm9yBpP4GHDD5ky5eys4,37866
101
+ simo/core/controllers.py,sha256=1TdJXaPcHIo7OfRXrqe8swxhXthpO2-Pji45StCxlC0,39350
102
102
  simo/core/dynamic_settings.py,sha256=bUs58XEZOCIEhg1TigR3LmYggli13KMryBZ9pC7ugAQ,1872
103
103
  simo/core/events.py,sha256=hnv17g37kqwpfrkMUStfk_Rk_-G28MU1HEjolwWNNFg,4978
104
104
  simo/core/filters.py,sha256=6wbn8C2WvKTTjtfMwwLBp2Fib1V0-DMpS4iqJd6jJQo,2540
@@ -115,7 +115,7 @@ simo/core/serializers.py,sha256=Yeu6YRGMIZzAgnfIgm0_v4pWXDgsBCSlwWaDws_bMUg,2310
115
115
  simo/core/signal_receivers.py,sha256=PsAHAvdFKuNLjxNbb7UqjrX7VHtjdPAhP2y3vbhVGXs,7114
116
116
  simo/core/socket_consumers.py,sha256=UlxV9OvTUUXaoKKYT3-qf1TyGxyOPxIpFH5cPFepH1o,9584
117
117
  simo/core/storage.py,sha256=_5igjaoWZAiExGWFEJMElxUw55DzJG1jqFty33xe8BE,342
118
- simo/core/tasks.py,sha256=-yBjOPILEnKRga-6MK-hBpEINQcY5mpwZwUWiDpqISs,17091
118
+ simo/core/tasks.py,sha256=Y9gXMd1QBjpO_4EAOuMSoHfK4H4qVcsg-36mPcx2sPU,17322
119
119
  simo/core/todos.py,sha256=eYVXfLGiapkxKK57XuviSNe3WsUYyIWZ0hgQJk7ThKo,665
120
120
  simo/core/types.py,sha256=WJEq48mIbFi_5Alt4wxWMGXxNxUTXqfQU5koH7wqHHI,1108
121
121
  simo/core/views.py,sha256=BBh8U2P3rS_WccKN9hN6dhtiyXIfCX9I80zNwejqfe4,3666
@@ -142,7 +142,7 @@ simo/core/__pycache__/base_types.cpython-312.pyc,sha256=Lnq2NL9B5hfwJARJYC447Rdv
142
142
  simo/core/__pycache__/base_types.cpython-38.pyc,sha256=CX-qlF7CefRi_mCE954wYa9rUFR88mOl6g7fybDRu7g,803
143
143
  simo/core/__pycache__/context.cpython-312.pyc,sha256=8rsN2Er-Sx3rrVmO0Gk4cem3euGh0kTELXj667GGZ5E,2193
144
144
  simo/core/__pycache__/context.cpython-38.pyc,sha256=NlTHt2GvXxA21AhBkeyOLfRFUuXw7wmwqyNhhcDl2cw,1373
145
- simo/core/__pycache__/controllers.cpython-312.pyc,sha256=kvZDEjt6C9DkZHojupG8AO_r64yA--BnhjEiLVhFSfU,53786
145
+ simo/core/__pycache__/controllers.cpython-312.pyc,sha256=XovbuP57Sn0QBCGvpOoHaDGLPBBMa0INLQ0n7iWkQf4,56673
146
146
  simo/core/__pycache__/controllers.cpython-38.pyc,sha256=LtrQQ8egOIOuQbAckeM-z8OfbzS4W8VQ3vBnryAm3iU,32086
147
147
  simo/core/__pycache__/dynamic_settings.cpython-312.pyc,sha256=WUZ6XF4kZb6zPf541PkKmiQaBIw-r5C6F3EUUZiTEnE,3331
148
148
  simo/core/__pycache__/dynamic_settings.cpython-38.pyc,sha256=wGpnscX1DxFpRl54MQURhjz2aD3NJohSzw9JCFnzh2Y,2384
@@ -176,7 +176,7 @@ simo/core/__pycache__/socket_consumers.cpython-312.pyc,sha256=Yph9SQTj6c3xr2HXKn
176
176
  simo/core/__pycache__/socket_consumers.cpython-38.pyc,sha256=Mr1-x-vGjBNffbz0S6AKpJCuzHJgRm8kXefv3qVVY_E,8397
177
177
  simo/core/__pycache__/storage.cpython-312.pyc,sha256=VOD9via7BtpTbXe2jWsRovCIkeJofoX7Ts_Eyl9I_a4,934
178
178
  simo/core/__pycache__/storage.cpython-38.pyc,sha256=9R1Xu0FJDflfRXUPsqEgt0SpwiP7FGk7HaR8s8XRyI8,721
179
- simo/core/__pycache__/tasks.cpython-312.pyc,sha256=RCYzywZ6ZgKfUhm3MAO_I7nbm4dcHqKPLHGMCMpf62g,22382
179
+ simo/core/__pycache__/tasks.cpython-312.pyc,sha256=YNRlJSItp3_hbF6uhcksjS3Px1LQOVbjkAaAXRu1Znw,22818
180
180
  simo/core/__pycache__/tasks.cpython-38.pyc,sha256=-J2is-l5XsfhamreN2TPQDTK-Jw6XGYL81bcVfjXsU8,11213
181
181
  simo/core/__pycache__/todos.cpython-312.pyc,sha256=bqguSv-oCCdlvzbMsm-7jAB-g0AvMFux1GL97XNJjyk,263
182
182
  simo/core/__pycache__/todos.cpython-38.pyc,sha256=lOqGZ58siHM3isoJV4r7sg8igrfE9fFd-jSfeBa0AQI,253
@@ -10606,9 +10606,9 @@ simo/fleet/templates/fleet/controllers_info/RoomZonePresenceSensor.md,sha256=Nun
10606
10606
  simo/generic/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10607
10607
  simo/generic/app_widgets.py,sha256=y8W3jR76Hh26O9pPQyg2SophMbYIOtAWD33MPKbB8Mg,856
10608
10608
  simo/generic/base_types.py,sha256=u3SlfpNYaCwkVBwomWgso4ODzL71ay9MhiAW-bxgnDU,341
10609
- simo/generic/controllers.py,sha256=JQnRl_gbaX0rzPhr4wORnBiswRJePEq9VZA_KsoN4VA,46410
10610
- simo/generic/forms.py,sha256=GJ17Ah00NlfqkD1Smt_Hu5U2fAg_S7QMVDObSfsBZqk,26347
10611
- simo/generic/gateways.py,sha256=kjqGMjlLXlGNreKLQtHoSnw4xan1IB8-KF8AqiZ82T0,16057
10609
+ simo/generic/controllers.py,sha256=mtMkWH-76KlmXNwzHSfgbCEuvK1D4S9R1yvIdkjtl4E,48202
10610
+ simo/generic/forms.py,sha256=dItnnUTwc5EtTfDAHcL8Ihy_b9DVVA5ajFd8FXpA_dQ,26568
10611
+ simo/generic/gateways.py,sha256=Vth-jSq1i81cm5G7cbuOlFJ4xKjmjLtvo2hn_djfPR4,19445
10612
10612
  simo/generic/models.py,sha256=59fkYowOX0imviIhA6uwupvuharrpBykmBm674rJNoI,7279
10613
10613
  simo/generic/routing.py,sha256=elQVZmgnPiieEuti4sJ7zITk1hlRxpgbotcutJJgC60,228
10614
10614
  simo/generic/socket_consumers.py,sha256=qesKZVhI56Kh7vdIUDD3hzDUi0FcXwIfcmE_a3YS6JQ,1772
@@ -10619,11 +10619,11 @@ simo/generic/__pycache__/app_widgets.cpython-312.pyc,sha256=ywoKk91YSEZxpyt9haG5
10619
10619
  simo/generic/__pycache__/app_widgets.cpython-38.pyc,sha256=D9b13pbMlirgHmjDnQhfLIDGSVINoSouHb4SWOeCRrs,1642
10620
10620
  simo/generic/__pycache__/base_types.cpython-312.pyc,sha256=h8Mwu49i-zmwTbL33JaLJfRDGOgkQh2_VqrfzEc4UQ4,616
10621
10621
  simo/generic/__pycache__/base_types.cpython-38.pyc,sha256=aV5NdIuvXR-ItKpI__MwcyPZHD6Z882TFdgYkPCkr1I,493
10622
- simo/generic/__pycache__/controllers.cpython-312.pyc,sha256=n37Bav8SPpv2CQi04d3ksniYBhcE5oe6-wlocy0rwqo,53145
10622
+ simo/generic/__pycache__/controllers.cpython-312.pyc,sha256=mPRJKmyjAiayb9ccDF1Wdm7-yI1q5DPvgqAdVGv1pf0,54042
10623
10623
  simo/generic/__pycache__/controllers.cpython-38.pyc,sha256=jJjwKVaDYyazrRGNjUFoY74nr_jX_DEnsC9KjyxZCgc,30427
10624
- simo/generic/__pycache__/forms.cpython-312.pyc,sha256=-SzYfBaKjiZ0puks8JacGc3FMBL4RWAc9kO2WoJtPl8,34869
10624
+ simo/generic/__pycache__/forms.cpython-312.pyc,sha256=kjg_rV74sV2V3qg5qYuAPRySVHLbHrJeLQzlAIgajNs,35047
10625
10625
  simo/generic/__pycache__/forms.cpython-38.pyc,sha256=k8lz3taXdWAg5P9jcnw66mWH51pCc4SOsg32kVEtBCg,19416
10626
- simo/generic/__pycache__/gateways.cpython-312.pyc,sha256=ixeotxPeqHkINziDnnkymlKR2wYMb5UR2sxiioMQxhY,19309
10626
+ simo/generic/__pycache__/gateways.cpython-312.pyc,sha256=naa_lnv2qQO9EALDYGajGAdU0wPqnaKjWE0ZKFjYJKQ,23195
10627
10627
  simo/generic/__pycache__/gateways.cpython-38.pyc,sha256=GIeMT51oZU2OCFD4eUDFdSRRYE0Qf14AcOr_gdUqG94,12705
10628
10628
  simo/generic/__pycache__/models.cpython-312.pyc,sha256=ggaeX6BQa-0-KG50HadpRCWeW84Fbog0muT2gBkqLNQ,10190
10629
10629
  simo/generic/__pycache__/models.cpython-38.pyc,sha256=MZpum7syAFxuulf47K7gtUlJJ7xRD-IBUBAwUM1ZRnw,5825
@@ -10634,11 +10634,13 @@ simo/generic/__pycache__/socket_consumers.cpython-38.pyc,sha256=FaVCf_uJI2uwj1Zz
10634
10634
  simo/generic/__pycache__/tasks.cpython-312.pyc,sha256=zEyNpFVmEJoZdeYKNZ7cEmPtIyZMTwEqzZJZZwMYl-o,4494
10635
10635
  simo/generic/migrations/0001_initial.py,sha256=7FpPcfpRU5ya0b8s2KbxR5a3npf92YruvZltUybjzys,676
10636
10636
  simo/generic/migrations/0002_auto_20241126_0726.py,sha256=SX38JwP732QooOm5HM1Xo7Th_Mv_6YZloT3eozULOhs,922
10637
+ simo/generic/migrations/0003_auto_20250409_1404.py,sha256=Jf8fFspN_V6woeUVvez36KVAcYR5tb23arLguagnm7Q,875
10637
10638
  simo/generic/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10638
10639
  simo/generic/migrations/__pycache__/0001_initial.cpython-312.pyc,sha256=n0KtsGZlJ5Tr7sPAgxQB9-PFGc_wRYMaCMV9Sp3cLEo,1267
10639
10640
  simo/generic/migrations/__pycache__/0001_initial.cpython-38.pyc,sha256=xy_fN8vnebC_X8hArqHAOLYHLm3puD_stzJ-LvUOhgk,1009
10640
10641
  simo/generic/migrations/__pycache__/0002_auto_20241126_0726.cpython-312.pyc,sha256=Iqo7MhrkP7TTV5_BdI4DoEn_6yqXqyGO9mSjWjWmmRA,1676
10641
10642
  simo/generic/migrations/__pycache__/0002_auto_20241126_0726.cpython-38.pyc,sha256=vfPHUDBG6UlfdqsIi35ZcdQ0NfsSeawvTNzZ7YTKQd0,1130
10643
+ simo/generic/migrations/__pycache__/0003_auto_20250409_1404.cpython-312.pyc,sha256=TIh9Y-e8_9-wot9-o97-MjyR36HtREwqE-eVPyz78DI,1651
10642
10644
  simo/generic/migrations/__pycache__/__init__.cpython-312.pyc,sha256=1n3ngIGcB4TVFUtZfXMMaVfYZnAEWPtIxwI-88in_Mc,178
10643
10645
  simo/generic/migrations/__pycache__/__init__.cpython-38.pyc,sha256=nJV0NkIT8MuONj1hUX-V6aCU2lX3BXHyPjisapnBsPA,172
10644
10646
  simo/generic/static/weather_icons/01d@2x.png,sha256=TZfWi6Rfddb2P-oldWWcjUiuCHiU9Yrc5hyrQAhF26I,948
@@ -10752,11 +10754,11 @@ simo/notifications/migrations/__pycache__/__init__.cpython-312.pyc,sha256=YMFc4Q
10752
10754
  simo/notifications/migrations/__pycache__/__init__.cpython-38.pyc,sha256=YMBRHVon2nWDtIUbghckjnC12sIg_ykPWhV5aM0tto4,178
10753
10755
  simo/users/__init__.py,sha256=6a7uBpCWB_DR7p54rbHusc0xvi1qfT1ZCCQGb6TiBh8,52
10754
10756
  simo/users/admin.py,sha256=2J_ZwrwAPw03Stb6msx5GvRpGCsDSC39RPpYBLGf8UY,7441
10755
- simo/users/api.py,sha256=hOLwJhA5Ik8UbW0Qmp2Vl7tB8_uDHnP4EZq-OSaknyw,13218
10757
+ simo/users/api.py,sha256=K0hGtuHkjwlLiWjne6jruI-lfswHrPXfCHSLP-xqOfY,15050
10756
10758
  simo/users/apps.py,sha256=cq0A8-U1HALEwev0TicgFhr4CAu7Icz8rwq0HfOaL4E,207
10757
10759
  simo/users/auth_backends.py,sha256=HxRp9iFvU1KqUhE7pA9YKEjqtBCJDbDqk_UMCD2Dwww,4361
10758
10760
  simo/users/auto_urls.py,sha256=RSUW3ai5LbMTknS8M7M5aOnG_YlFOVQrnNVNH-fkwlg,357
10759
- simo/users/dynamic_settings.py,sha256=sEIsi4yJw3kH46Jq_aOkSuK7QTfQACGUE-lkyBogCaM,570
10761
+ simo/users/dynamic_settings.py,sha256=yDtjpcEKA5uS2fcma6e-Zznh2iyMT3x8N7aRqNCtzSM,569
10760
10762
  simo/users/managers.py,sha256=OHgEP85MBtdkdYxdstBd8RavTBT8F_2WyDxUJ9aCqqM,246
10761
10763
  simo/users/middleware.py,sha256=tNPmnzo0eTrJ25SLHP7NotqYKI2cKnmv8hf6v5DLOWo,427
10762
10764
  simo/users/models.py,sha256=TKHs7mVjVC84WK-qXDz--4YfaD_TsUDrxxZ-oEXF-Pc,20527
@@ -10771,7 +10773,7 @@ simo/users/__pycache__/__init__.cpython-312.pyc,sha256=n0GE5zxjujBUkv1t1CswZ9Gby
10771
10773
  simo/users/__pycache__/__init__.cpython-38.pyc,sha256=VFoDJE_SKKaPqqYaaBYd1Ndb1hjakkTo_u0EG_XJ1GM,211
10772
10774
  simo/users/__pycache__/admin.cpython-312.pyc,sha256=lTVuZQwkuw1bzSn0A0kt-8H6ruKas_YPb_61AxSYSoo,11465
10773
10775
  simo/users/__pycache__/admin.cpython-38.pyc,sha256=uL8TwAipkatZxanvQtBKKcOv8Fm3UvinBxsP0okrOZg,8443
10774
- simo/users/__pycache__/api.cpython-312.pyc,sha256=p8nEOe95slqWK0QxhAkmXXbyPiZd-0VcA-ejvFcS6Mo,18333
10776
+ simo/users/__pycache__/api.cpython-312.pyc,sha256=UxOSngKsHG8FD0d0BE0734v_uj9Ynz5kL9j5zXB91Vs,19424
10775
10777
  simo/users/__pycache__/api.cpython-38.pyc,sha256=zZ4DfNktgeVvLAtMpaPUv7AoAgbKr7SCt-4ghJk1zp4,10386
10776
10778
  simo/users/__pycache__/apps.cpython-312.pyc,sha256=6KefztC11cSg6VHJ7-sVDvOA-oq_jNMB8jMvmhcVZD4,707
10777
10779
  simo/users/__pycache__/apps.cpython-38.pyc,sha256=dgbWL8CxzzISJQTmq_4IztPJ2UzykNVdqA2Ae1PmeGk,605
@@ -10779,7 +10781,7 @@ simo/users/__pycache__/auth_backends.cpython-312.pyc,sha256=NrdX57hAzX0sRI7BHiSx
10779
10781
  simo/users/__pycache__/auth_backends.cpython-38.pyc,sha256=jYS2hlbTZh_ZtPeWcN50pc0IpyfCSO7_MvIbuVwEp8M,3144
10780
10782
  simo/users/__pycache__/auto_urls.cpython-312.pyc,sha256=nRaBVD1YnibleMC8AdZdk1ZpwEgMyIPm9VW0quIuj5E,565
10781
10783
  simo/users/__pycache__/auto_urls.cpython-38.pyc,sha256=bc6BOgghLdMzUroGMj7eB6YX9kIDWB9JvTc0iJQrEa4,478
10782
- simo/users/__pycache__/dynamic_settings.cpython-312.pyc,sha256=lRVrJzyYRU2CVOKc_JtL2pJ4ohbPa54BWl5ChJdlQBY,981
10784
+ simo/users/__pycache__/dynamic_settings.cpython-312.pyc,sha256=TZ6YtXDJzsdO0qJLtNhvottM2kK1WYugwG3DaXt1CF0,981
10783
10785
  simo/users/__pycache__/dynamic_settings.cpython-38.pyc,sha256=6F8JBjZkHykySnmZjNEzjS0ijbmPdcp9yUAZ5kqq_Fo,864
10784
10786
  simo/users/__pycache__/managers.cpython-312.pyc,sha256=A9-yF1dilDc1H_-BtrIw9USpH6MYwkUI02gVmDOxsOA,821
10785
10787
  simo/users/__pycache__/managers.cpython-38.pyc,sha256=O0Y8ABp42RAosrbODmYsPMaj9AyOPyJ-aqzuO0Qpi2s,679
@@ -10943,9 +10945,9 @@ simo/users/templates/invitations/expired_msg.html,sha256=47DEQpj8HBSa-_TImW-5JCe
10943
10945
  simo/users/templates/invitations/expired_suggestion.html,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10944
10946
  simo/users/templates/invitations/taken_msg.html,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10945
10947
  simo/users/templates/invitations/taken_suggestion.html,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10946
- simo-2.9.1.dist-info/licenses/LICENSE.md,sha256=M7wm1EmMGDtwPRdg7kW4d00h1uAXjKOT3HFScYQMeiE,34916
10947
- simo-2.9.1.dist-info/METADATA,sha256=fAKnVdMivUaBR6WGF13CaTuUzfvwDy2Gbs9UZT8VzH4,2027
10948
- simo-2.9.1.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
10949
- simo-2.9.1.dist-info/entry_points.txt,sha256=S9PwnUYmTSW7681GKDCxUbL0leRJIaRk6fDQIKgbZBA,135
10950
- simo-2.9.1.dist-info/top_level.txt,sha256=GmS1hrAbpVqn9OWZh6UX82eIOdRLgYA82RG9fe8v4Rs,5
10951
- simo-2.9.1.dist-info/RECORD,,
10948
+ simo-2.10.1.dist-info/licenses/LICENSE.md,sha256=M7wm1EmMGDtwPRdg7kW4d00h1uAXjKOT3HFScYQMeiE,34916
10949
+ simo-2.10.1.dist-info/METADATA,sha256=9c6MSVrGuXjWD5IIrfhZ5CDlKqLEoZFxAnC20XDyNEg,2028
10950
+ simo-2.10.1.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
10951
+ simo-2.10.1.dist-info/entry_points.txt,sha256=S9PwnUYmTSW7681GKDCxUbL0leRJIaRk6fDQIKgbZBA,135
10952
+ simo-2.10.1.dist-info/top_level.txt,sha256=GmS1hrAbpVqn9OWZh6UX82eIOdRLgYA82RG9fe8v4Rs,5
10953
+ simo-2.10.1.dist-info/RECORD,,
File without changes