simo 2.0.25__py3-none-any.whl → 2.0.27__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 (30) hide show
  1. simo/core/__pycache__/api.cpython-38.pyc +0 -0
  2. simo/core/__pycache__/forms.cpython-38.pyc +0 -0
  3. simo/core/__pycache__/permissions.cpython-38.pyc +0 -0
  4. simo/core/__pycache__/serializers.cpython-38.pyc +0 -0
  5. simo/core/__pycache__/tasks.cpython-38.pyc +0 -0
  6. simo/core/api.py +15 -3
  7. simo/core/forms.py +29 -1
  8. simo/core/migrations/0032_auto_20240506_0834.py +24 -0
  9. simo/core/migrations/__pycache__/0032_auto_20240506_0834.cpython-38.pyc +0 -0
  10. simo/core/permissions.py +5 -0
  11. simo/core/serializers.py +16 -3
  12. simo/core/tasks.py +25 -0
  13. simo/fleet/__pycache__/forms.cpython-38.pyc +0 -0
  14. simo/fleet/forms.py +121 -172
  15. simo/generic/__pycache__/forms.cpython-38.pyc +0 -0
  16. simo/generic/__pycache__/models.cpython-38.pyc +0 -0
  17. simo/generic/forms.py +24 -25
  18. simo/generic/models.py +22 -6
  19. simo/notifications/utils.py +4 -3
  20. simo/users/__pycache__/models.cpython-38.pyc +0 -0
  21. simo/users/migrations/0027_permissionsrole_can_manage_components.py +18 -0
  22. simo/users/migrations/0028_auto_20240506_1146.py +22 -0
  23. simo/users/migrations/__pycache__/0027_permissionsrole_can_manage_components.cpython-38.pyc +0 -0
  24. simo/users/migrations/__pycache__/0028_auto_20240506_1146.cpython-38.pyc +0 -0
  25. simo/users/models.py +7 -6
  26. {simo-2.0.25.dist-info → simo-2.0.27.dist-info}/METADATA +1 -1
  27. {simo-2.0.25.dist-info → simo-2.0.27.dist-info}/RECORD +30 -24
  28. {simo-2.0.25.dist-info → simo-2.0.27.dist-info}/LICENSE.md +0 -0
  29. {simo-2.0.25.dist-info → simo-2.0.27.dist-info}/WHEEL +0 -0
  30. {simo-2.0.25.dist-info → simo-2.0.27.dist-info}/top_level.txt +0 -0
Binary file
Binary file
Binary file
simo/core/api.py CHANGED
@@ -217,7 +217,10 @@ class ComponentViewSet(
217
217
  @action(detail=True, methods=['post'])
218
218
  def subcomponent(self, request, pk=None, *args, **kwargs):
219
219
  component = self.get_object()
220
- json_data = restore_json(request.data.dict())
220
+ data = request.data
221
+ if not isinstance(request.data, dict):
222
+ data = data.dict()
223
+ json_data = restore_json(data)
221
224
  subcomponent_id = json_data.pop('id', -1)
222
225
  try:
223
226
  subcomponent = component.slaves.get(pk=subcomponent_id)
@@ -237,13 +240,20 @@ class ComponentViewSet(
237
240
  @action(detail=True, methods=['post'])
238
241
  def controller(self, request, pk=None, *args, **kwargs):
239
242
  component = self.get_object()
243
+ data = request.data
244
+ if not isinstance(request.data, dict):
245
+ data = data.dict()
246
+ request_data = restore_json(data)
240
247
  return self.perform_controller_method(
241
- restore_json(request.data.dict()), component
248
+ restore_json(request_data), component
242
249
  )
243
250
 
244
251
  @action(detail=False, methods=['post'])
245
252
  def control(self, request, *args, **kwargs):
246
- request_data = restore_json(request.data.dict())
253
+ data = request.data
254
+ if not isinstance(request.data, dict):
255
+ data = data.dict()
256
+ request_data = restore_json(data)
247
257
  component = self.get_queryset().filter(id=request_data.pop('id', 0)).first()
248
258
  if not component:
249
259
  raise Http404()
@@ -262,6 +272,8 @@ class ComponentViewSet(
262
272
  return RESTResponse(resp_data)
263
273
 
264
274
 
275
+
276
+
265
277
  class HistoryResultsSetPagination(PageNumberPagination):
266
278
  page_size = 50
267
279
  page_size_query_param = 'page_size'
simo/core/forms.py CHANGED
@@ -195,6 +195,9 @@ class ConfigFieldsMixin:
195
195
 
196
196
  def save(self, commit=True):
197
197
  for field_name in self.config_fields:
198
+ # support for partial forms
199
+ if field_name not in self.cleaned_data:
200
+ continue
198
201
  if isinstance(self.cleaned_data[field_name], models.Model):
199
202
  self.instance.config[field_name] = \
200
203
  self.cleaned_data[field_name].pk
@@ -282,6 +285,10 @@ class ComponentAdminForm(forms.ModelForm):
282
285
  has_icon = True
283
286
  has_alarm = True
284
287
 
288
+ # fields that can be edited via SIMO.io app by instance owners.
289
+ # Users who have is_owner enabled on their user role.
290
+ basic_fields = ['name', 'icon', 'zone', 'category']
291
+
285
292
  class Meta:
286
293
  model = Component
287
294
  fields = '__all__'
@@ -324,6 +331,27 @@ class ComponentAdminForm(forms.ModelForm):
324
331
  self.instance.config = self.controller.default_config
325
332
  self.instance.meta = self.controller.default_meta
326
333
 
334
+ self.cleanup_missing_keys(kwargs.get("data"))
335
+
336
+ def cleanup_missing_keys(self, data):
337
+ """
338
+ Removes missing keys from fields on form submission.
339
+ This avoids resetting fields that are not present in
340
+ the submitted data, which may be the sign of a buggy
341
+ or incomplete template.
342
+ Note that this cleanup relies on the HTML form being
343
+ patched to send all keys, even for checkboxes, via
344
+ input[type="hidden"] fields or some JS magic.
345
+ """
346
+ if data is None:
347
+ # not a form submission, don't modify self.fields
348
+ return
349
+
350
+ got_keys = data.keys()
351
+ field_names = self.fields.keys()
352
+ for missing in set(field_names) - set(got_keys):
353
+ del self.fields[missing]
354
+
327
355
  @classmethod
328
356
  def get_admin_fieldsets(cls, request, obj=None):
329
357
  main_fields = (
@@ -518,7 +546,7 @@ class SwitchForm(BaseComponentForm):
518
546
 
519
547
  def save(self, commit=True):
520
548
  obj = super().save(commit=commit)
521
- if commit:
549
+ if commit and 'slaves' in self.cleaned_data:
522
550
  obj.slaves.set(self.cleaned_data['slaves'])
523
551
  return obj
524
552
 
@@ -0,0 +1,24 @@
1
+ # Generated by Django 3.2.9 on 2024-05-06 08:34
2
+
3
+ from django.db import migrations, models
4
+ import django.db.models.deletion
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ dependencies = [
10
+ ('core', '0031_auto_20240429_1231'),
11
+ ]
12
+
13
+ operations = [
14
+ migrations.AlterField(
15
+ model_name='category',
16
+ name='order',
17
+ field=models.PositiveIntegerField(db_index=True),
18
+ ),
19
+ migrations.AlterField(
20
+ model_name='instance',
21
+ name='indoor_climate_sensor',
22
+ field=models.ForeignKey(blank=True, limit_choices_to={'base_type__in': ['numeric-sensor', 'multi-sensor']}, null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.component'),
23
+ ),
24
+ ]
simo/core/permissions.py CHANGED
@@ -59,6 +59,9 @@ class InstanceSuperuserCanEdit(BasePermission):
59
59
  if user_role.is_superuser:
60
60
  return True
61
61
 
62
+ if user_role.is_owner and request.method != 'DELETE':
63
+ return True
64
+
62
65
  return False
63
66
 
64
67
 
@@ -73,6 +76,8 @@ class ComponentPermission(BasePermission):
73
76
  user_role = request.user.get_role(view.instance)
74
77
  if user_role.is_superuser:
75
78
  return True
79
+ if user_role.is_owner and request.method != 'DELETE':
80
+ return True
76
81
  if request.method == 'POST' and user_role.component_permissions.filter(
77
82
  write=True, component=obj
78
83
  ).count():
simo/core/serializers.py CHANGED
@@ -269,9 +269,10 @@ class ComponentSerializer(FormSerializer):
269
269
  )
270
270
 
271
271
  if not self.instance or isinstance(self.instance, Iterable):
272
- form = self.Meta.form()
272
+ form = self.get_form()
273
273
  else:
274
- form = self.Meta.form(instance=self.instance)
274
+ form = self.get_form(instance=self.instance)
275
+
275
276
  for field_name in form.fields:
276
277
  # if field is specified as excluded field
277
278
  if field_name in getattr(self.Meta, 'exclude', []):
@@ -355,7 +356,7 @@ class ComponentSerializer(FormSerializer):
355
356
 
356
357
  def get_form(self, data=None, **kwargs):
357
358
  self.set_form_cls()
358
- if not self.instance:
359
+ if not self.instance or isinstance(self.instance, Iterable):
359
360
  #controller_uid = 'simo.generic.controllers.AlarmClock'
360
361
  controller_uid = self.context['request'].META.get('HTTP_CONTROLLER')
361
362
  else:
@@ -365,6 +366,16 @@ class ComponentSerializer(FormSerializer):
365
366
  controller_uid=controller_uid,
366
367
  **kwargs
367
368
  )
369
+ if not self.context['request'].user.is_master:
370
+ user_role = self.context['request'].user.get_role(
371
+ self.context['instance']
372
+ )
373
+ print("FORM BASIC FIELDS: ", form.basic_fields)
374
+ if not user_role.is_superuser and user_role.is_owner:
375
+ for field_name in list(form.fields.keys()):
376
+ if field_name not in form.basic_fields:
377
+ print("DELETE FIELD: ", field_name)
378
+ del form.fields[field_name]
368
379
  return form
369
380
 
370
381
  def accomodate_formsets(self, form, data):
@@ -395,6 +406,7 @@ class ComponentSerializer(FormSerializer):
395
406
  )
396
407
  form = self.get_form(instance=self.instance)
397
408
  a_data = self.accomodate_formsets(form, data)
409
+
398
410
  form = self.get_form(
399
411
  data=a_data, instance=self.instance
400
412
  )
@@ -410,6 +422,7 @@ class ComponentSerializer(FormSerializer):
410
422
  a_data = self.accomodate_formsets(form, validated_data)
411
423
  form = self.get_form(instance=instance, data=a_data)
412
424
  if form.is_valid():
425
+ print("FORM FIELDS", form.fields)
413
426
  instance = form.save(commit=True)
414
427
  return instance
415
428
  raise serializers.ValidationError(form.errors)
simo/core/tasks.py CHANGED
@@ -331,6 +331,30 @@ def restart_postgresql():
331
331
  proc.communicate()
332
332
 
333
333
 
334
+ @celery_app.task
335
+ def low_battery_notifications():
336
+ from simo.users.models import User
337
+ from simo.notifications.utils import notify_users
338
+ for instance in Instance.objects.all():
339
+ timezone.activate(instance.timezone)
340
+ if timezone.localtime().hour != 10:
341
+ continue
342
+ for comp in Component.objects.filter(
343
+ zone__instance=instance,
344
+ battery_level__isnull=False, battery_level__lt=20
345
+ ):
346
+ users = User.objects.filter(
347
+ roles__is_owner=True, roles__instance=comp.zone.instance
348
+ ).distinct()
349
+ if users:
350
+ notify_users(
351
+ comp.zone.instance, 'warning',
352
+ f"Low battery ({comp.battery_level}%) on {comp}",
353
+ component=comp, users=users
354
+ )
355
+
356
+
357
+
334
358
  @celery_app.on_after_finalize.connect
335
359
  def setup_periodic_tasks(sender, **kwargs):
336
360
  sender.add_periodic_task(1, watch_timers.s())
@@ -339,3 +363,4 @@ def setup_periodic_tasks(sender, **kwargs):
339
363
  sender.add_periodic_task(60 * 60 * 6, update_latest_version_available.s())
340
364
  sender.add_periodic_task(60, drop_fingerprints_learn.s())
341
365
  sender.add_periodic_task(60 * 60 * 24, restart_postgresql.s())
366
+ sender.add_periodic_task(60 * 60, low_battery_notifications.s())
Binary file
simo/fleet/forms.py CHANGED
@@ -91,13 +91,16 @@ class ColonelComponentForm(BaseComponentForm):
91
91
  def clean_colonel(self):
92
92
  if not self.instance.pk:
93
93
  return self.cleaned_data['colonel']
94
+ colonel = self.cleaned_data.get('colonel')
95
+ if not colonel:
96
+ return
94
97
  org = self.instance.config.get('colonel')
95
- if org and org != self.cleaned_data['colonel'].id:
98
+ if org and org != colonel.id:
96
99
  raise forms.ValidationError(
97
100
  "Changing colonel after component is created "
98
101
  "it is not allowed!"
99
102
  )
100
- return self.cleaned_data['colonel']
103
+ return colonel
101
104
 
102
105
  def _clean_pin(self, field_name):
103
106
  if self.cleaned_data[field_name].colonel != self.cleaned_data['colonel']:
@@ -153,7 +156,7 @@ class ColonelComponentForm(BaseComponentForm):
153
156
 
154
157
  def save(self, commit=True):
155
158
  obj = super().save(commit)
156
- if commit:
159
+ if commit and 'colonel' in self.cleaned_data:
157
160
  self.cleaned_data['colonel'].components.add(obj)
158
161
  self.cleaned_data['colonel'].rebuild_occupied_pins()
159
162
  self.cleaned_data['colonel'].save()
@@ -211,7 +214,7 @@ class ColonelBinarySensorConfigForm(ColonelComponentForm):
211
214
 
212
215
  def clean(self):
213
216
  super().clean()
214
- if not self.cleaned_data.get('colonel'):
217
+ if 'colonel' not in self.cleaned_data:
215
218
  return self.cleaned_data
216
219
  if 'pin' not in self.cleaned_data:
217
220
  return self.cleaned_data
@@ -251,7 +254,8 @@ class ColonelBinarySensorConfigForm(ColonelComponentForm):
251
254
 
252
255
 
253
256
  def save(self, commit=True):
254
- self.instance.config['pin_no'] = self.cleaned_data['pin'].no
257
+ if 'pin' in self.cleaned_data:
258
+ self.instance.config['pin_no'] = self.cleaned_data['pin'].no
255
259
  return super().save(commit=commit)
256
260
 
257
261
 
@@ -291,7 +295,7 @@ class ColonelNumericSensorConfigForm(ColonelComponentForm, NumericSensorForm):
291
295
 
292
296
  def clean(self):
293
297
  super().clean()
294
- if not self.cleaned_data.get('colonel'):
298
+ if 'colonel' not in self.cleaned_data:
295
299
  return self.cleaned_data
296
300
  if 'pin' not in self.cleaned_data:
297
301
  return self.cleaned_data
@@ -302,7 +306,8 @@ class ColonelNumericSensorConfigForm(ColonelComponentForm, NumericSensorForm):
302
306
 
303
307
 
304
308
  def save(self, commit=True):
305
- self.instance.config['pin_no'] = self.cleaned_data['pin'].no
309
+ if 'pin' in self.cleaned_data:
310
+ self.instance.config['pin_no'] = self.cleaned_data['pin'].no
306
311
  return super().save(commit=commit)
307
312
 
308
313
 
@@ -329,7 +334,7 @@ class DS18B20SensorConfigForm(ColonelComponentForm, NumericSensorForm):
329
334
 
330
335
  def clean(self):
331
336
  super().clean()
332
- if not self.cleaned_data.get('colonel'):
337
+ if 'colonel' not in self.cleaned_data:
333
338
  return self.cleaned_data
334
339
  if 'pin' not in self.cleaned_data:
335
340
  return self.cleaned_data
@@ -339,7 +344,8 @@ class DS18B20SensorConfigForm(ColonelComponentForm, NumericSensorForm):
339
344
  return self.cleaned_data
340
345
 
341
346
  def save(self, commit=True):
342
- self.instance.config['pin_no'] = self.cleaned_data['pin'].no
347
+ if 'pin' in self.cleaned_data:
348
+ self.instance.config['pin_no'] = self.cleaned_data['pin'].no
343
349
  return super().save(commit=commit)
344
350
 
345
351
 
@@ -385,7 +391,8 @@ class ColonelDHTSensorConfigForm(ColonelComponentForm):
385
391
  return self.cleaned_data
386
392
 
387
393
  def save(self, commit=True):
388
- self.instance.config['pin_no'] = self.cleaned_data['pin'].no
394
+ if 'pin' in self.cleaned_data:
395
+ self.instance.config['pin_no'] = self.cleaned_data['pin'].no
389
396
  return super().save(commit=commit)
390
397
 
391
398
 
@@ -415,7 +422,8 @@ class BME680SensorConfigForm(ColonelComponentForm):
415
422
  )
416
423
 
417
424
  def save(self, commit=True):
418
- self.instance.config['i2c_interface'] = self.cleaned_data['interface'].no
425
+ if 'interface' in self.cleaned_data:
426
+ self.instance.config['i2c_interface'] = self.cleaned_data['interface'].no
419
427
  return super().save(commit=commit)
420
428
 
421
429
 
@@ -445,7 +453,8 @@ class MPC9808SensorConfigForm(ColonelComponentForm):
445
453
  )
446
454
 
447
455
  def save(self, commit=True):
448
- self.instance.config['i2c_interface'] = self.cleaned_data['interface'].no
456
+ if 'interface' in self.cleaned_data:
457
+ self.instance.config['i2c_interface'] = self.cleaned_data['interface'].no
449
458
  return super().save(commit=commit)
450
459
 
451
460
 
@@ -472,7 +481,7 @@ class ColonelTouchSensorConfigForm(ColonelComponentForm):
472
481
 
473
482
  def clean(self):
474
483
  super().clean()
475
- if not self.cleaned_data.get('colonel'):
484
+ if 'colonel' not in self.cleaned_data:
476
485
  return self.cleaned_data
477
486
  if 'pin' not in self.cleaned_data:
478
487
  return self.cleaned_data
@@ -483,7 +492,8 @@ class ColonelTouchSensorConfigForm(ColonelComponentForm):
483
492
 
484
493
 
485
494
  def save(self, commit=True):
486
- self.instance.config['pin_no'] = self.cleaned_data['pin'].no
495
+ if 'pin' in self.cleaned_data:
496
+ self.instance.config['pin_no'] = self.cleaned_data['pin'].no
487
497
  return super().save(commit=commit)
488
498
 
489
499
 
@@ -528,35 +538,33 @@ class ColonelSwitchConfigForm(ColonelComponentForm):
528
538
 
529
539
  def __init__(self, *args, **kwargs):
530
540
  super().__init__(*args, **kwargs)
531
- if self.instance.pk:
541
+ self.basic_fields.append('auto_off')
542
+ if self.instance.pk and 'slaves' in self.fields:
532
543
  self.fields['slaves'].initial = self.instance.slaves.all()
533
544
 
534
545
  def clean_slaves(self):
546
+ if 'slaves' not in self.cleaned_data:
547
+ return
535
548
  if not self.cleaned_data['slaves'] or not self.instance:
536
549
  return self.cleaned_data['slaves']
537
550
  return validate_slaves(self.cleaned_data['slaves'], self.instance)
538
551
 
539
552
  def clean(self):
540
553
  super().clean()
541
- if not self.cleaned_data.get('colonel'):
542
- return self.cleaned_data
543
- if not self.cleaned_data.get('output_pin'):
544
- return self.cleaned_data
545
-
546
- self._clean_pin('output_pin')
547
-
548
- if not self.cleaned_data.get('controls'):
549
- return self.cleaned_data
550
554
 
551
- self._clean_controls()
555
+ if self.cleaned_data.get('output_pin'):
556
+ self._clean_pin('output_pin')
557
+ if self.cleaned_data.get('controls'):
558
+ self._clean_controls()
552
559
 
553
560
  return self.cleaned_data
554
561
 
555
562
 
556
563
  def save(self, commit=True):
557
- self.instance.config['output_pin_no'] = self.cleaned_data['output_pin'].no
564
+ if 'output_pin' in self.cleaned_data:
565
+ self.instance.config['output_pin_no'] = self.cleaned_data['output_pin'].no
558
566
  obj = super().save(commit=commit)
559
- if commit:
567
+ if commit and 'slaves' in self.cleaned_data:
560
568
  obj.slaves.set(self.cleaned_data['slaves'])
561
569
  return obj
562
570
 
@@ -628,7 +636,8 @@ class ColonelPWMOutputConfigForm(ColonelComponentForm):
628
636
 
629
637
  def __init__(self, *args, **kwargs):
630
638
  super().__init__(*args, **kwargs)
631
- if self.instance.pk:
639
+ self.basic_fields.extend(['turn_on_time', 'turn_off_time', 'skew'])
640
+ if self.instance.pk and 'slaves' in self.fields:
632
641
  self.fields['slaves'].initial = self.instance.slaves.all()
633
642
 
634
643
  def clean_slaves(self):
@@ -638,25 +647,18 @@ class ColonelPWMOutputConfigForm(ColonelComponentForm):
638
647
 
639
648
  def clean(self):
640
649
  super().clean()
641
- if not self.cleaned_data.get('colonel'):
642
- return self.cleaned_data
643
- if not self.cleaned_data.get('output_pin'):
644
- return self.cleaned_data
645
-
646
- self._clean_pin('output_pin')
647
-
648
- if not self.cleaned_data.get('controls'):
649
- return self.cleaned_data
650
-
651
- self._clean_controls()
652
-
650
+ if self.cleaned_data.get('output_pin'):
651
+ self._clean_pin('output_pin')
652
+ if self.cleaned_data.get('controls'):
653
+ self._clean_controls()
653
654
  return self.cleaned_data
654
655
 
655
656
 
656
657
  def save(self, commit=True):
657
- self.instance.config['output_pin_no'] = self.cleaned_data['output_pin'].no
658
+ if 'output_pin' in self.cleaned_data:
659
+ self.instance.config['output_pin_no'] = self.cleaned_data['output_pin'].no
658
660
  obj = super().save(commit=commit)
659
- if commit:
661
+ if commit and 'slaves' in self.cleaned_data:
660
662
  obj.slaves.set(self.cleaned_data['slaves'])
661
663
  return obj
662
664
 
@@ -716,7 +718,9 @@ class ColonelRGBLightConfigForm(ColonelComponentForm):
716
718
  )
717
719
 
718
720
  def save(self, commit=True):
719
- self.instance.config['output_pin_no'] = self.cleaned_data['output_pin'].no
721
+ if 'output_pin' in self.cleaned_data:
722
+ self.instance.config['output_pin_no'] = \
723
+ self.cleaned_data['output_pin'].no
720
724
  return super().save(commit)
721
725
 
722
726
  def clean_custom_timing(self):
@@ -740,31 +744,25 @@ class ColonelRGBLightConfigForm(ColonelComponentForm):
740
744
 
741
745
  def clean(self):
742
746
  super().clean()
743
- if not self.cleaned_data.get('colonel'):
744
- return self.cleaned_data
745
- if not self.cleaned_data.get('output_pin'):
746
- return self.cleaned_data
747
-
748
- if self.cleaned_data.get('color_order'):
749
- if self.cleaned_data['has_white']:
750
- if len(self.cleaned_data['color_order']) != 4:
751
- self.add_error(
752
- "color_order",
753
- _("4 colors expected for stripes with dedicated White led.")
754
- )
755
- else:
756
- if len(self.cleaned_data['color_order']) != 3:
757
- self.add_error(
758
- "color_order",
759
- _("3 colors expected for stripes without dedicated White led.")
760
- )
761
747
 
762
748
  self._clean_pin('output_pin')
763
-
764
- if not self.cleaned_data.get('controls'):
765
- return self.cleaned_data
766
-
767
- self._clean_controls()
749
+ if self.cleaned_data.get('controls'):
750
+ self._clean_controls()
751
+
752
+ if 'color_order' in self.cleaned_data:
753
+ if self.cleaned_data.get('color_order'):
754
+ if self.cleaned_data['has_white']:
755
+ if len(self.cleaned_data['color_order']) != 4:
756
+ self.add_error(
757
+ "color_order",
758
+ _("4 colors expected for stripes with dedicated White led.")
759
+ )
760
+ else:
761
+ if len(self.cleaned_data['color_order']) != 3:
762
+ self.add_error(
763
+ "color_order",
764
+ _("3 colors expected for stripes without dedicated White led.")
765
+ )
768
766
 
769
767
  return self.cleaned_data
770
768
 
@@ -811,21 +809,19 @@ class DualMotorValveForm(ColonelComponentForm):
811
809
 
812
810
  def clean(self):
813
811
  super().clean()
814
- if not self.cleaned_data.get('colonel'):
815
- return self.cleaned_data
816
- if not self.cleaned_data.get('open_pin'):
817
- return self.cleaned_data
818
- if not self.cleaned_data.get('close_pin'):
819
- return self.cleaned_data
820
-
821
- self._clean_pin('open_pin')
822
- self._clean_pin('close_pin')
823
-
812
+ if self.cleaned_data.get('open_pin'):
813
+ self._clean_pin('open_pin')
814
+ if self.cleaned_data.get('close_pin'):
815
+ self._clean_pin('close_pin')
824
816
  return self.cleaned_data
825
817
 
826
818
  def save(self, commit=True):
827
- self.instance.config['open_pin_no'] = self.cleaned_data['open_pin'].no
828
- self.instance.config['close_pin_no'] = self.cleaned_data['close_pin'].no
819
+ if 'open_pin' in self.cleaned_data:
820
+ self.instance.config['open_pin_no'] = \
821
+ self.cleaned_data['open_pin'].no
822
+ if 'close_pin' in self.cleaned_data:
823
+ self.instance.config['close_pin_no'] = \
824
+ self.cleaned_data['close_pin'].no
829
825
  return super().save(commit=commit)
830
826
 
831
827
 
@@ -903,40 +899,37 @@ class BlindsConfigForm(ColonelComponentForm):
903
899
 
904
900
  def clean(self):
905
901
  super().clean()
906
- if not self.cleaned_data.get('colonel'):
907
- return self.cleaned_data
908
- if not self.cleaned_data.get('open_pin'):
909
- return self.cleaned_data
910
- if not self.cleaned_data.get('close_pin'):
911
- return self.cleaned_data
912
-
913
- self._clean_pin('open_pin')
914
- self._clean_pin('close_pin')
915
-
916
- if 'controls' not in self.cleaned_data:
917
- return self.cleaned_data
918
-
919
- if len(self.cleaned_data['controls']) not in (0, 2):
920
- self.add_error('controls', "Must have 0 or 2 controls")
921
- return self.cleaned_data
922
-
923
- if len(self.cleaned_data['controls']) == 2:
924
- method = None
925
- for c in self.cleaned_data['controls']:
926
- if not method:
927
- method = c['method']
928
- else:
929
- if c['method'] != method:
930
- self.add_error('controls', "Both must use the same control method.")
931
- return self.cleaned_data
932
-
933
- self._clean_controls()
934
902
 
903
+ if self.cleaned_data.get('open_pin'):
904
+ self._clean_pin('open_pin')
905
+ if self.cleaned_data.get('close_pin'):
906
+ self._clean_pin('close_pin')
907
+
908
+ if 'controls' in self.cleaned_data:
909
+ if len(self.cleaned_data['controls']) not in (0, 2):
910
+ self.add_error('controls', "Must have 0 or 2 controls")
911
+ return self.cleaned_data
912
+
913
+ if len(self.cleaned_data['controls']) == 2:
914
+ method = None
915
+ for c in self.cleaned_data['controls']:
916
+ if not method:
917
+ method = c['method']
918
+ else:
919
+ if c['method'] != method:
920
+ self.add_error('controls', "Both must use the same control method.")
921
+ return self.cleaned_data
922
+
923
+ self._clean_controls()
935
924
  return self.cleaned_data
936
925
 
937
926
  def save(self, commit=True):
938
- self.instance.config['open_pin_no'] = self.cleaned_data['open_pin'].no
939
- self.instance.config['close_pin_no'] = self.cleaned_data['close_pin'].no
927
+ if 'open_pin' in self.cleaned_data:
928
+ self.instance.config['open_pin_no'] = \
929
+ self.cleaned_data['open_pin'].no
930
+ if 'close_pin' in self.cleaned_data:
931
+ self.instance.config['close_pin_no'] = \
932
+ self.cleaned_data['close_pin'].no
940
933
  return super().save(commit=commit)
941
934
 
942
935
 
@@ -952,9 +945,6 @@ class BurglarSmokeDetectorConfigForm(ColonelComponentForm):
952
945
  ]
953
946
  )
954
947
  )
955
- power_action = forms.ChoiceField(
956
- choices=(('HIGH', "HIGH"), ('LOW', "LOW")),
957
- )
958
948
  sensor_pin = ColonelPinChoiceField(
959
949
  queryset=ColonelPin.objects.filter(input=True),
960
950
  widget=autocomplete.ListSelect2(
@@ -966,67 +956,28 @@ class BurglarSmokeDetectorConfigForm(ColonelComponentForm):
966
956
  ]
967
957
  )
968
958
  )
969
- sensor_pull = forms.ChoiceField(
970
- choices=(
971
- ('HIGH', "HIGH"), ('LOW', "LOW"), ("FLOATING", "leave floating"),
972
- ),
973
- help_text="If you are not sure what is this all about, "
974
- "you are most definitely want to pull this HIGH or LOW "
975
- "but not leave it floating!"
976
- )
977
959
  sensor_inverse = forms.TypedChoiceField(
978
- choices=((1, "Yes"), (0, "No")), coerce=int,
979
- help_text="Hint: Set pull HIGH and inverse to Yes, to get ON signal when "
960
+ choices=((0, "No"), (1, "Yes")), coerce=int, initial=0,
961
+ help_text="Hint: Set to Yes, to get ON signal when "
980
962
  "you deliver GND to the pin and OFF when you cut it out."
981
963
  )
982
964
 
983
-
984
965
  def clean(self):
985
966
  super().clean()
986
- if not self.cleaned_data.get('colonel'):
987
- return self.cleaned_data
988
- if 'sensor_pin' not in self.cleaned_data:
989
- return self.cleaned_data
990
- if 'power_pin' not in self.cleaned_data:
991
- return self.cleaned_data
992
-
993
- self._clean_pin('sensor_pin')
994
- self._clean_pin('power_pin')
995
-
996
-
997
- if self.cleaned_data['sensor_pin'].no > 100:
998
- if self.cleaned_data['sensor_pin'].no < 126:
999
- if self.cleaned_data.get('sensor_pull') == 'HIGH':
1000
- self.add_error(
1001
- 'sensor_pull',
1002
- "Sorry, but this pin is already pulled LOW and "
1003
- "it can not be changed by this setting. "
1004
- "Please use 5kohm resistor to physically pull it HIGH "
1005
- "if that's what you want to do."
1006
- )
1007
- else:
1008
- if self.cleaned_data.get('sensor_pull') == 'LOW':
1009
- self.add_error(
1010
- 'sensor_pull',
1011
- "Sorry, but this pin is already pulled HIGH and "
1012
- "it can not be changed by this setting. "
1013
- "Please use 5kohm resistor to physically pull it LOW "
1014
- "if that's what you want to do."
1015
- )
1016
- elif self.cleaned_data.get('sensor_pull') != 'FLOATING':
1017
- if not self.cleaned_data['sensor_pin'].output:
1018
- self.add_error(
1019
- 'sensor_pin',
1020
- f"Sorry, but {self.cleaned_data['sensor_pin']} "
1021
- f"does not have internal pull HIGH/LOW"
1022
- " resistance capability"
1023
- )
967
+ if 'sensor_pin' in self.cleaned_data:
968
+ self._clean_pin('sensor_pin')
969
+ if 'power_pin' in self.cleaned_data:
970
+ self._clean_pin('power_pin')
1024
971
 
1025
972
  return self.cleaned_data
1026
973
 
1027
974
  def save(self, commit=True):
1028
- self.instance.config['sensor_pin_no'] = self.cleaned_data['sensor_pin'].no
1029
- self.instance.config['power_pin_no'] = self.cleaned_data['power_pin'].no
975
+ if 'sensor_pin' in self.cleaned_data:
976
+ self.instance.config['sensor_pin_no'] = \
977
+ self.cleaned_data['sensor_pin'].no
978
+ if 'power_pin' in self.cleaned_data:
979
+ self.instance.config['power_pin_no'] = \
980
+ self.cleaned_data['power_pin'].no
1030
981
  return super().save(commit=commit)
1031
982
 
1032
983
 
@@ -1059,18 +1010,14 @@ class TTLockConfigForm(ColonelComponentForm):
1059
1010
 
1060
1011
  def save(self, commit=True):
1061
1012
  obj = super(ColonelComponentForm, self).save(commit)
1062
- if commit:
1063
- self.cleaned_data['colonel'].components.add(obj)
1064
- self.cleaned_data['colonel'].save()
1065
- if self.cleaned_data['door_sensor']:
1066
- GatewayObjectCommand(
1067
- self.instance.gateway, self.cleaned_data['door_sensor'],
1068
- command='watch_lock_sensor'
1069
- ).publish()
1013
+ if commit and 'door_sensor' in self.cleaned_data:
1014
+ GatewayObjectCommand(
1015
+ self.instance.gateway, self.cleaned_data['door_sensor'],
1016
+ command='watch_lock_sensor'
1017
+ ).publish()
1070
1018
  return obj
1071
1019
 
1072
1020
 
1073
-
1074
1021
  class DALIDeviceConfigForm(ColonelComponentForm):
1075
1022
  interface = ColonelInterfacesChoiceField(
1076
1023
  queryset=Interface.objects.filter(type='dali'),
@@ -1087,7 +1034,9 @@ class DALIDeviceConfigForm(ColonelComponentForm):
1087
1034
  )
1088
1035
 
1089
1036
  def save(self, commit=True):
1090
- self.instance.config['dali_interface'] = self.cleaned_data['interface'].no
1037
+ if 'interface' in self.cleaned_data:
1038
+ self.instance.config['dali_interface'] = \
1039
+ self.cleaned_data['interface'].no
1091
1040
  return super().save(commit=commit)
1092
1041
 
1093
1042
 
simo/generic/forms.py CHANGED
@@ -137,8 +137,6 @@ class ThermostatConfigForm(BaseComponentForm):
137
137
  else:
138
138
  self.fields['use_real_feel'].disabled = True
139
139
 
140
-
141
-
142
140
  def save(self, commit=True):
143
141
  self.instance.value_units = self.cleaned_data[
144
142
  'temperature_sensor'
@@ -314,8 +312,9 @@ class AlarmGroupConfigForm(BaseComponentForm):
314
312
  def save(self, *args, **kwargs):
315
313
  self.instance.value_units = 'status'
316
314
  from .controllers import AlarmGroup
317
- if self.fields['is_main'].widget.attrs.get('disabled'):
318
- self.cleaned_data['is_main'] = self.fields['is_main'].initial
315
+ if 'is_main' in self.cleaned_data:
316
+ if self.fields['is_main'].widget.attrs.get('disabled'):
317
+ self.cleaned_data['is_main'] = self.fields['is_main'].initial
319
318
  obj = super().save(*args, **kwargs)
320
319
  if obj.config.get('is_main'):
321
320
  for c in Component.objects.filter(
@@ -366,8 +365,9 @@ class WeatherForecastForm(BaseComponentForm):
366
365
  def save(self, *args, **kwargs):
367
366
  self.instance.value_units = 'status'
368
367
  from .controllers import WeatherForecast
369
- if self.fields['is_main'].widget.attrs.get('disabled'):
370
- self.cleaned_data['is_main'] = self.fields['is_main'].initial
368
+ if 'is_main' in self.fields and 'is_main' in self.cleaned_data:
369
+ if self.fields['is_main'].widget.attrs.get('disabled'):
370
+ self.cleaned_data['is_main'] = self.fields['is_main'].initial
371
371
  obj = super().save(*args, **kwargs)
372
372
  if obj.config.get('is_main'):
373
373
  for c in Component.objects.filter(
@@ -541,11 +541,12 @@ class WateringConfigForm(BaseComponentForm):
541
541
  return contours
542
542
 
543
543
  def save(self, commit=True):
544
- self.instance.config['program'] = self.controller._build_program(
545
- self.cleaned_data['contours']
546
- )
544
+ if 'contours' in self.cleaned_data:
545
+ self.instance.config['program'] = self.controller._build_program(
546
+ self.cleaned_data['contours']
547
+ )
547
548
  obj = super().save(commit=commit)
548
- if commit:
549
+ if commit and 'contours' in self.cleaned_data:
549
550
  obj.slaves.clear()
550
551
  for contour in self.cleaned_data['contours']:
551
552
  obj.slaves.add(
@@ -565,10 +566,6 @@ class StateForm(forms.Form):
565
566
  help_text = forms.CharField(required=False, widget=forms.Textarea(attrs={'rows': 3}))
566
567
  prefix = 'states'
567
568
 
568
- def clean(self):
569
- print("Let's clean the data! ", self.cleaned_data)
570
- return self.cleaned_data
571
-
572
569
 
573
570
  class StateSelectForm(BaseComponentForm):
574
571
  states = FormsetField(
@@ -602,18 +599,20 @@ class AlarmClockEventForm(forms.Form):
602
599
  if not self.cleaned_data.get('play_action'):
603
600
  return self.cleaned_data
604
601
  component = self.cleaned_data.get('component')
605
- if not hasattr(component, self.cleaned_data['play_action']):
606
- self.add_error(
607
- 'play_action',
608
- f"{component} has no {self.cleaned_data['play_action']} action!"
609
- )
610
- if self.cleaned_data.get('reverse_action'):
611
- if not hasattr(component, self.cleaned_data['reverse_action']):
602
+ if 'play_action' in self.cleaned_data:
603
+ if not hasattr(component, self.cleaned_data['play_action']):
612
604
  self.add_error(
613
- 'reverse_action',
614
- f"{component} has no "
615
- f"{self.cleaned_data['reverse_action']} action!"
605
+ 'play_action',
606
+ f"{component} has no {self.cleaned_data['play_action']} action!"
616
607
  )
608
+ if 'reverse_action' in self.cleaned_data:
609
+ if self.cleaned_data.get('reverse_action'):
610
+ if not hasattr(component, self.cleaned_data['reverse_action']):
611
+ self.add_error(
612
+ 'reverse_action',
613
+ f"{component} has no "
614
+ f"{self.cleaned_data['reverse_action']} action!"
615
+ )
617
616
  return self.cleaned_data
618
617
 
619
618
 
@@ -633,7 +632,7 @@ class AlarmClockConfigForm(BaseComponentForm):
633
632
 
634
633
  def save(self, commit=True):
635
634
  obj = super().save(commit=commit)
636
- if commit:
635
+ if commit and 'default_events' in self.cleaned_data:
637
636
  obj.slaves.clear()
638
637
  for comp in self.cleaned_data['default_events']:
639
638
  c = Component.objects.filter(pk=comp['component']).first()
simo/generic/models.py CHANGED
@@ -125,11 +125,27 @@ def bind_controlling_locks_to_alarm_groups(sender, instance, *args, **kwargs):
125
125
  base_type=AlarmGroup.base_type,
126
126
  config__arming_locks__contains=instance.id
127
127
  ):
128
- if ag.config.get(
129
- 'arm_on_away', ''
130
- ).startswith('on_away_and_locked'):
128
+ if ag.config.get('arm_on_away') in (None, 'on_away'):
131
129
  continue
132
- ag.arm()
130
+ users_at_home = InstanceUser.objects.filter(
131
+ instance=instance.instance, at_home=True
132
+ ).exclude(is_active=False).exclude(id=instance.id).count()
133
+ if users_at_home:
134
+ continue
135
+ if ag.config.get('arm_on_away') == 'on_away_and_locked':
136
+ print(f"Nobody is at home, lock was locked. Arm {ag}!")
137
+ ag.arm()
138
+ continue
139
+ locked_states = [
140
+ True if l['value'] == 'locked' else False
141
+ for l in Component.objects.filter(
142
+ base_type='lock', id__in=ag.config.get('arming_locks', []),
143
+ ).values('value')
144
+ ]
145
+ if all(locked_states):
146
+ print(f"Nobody is at home, all locks are now locked. Arm {ag}!")
147
+ ag.arm()
148
+
133
149
  elif instance.value == 'unlocked':
134
150
  for ag in Component.objects.filter(
135
151
  base_type=AlarmGroup.base_type,
@@ -148,8 +164,8 @@ def bind_alarm_groups(sender, instance, created, *args, **kwargs):
148
164
  return
149
165
  users_at_home = InstanceUser.objects.filter(
150
166
  instance=instance.instance, at_home=True
151
- ).exclude(is_active=False).exclude(id=instance.id)
152
- if users_at_home.count():
167
+ ).exclude(is_active=False).exclude(id=instance.id).count()
168
+ if users_at_home:
153
169
  return
154
170
  for ag in Component.objects.filter(
155
171
  zone__instance=instance.instance,
@@ -6,9 +6,7 @@ def notify_users(instance, severity, title, body=None, component=None, users=Non
6
6
  assert severity in ('info', 'warning', 'alarm')
7
7
  notification = Notification.objects.create(
8
8
  instance=instance,
9
- title='<strong>%s:</strong> %s' % (
10
- instance.name, title
11
- ),
9
+ title=f'{instance.name}: {title}',
12
10
  severity=severity, body=body,
13
11
  component=component
14
12
  )
@@ -18,6 +16,9 @@ def notify_users(instance, severity, title, body=None, component=None, users=Non
18
16
  instance_roles__is_active=True
19
17
  )
20
18
  for user in users:
19
+ # do not send emails to system users
20
+ if user.email.endswith('simo.io'):
21
+ continue
21
22
  if instance not in user.instances:
22
23
  continue
23
24
  if component and not component.can_write(user):
@@ -0,0 +1,18 @@
1
+ # Generated by Django 3.2.9 on 2024-05-06 08:34
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('users', '0026_fingerprint_name'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AddField(
14
+ model_name='permissionsrole',
15
+ name='can_manage_components',
16
+ field=models.BooleanField(default=False, help_text='Can manage zones and basic component parameters via SIMO.io app.'),
17
+ ),
18
+ ]
@@ -0,0 +1,22 @@
1
+ # Generated by Django 3.2.9 on 2024-05-06 11:46
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('users', '0027_permissionsrole_can_manage_components'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.RemoveField(
14
+ model_name='permissionsrole',
15
+ name='can_manage_components',
16
+ ),
17
+ migrations.AddField(
18
+ model_name='permissionsrole',
19
+ name='is_owner',
20
+ field=models.BooleanField(default=False, help_text='Can manage zones, basic component parametersand other things via SIMO.io app, but is not yet allowed to perform any serious system changes, like superusers can.'),
21
+ ),
22
+ ]
simo/users/models.py CHANGED
@@ -35,6 +35,12 @@ class PermissionsRole(models.Model):
35
35
  help_text="Global role if instance is not set."
36
36
  )
37
37
  name = models.CharField(max_length=100, db_index=True)
38
+ is_owner = models.BooleanField(
39
+ default=False,
40
+ help_text="Can manage zones, basic component parameters"
41
+ "and other things via SIMO.io app, but is not yet allowed "
42
+ "to perform any serious system changes, like superusers can."
43
+ )
38
44
  can_manage_users = models.BooleanField(default=False)
39
45
  is_superuser = models.BooleanField(
40
46
  default=False,
@@ -213,12 +219,7 @@ class User(AbstractBaseUser, SimoAdminMixin):
213
219
  return self.is_active and self.ssh_key and self.is_master
214
220
 
215
221
  def get_role(self, instance):
216
- for role in self.roles.all():
217
- if not role.instance:
218
- return role
219
- for role in self.roles.all():
220
- if role.instance == instance:
221
- return role
222
+ return self.roles.filter(instance=instance).first()
222
223
 
223
224
  def set_instance(self, instance):
224
225
  self._instance = instance
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: simo
3
- Version: 2.0.25
3
+ Version: 2.0.27
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
@@ -26,7 +26,7 @@ simo/_hub_template/hub/supervisor.conf,sha256=IY3fdK0fDD2eAothB0n54xhjQj8LYoXIR9
26
26
  simo/_hub_template/hub/urls.py,sha256=Ydm-1BkYAzWeEF-MKSDIFf-7aE4qNLPm48-SA51XgJQ,25
27
27
  simo/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
28
  simo/core/admin.py,sha256=vZp2TZTt41YararDkIEs35X6ARqRp94wrIddsIfe-Bs,17738
29
- simo/core/api.py,sha256=9lPX8ShvAorn5p1qjPUxOKv_cmkOxoWyiIlHtCzgl5Q,23088
29
+ simo/core/api.py,sha256=YLO6r1htHgBjokAHoTXCtEFmx_Ug5xIlPg6sQX-CT4A,23413
30
30
  simo/core/api_auth.py,sha256=_3hG4e1eLKrcRnSAOB_xTL6cwtOJ2_7JS7GZU_iqTgA,1251
31
31
  simo/core/api_meta.py,sha256=ySmmhtVrWatI3yqnYPuP5ipapmJfyfEbl32w-7_W5O4,3551
32
32
  simo/core/app_widgets.py,sha256=EEQOto3fGR0syDqpJE38tQrx8DoTTyg26nF5kYzHY38,2018
@@ -38,26 +38,26 @@ simo/core/controllers.py,sha256=iJ7cIGv2WhSGjyxCFfK49pZXuEMfSe75bMEAkRN8G4g,2710
38
38
  simo/core/dynamic_settings.py,sha256=U2WNL96JzVXdZh0EqMPWrxqO6BaRR2Eo5KTDqz7MC4o,1943
39
39
  simo/core/events.py,sha256=LvtonJGNyCb6HLozs4EG0WZItnDwNdtnGQ4vTcnKvUs,4438
40
40
  simo/core/filters.py,sha256=ghtOZcrwNAkIyF5_G9Sn73NkiI71mXv0NhwCk4IyMIM,411
41
- simo/core/forms.py,sha256=FnnK4pHZ8EaMvubN0NQbz2SdTTgMXvGM-heklzN4RZo,22465
41
+ simo/core/forms.py,sha256=tsWBsoBJeUiXDVU6lKbMbnBZORmNrnK5XnoBS_9uWsU,23583
42
42
  simo/core/gateways.py,sha256=s_c2W0v2_te89i6LS4Nj7F2wn9UwjZXPT7pfy6SToVo,3714
43
43
  simo/core/loggers.py,sha256=EBdq23gTQScVfQVH-xeP90-wII2DQFDjoROAW6ggUP4,1645
44
44
  simo/core/managers.py,sha256=WoQ4OX3akIvoroSYji-nLVqXBSJzCiC1u_IiWkKbKmA,2413
45
45
  simo/core/middleware.py,sha256=pO52hQOJV_JRmNyUe7zfufSnJFlRITOWX6jwkoPWJhk,2052
46
46
  simo/core/models.py,sha256=m05fXzOtLrq4LEGknU1mjTHzaTmIEBi1zhwvtDWDknE,19773
47
- simo/core/permissions.py,sha256=m0GmmSeVQiXl1EZWgaVlK-Ud4lbwc1CLlcryomhKg6Y,2534
47
+ simo/core/permissions.py,sha256=yqVXq6SNZccSKcOoGdb0oh-WHsyTTtI9ovJdJyhjv28,2707
48
48
  simo/core/routing.py,sha256=X1_IHxyA-_Q7hw1udDoviVP4_FSBDl8GYETTC2zWTbY,499
49
- simo/core/serializers.py,sha256=kmja52o-BUOcUTX2ZsKWixvKRZSXB7lGe866Q1ajlmo,17563
49
+ simo/core/serializers.py,sha256=vOrWlfSkmdyaG0VPM0fmRLd7OkkNNGyAkc6Q8Cwa_1Q,18179
50
50
  simo/core/signal_receivers.py,sha256=EZ8NSYZxUgSaLS16YZdK7T__l8dl0joMRllOxx5PUt4,2809
51
51
  simo/core/socket_consumers.py,sha256=n7VE2Fvqt4iEAYLTRbTPOcI-7tszMAADu7gimBxB-Fg,9635
52
52
  simo/core/storage.py,sha256=YlxmdRs-zhShWtFKgpJ0qp2NDBuIkJGYC1OJzqkbttQ,572
53
- simo/core/tasks.py,sha256=4QShdl39MB6InYfBYiGJ167ARKBhO7WHMvU7i-kvsRU,11325
53
+ simo/core/tasks.py,sha256=PEGGsKBbkQOl3LhLfGR5orbYtCxzcv8peMLiVxgl3R0,12228
54
54
  simo/core/todos.py,sha256=eYVXfLGiapkxKK57XuviSNe3WsUYyIWZ0hgQJk7ThKo,665
55
55
  simo/core/types.py,sha256=WJEq48mIbFi_5Alt4wxWMGXxNxUTXqfQU5koH7wqHHI,1108
56
56
  simo/core/views.py,sha256=hlAKpAbCbqI3a-uL5tDp532T2oLFiF0MBzKUJ_SNzo0,5833
57
57
  simo/core/widgets.py,sha256=J9e06C6I22F6xKic3VMgG7WeX07glAcl-4bF2Mg180A,2827
58
58
  simo/core/__pycache__/__init__.cpython-38.pyc,sha256=y0IW37wBUIGa3Eh_ZG28pRqHKoLiPyTgUX2OnbkEPlc,158
59
59
  simo/core/__pycache__/admin.cpython-38.pyc,sha256=gZpNwM9f-JMbGe9sbQKt2hU2BBYhRzKSH9XRJYKN3Vo,13534
60
- simo/core/__pycache__/api.cpython-38.pyc,sha256=FaLLTnRx1VwRVRsuJKT3v2gWzLtI26iTq2BYb3lq0r4,18458
60
+ simo/core/__pycache__/api.cpython-38.pyc,sha256=yL3vQiZ6McrOILD37EmuRdzZHZ0bPCsboDAeHZP_iyM,18581
61
61
  simo/core/__pycache__/api_auth.cpython-38.pyc,sha256=5UTBr3rDMERAfc0OuOVDwGeQkt6Q7GLBtZJAMBse1sg,1712
62
62
  simo/core/__pycache__/api_meta.cpython-38.pyc,sha256=94T3_rybn2T1_bkaDQnQRyjy21LBaGOnz-mmkJ6T0N8,2840
63
63
  simo/core/__pycache__/app_widgets.cpython-38.pyc,sha256=9Es2wZNduzUJv-jZ_HX0-L3vqwpXWBbseEwoC5K6b-w,3465
@@ -69,19 +69,19 @@ simo/core/__pycache__/controllers.cpython-38.pyc,sha256=40wJ3mu0CnzexxDzXyDIlSxa
69
69
  simo/core/__pycache__/dynamic_settings.cpython-38.pyc,sha256=ELu06Hub4DOidja71ybvD3ZM4HdXiyZjNJrZfnXZXNA,2476
70
70
  simo/core/__pycache__/events.cpython-38.pyc,sha256=A1Axx-qftd1r7st7wkO3DkvTdt9-RkcJe5KJhpzJVk8,5109
71
71
  simo/core/__pycache__/filters.cpython-38.pyc,sha256=VIMADCBiYhziIyRmxAyUDJluZvuZmiC4bNYWTRsGSao,721
72
- simo/core/__pycache__/forms.cpython-38.pyc,sha256=U5pV_JyfoGwymn3tsws8FR2GrxihjKFa5dmtdzijexA,19071
72
+ simo/core/__pycache__/forms.cpython-38.pyc,sha256=d0ruviTG6zihcc9hJ1TvVxPODwe7LJCyo_ZpLIUt5pI,19850
73
73
  simo/core/__pycache__/gateways.cpython-38.pyc,sha256=XBiwMfBkjoQ2re6jvADJOwK0_0Aav-crzie9qtfqT9U,4599
74
74
  simo/core/__pycache__/loggers.cpython-38.pyc,sha256=Z-cdQnC6XlIonPV4Sl4E52tP4NMEdPAiHK0cFaIL7I8,1623
75
75
  simo/core/__pycache__/managers.cpython-38.pyc,sha256=5vstOMfm997CZBBkaSiaS7EojhLTWZlbeA_EQ8u-yfg,2554
76
76
  simo/core/__pycache__/middleware.cpython-38.pyc,sha256=ESR5JPtITo9flczO0672sfzYUxrc_cQU0e0w5DFL-60,2038
77
77
  simo/core/__pycache__/models.cpython-38.pyc,sha256=kYcojazryAAgZCXMoqqutIG2p8ofSKxza9TMx1SR2dQ,17233
78
- simo/core/__pycache__/permissions.cpython-38.pyc,sha256=uk2NsUixTKBM31T08YixwPO35TRp99hF5BmAf8bj5Vw,2720
78
+ simo/core/__pycache__/permissions.cpython-38.pyc,sha256=flJOCh94U8mFhE0XWzUD0sGR6Xe1HlfG4hQtNSnAGZ4,2788
79
79
  simo/core/__pycache__/routing.cpython-38.pyc,sha256=3T3FPJ8Cn99xZCGvMyg2xjl7al-Shm9CelbSpkJtNP8,599
80
- simo/core/__pycache__/serializers.cpython-38.pyc,sha256=x-obnrdPnoYRhriP_51_Jy_0saCsasWdFQSH1kalVqo,16869
80
+ simo/core/__pycache__/serializers.cpython-38.pyc,sha256=Pi9lddoS_DtMuA1iZ4mFHu_dG2mjouWxajM4udPQKqo,17185
81
81
  simo/core/__pycache__/signal_receivers.cpython-38.pyc,sha256=sgjH_wv-1U99auH5uHb3or0qettPeHAlsz8P7B03ajY,2430
82
82
  simo/core/__pycache__/socket_consumers.cpython-38.pyc,sha256=NJUr7nRyHFvmAumxxWpsod5wzVVZM99rCEuJs1utHA4,8432
83
83
  simo/core/__pycache__/storage.cpython-38.pyc,sha256=BTkYH8QQyjqI0WOtJC8fHNtgu0YA1vjqZclXjC2vCVI,1116
84
- simo/core/__pycache__/tasks.cpython-38.pyc,sha256=JTqXI_QPhU8u5c8MdhxILLKcn-5ZlIwtG1mGO4bgB7A,8474
84
+ simo/core/__pycache__/tasks.cpython-38.pyc,sha256=8VyzcG4psw5cS2oTy8W7EVW_nP7fzhoMvxXczTe1rcw,9134
85
85
  simo/core/__pycache__/todos.cpython-38.pyc,sha256=lOqGZ58siHM3isoJV4r7sg8igrfE9fFd-jSfeBa0AQI,253
86
86
  simo/core/__pycache__/views.cpython-38.pyc,sha256=YrKRZPaV_JM4VGpdhVhsK-90UwUTOqp-V-Yj0SRGZgs,4212
87
87
  simo/core/__pycache__/widgets.cpython-38.pyc,sha256=sR0ZeHCHrhnNDBJuRrxp3zUsfBp0xrtF0xrK2TkQv1o,3520
@@ -173,6 +173,7 @@ simo/core/migrations/0028_rename_subcomponents_component_slaves.py,sha256=ioQcfO
173
173
  simo/core/migrations/0029_auto_20240229_1331.py,sha256=BYXPNwjXApAx7mxE5li3QssfksWTsSjDf_VPQ8iGV8c,1140
174
174
  simo/core/migrations/0030_alter_instance_timezone.py,sha256=XZuYr2eD9MvE21Jxfdat8RC3sbc7PaGUfNPaqqxBNzE,23248
175
175
  simo/core/migrations/0031_auto_20240429_1231.py,sha256=kskD8dylxqg-l-ZQgxl6ZdZd7iNcJ52rOGPJFa9s-wk,829
176
+ simo/core/migrations/0032_auto_20240506_0834.py,sha256=w-dXSZVYZD0B6pRmMXLy27Y2twRR3WbN9WdNJCQqqwk,746
176
177
  simo/core/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
177
178
  simo/core/migrations/__pycache__/0001_initial.cpython-38.pyc,sha256=w6GiBXVxWj30Bg4Sn_pFVeA041d-pCrkaq8mR3KuF70,5381
178
179
  simo/core/migrations/__pycache__/0002_load_icons.cpython-38.pyc,sha256=Nb9RrPjVYo_RpZ5PmzoaEIWGCeVt4kicpmGiKlBrpIw,2123
@@ -205,6 +206,7 @@ simo/core/migrations/__pycache__/0028_rename_subcomponents_component_slaves.cpyt
205
206
  simo/core/migrations/__pycache__/0029_auto_20240229_1331.cpython-38.pyc,sha256=nO6lBU-1SxMQv67jQs7cGgNnot722UVL9pj3UE14I1k,1086
206
207
  simo/core/migrations/__pycache__/0030_alter_instance_timezone.cpython-38.pyc,sha256=paRgdijg5o8QzluT4GjSEQIMk6UAF7XmSwjqknXPE_4,16333
207
208
  simo/core/migrations/__pycache__/0031_auto_20240429_1231.cpython-38.pyc,sha256=Kl76gU1VUyTWMz9RIZUrbnb_xeteWLDSRfs_GtNCVow,863
209
+ simo/core/migrations/__pycache__/0032_auto_20240506_0834.cpython-38.pyc,sha256=UhrErBBhvAOPVph1x3ibQvSrcVUrITIsU9CGmjT8nLk,895
208
210
  simo/core/migrations/__pycache__/__init__.cpython-38.pyc,sha256=VZmDQ57BTcebuM0KMhjiTOabgWZCBxQmSJzWZos9SO8,169
209
211
  simo/core/static/ansi_styles.css,sha256=4ieJGrjZPKyPSago9FdB_gflHoGE1vxCHi8qVn5tY-Y,37352
210
212
  simo/core/static/admin/Img/plus.svg,sha256=2NpSFPWqGIjpAQGFI7LDQHPKagEhYkJiJX95ufCoZaI,741
@@ -10173,7 +10175,7 @@ simo/fleet/auto_urls.py,sha256=X04oKJWA48wFW5iXg3PPROY2KDdHn_a99orQSE28QC4,518
10173
10175
  simo/fleet/base_types.py,sha256=wL9RVkHr0gA7HI1wZq0pruGEIgvQqpfnCL4cC3ywsvw,102
10174
10176
  simo/fleet/ble.py,sha256=eHA_9ABjbmH1vUVCv9hiPXQL2GZZSEVwfO0xyI1S0nI,1081
10175
10177
  simo/fleet/controllers.py,sha256=rTxRFf-LKWAZxzixrsLZHHm51BmMx9a1PLdgf6inlNM,20533
10176
- simo/fleet/forms.py,sha256=QzHmAsWC258w2mFkQrnxL0k7h3Y9aN8oduEmrPGnqI4,38430
10178
+ simo/fleet/forms.py,sha256=ivP30jRtABtf1NkAuOaRRwZB7khHIJ7XwEzS5aCF480,36881
10177
10179
  simo/fleet/gateways.py,sha256=KV5i5fxXIrlK-k6zyEkk83x11GJt-ELQ0npb4Ac83cM,3693
10178
10180
  simo/fleet/managers.py,sha256=XOpDOA9L-f_550TNSyXnJbun2EmtGz1TenVTMlUSb8E,807
10179
10181
  simo/fleet/models.py,sha256=ve-97F1cwGt-AmwfSJK0d-57pP3NyZpeu0XlHu2oK28,14494
@@ -10190,7 +10192,7 @@ simo/fleet/__pycache__/auto_urls.cpython-38.pyc,sha256=SqyTuaz_kEBvx-bL46SclsZEE
10190
10192
  simo/fleet/__pycache__/base_types.cpython-38.pyc,sha256=deyPwjpT6xZiFxBGFnj5b7R-lbdOTh2krgpJhrcGVhc,274
10191
10193
  simo/fleet/__pycache__/ble.cpython-38.pyc,sha256=Nrof9w7cm4OlpFWHeVnmvvanh2_oF9oQ3TknJiV93-0,1267
10192
10194
  simo/fleet/__pycache__/controllers.cpython-38.pyc,sha256=l9bz18Qp33C12TJOKPSn9vIXnlBKnBusODNk7Fg64qA,18103
10193
- simo/fleet/__pycache__/forms.cpython-38.pyc,sha256=H3-EYmeHeNWIghaKBqduJk3DAKvayStrPrNp2i43274,27751
10195
+ simo/fleet/__pycache__/forms.cpython-38.pyc,sha256=TH4NC-otAl9V6YlvrffE9sghlT-tw_3RFK1ztO-fzfg,27368
10194
10196
  simo/fleet/__pycache__/gateways.cpython-38.pyc,sha256=YAcgTOqJbtjGI03lvEcU6keFfrwAHkObVmErYzfRvjk,3569
10195
10197
  simo/fleet/__pycache__/managers.cpython-38.pyc,sha256=8uz-xpUiqbGDgXIZ_XRZtFb-Tju6NGxflGg-Ee4Yo6k,1310
10196
10198
  simo/fleet/__pycache__/models.cpython-38.pyc,sha256=pHNRUiPRjH0SLp14pzbSIxHi_-27SpZFgSh_7lzA8Wo,12359
@@ -10274,18 +10276,18 @@ simo/generic/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10274
10276
  simo/generic/app_widgets.py,sha256=E_pnpA1hxMIhenRCrHoQ5cik06jm2BAHCkl_eo-OudU,1264
10275
10277
  simo/generic/base_types.py,sha256=djymox_boXTHX1BTTCLXrCH7ED-uAsV_idhaDOc3OLI,409
10276
10278
  simo/generic/controllers.py,sha256=WYuOUzDWvkYRaTvlbdGy_qmwp1o_ohqKDfV7OrOq2QU,52218
10277
- simo/generic/forms.py,sha256=U6MIS5U7jLG6XabqCZvrfSoiNCQiWZYE8VZPnyN3SJw,23757
10279
+ simo/generic/forms.py,sha256=uLZ25Fw1xGqyDBngWPve9EaHpyldFegY_TPBKV4Oby0,24051
10278
10280
  simo/generic/gateways.py,sha256=b3tQ2bAkDVYXCF5iZi2yi-6nZAM8WmHE9ICwxMyR0to,17034
10279
- simo/generic/models.py,sha256=ni1cgqVZ8ttk1snW26EB-2dqsHRZkLg6463my_Cu3EU,6618
10281
+ simo/generic/models.py,sha256=92TACMhJHadAg0TT9GnARO_R3_Sl6i-GGjhG_x7YdFI,7391
10280
10282
  simo/generic/routing.py,sha256=elQVZmgnPiieEuti4sJ7zITk1hlRxpgbotcutJJgC60,228
10281
10283
  simo/generic/socket_consumers.py,sha256=NfTQGYtVAc864IoogZRxf_0xpDPM0eMCWn0SlKA5P7Y,1751
10282
10284
  simo/generic/__pycache__/__init__.cpython-38.pyc,sha256=mLu54WS9KIl-pHwVCBKpsDFIlOqml--JsOVzAUHg6cU,161
10283
10285
  simo/generic/__pycache__/app_widgets.cpython-38.pyc,sha256=0IoKRG9n1tkNRRkrqAeOQwWBPd_33u98JBcVtMVVCio,2374
10284
10286
  simo/generic/__pycache__/base_types.cpython-38.pyc,sha256=ptw6axyAqemZA35oa6vzr7EihzvbhW9w7Y-G6kfDedU,555
10285
10287
  simo/generic/__pycache__/controllers.cpython-38.pyc,sha256=e0bvgyePgJbIs1omBq0TRPlVSKar2sK_JbUKqDRj7mY,33235
10286
- simo/generic/__pycache__/forms.cpython-38.pyc,sha256=lqpRTX5rnrkyJZ5Dga4e9y2WJ81r394AW5DV4KkvxXQ,17856
10288
+ simo/generic/__pycache__/forms.cpython-38.pyc,sha256=U67jiwt6R8J547n7d84-_9mUxCXxKl-L0VFdYfo1aiM,17787
10287
10289
  simo/generic/__pycache__/gateways.cpython-38.pyc,sha256=a4lLIMPyxm9tNzIqorXHIPZFVTcXlPsM1ycJMghxcHA,12673
10288
- simo/generic/__pycache__/models.cpython-38.pyc,sha256=yr64EX5n9u9uaOHNbPhNIBoBJo4Y3V_lvDj1ggMAZCQ,5123
10290
+ simo/generic/__pycache__/models.cpython-38.pyc,sha256=PzlZsM1jxo3FVb7QDm3bny8UFwTsGrMQe4mj4tJ06eQ,5675
10289
10291
  simo/generic/__pycache__/routing.cpython-38.pyc,sha256=xtxTUTBTdivzFyA5Wh7k-hUj1WDO_FiRq6HYXdbr9Ks,382
10290
10292
  simo/generic/__pycache__/socket_consumers.cpython-38.pyc,sha256=piFHces0J9QuXu_CNBCQCYjoZEeoaxyVjLfJ9KaR8C8,1898
10291
10293
  simo/generic/static/weather_icons/01d@2x.png,sha256=TZfWi6Rfddb2P-oldWWcjUiuCHiU9Yrc5hyrQAhF26I,948
@@ -10346,7 +10348,7 @@ simo/notifications/admin.py,sha256=y_gmHYXbDh98LUUa-lp9DilTIgM6-pIujWPQPLQsJo8,8
10346
10348
  simo/notifications/api.py,sha256=GXQpq68ULBaJpU8w3SJKaCKuxYGWYehKnGeocGB1RVc,1783
10347
10349
  simo/notifications/models.py,sha256=VZcvweii59j89nPKlWeUSJ44Qz3ZLjJ6mXN6uB9F1Sw,2506
10348
10350
  simo/notifications/serializers.py,sha256=altDEAPWwOhxRcEzE9-34jL8EFpyf3vPoEdAPoVLfGc,523
10349
- simo/notifications/utils.py,sha256=5CtKOvX0vbWLXFvJD_8WfWulMEp1FuMwrfCGDLHySdA,907
10351
+ simo/notifications/utils.py,sha256=VCBKcHrtZrdCalKe2w7umt7-trhG3vw-fKn1ndyr3CA,971
10350
10352
  simo/notifications/__pycache__/__init__.cpython-38.pyc,sha256=YvucUfu98XFvEEg1LYFMlOZJpo_jSGxTVrM-ylAFLOg,167
10351
10353
  simo/notifications/__pycache__/admin.cpython-38.pyc,sha256=Fl4crSZTFQOTYQioV6ff9fBRV4MhNiwQgMS2VnmCI4M,1632
10352
10354
  simo/notifications/__pycache__/api.cpython-38.pyc,sha256=ys6E4AFghX6bq-rQ0gtA9s0Y2Hh-ypsWH8-Yz4edMrc,2073
@@ -10365,7 +10367,7 @@ simo/users/auth_backends.py,sha256=I5pnaTa20-Lxfw_dFG8471xDITb0_fQl1PVhJalp5vU,3
10365
10367
  simo/users/auto_urls.py,sha256=lcJvteBsbHQMJieZpDz-63tDYejLApqsW3CUnDakd7k,272
10366
10368
  simo/users/dynamic_settings.py,sha256=sEIsi4yJw3kH46Jq_aOkSuK7QTfQACGUE-lkyBogCaM,570
10367
10369
  simo/users/middleware.py,sha256=GMCrnWSc_2qCleyQIkfQGdL-pU-UTEcSg1wPvIKZ9uk,1210
10368
- simo/users/models.py,sha256=32Qv3d1gBqagX67irTmUzMY_svOhJA5ECK1TvXshRWw,18865
10370
+ simo/users/models.py,sha256=VJWjqDXZpCzsrTYeXmxc8JrsxmK0Ml9-X9eOHj4BP78,19005
10369
10371
  simo/users/permissions.py,sha256=IwtYS8yQdupWbYKR9VimSRDV3qCJ2jXP57Lyjpb2EQM,242
10370
10372
  simo/users/serializers.py,sha256=e6yIUsO7BfvrZ4IQHBn-FdpAUMgic5clmGQdTtRlGRY,2515
10371
10373
  simo/users/sso_urls.py,sha256=gQOaPvGMYFD0NCVSwyoWO-mTEHe5j9sbzV_RK7kdvp0,251
@@ -10380,7 +10382,7 @@ simo/users/__pycache__/auth_backends.cpython-38.pyc,sha256=MuOieBIXt6lrDx83-UQtd
10380
10382
  simo/users/__pycache__/auto_urls.cpython-38.pyc,sha256=K-3sz2h-cEitoflSmZk1t0eUg5mQMMGLNZFREVwG7_o,430
10381
10383
  simo/users/__pycache__/dynamic_settings.cpython-38.pyc,sha256=6F8JBjZkHykySnmZjNEzjS0ijbmPdcp9yUAZ5kqq_Fo,864
10382
10384
  simo/users/__pycache__/middleware.cpython-38.pyc,sha256=Tj4nVEAvxEW3xA63fBRiJWRJpz_M848ZOqbHioc_IPE,1149
10383
- simo/users/__pycache__/models.cpython-38.pyc,sha256=cyTY-fgeERHebof7-MSxJXfwlWkrwWZyqKT9fl198z0,17614
10385
+ simo/users/__pycache__/models.cpython-38.pyc,sha256=Hui7Uu0kCIENoQuDZ5xoCHVNqsoI_hygaxKAgqqyPo8,17739
10384
10386
  simo/users/__pycache__/permissions.cpython-38.pyc,sha256=ez5NxoL_JUeeH6GsKhvFreuA3FCBgGf9floSypdXUtM,633
10385
10387
  simo/users/__pycache__/serializers.cpython-38.pyc,sha256=ylapsfu5qbSzbfX2lG3uc4wV6hhndFbIvI109lhhKOo,3461
10386
10388
  simo/users/__pycache__/sso_urls.cpython-38.pyc,sha256=uAwDozpOmrhUald-8tOHANILXkH7-TI8fNYXOtPkSY8,402
@@ -10414,6 +10416,8 @@ simo/users/migrations/0023_auto_20240105_0719.py,sha256=OAkWJusXjzT6dx4EgwjvNvME
10414
10416
  simo/users/migrations/0024_fingerprint.py,sha256=0wfplJ3iHv_6heJx7yIQYX3D68Nf9pLPlIZoM5NcPk8,1021
10415
10417
  simo/users/migrations/0025_rename_name_fingerprint_type_and_more.py,sha256=Azw_a1qxIDttdG4m0DcLa82amv-mFsbW8PsjG9qFL0Y,466
10416
10418
  simo/users/migrations/0026_fingerprint_name.py,sha256=DPmfi1brbaPymdNiPgc7dINSKy97yVHdKpKp-ZfnS3I,428
10419
+ simo/users/migrations/0027_permissionsrole_can_manage_components.py,sha256=VcGZE6u-q6UkGo7D01K_T1XBtIvIGe8SCk5ZPRrPpGo,485
10420
+ simo/users/migrations/0028_auto_20240506_1146.py,sha256=7RUFF2rJH-bnPeHwc77p8Q4kEAc3owyG4qp9Kc4aKhU,716
10417
10421
  simo/users/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10418
10422
  simo/users/migrations/__pycache__/0001_initial.cpython-38.pyc,sha256=ngXA1QR-Qc2VS-BTTZWybVXiEfifIgKaVS6bADiN8nU,4269
10419
10423
  simo/users/migrations/__pycache__/0002_componentpermission.cpython-38.pyc,sha256=pknJnpic8p6Vdx9DX41FfODXNnvexDswJtUCmC5w1tg,995
@@ -10441,6 +10445,8 @@ simo/users/migrations/__pycache__/0023_auto_20240105_0719.cpython-38.pyc,sha256=
10441
10445
  simo/users/migrations/__pycache__/0024_fingerprint.cpython-38.pyc,sha256=QLW3v4BK78HL682qeHkK_aXZgCJSs9sIIte60ieF5ZY,1132
10442
10446
  simo/users/migrations/__pycache__/0025_rename_name_fingerprint_type_and_more.cpython-38.pyc,sha256=21CgVotrTT02MW58zqePt_-gsbpdN_m01T_SOHgWkBo,631
10443
10447
  simo/users/migrations/__pycache__/0026_fingerprint_name.cpython-38.pyc,sha256=Ti0NLIKb0Wffn33LCBQQ-cumFCX6JFxSi1FYoV8C0ZE,642
10448
+ simo/users/migrations/__pycache__/0027_permissionsrole_can_manage_components.cpython-38.pyc,sha256=Ju4FLSKUNoJ419bAp_Np_MKrSyxzlJnYGfeNYg7HC5M,721
10449
+ simo/users/migrations/__pycache__/0028_auto_20240506_1146.cpython-38.pyc,sha256=e8J_lTYJMixdqV37OgonG9ndd9gjn1E9hVj-jh4bxGw,874
10444
10450
  simo/users/migrations/__pycache__/__init__.cpython-38.pyc,sha256=NKq7WLgktK8WV1oOqCPbAbdkrPV5GRGhYx4VxxI4dcs,170
10445
10451
  simo/users/templates/conf/mosquitto.conf,sha256=1eIGNuRu4Y3hfAU6qiWix648eCRrw0oOT24PnyFI4ys,189
10446
10452
  simo/users/templates/conf/mosquitto_acls.conf,sha256=ga44caTDNQE0CBKw55iM2jOuna6-9fKGwAhjyERZdRE,500
@@ -10450,8 +10456,8 @@ simo/users/templates/invitations/expired_msg.html,sha256=47DEQpj8HBSa-_TImW-5JCe
10450
10456
  simo/users/templates/invitations/expired_suggestion.html,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10451
10457
  simo/users/templates/invitations/taken_msg.html,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10452
10458
  simo/users/templates/invitations/taken_suggestion.html,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10453
- simo-2.0.25.dist-info/LICENSE.md,sha256=M7wm1EmMGDtwPRdg7kW4d00h1uAXjKOT3HFScYQMeiE,34916
10454
- simo-2.0.25.dist-info/METADATA,sha256=z4jsvR3Vf9Zbu9KiGZv12Q0IwiW5FHorEehCTxsVjx0,1730
10455
- simo-2.0.25.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
10456
- simo-2.0.25.dist-info/top_level.txt,sha256=GmS1hrAbpVqn9OWZh6UX82eIOdRLgYA82RG9fe8v4Rs,5
10457
- simo-2.0.25.dist-info/RECORD,,
10459
+ simo-2.0.27.dist-info/LICENSE.md,sha256=M7wm1EmMGDtwPRdg7kW4d00h1uAXjKOT3HFScYQMeiE,34916
10460
+ simo-2.0.27.dist-info/METADATA,sha256=ZfFs5I2gydjhtWLdXn1RkHW05xtXHAe7R9uZed92SUQ,1730
10461
+ simo-2.0.27.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
10462
+ simo-2.0.27.dist-info/top_level.txt,sha256=GmS1hrAbpVqn9OWZh6UX82eIOdRLgYA82RG9fe8v4Rs,5
10463
+ simo-2.0.27.dist-info/RECORD,,
File without changes