simo 2.0.35__py3-none-any.whl → 2.0.38__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 (31) hide show
  1. simo/core/__pycache__/api.cpython-38.pyc +0 -0
  2. simo/core/__pycache__/controllers.cpython-38.pyc +0 -0
  3. simo/core/__pycache__/forms.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/controllers.py +10 -0
  7. simo/core/forms.py +15 -1
  8. simo/core/serializers.py +0 -1
  9. simo/core/utils/__pycache__/formsets.cpython-38.pyc +0 -0
  10. simo/core/utils/__pycache__/operations.cpython-38.pyc +0 -0
  11. simo/core/utils/formsets.py +20 -2
  12. simo/core/utils/operations.py +10 -0
  13. simo/fleet/__pycache__/controllers.cpython-38.pyc +0 -0
  14. simo/fleet/__pycache__/forms.cpython-38.pyc +0 -0
  15. simo/fleet/__pycache__/models.cpython-38.pyc +0 -0
  16. simo/fleet/forms.py +122 -3
  17. simo/generic/__pycache__/controllers.cpython-38.pyc +0 -0
  18. simo/generic/__pycache__/forms.cpython-38.pyc +0 -0
  19. simo/generic/__pycache__/gateways.cpython-38.pyc +0 -0
  20. simo/generic/controllers.py +101 -1
  21. simo/generic/forms.py +144 -5
  22. simo/generic/gateways.py +16 -7
  23. simo/users/__pycache__/api.cpython-38.pyc +0 -0
  24. simo/users/__pycache__/serializers.cpython-38.pyc +0 -0
  25. simo/users/api.py +3 -1
  26. simo/users/serializers.py +3 -2
  27. {simo-2.0.35.dist-info → simo-2.0.38.dist-info}/METADATA +1 -1
  28. {simo-2.0.35.dist-info → simo-2.0.38.dist-info}/RECORD +31 -29
  29. {simo-2.0.35.dist-info → simo-2.0.38.dist-info}/LICENSE.md +0 -0
  30. {simo-2.0.35.dist-info → simo-2.0.38.dist-info}/WHEEL +0 -0
  31. {simo-2.0.35.dist-info → simo-2.0.38.dist-info}/top_level.txt +0 -0
Binary file
Binary file
Binary file
simo/core/controllers.py CHANGED
@@ -180,6 +180,16 @@ class ControllerBase(ABC):
180
180
  else:
181
181
  return self.component.change_init_by
182
182
 
183
+ def _string_to_vals(self, v):
184
+ '''
185
+ Convert a string containing list of values to approporiate list of component values
186
+ :param v:
187
+ :return:
188
+ '''
189
+ val_type = type(self.default_value)
190
+ v = str(v).strip('(').strip('[').rstrip(')').rstrip(']')
191
+ return [val_type(val.strip()) for val in v.split(',')]
192
+
183
193
  def send(self, value):
184
194
  self.component.refresh_from_db()
185
195
 
simo/core/forms.py CHANGED
@@ -291,7 +291,7 @@ class ComponentAdminForm(forms.ModelForm):
291
291
 
292
292
  class Meta:
293
293
  model = Component
294
- fields = 'name', 'icon', 'zone', 'category', 'show_in_app', 'notes'
294
+ fields = 'name', 'icon', 'zone', 'category', 'show_in_app', 'notes',
295
295
  widgets = {
296
296
  'icon': autocomplete.ModelSelect2(
297
297
  url='autocomplete-icon', attrs={'data-html': True}
@@ -466,6 +466,13 @@ class NumericSensorForm(BaseComponentForm):
466
466
  ValueLimitForm, can_delete=True, can_order=True, extra=0, max_num=3
467
467
  ), label="Graph Limits"
468
468
  )
469
+ value_units = forms.CharField(required=False)
470
+
471
+
472
+ def __init__(self, *args, **kwargs):
473
+ super().__init__(*args, **kwargs)
474
+ self.fields['value_units'].initial = self.controller.default_value_units
475
+
469
476
 
470
477
 
471
478
  class MultiSensorConfigForm(BaseComponentForm):
@@ -660,6 +667,7 @@ class DimmerConfigForm(BaseComponentForm):
660
667
  max = forms.FloatField(
661
668
  initial=1.0, help_text="Maximum component value."
662
669
  )
670
+ value_units = forms.CharField(required=False)
663
671
  inverse = forms.BooleanField(
664
672
  label=_("Inverse dimmer signal"), required=False
665
673
  )
@@ -676,6 +684,7 @@ class DimmerConfigForm(BaseComponentForm):
676
684
 
677
685
  def __init__(self, *args, **kwargs):
678
686
  super().__init__(*args, **kwargs)
687
+ self.fields['value_units'].initial = self.controller.default_value_units
679
688
  if self.instance.pk:
680
689
  self.fields['slaves'].initial = self.instance.slaves.all()
681
690
 
@@ -704,6 +713,11 @@ class DimmerPlusConfigForm(BaseComponentForm):
704
713
  secondary_max = forms.FloatField(
705
714
  initial=1.0, help_text="Maximum secondary value."
706
715
  )
716
+ value_units = forms.CharField(required=False)
717
+
718
+ def __init__(self, *args, **kwargs):
719
+ super().__init__(*args, **kwargs)
720
+ self.fields['value_units'].initial = self.controller.default_value_units
707
721
 
708
722
 
709
723
  class RGBWConfigForm(BaseComponentForm):
simo/core/serializers.py CHANGED
@@ -375,7 +375,6 @@ class ComponentSerializer(FormSerializer):
375
375
  user_role = self.context['request'].user.get_role(
376
376
  self.context['instance']
377
377
  )
378
- print("FORM BASIC FIELDS: ", form.basic_fields)
379
378
  if not user_role.is_superuser and user_role.is_owner:
380
379
  for field_name in list(form.fields.keys()):
381
380
  if field_name not in form.basic_fields:
@@ -1,4 +1,3 @@
1
- import copy
2
1
  from django import forms
3
2
  from django.db import models
4
3
  from django.template.loader import render_to_string
@@ -148,10 +147,21 @@ class FormsetField(forms.Field):
148
147
  if formset_data.get('%s-%d-DELETE' % (prefix, i)) == 'on':
149
148
  continue
150
149
  form_data = {}
150
+
151
151
  for field_name, field in self.formset_cls().form.declared_fields.items():
152
152
  form_data[field_name] = formset_data.get(
153
153
  '%s-%d-%s' % (prefix, i, field_name)
154
154
  )
155
+
156
+ f_data = {}
157
+ for key, val in form_data.items():
158
+ f_data[f"{self.formset_cls.form.prefix}-{key}"] = val
159
+ form = self.formset_cls.form(f_data)
160
+ if form.is_valid():
161
+ form_data = form.cleaned_data
162
+
163
+ for field_name, field in self.formset_cls().form.declared_fields.items():
164
+
155
165
  if isinstance(field, forms.models.ModelChoiceField):
156
166
  if isinstance(form_data[field_name], models.Model):
157
167
  form_data[field_name] = form_data[field_name].pk
@@ -164,13 +174,21 @@ class FormsetField(forms.Field):
164
174
  elif isinstance(field, forms.fields.BooleanField):
165
175
  form_data[field_name] = form_data[field_name] == 'on'
166
176
  elif isinstance(field, forms.fields.IntegerField):
167
- form_data[field_name] = int(form_data[field_name])
177
+ try:
178
+ form_data[field_name] = int(form_data[field_name])
179
+ except:
180
+ form_data[field_name] = None
181
+
168
182
  if self.widget.formset.can_order:
169
183
  form_data['order'] = int(formset_data.get(
170
184
  '%s-%d-ORDER' % (prefix, i), 0
171
185
  ))
186
+
187
+
172
188
  cleaned_value.append(form_data)
173
189
 
190
+
191
+
174
192
  if self.widget.formset.can_order:
175
193
  cleaned_value = sorted(cleaned_value, key=lambda d: d['order'])
176
194
  for i in range(len(cleaned_value)):
@@ -0,0 +1,10 @@
1
+ import operator
2
+
3
+ OPERATIONS = {
4
+ '==': operator.eq,
5
+ '>': operator.gt,
6
+ '>=': operator.ge,
7
+ '<': operator.lt,
8
+ '<=': operator.le,
9
+ 'in': lambda a, b: a in b
10
+ }
Binary file
simo/fleet/forms.py CHANGED
@@ -84,8 +84,7 @@ class InterfaceAdminForm(forms.ModelForm):
84
84
 
85
85
  class ColonelComponentForm(BaseComponentForm):
86
86
  colonel = forms.ModelChoiceField(
87
- label="Colonel", queryset=Colonel.objects.all(),
88
- help_text="ATENTION! Changing Colonel after component creation is not recommended!"
87
+ label="Colonel", queryset=Colonel.objects.all()
89
88
  )
90
89
 
91
90
  def clean_colonel(self):
@@ -133,6 +132,7 @@ class ColonelComponentForm(BaseComponentForm):
133
132
  updated_vals[key] = int(val)
134
133
  self.cleaned_data['controls'][i] = updated_vals
135
134
 
135
+ pins_in_use = []
136
136
  formset_errors = {}
137
137
  for i, control in enumerate(self.cleaned_data['controls']):
138
138
  if pin_instances[i].colonel != self.cleaned_data['colonel']:
@@ -144,6 +144,11 @@ class ColonelComponentForm(BaseComponentForm):
144
144
  formset_errors[i] = {
145
145
  'pin': f"{pin_instances[i]} is already occupied by {pin_instances[i].occupied_by}!"
146
146
  }
147
+ elif pin_instances[i].no in pins_in_use:
148
+ formset_errors[i] = {
149
+ 'pin': f"{pin_instances[i].no} is already in use!"
150
+ }
151
+ pins_in_use.append(pin_instances[i].no)
147
152
 
148
153
  errors_list = []
149
154
  if formset_errors:
@@ -214,6 +219,22 @@ class ColonelBinarySensorConfigForm(ColonelComponentForm):
214
219
  "Set debounce value in milliseconds, to remediate this. "
215
220
  "50ms offers a good starting point!"
216
221
  )
222
+ hold_time = forms.TypedChoiceField(
223
+ initial=0, coerce=int, choices=(
224
+ (0, "-----"),
225
+ (1, "10 s"), (2, "20 s"), (3, "30 s"), (4, "40 s"), (5, "50 s"),
226
+ (6, "1 min"), (9, "1.5 min"), (12, "2 min"), (18, "3 min"),
227
+ (30, "5 min"), (60, "10 min"), (120, "20 min"),
228
+ ), required=False,
229
+ help_text="Holds positive value for given amount of time "
230
+ "after last negative value has been observed. "
231
+ "Super useful with regular motion detectors for controlling "
232
+ "lights or other means of automation."
233
+ )
234
+
235
+ def __init__(self, *args, **kwargs):
236
+ super().__init__(*args, **kwargs)
237
+ self.basic_fields.append('hold_time')
217
238
 
218
239
  def clean(self):
219
240
  super().clean()
@@ -424,6 +445,17 @@ class BME680SensorConfigForm(ColonelComponentForm):
424
445
 
425
446
  )
426
447
 
448
+ def clean(self):
449
+ if not self.cleaned_data.get('colonel'):
450
+ return self.cleaned_data
451
+ if self.cleaned_data['interface'].colonel != self.cleaned_data['colonel']:
452
+ self.add_error(
453
+ 'interface',
454
+ f"This interface is on {self.cleaned_data['interface'].colonel}, "
455
+ f"however we need an interface from {self.cleaned_data['colonel']}."
456
+ )
457
+ return self.cleaned_data
458
+
427
459
  def save(self, commit=True):
428
460
  if 'interface' in self.cleaned_data:
429
461
  self.instance.config['i2c_interface'] = self.cleaned_data['interface'].no
@@ -455,6 +487,17 @@ class MPC9808SensorConfigForm(ColonelComponentForm):
455
487
 
456
488
  )
457
489
 
490
+ def clean(self):
491
+ if not self.cleaned_data.get('colonel'):
492
+ return self.cleaned_data
493
+ if self.cleaned_data['interface'].colonel != self.cleaned_data['colonel']:
494
+ self.add_error(
495
+ 'interface',
496
+ f"This interface is on {self.cleaned_data['interface'].colonel}, "
497
+ f"however we need an interface from {self.cleaned_data['colonel']}."
498
+ )
499
+ return self.cleaned_data
500
+
458
501
  def save(self, commit=True):
459
502
  if 'interface' in self.cleaned_data:
460
503
  self.instance.config['i2c_interface'] = self.cleaned_data['interface'].no
@@ -560,6 +603,14 @@ class ColonelSwitchConfigForm(ColonelComponentForm):
560
603
  if self.cleaned_data.get('controls'):
561
604
  self._clean_controls()
562
605
 
606
+ if self.cleaned_data.get('output_pin') and self.cleaned_data.get('controls'):
607
+ for ctrl in self.cleaned_data['controls']:
608
+ if ctrl['pin'] == self.cleaned_data['output_pin']:
609
+ self.add_error(
610
+ "output_pin",
611
+ "Can't be used as control pin at the same time!"
612
+ )
613
+
563
614
  return self.cleaned_data
564
615
 
565
616
 
@@ -592,6 +643,7 @@ class ColonelPWMOutputConfigForm(ColonelComponentForm):
592
643
  required=True, initial=100,
593
644
  help_text="Maximum component value"
594
645
  )
646
+ value_units = forms.CharField(required=False)
595
647
  duty_min = forms.IntegerField(
596
648
  min_value=0, max_value=1023, required=True, initial=0,
597
649
  help_text="Minumum PWM signal output duty (0 - 1023)"
@@ -621,6 +673,7 @@ class ColonelPWMOutputConfigForm(ColonelComponentForm):
621
673
  required=True, initial=100,
622
674
  help_text="Component ON value when used with toggle switch"
623
675
  )
676
+
624
677
  slaves = forms.ModelMultipleChoiceField(
625
678
  required=False,
626
679
  queryset=Component.objects.filter(
@@ -639,7 +692,10 @@ class ColonelPWMOutputConfigForm(ColonelComponentForm):
639
692
 
640
693
  def __init__(self, *args, **kwargs):
641
694
  super().__init__(*args, **kwargs)
642
- self.basic_fields.extend(['turn_on_time', 'turn_off_time', 'skew'])
695
+ self.fields['value_units'].initial = self.controller.default_value_units
696
+ self.basic_fields.extend(
697
+ ['value_units', 'turn_on_time', 'turn_off_time', 'skew']
698
+ )
643
699
  if self.instance.pk and 'slaves' in self.fields:
644
700
  self.fields['slaves'].initial = self.instance.slaves.all()
645
701
 
@@ -654,6 +710,13 @@ class ColonelPWMOutputConfigForm(ColonelComponentForm):
654
710
  self._clean_pin('output_pin')
655
711
  if 'controls' in self.cleaned_data:
656
712
  self._clean_controls()
713
+ if self.cleaned_data.get('output_pin') and self.cleaned_data.get('controls'):
714
+ for ctrl in self.cleaned_data['controls']:
715
+ if ctrl['pin'] == self.cleaned_data['output_pin']:
716
+ self.add_error(
717
+ "output_pin",
718
+ "Can't be used as control pin at the same time!"
719
+ )
657
720
  return self.cleaned_data
658
721
 
659
722
 
@@ -754,6 +817,14 @@ class ColonelRGBLightConfigForm(ColonelComponentForm):
754
817
  if self.cleaned_data.get('controls'):
755
818
  self._clean_controls()
756
819
 
820
+ if self.cleaned_data.get('output_pin') and self.cleaned_data.get('controls'):
821
+ for ctrl in self.cleaned_data['controls']:
822
+ if ctrl['pin'] == self.cleaned_data['output_pin']:
823
+ self.add_error(
824
+ "output_pin",
825
+ "Can't be used as control pin at the same time!"
826
+ )
827
+
757
828
  if 'color_order' in self.cleaned_data:
758
829
  if self.cleaned_data.get('color_order'):
759
830
  if self.cleaned_data['has_white']:
@@ -818,6 +889,12 @@ class DualMotorValveForm(ColonelComponentForm):
818
889
  self._clean_pin('open_pin')
819
890
  if self.cleaned_data.get('close_pin'):
820
891
  self._clean_pin('close_pin')
892
+ if self.cleaned_data.get('open_pin') \
893
+ and self.cleaned_data.get('close_pin') \
894
+ and self.cleaned_data['open_pin'] == self.cleaned_data['close_pin']:
895
+ self.add_error(
896
+ 'close_pin', "Can't be the same as open pin!"
897
+ )
821
898
  return self.cleaned_data
822
899
 
823
900
  def save(self, commit=True):
@@ -905,6 +982,13 @@ class BlindsConfigForm(ColonelComponentForm):
905
982
  def clean(self):
906
983
  super().clean()
907
984
 
985
+ if self.cleaned_data.get('open_pin') \
986
+ and self.cleaned_data.get('close_pin') \
987
+ and self.cleaned_data['open_pin'] == self.cleaned_data['close_pin']:
988
+ self.add_error(
989
+ 'close_pin', "Can't be the same as open pin!"
990
+ )
991
+
908
992
  if self.cleaned_data.get('open_pin'):
909
993
  self._clean_pin('open_pin')
910
994
  if self.cleaned_data.get('close_pin'):
@@ -926,6 +1010,23 @@ class BlindsConfigForm(ColonelComponentForm):
926
1010
  return self.cleaned_data
927
1011
 
928
1012
  self._clean_controls()
1013
+
1014
+ if self.cleaned_data.get('open_pin'):
1015
+ for ctrl in self.cleaned_data['controls']:
1016
+ if ctrl['pin'] == self.cleaned_data['output_pin']:
1017
+ self.add_error(
1018
+ "open_pin",
1019
+ "Can't be used as control pin at the same time!"
1020
+ )
1021
+
1022
+ if self.cleaned_data.get('close_pin'):
1023
+ for ctrl in self.cleaned_data['controls']:
1024
+ if ctrl['pin'] == self.cleaned_data['close_pin']:
1025
+ self.add_error(
1026
+ "close_pin",
1027
+ "Can't be used as control pin at the same time!"
1028
+ )
1029
+
929
1030
  return self.cleaned_data
930
1031
 
931
1032
  def save(self, commit=True):
@@ -974,6 +1075,13 @@ class BurglarSmokeDetectorConfigForm(ColonelComponentForm):
974
1075
  if 'power_pin' in self.cleaned_data:
975
1076
  self._clean_pin('power_pin')
976
1077
 
1078
+ if self.cleaned_data.get('sensor_pin') \
1079
+ and self.cleaned_data.get('power_pin') \
1080
+ and self.cleaned_data['sensor_pin'] == self.cleaned_data['power_pin']:
1081
+ self.add_error(
1082
+ 'power_pin', "Can't be the same as sensor pin!"
1083
+ )
1084
+
977
1085
  return self.cleaned_data
978
1086
 
979
1087
  def save(self, commit=True):
@@ -1049,6 +1157,17 @@ class DALIDeviceConfigForm(ColonelComponentForm):
1049
1157
  )
1050
1158
  return self.cleaned_data['interface']
1051
1159
 
1160
+ def clean(self):
1161
+ if not self.cleaned_data.get('colonel'):
1162
+ return self.cleaned_data
1163
+ if self.cleaned_data['interface'].colonel != self.cleaned_data['colonel']:
1164
+ self.add_error(
1165
+ 'interface',
1166
+ f"This interface is on {self.cleaned_data['interface'].colonel}, "
1167
+ f"however we need an interface from {self.cleaned_data['colonel']}."
1168
+ )
1169
+ return self.cleaned_data
1170
+
1052
1171
  def save(self, commit=True, update_colonel_config=True):
1053
1172
  if 'interface' in self.cleaned_data:
1054
1173
  self.instance.config['dali_interface'] = \
@@ -15,6 +15,7 @@ from simo.users.utils import get_system_user
15
15
  from simo.core.events import GatewayObjectCommand
16
16
  from simo.core.models import RUN_STATUS_CHOICES_MAP, Component
17
17
  from simo.core.utils.helpers import get_random_string
18
+ from simo.core.utils.operations import OPERATIONS
18
19
  from simo.core.controllers import (
19
20
  BEFORE_SEND, BEFORE_SET, ControllerBase,
20
21
  BinarySensor, NumericSensor, MultiSensor, Switch, Dimmer, DimmerPlus,
@@ -35,7 +36,8 @@ from .app_widgets import (
35
36
  WateringWidget, StateSelectWidget, AlarmClockWidget
36
37
  )
37
38
  from .forms import (
38
- ScriptConfigForm, ThermostatConfigForm, AlarmGroupConfigForm,
39
+ ScriptConfigForm, PresenceLightingConfigForm,
40
+ ThermostatConfigForm, AlarmGroupConfigForm,
39
41
  IPCameraConfigForm, WeatherForecastForm, GateConfigForm,
40
42
  BlindsConfigForm, WateringConfigForm, StateSelectForm,
41
43
  AlarmClockConfigForm
@@ -99,6 +101,104 @@ class Script(ControllerBase, TimerMixin):
99
101
  self.send('start')
100
102
 
101
103
 
104
+ class PresenceLighting(Script):
105
+ name = _("Presence lighting")
106
+ config_form = PresenceLightingConfigForm
107
+
108
+ # script specific variables
109
+ sensors = {}
110
+ light_org_values = {}
111
+ is_on = None
112
+ turn_off_task = None
113
+
114
+ def _run(self):
115
+ while True:
116
+ self._on_sensor()
117
+ time.sleep(10)
118
+
119
+ def _on_sensor(self, sensor=None):
120
+
121
+ self.component.refresh_from_db()
122
+ for id in self.component.config['presence_sensors']:
123
+ if id not in self.sensors:
124
+ sensor = Component.objects.filter(id=id).first()
125
+ if sensor:
126
+ sensor.on_change(self._on_sensor)
127
+ self.sensors[id] = sensor
128
+
129
+ if sensor:
130
+ self.sensors[sensor.id] = sensor
131
+
132
+ presence_values = [s.value for id, s in self.sensors.items()]
133
+ if self.component.config.get('act_on', 0):
134
+ must_on = any(presence_values)
135
+ else:
136
+ must_on = all(presence_values)
137
+
138
+ additional_conditions_met = True
139
+ for condition in self.component.config.get('conditions', []):
140
+ if not additional_conditions_met:
141
+ continue
142
+ comp = Component.objects.filter(
143
+ id=condition.get('component', 0)
144
+ ).first()
145
+ if not comp:
146
+ continue
147
+ op = OPERATIONS.get(condition.get('op'))
148
+ if not op:
149
+ continue
150
+ if condition['op'] == 'in':
151
+ if comp.value not in self._string_to_vals(condition['value']):
152
+ additional_conditions_met = False
153
+ continue
154
+
155
+ if not op(comp.value, condition['value']):
156
+ additional_conditions_met = False
157
+
158
+ if must_on and not additional_conditions_met:
159
+ print("Presence detected, but additional conditions not met!")
160
+
161
+ if must_on and additional_conditions_met and not self.is_on:
162
+ print("Turn the lights ON!")
163
+ self.is_on = True
164
+ if self.turn_off_task:
165
+ self.turn_off_task.cancel()
166
+ self.turn_off_task = None
167
+ self.light_org_values = {}
168
+ for id in self.component.config['lights']:
169
+ comp = Component.objects.filter(id=id).first()
170
+ if not comp or not comp.controller:
171
+ continue
172
+ self.light_org_values[comp.id] = comp.value
173
+ comp.controller.send(self.component.config['on_value'])
174
+ return
175
+
176
+ if self.is_on or self.is_on is None:
177
+ if not additional_conditions_met:
178
+ return self._turn_it_off()
179
+ if not any(presence_values):
180
+ if not self.component.config.get('hold_time', 0):
181
+ return self._turn_it_off()
182
+ if not self.turn_off_task:
183
+ self.turn_off_task = threading.Timer(
184
+ self.component.config['hold_time'] * 10,
185
+ self._turn_it_off
186
+ )
187
+
188
+ def _turn_it_off(self):
189
+ print("Turn the lights OFF!")
190
+ self.is_on = False
191
+ self.turn_off_task = None
192
+ for id in self.component.config['lights']:
193
+ comp = Component.objects.filter(id=id).first()
194
+ if not comp or not comp.controller:
195
+ continue
196
+ if self.component.config['off_value'] == 0:
197
+ comp.send(0)
198
+ else:
199
+ comp.send(self.light_org_values.get(comp.id, 0))
200
+
201
+
102
202
  class Thermostat(ControllerBase):
103
203
  name = _("Thermostat")
104
204
  base_type = 'thermostat'
simo/generic/forms.py CHANGED
@@ -7,7 +7,7 @@ from django.contrib.contenttypes.models import ContentType
7
7
  from simo.core.forms import HiddenField, BaseComponentForm
8
8
  from simo.core.models import Icon, Component
9
9
  from simo.core.controllers import (
10
- BinarySensor, NumericSensor, MultiSensor, Switch
10
+ BEFORE_SET, BinarySensor, NumericSensor, MultiSensor, Switch
11
11
  )
12
12
  from simo.core.widgets import PythonCode, LogOutputWidget
13
13
  from dal import autocomplete, forward
@@ -72,6 +72,145 @@ class ScriptConfigForm(BaseComponentForm):
72
72
  return fieldsets
73
73
 
74
74
 
75
+ class ConditionForm(forms.Form):
76
+ component = forms.ModelChoiceField(
77
+ Component.objects.all(),
78
+ widget=autocomplete.ModelSelect2(
79
+ url='autocomplete-component', attrs={'data-html': True},
80
+ ),
81
+ )
82
+ op = forms.ChoiceField(
83
+ initial="==", choices=(
84
+ ('==', "is equal to"),
85
+ ('>', "is greather than"), ('>=', "Is greather or equal to"),
86
+ ('<', "is lower than"), ('<=', "is lower or equal to"),
87
+ ('in', "is one of")
88
+ )
89
+ )
90
+ value = forms.CharField()
91
+ prefix = 'breach_events'
92
+
93
+ def clean(self):
94
+ if not self.cleaned_data.get('component'):
95
+ return self.cleaned_data
96
+ if not self.cleaned_data.get('op'):
97
+ return self.cleaned_data
98
+ component = self.cleaned_data.get('component')
99
+
100
+ if self.cleaned_data['op'] == 'in':
101
+ self.cleaned_data['value'] = self.cleaned_data['value']\
102
+ .strip('(').strip('[').rstrip(')').rstrip(']').strip()
103
+ values = self.cleaned_data['value'].split(',')
104
+ else:
105
+ values = [self.cleaned_data['value']]
106
+
107
+ final_values = []
108
+ controller_val_type = type(component.controller.default_value)
109
+ for val in values:
110
+ val = val.strip()
111
+ if controller_val_type == 'bool':
112
+ if val.lower() in ('0', 'false', 'none', 'null'):
113
+ final_val = False
114
+ else:
115
+ final_val = True
116
+ else:
117
+ try:
118
+ final_val = controller_val_type(val)
119
+ except:
120
+ self.add_error(
121
+ 'value', f"{val} bad value type for selected component."
122
+ )
123
+ continue
124
+ try:
125
+ component.controller._validate_val(final_val, BEFORE_SET)
126
+ except Exception as e:
127
+ self.add_error(
128
+ 'value', f"{val} is not compatible with selected component."
129
+ )
130
+ continue
131
+ final_values.append(final_val)
132
+
133
+ if self.cleaned_data['op'] == 'in':
134
+ self.cleaned_data['value'] = ', '.join(str(v) for v in final_values)
135
+ else:
136
+ self.cleaned_data['value'] = final_values[0]
137
+
138
+ return self.cleaned_data
139
+
140
+
141
+ class PresenceLightingConfigForm(BaseComponentForm):
142
+ lights = forms.ModelMultipleChoiceField(
143
+ Component.objects.filter(
144
+ base_type__in=('switch', 'dimmer', 'rgbw-light', 'rgb-light')
145
+ ),
146
+ required=True,
147
+ widget=autocomplete.ModelSelect2Multiple(
148
+ url='autocomplete-component', attrs={'data-html': True},
149
+ forward=(
150
+ forward.Const(['switch', 'dimmer', 'rgbw-light', 'rgb-light'],
151
+ 'base_type'),
152
+ )
153
+ )
154
+ )
155
+ on_value = forms.IntegerField(
156
+ min_value=0, initial=100,
157
+ help_text="Value applicable for dimmers. "
158
+ "Switches will receive tunrn on command."
159
+ )
160
+ off_value = forms.TypedChoiceField(
161
+ coerce=int, initial=1, choices=(
162
+ (0, "0"), (1, "Original value before turning the lights on.")
163
+ )
164
+ )
165
+ presence_sensors = forms.ModelMultipleChoiceField(
166
+ Component.objects.filter(base_type='binary-sensor'),
167
+ required=True,
168
+ widget=autocomplete.ModelSelect2Multiple(
169
+ url='autocomplete-component', attrs={'data-html': True},
170
+ forward=(forward.Const(['binary-sensor'], 'base_type'),)
171
+ )
172
+ )
173
+ act_on = forms.TypedChoiceField(
174
+ coerce=int, initial=0, choices=(
175
+ (0, "At least one sensor detects presence"),
176
+ (1, "All sensors detect presence"),
177
+ )
178
+ )
179
+ hold_time = forms.TypedChoiceField(
180
+ initial=3, coerce=int, required=False, choices=(
181
+ (0, '----'),
182
+ (1, "10 s"), (2, "20 s"), (3, "30 s"), (4, "40 s"), (5, "50 s"),
183
+ (6, "1 min"), (9, "1.5 min"), (12, "2 min"), (18, "3 min"),
184
+ (30, "5 min"), (60, "10 min"), (120, "20 min"),
185
+ ),
186
+ help_text="Hold off time after last presence detector is deactivated."
187
+ )
188
+ conditions = FormsetField(
189
+ formset_factory(
190
+ ConditionForm, can_delete=True, can_order=True, extra=0
191
+ ), label='Additional conditions'
192
+ )
193
+ log = forms.CharField(
194
+ widget=forms.HiddenInput, required=False
195
+ )
196
+
197
+ def __init__(self, *args, **kwargs):
198
+ super().__init__(*args, **kwargs)
199
+ if self.instance.pk:
200
+ prefix = get_script_prefix()
201
+ if prefix == '/':
202
+ prefix = ''
203
+ self.fields['log'].widget = LogOutputWidget(
204
+ prefix + '/ws/log/%d/%d/' % (
205
+ ContentType.objects.get_for_model(Component).id,
206
+ self.instance.id
207
+ )
208
+ )
209
+
210
+
211
+
212
+
213
+
75
214
  class ThermostatConfigForm(BaseComponentForm):
76
215
  temperature_sensor = forms.ModelChoiceField(
77
216
  Component.objects.filter(
@@ -270,7 +409,6 @@ class AlarmGroupConfigForm(BaseComponentForm):
270
409
  )
271
410
  has_alarm = False
272
411
 
273
-
274
412
  def __init__(self, *args, **kwargs):
275
413
  super().__init__(*args, **kwargs)
276
414
  from .controllers import AlarmGroup
@@ -281,9 +419,10 @@ class AlarmGroupConfigForm(BaseComponentForm):
281
419
  config__is_main=True
282
420
  ).count()
283
421
  )
284
- self.fields['is_main'].initial = first_alarm_group
285
- if first_alarm_group:
286
- self.fields['is_main'].widget.attrs['disabled'] = 'disabled'
422
+ if 'is_main' in self.fields:
423
+ self.fields['is_main'].initial = first_alarm_group
424
+ if first_alarm_group:
425
+ self.fields['is_main'].widget.attrs['disabled'] = 'disabled'
287
426
  else:
288
427
  if self.instance.config.get('is_main'):
289
428
  self.fields['is_main'].widget.attrs['disabled'] = 'disabled'
simo/generic/gateways.py CHANGED
@@ -136,14 +136,22 @@ class ScriptRunHandler(multiprocessing.Process):
136
136
  sys.stderr = StreamToLogger(self.logger, logging.ERROR)
137
137
  self.component.value = 'running'
138
138
  self.component.save(update_fields=['value'])
139
- code = self.component.config.get('code')
140
- if not code:
141
- self.component.value = 'finished'
142
- self.component.save(update_fields=['value'])
143
- return
139
+
140
+ if hasattr(self.component.controller, '_run'):
141
+ def run_code():
142
+ self.component.controller._run()
143
+ else:
144
+ code = self.component.config.get('code')
145
+ def run_code():
146
+ exec(code, globals())
147
+
148
+ if not code:
149
+ self.component.value = 'finished'
150
+ self.component.save(update_fields=['value'])
151
+ return
144
152
  print("------START-------")
145
153
  try:
146
- exec(code, globals())
154
+ run_code()
147
155
  except:
148
156
  print("------ERROR------")
149
157
  self.component.value = 'error'
@@ -259,7 +267,7 @@ class GenericGatewayHandler(BaseObjectCommandsGatewayHandler):
259
267
  # as well as those who are designed to be kept alive, but
260
268
  # got terminated unexpectedly
261
269
  for script in Component.objects.filter(
262
- controller_uid=Script.uid,
270
+ base_type='script',
263
271
  ).filter(
264
272
  Q(config__autostart=True) |
265
273
  Q(value='error', config__keep_alive=True)
@@ -302,6 +310,7 @@ class GenericGatewayHandler(BaseObjectCommandsGatewayHandler):
302
310
 
303
311
  if isinstance(component.controller, Script):
304
312
  if payload.get('set_val') == 'start':
313
+ print("START THIS SCRIPT!!!", component)
305
314
  self.start_script(component)
306
315
  elif payload.get('set_val') == 'stop':
307
316
  self.stop_script(component)
Binary file
simo/users/api.py CHANGED
@@ -35,7 +35,9 @@ class UsersViewSet(mixins.RetrieveModelMixin,
35
35
  email__in=('system@simo.io', 'device@simo.io')
36
36
  ) # Exclude system user
37
37
 
38
- return queryset.filter(roles__instance=self.instance)
38
+ return queryset.filter(
39
+ Q(roles__instance=self.instance) | Q(id=self.request.user.id)
40
+ )
39
41
 
40
42
 
41
43
  def check_permission_to_change(self, request, target_user):
simo/users/serializers.py CHANGED
@@ -24,11 +24,12 @@ class UserSerializer(serializers.ModelSerializer):
24
24
  class Meta:
25
25
  model = User
26
26
  fields = (
27
- 'id', 'email', 'name', 'avatar', 'role', 'is_active',
27
+ 'id', 'email', 'name', 'avatar', 'role', 'is_master', 'is_active',
28
28
  'at_home', 'last_action'
29
29
  )
30
30
  read_only_fields = (
31
- 'id', 'email', 'name', 'avatar', 'at_home', 'last_action', 'ssh_key'
31
+ 'id', 'email', 'name', 'avatar', 'at_home', 'last_action', 'ssh_key',
32
+ 'is_master'
32
33
  )
33
34
 
34
35
  def get_is_active(self, obj):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: simo
3
- Version: 2.0.35
3
+ Version: 2.0.38
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
@@ -34,11 +34,11 @@ simo/core/auto_urls.py,sha256=0gu-IL7PHobrmKW6ksffiOkAYu-aIorykWdxRNtwGYo,1194
34
34
  simo/core/autocomplete_views.py,sha256=JT5LA2_Wtr60XYSAIqaXFKFYPjrmkEf6yunXD9y2zco,4022
35
35
  simo/core/base_types.py,sha256=yqbIZqBksrAkEuHRbt6iExwPDDy0K5II2NzRCkmOvMU,589
36
36
  simo/core/context.py,sha256=98PXAMie43faRVBFkOG22uNpvGRNprcGhzjBFkrxaRY,1367
37
- simo/core/controllers.py,sha256=Z_sXPaehJcOS8Zz1UZZ2n7-1cjx3_B9dVjHBQHgjtTc,27281
37
+ simo/core/controllers.py,sha256=oPyAKJjPMQFP-gz5V0aDfIxs__6v3b90DocX3R-8wjM,27639
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=VoNKoLDR7AAJWHiEUTT8tYcNoVwZh4XxX1-lZEoRzpI,23688
41
+ simo/core/forms.py,sha256=f0_qnyrIem9yj5BKDAenzG5XK_tW7PGKWLuLKb3wPgg,24252
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=Qdg2-Qh4dLbW0A5Dmtnpct6CUhEuuvdbIskBijQxopU,2360
@@ -46,7 +46,7 @@ simo/core/middleware.py,sha256=pO52hQOJV_JRmNyUe7zfufSnJFlRITOWX6jwkoPWJhk,2052
46
46
  simo/core/models.py,sha256=W5rShDy8l6GQzTRFiBZFAuibe_fg_LRwSNk973mv_m8,20134
47
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=dbQ78K9ePBChFPjJHXGj8BkqXQfIFcfFpOtcupGUuz8,18337
49
+ simo/core/serializers.py,sha256=7iE5_ke56GpcQ_cV6LgQIsfG3pAQXPc3t4e60HwogUs,18277
50
50
  simo/core/signal_receivers.py,sha256=C6Jk7wVEtyo4hwcrU7L0ijtpK0wce2MNwpyBgSfSJ-U,5467
51
51
  simo/core/socket_consumers.py,sha256=n7VE2Fvqt4iEAYLTRbTPOcI-7tszMAADu7gimBxB-Fg,9635
52
52
  simo/core/storage.py,sha256=YlxmdRs-zhShWtFKgpJ0qp2NDBuIkJGYC1OJzqkbttQ,572
@@ -57,7 +57,7 @@ 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=1OisxqtyWMbzpgeeu5vtBW3gp3Nts4Ei4ff1P-SPpq4,13545
60
- simo/core/__pycache__/api.cpython-38.pyc,sha256=RsMxUiiMEEXsSh2fBbrkjsIUOKUy81dmN9_20SB7CR8,19942
60
+ simo/core/__pycache__/api.cpython-38.pyc,sha256=YBk4BVAMxAkShEyWgeTkHtTpr4P-tMwrTQI5TTP7vFE,19968
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
@@ -65,11 +65,11 @@ simo/core/__pycache__/auto_urls.cpython-38.pyc,sha256=SVl4fF0-yiq7e9gt08jIM6_rL4
65
65
  simo/core/__pycache__/autocomplete_views.cpython-38.pyc,sha256=hJ6JILI1LqrAtpQMvxnLvljGdW1v1gpvBsD79vFkZ58,3972
66
66
  simo/core/__pycache__/base_types.cpython-38.pyc,sha256=CasZJN42cK_ymoQgn5E4s8oOkuZJ18fVHCgN4GPuT7c,735
67
67
  simo/core/__pycache__/context.cpython-38.pyc,sha256=MSZPDhqMhCpUuBJl3HCIBHZA3BntYeP8RAnQcdqAH9k,1278
68
- simo/core/__pycache__/controllers.cpython-38.pyc,sha256=HX7XcVpmNkqM8gVnWru43nmrkR5B9tlgWdZrosCaRjE,23978
68
+ simo/core/__pycache__/controllers.cpython-38.pyc,sha256=Rsmre6XaYAbQH4aHHs6c-9EgmEZ8SyHm9Y7FYEteTpM,24542
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=69AqXKv_0SgFX_m6PCC1VdBEGjgGz-kkBTCaxIQZ0t0,19936
72
+ simo/core/__pycache__/forms.cpython-38.pyc,sha256=ksJVqk2X2XV1SmWFeEP2DStU1wvPQ7GaKO0CaFj_pTE,20465
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=ObkzRjSOs2UQmjwFWDvZHreNzc_P5k7dVA_f7L7S7Q4,2529
@@ -77,11 +77,11 @@ simo/core/__pycache__/middleware.cpython-38.pyc,sha256=ESR5JPtITo9flczO0672sfzYU
77
77
  simo/core/__pycache__/models.cpython-38.pyc,sha256=UNX6Btm5ZnpLzSCWgevQnSYzAyDWttA2Ivy7CGPu8DU,17288
78
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=ltj2lqyqt2ctV6svTQ_VkTmq_aOx8iPhTdu5QQhScdk,17213
80
+ simo/core/__pycache__/serializers.cpython-38.pyc,sha256=8QlBSWOtgmhTpFbMMgGoXtMoTMETSnpA37z2LBgRzNQ,17178
81
81
  simo/core/__pycache__/signal_receivers.cpython-38.pyc,sha256=UcKT8RK_14CI-JEWfplnIxskmWec_w5-gqKUXITLDA4,4323
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=7qQsnj1VkWTrYQNwfXH8SKkiaXBQteibciubKWPClXU,9139
84
+ simo/core/__pycache__/tasks.cpython-38.pyc,sha256=yt2G8HPIkx1Djz4oVBfhUW9WOR9GOdxLhHFXViQJc6A,9139
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
@@ -10149,12 +10149,13 @@ simo/core/utils/config_values.py,sha256=4HCQmv5wQdupd16WOp80oJSyU7EDccjUO6blX--d
10149
10149
  simo/core/utils/easing.py,sha256=N2NwD0CjLh82RGaYJKjyt-VVpVeS9z0mba8fqr8A1t0,1499
10150
10150
  simo/core/utils/form_fields.py,sha256=UOzYdPd71qgCw1H3qH01u85YjrOlETPJAHOJrZKhyD0,627
10151
10151
  simo/core/utils/form_widgets.py,sha256=Zxn9jJqPle9Q_BKNJnyTDn7MosYwNp1TFu5LoKs0bfc,408
10152
- simo/core/utils/formsets.py,sha256=1u34QGZ2P67cxZD2uUJS3lAf--E8XsiiqFmZ4P41Vw4,6463
10152
+ simo/core/utils/formsets.py,sha256=ZpExLsnDihnrlsPfYQrwy5qx54IowEmL8hnlO7KlyqE,6924
10153
10153
  simo/core/utils/helpers.py,sha256=TOWy3slspaEYEhe9zDcb0RgzHUYslF6LZDlrWPGSqUI,3791
10154
10154
  simo/core/utils/json.py,sha256=x3kMiiK30vuyWSYfghLVsDKo0N2JlCxZ5n8cwel85Vk,464
10155
10155
  simo/core/utils/logs.py,sha256=Zn9JQxqCH9Odx2J1BWT84nFCfkJ4Z4p5X8psdll7hNc,2366
10156
10156
  simo/core/utils/mixins.py,sha256=X6kUPKAi_F-uw7tgm8LEaYalBXpvDA-yrLNFCGr2rks,259
10157
10157
  simo/core/utils/model_helpers.py,sha256=3IzJeOvBoYdUJVXCJkY20npOZXPjNPAiEFvuT0OPhwA,884
10158
+ simo/core/utils/operations.py,sha256=W234NEetDnMWP7_tvxJq5CWo_rT6Xhe4Cw7n9-VOpyU,176
10158
10159
  simo/core/utils/relay.py,sha256=i1xy_nPTgY5Xn0l2W4lNI3xeVUpDQTUUfV3M8h2DeBg,457
10159
10160
  simo/core/utils/serialization.py,sha256=v0HLQ98r3zOlsf6dv6S4WxOEt6BzmFz2eTXa_iuKjSM,2057
10160
10161
  simo/core/utils/type_constants.py,sha256=xR2HXZOw9GZhC47iO1Py5B8mpaQMPbzvqX5nHWakhsY,4116
@@ -10166,12 +10167,13 @@ simo/core/utils/__pycache__/config_values.cpython-38.pyc,sha256=fqTVDhkjaWFv14Pr
10166
10167
  simo/core/utils/__pycache__/easing.cpython-38.pyc,sha256=LupxHv19OiBqL0VXmTbMCtAOpgvBpq_b0XwLU8El-Jk,2137
10167
10168
  simo/core/utils/__pycache__/form_fields.cpython-38.pyc,sha256=nBk6k9aj6BpWwdkpceIXdl5NU0fB6NPFhSBPaA-VtPs,1252
10168
10169
  simo/core/utils/__pycache__/form_widgets.cpython-38.pyc,sha256=MYAYEq0I4P0WErG9FamTJYWue7-cPartAWbFAiSSg5w,908
10169
- simo/core/utils/__pycache__/formsets.cpython-38.pyc,sha256=10Fe8s0Wx2ISyuKt9TX0rg7_vbFneV0Hvjbj8V4VIqA,4637
10170
+ simo/core/utils/__pycache__/formsets.cpython-38.pyc,sha256=vwlFLdQ2bpZgXNUpekhtapwfouNPCIRo-SMrgOdAIMA,4813
10170
10171
  simo/core/utils/__pycache__/helpers.cpython-38.pyc,sha256=jTGaN7kSJRwouP0EuYSaiJeMylo_RzJwSm-DKRwceHA,4291
10171
10172
  simo/core/utils/__pycache__/json.cpython-38.pyc,sha256=akSSiSUOnza4N15GAH399gTz-X8x-5gijxZdjZoPz5Q,504
10172
10173
  simo/core/utils/__pycache__/logs.cpython-38.pyc,sha256=BVVeQoOhfRHm3SHnCoE1d5G84kTpJZFmr_btc3jDYTU,2156
10173
10174
  simo/core/utils/__pycache__/mixins.cpython-38.pyc,sha256=8Js2T7jVQ7hugRUIRu3rdxW86dJW4KeUUWqKqSkIGb0,615
10174
10175
  simo/core/utils/__pycache__/model_helpers.cpython-38.pyc,sha256=QzO0rh1NuQePHDCSLmUCRrAZEnV4o8jh9CF_jp7IoUo,1351
10176
+ simo/core/utils/__pycache__/operations.cpython-38.pyc,sha256=1BUP7gbpBgX7mwWLpkmRH0eXgisbjn-6TIa8VDPj6v8,381
10175
10177
  simo/core/utils/__pycache__/relay.cpython-38.pyc,sha256=gs4iN9TWBo_JIW07emIggIcv6gHKuOY_4jfmAFhuL3k,697
10176
10178
  simo/core/utils/__pycache__/serialization.cpython-38.pyc,sha256=9nTbzozDi8Avl6krHvAo67CLdiTrYW0ij3hQtucHty0,1338
10177
10179
  simo/core/utils/__pycache__/type_constants.cpython-38.pyc,sha256=bEMvzkBxzc6MKq6gn9A6wszXzMjLTbX-V-IK4NMht8E,3363
@@ -10183,7 +10185,7 @@ simo/fleet/auto_urls.py,sha256=X04oKJWA48wFW5iXg3PPROY2KDdHn_a99orQSE28QC4,518
10183
10185
  simo/fleet/base_types.py,sha256=wL9RVkHr0gA7HI1wZq0pruGEIgvQqpfnCL4cC3ywsvw,102
10184
10186
  simo/fleet/ble.py,sha256=eHA_9ABjbmH1vUVCv9hiPXQL2GZZSEVwfO0xyI1S0nI,1081
10185
10187
  simo/fleet/controllers.py,sha256=WCqOA5Qrn9RavdfcB8X06WwaTE-9TGUprTQHZ8V8-nA,23172
10186
- simo/fleet/forms.py,sha256=Kvw-MG-3mI4OIPwo0uiJ_1E5QX3eIr8WR1_U2ggO3U0,43697
10188
+ simo/fleet/forms.py,sha256=VVjFlWfU2jSfRp8CW9ntEd21Mr272LacRokzMon-YdU,48870
10187
10189
  simo/fleet/gateways.py,sha256=KV5i5fxXIrlK-k6zyEkk83x11GJt-ELQ0npb4Ac83cM,3693
10188
10190
  simo/fleet/managers.py,sha256=XOpDOA9L-f_550TNSyXnJbun2EmtGz1TenVTMlUSb8E,807
10189
10191
  simo/fleet/models.py,sha256=bD5AebGFCAYGXPYhTA2nK1X9KpMG4WK4zFk9OzBDoHI,15301
@@ -10199,11 +10201,11 @@ simo/fleet/__pycache__/api.cpython-38.pyc,sha256=rL9fb7cCQatyFvXyKmlNOKmxVo8vHYe
10199
10201
  simo/fleet/__pycache__/auto_urls.cpython-38.pyc,sha256=SqyTuaz_kEBvx-bL46SclsZEEP5RFh6U6TGKyXDdiOE,565
10200
10202
  simo/fleet/__pycache__/base_types.cpython-38.pyc,sha256=deyPwjpT6xZiFxBGFnj5b7R-lbdOTh2krgpJhrcGVhc,274
10201
10203
  simo/fleet/__pycache__/ble.cpython-38.pyc,sha256=Nrof9w7cm4OlpFWHeVnmvvanh2_oF9oQ3TknJiV93-0,1267
10202
- simo/fleet/__pycache__/controllers.cpython-38.pyc,sha256=IInB1lBX4uFj1ZDyzCJEDABPXWTMwUpKBczkoJyj4SI,19842
10203
- simo/fleet/__pycache__/forms.cpython-38.pyc,sha256=aW_L2Q8y7YACrV6wlO3_JL-Id_16biEkXT_BAPnqzHg,31617
10204
+ simo/fleet/__pycache__/controllers.cpython-38.pyc,sha256=TN3yvfZJgS7FwzgP4S1aDoaOqxbKj2oXfXOxqbkIXJU,19856
10205
+ simo/fleet/__pycache__/forms.cpython-38.pyc,sha256=0js770XdKp9PePLN-az0TLnpbfWE6u2Lwr8_sbfqOow,33943
10204
10206
  simo/fleet/__pycache__/gateways.cpython-38.pyc,sha256=YAcgTOqJbtjGI03lvEcU6keFfrwAHkObVmErYzfRvjk,3569
10205
10207
  simo/fleet/__pycache__/managers.cpython-38.pyc,sha256=8uz-xpUiqbGDgXIZ_XRZtFb-Tju6NGxflGg-Ee4Yo6k,1310
10206
- simo/fleet/__pycache__/models.cpython-38.pyc,sha256=dQ5Fj9nXg7vgFfACzxWurhqGw8EnYa9Px7SwmGiAih4,12904
10208
+ simo/fleet/__pycache__/models.cpython-38.pyc,sha256=LjcLsSytCQd17xhH-5RrzvnZ6JYI1ilvNdCY2iUCsGc,12935
10207
10209
  simo/fleet/__pycache__/routing.cpython-38.pyc,sha256=aPrCmxFKVyB8R8ZbJDwdPdFfvT7CvobovvZeq_mqRgY,314
10208
10210
  simo/fleet/__pycache__/serializers.cpython-38.pyc,sha256=9ljhwoHkolcVrJwOVbYCbGPAUKgALRwor_M3W_K0adE,3173
10209
10211
  simo/fleet/__pycache__/socket_consumers.cpython-38.pyc,sha256=0-WhvzVsVJ5A_AgoKnWKOJjoJioLDNsYX4C6bGJANwQ,13551
@@ -10285,18 +10287,18 @@ simo/fleet/migrations/__pycache__/__init__.cpython-38.pyc,sha256=5k1KW0jeSDzw6Rn
10285
10287
  simo/generic/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10286
10288
  simo/generic/app_widgets.py,sha256=E_pnpA1hxMIhenRCrHoQ5cik06jm2BAHCkl_eo-OudU,1264
10287
10289
  simo/generic/base_types.py,sha256=djymox_boXTHX1BTTCLXrCH7ED-uAsV_idhaDOc3OLI,409
10288
- simo/generic/controllers.py,sha256=WYuOUzDWvkYRaTvlbdGy_qmwp1o_ohqKDfV7OrOq2QU,52218
10289
- simo/generic/forms.py,sha256=NPHNg_8BPTIJt-DR0-GkNWgVo9bE0_50PUWSnpIE4Dg,24262
10290
- simo/generic/gateways.py,sha256=RcHubz3oyY_ysPLNPLRoyh8C6r-WfPzpD98OVkDvPPI,17731
10290
+ simo/generic/controllers.py,sha256=qbcKSrKLvi4DCN4Tt1GpHMScVSGz0snQRRuWi4_-r_s,55823
10291
+ simo/generic/forms.py,sha256=FVhQ-buK6vUp7x7PADWo31pPVtHWgBMgshnyH-CN8wI,29239
10292
+ simo/generic/gateways.py,sha256=dZQPzO23UbW9q4dEB9fqgt9Meg8mX94euXnRvfwxusY,18004
10291
10293
  simo/generic/models.py,sha256=92TACMhJHadAg0TT9GnARO_R3_Sl6i-GGjhG_x7YdFI,7391
10292
10294
  simo/generic/routing.py,sha256=elQVZmgnPiieEuti4sJ7zITk1hlRxpgbotcutJJgC60,228
10293
10295
  simo/generic/socket_consumers.py,sha256=NfTQGYtVAc864IoogZRxf_0xpDPM0eMCWn0SlKA5P7Y,1751
10294
10296
  simo/generic/__pycache__/__init__.cpython-38.pyc,sha256=mLu54WS9KIl-pHwVCBKpsDFIlOqml--JsOVzAUHg6cU,161
10295
10297
  simo/generic/__pycache__/app_widgets.cpython-38.pyc,sha256=0IoKRG9n1tkNRRkrqAeOQwWBPd_33u98JBcVtMVVCio,2374
10296
10298
  simo/generic/__pycache__/base_types.cpython-38.pyc,sha256=ptw6axyAqemZA35oa6vzr7EihzvbhW9w7Y-G6kfDedU,555
10297
- simo/generic/__pycache__/controllers.cpython-38.pyc,sha256=e0bvgyePgJbIs1omBq0TRPlVSKar2sK_JbUKqDRj7mY,33235
10298
- simo/generic/__pycache__/forms.cpython-38.pyc,sha256=du8pawGkAyGP51Cb6uWgvVLx48JZKNkVCdOpRmqQFaA,17915
10299
- simo/generic/__pycache__/gateways.cpython-38.pyc,sha256=IbRdwj0oIUyyuR-AOlaPmEwKXr5utWf0LsLTN-HHSuI,12962
10299
+ simo/generic/__pycache__/controllers.cpython-38.pyc,sha256=6WhWx_QaWrYV_WqU5coEp9VFR11DxJVjPGTrhcUoYQ4,35583
10300
+ simo/generic/__pycache__/forms.cpython-38.pyc,sha256=LsYmGecSEdOnWnskBoTktLEUfYYKWMtd40lIGHbS-ZY,21207
10301
+ simo/generic/__pycache__/gateways.cpython-38.pyc,sha256=B35GiB4wBRzvd91ugL89Z3vYADiLiERP0T-21CnLqKc,13324
10300
10302
  simo/generic/__pycache__/models.cpython-38.pyc,sha256=PzlZsM1jxo3FVb7QDm3bny8UFwTsGrMQe4mj4tJ06eQ,5675
10301
10303
  simo/generic/__pycache__/routing.cpython-38.pyc,sha256=xtxTUTBTdivzFyA5Wh7k-hUj1WDO_FiRq6HYXdbr9Ks,382
10302
10304
  simo/generic/__pycache__/socket_consumers.cpython-38.pyc,sha256=piFHces0J9QuXu_CNBCQCYjoZEeoaxyVjLfJ9KaR8C8,1898
@@ -10372,14 +10374,14 @@ simo/notifications/migrations/__pycache__/0002_notification_instance.cpython-38.
10372
10374
  simo/notifications/migrations/__pycache__/__init__.cpython-38.pyc,sha256=YMBRHVon2nWDtIUbghckjnC12sIg_ykPWhV5aM0tto4,178
10373
10375
  simo/users/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10374
10376
  simo/users/admin.py,sha256=6RKGnwcrmewJFPzpqnxYn8rxjHO4tJPVFJvA3eMum2s,6746
10375
- simo/users/api.py,sha256=FVo2bHkGA9VKavyjQm8_wfvI5rQ5KcMZliRiTJjixmI,9462
10377
+ simo/users/api.py,sha256=vf11fV8ZCao3Q2TdUO8mDTF3n0N3JYad7CUhz7_ZrZQ,9516
10376
10378
  simo/users/auth_backends.py,sha256=I5pnaTa20-Lxfw_dFG8471xDITb0_fQl1PVhJalp5vU,3992
10377
10379
  simo/users/auto_urls.py,sha256=lcJvteBsbHQMJieZpDz-63tDYejLApqsW3CUnDakd7k,272
10378
10380
  simo/users/dynamic_settings.py,sha256=sEIsi4yJw3kH46Jq_aOkSuK7QTfQACGUE-lkyBogCaM,570
10379
10381
  simo/users/middleware.py,sha256=GMCrnWSc_2qCleyQIkfQGdL-pU-UTEcSg1wPvIKZ9uk,1210
10380
10382
  simo/users/models.py,sha256=I_iAa-6CLORshmGszSl021zl97oaXNIEOZYMSq5Wn5M,18902
10381
10383
  simo/users/permissions.py,sha256=IwtYS8yQdupWbYKR9VimSRDV3qCJ2jXP57Lyjpb2EQM,242
10382
- simo/users/serializers.py,sha256=e6yIUsO7BfvrZ4IQHBn-FdpAUMgic5clmGQdTtRlGRY,2515
10384
+ simo/users/serializers.py,sha256=DwbFGi4WeTYXOSnfrBfd5rC5OGtevYurn27EaTVa1EU,2553
10383
10385
  simo/users/sso_urls.py,sha256=gQOaPvGMYFD0NCVSwyoWO-mTEHe5j9sbzV_RK7kdvp0,251
10384
10386
  simo/users/sso_views.py,sha256=-XI67TvQ7SN3goU4OuAHyn84u_1vtusvpn7Pu0K97zo,4648
10385
10387
  simo/users/tasks.py,sha256=v9J7t4diB0VnqUDVZAQ8H-rlr4ZR14bgEUuEGpODyOI,854
@@ -10387,14 +10389,14 @@ simo/users/utils.py,sha256=7gU_TDnAOsDYqJM0CFo8efPah2bTXfGpXxRqzD5RiSs,1270
10387
10389
  simo/users/views.py,sha256=dOQVvmlHG7ihWKJLFUBcqKOA0UDctlMKR0pTc36JZqg,3487
10388
10390
  simo/users/__pycache__/__init__.cpython-38.pyc,sha256=9otuYxq331c4lGy0DR8pigaPpzq0lQ4nrNLhlYiFAF0,159
10389
10391
  simo/users/__pycache__/admin.cpython-38.pyc,sha256=53Do-W6tvaOUPJ1BDx0abBbtvmQxAgdI2ShlqmYfUvI,7500
10390
- simo/users/__pycache__/api.cpython-38.pyc,sha256=4Fmi4fvgQqc8bM1lyQFVN3nKyWol9RgnUvy7YSNhwuE,8256
10392
+ simo/users/__pycache__/api.cpython-38.pyc,sha256=GcGFVxv0GUcH-TVdvj3v3hty1snKJw3O3-f4PM8DIyM,8305
10391
10393
  simo/users/__pycache__/auth_backends.cpython-38.pyc,sha256=MuOieBIXt6lrDx83-UQtdDyI_U8kE3pU9XR4yFLKBnE,3007
10392
10394
  simo/users/__pycache__/auto_urls.cpython-38.pyc,sha256=K-3sz2h-cEitoflSmZk1t0eUg5mQMMGLNZFREVwG7_o,430
10393
10395
  simo/users/__pycache__/dynamic_settings.cpython-38.pyc,sha256=6F8JBjZkHykySnmZjNEzjS0ijbmPdcp9yUAZ5kqq_Fo,864
10394
10396
  simo/users/__pycache__/middleware.cpython-38.pyc,sha256=Tj4nVEAvxEW3xA63fBRiJWRJpz_M848ZOqbHioc_IPE,1149
10395
10397
  simo/users/__pycache__/models.cpython-38.pyc,sha256=D5N0lYn3U9jt0M8-Xiz94DbiPuJefZQ9AUFJqKrYmsg,17562
10396
10398
  simo/users/__pycache__/permissions.cpython-38.pyc,sha256=ez5NxoL_JUeeH6GsKhvFreuA3FCBgGf9floSypdXUtM,633
10397
- simo/users/__pycache__/serializers.cpython-38.pyc,sha256=ylapsfu5qbSzbfX2lG3uc4wV6hhndFbIvI109lhhKOo,3461
10399
+ simo/users/__pycache__/serializers.cpython-38.pyc,sha256=tZzdmCdSnqekAgRl0kyq-msm7QfUA0J_IipfrysAMRM,3477
10398
10400
  simo/users/__pycache__/sso_urls.cpython-38.pyc,sha256=uAwDozpOmrhUald-8tOHANILXkH7-TI8fNYXOtPkSY8,402
10399
10401
  simo/users/__pycache__/sso_views.cpython-38.pyc,sha256=sHEoxLOac3U3Epmhm197huFnW_J3gGCDZSji57itijU,3969
10400
10402
  simo/users/__pycache__/tasks.cpython-38.pyc,sha256=xq-XaJ5gzkpVVZRWe0bvGdA31Eh_WS2rKSY62p4eY5E,1111
@@ -10466,8 +10468,8 @@ simo/users/templates/invitations/expired_msg.html,sha256=47DEQpj8HBSa-_TImW-5JCe
10466
10468
  simo/users/templates/invitations/expired_suggestion.html,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10467
10469
  simo/users/templates/invitations/taken_msg.html,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10468
10470
  simo/users/templates/invitations/taken_suggestion.html,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10469
- simo-2.0.35.dist-info/LICENSE.md,sha256=M7wm1EmMGDtwPRdg7kW4d00h1uAXjKOT3HFScYQMeiE,34916
10470
- simo-2.0.35.dist-info/METADATA,sha256=K3eQYM55-6gro2zNR8B_egdy6IPqogizV_qnB1CTwR0,1730
10471
- simo-2.0.35.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
10472
- simo-2.0.35.dist-info/top_level.txt,sha256=GmS1hrAbpVqn9OWZh6UX82eIOdRLgYA82RG9fe8v4Rs,5
10473
- simo-2.0.35.dist-info/RECORD,,
10471
+ simo-2.0.38.dist-info/LICENSE.md,sha256=M7wm1EmMGDtwPRdg7kW4d00h1uAXjKOT3HFScYQMeiE,34916
10472
+ simo-2.0.38.dist-info/METADATA,sha256=lSORUFdH9kvVvxyK_3k0swsm7DHyYLhjUmxM_WTAFF8,1730
10473
+ simo-2.0.38.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
10474
+ simo-2.0.38.dist-info/top_level.txt,sha256=GmS1hrAbpVqn9OWZh6UX82eIOdRLgYA82RG9fe8v4Rs,5
10475
+ simo-2.0.38.dist-info/RECORD,,
File without changes