simo 2.0.37__py3-none-any.whl → 2.0.39__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.
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
 
@@ -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
@@ -168,12 +178,17 @@ class FormsetField(forms.Field):
168
178
  form_data[field_name] = int(form_data[field_name])
169
179
  except:
170
180
  form_data[field_name] = None
181
+
171
182
  if self.widget.formset.can_order:
172
183
  form_data['order'] = int(formset_data.get(
173
184
  '%s-%d-ORDER' % (prefix, i), 0
174
185
  ))
186
+
187
+
175
188
  cleaned_value.append(form_data)
176
189
 
190
+
191
+
177
192
  if self.widget.formset.can_order:
178
193
  cleaned_value = sorted(cleaned_value, key=lambda d: d['order'])
179
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
@@ -219,6 +219,22 @@ class ColonelBinarySensorConfigForm(ColonelComponentForm):
219
219
  "Set debounce value in milliseconds, to remediate this. "
220
220
  "50ms offers a good starting point!"
221
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')
222
238
 
223
239
  def clean(self):
224
240
  super().clean()
@@ -562,7 +578,7 @@ class ColonelSwitchConfigForm(ColonelComponentForm):
562
578
 
563
579
  controls = FormsetField(
564
580
  formset_factory(
565
- ControlPinForm, can_delete=True, can_order=True, extra=0, max_num=1
581
+ ControlPinForm, can_delete=True, can_order=True, extra=0, max_num=10
566
582
  )
567
583
  )
568
584
 
@@ -670,7 +686,7 @@ class ColonelPWMOutputConfigForm(ColonelComponentForm):
670
686
  )
671
687
  controls = FormsetField(
672
688
  formset_factory(
673
- ControlPinForm, can_delete=True, can_order=True, extra=0, max_num=1
689
+ ControlPinForm, can_delete=True, can_order=True, extra=0, max_num=10
674
690
  )
675
691
  )
676
692
 
@@ -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,112 @@ 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
+
202
+ # TODO: Night lighting
203
+ #
204
+ # Lights: components (switches, dimmers)
205
+ # On value: 40
206
+ # Sunset offset (mins): negative = earlier, positive = later
207
+ # Save energy at night: 1 - 6 turn the lights completely off at night.
208
+
209
+
102
210
  class Thermostat(ControllerBase):
103
211
  name = _("Thermostat")
104
212
  base_type = 'thermostat'
@@ -1335,7 +1443,7 @@ class StateSelect(ControllerBase):
1335
1443
  config_form = StateSelectForm
1336
1444
 
1337
1445
  default_config = {'states': []}
1338
- default_value = None
1446
+ default_value = ''
1339
1447
 
1340
1448
  def _validate_val(self, value, occasion=None):
1341
1449
  available_options = [s.get('slug') for s in self.component.config.get('states', [])]
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,153 @@ 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
+ elif final_values:
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
+ autostart = forms.BooleanField(
194
+ initial=True, required=False,
195
+ help_text="Start automatically on system boot."
196
+ )
197
+ keep_alive = forms.BooleanField(
198
+ initial=True, required=False,
199
+ help_text="Restart the script if it fails. "
200
+ )
201
+ log = forms.CharField(
202
+ widget=forms.HiddenInput, required=False
203
+ )
204
+
205
+ def __init__(self, *args, **kwargs):
206
+ super().__init__(*args, **kwargs)
207
+ if self.instance.pk:
208
+ prefix = get_script_prefix()
209
+ if prefix == '/':
210
+ prefix = ''
211
+ self.fields['log'].widget = LogOutputWidget(
212
+ prefix + '/ws/log/%d/%d/' % (
213
+ ContentType.objects.get_for_model(Component).id,
214
+ self.instance.id
215
+ )
216
+ )
217
+
218
+
219
+
220
+
221
+
75
222
  class ThermostatConfigForm(BaseComponentForm):
76
223
  temperature_sensor = forms.ModelChoiceField(
77
224
  Component.objects.filter(
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: simo
3
- Version: 2.0.37
3
+ Version: 2.0.39
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,7 +34,7 @@ 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
@@ -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
@@ -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=1v2folVgoOd915LMQjyb0OMBuQ535HPq6uymm5m35ZU,6573
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=7MuKBfOUgXuLDsSYo0jQHYxWJxQiTB95K7ogwULmp84,48121
10188
+ simo/fleet/forms.py,sha256=EwqFVVabGGDgtHaBAUo_n0Mu-F3vMXzr4IirMPKAYFg,48872
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
@@ -10200,7 +10202,7 @@ simo/fleet/__pycache__/auto_urls.cpython-38.pyc,sha256=SqyTuaz_kEBvx-bL46SclsZEE
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
10204
  simo/fleet/__pycache__/controllers.cpython-38.pyc,sha256=TN3yvfZJgS7FwzgP4S1aDoaOqxbKj2oXfXOxqbkIXJU,19856
10203
- simo/fleet/__pycache__/forms.cpython-38.pyc,sha256=spAw3w0y42aF0driUhxT6QrDDOom3KkOrgoFYwAwqak,31698
10205
+ simo/fleet/__pycache__/forms.cpython-38.pyc,sha256=1kYMcS0G_nPp3h6-MjHyTPNwcJHUiAGfGZIAX1db2rE,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
10208
  simo/fleet/__pycache__/models.cpython-38.pyc,sha256=LjcLsSytCQd17xhH-5RrzvnZ6JYI1ilvNdCY2iUCsGc,12935
@@ -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=ZKK7uqSHTff_sAX7ZWZZpf5l1uvyosdXa_BCX9tLuf0,24314
10290
- simo/generic/gateways.py,sha256=RcHubz3oyY_ysPLNPLRoyh8C6r-WfPzpD98OVkDvPPI,17731
10290
+ simo/generic/controllers.py,sha256=lxMdbNxbvoUPctoLBz8heCOb5aAUx9k3h56Y7ByRKdg,56036
10291
+ simo/generic/forms.py,sha256=sxNl_iAgZJ5cCDcwgK0m4S7fs-O8RJutoiFeC1w_DlQ,29522
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=HtPEtQfdbHCwO9BejEGPIsJyqyCmo7owteXE09VXZ7Y,35588
10300
+ simo/generic/__pycache__/forms.cpython-38.pyc,sha256=g4N2Zcarce0dMovJZt3TQHAhKWA1W3k2nurBg1NqHaY,21297
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
@@ -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.37.dist-info/LICENSE.md,sha256=M7wm1EmMGDtwPRdg7kW4d00h1uAXjKOT3HFScYQMeiE,34916
10470
- simo-2.0.37.dist-info/METADATA,sha256=xKOE5qzOHNzbeyUNG_nM_qrb0pPynAJuc-Qshb9UjUs,1730
10471
- simo-2.0.37.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
10472
- simo-2.0.37.dist-info/top_level.txt,sha256=GmS1hrAbpVqn9OWZh6UX82eIOdRLgYA82RG9fe8v4Rs,5
10473
- simo-2.0.37.dist-info/RECORD,,
10471
+ simo-2.0.39.dist-info/LICENSE.md,sha256=M7wm1EmMGDtwPRdg7kW4d00h1uAXjKOT3HFScYQMeiE,34916
10472
+ simo-2.0.39.dist-info/METADATA,sha256=OzMAb8fubyYGVoM40_4IcFpY-tK3Rw8oX7ZEN3G9niw,1730
10473
+ simo-2.0.39.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
10474
+ simo-2.0.39.dist-info/top_level.txt,sha256=GmS1hrAbpVqn9OWZh6UX82eIOdRLgYA82RG9fe8v4Rs,5
10475
+ simo-2.0.39.dist-info/RECORD,,
File without changes