simo 2.0.11__py3-none-any.whl → 2.0.13__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.
- simo/core/__pycache__/admin.cpython-38.pyc +0 -0
- simo/core/__pycache__/api_meta.cpython-38.pyc +0 -0
- simo/core/__pycache__/forms.cpython-38.pyc +0 -0
- simo/core/__pycache__/serializers.cpython-38.pyc +0 -0
- simo/core/admin.py +2 -2
- simo/core/api_meta.py +5 -2
- simo/core/drf_braces/serializers/__pycache__/form_serializer.cpython-38.pyc +0 -0
- simo/core/forms.py +15 -5
- simo/core/serializers.py +55 -13
- simo/fleet/__pycache__/controllers.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/forms.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/gateways.cpython-38.pyc +0 -0
- simo/fleet/controllers.py +47 -1
- simo/fleet/forms.py +18 -0
- simo/fleet/gateways.py +38 -2
- simo/fleet/migrations/__pycache__/0033_auto_20240415_0736.cpython-38.pyc +0 -0
- simo/generic/__pycache__/controllers.cpython-38.pyc +0 -0
- simo/generic/__pycache__/forms.cpython-38.pyc +0 -0
- simo/generic/__pycache__/gateways.cpython-38.pyc +0 -0
- simo/generic/__pycache__/models.cpython-38.pyc +0 -0
- simo/generic/controllers.py +15 -0
- simo/generic/forms.py +93 -13
- simo/generic/gateways.py +24 -1
- simo/generic/models.py +54 -13
- {simo-2.0.11.dist-info → simo-2.0.13.dist-info}/METADATA +1 -1
- {simo-2.0.11.dist-info → simo-2.0.13.dist-info}/RECORD +29 -29
- {simo-2.0.11.dist-info → simo-2.0.13.dist-info}/LICENSE.md +0 -0
- {simo-2.0.11.dist-info → simo-2.0.13.dist-info}/WHEEL +0 -0
- {simo-2.0.11.dist-info → simo-2.0.13.dist-info}/top_level.txt +0 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
simo/core/admin.py
CHANGED
|
@@ -336,9 +336,9 @@ class ComponentAdmin(admin.ModelAdmin):
|
|
|
336
336
|
'alarm_category', 'arm_status'
|
|
337
337
|
):
|
|
338
338
|
if field_neme in form.fields:
|
|
339
|
-
form.fields.pop(field_neme)
|
|
339
|
+
form.fields.pop(field_neme, None)
|
|
340
340
|
if 'slaves' not in form.declared_fields:
|
|
341
|
-
form.fields.pop('slaves')
|
|
341
|
+
form.fields.pop('slaves', None)
|
|
342
342
|
|
|
343
343
|
ctx['is_last'] = True
|
|
344
344
|
ctx['current_step'] = 3
|
simo/core/api_meta.py
CHANGED
|
@@ -3,7 +3,9 @@ from django.utils.encoding import force_str
|
|
|
3
3
|
from rest_framework import serializers
|
|
4
4
|
from rest_framework.metadata import SimpleMetadata
|
|
5
5
|
from rest_framework.utils.field_mapping import ClassLookupDict
|
|
6
|
-
from .serializers import
|
|
6
|
+
from .serializers import (
|
|
7
|
+
HiddenSerializerField, ComponentManyToManyRelatedField
|
|
8
|
+
)
|
|
7
9
|
|
|
8
10
|
|
|
9
11
|
class SIMOAPIMetadata(SimpleMetadata):
|
|
@@ -32,7 +34,8 @@ class SIMOAPIMetadata(SimpleMetadata):
|
|
|
32
34
|
serializers.Serializer: 'nested object',
|
|
33
35
|
serializers.RelatedField: 'related object',
|
|
34
36
|
serializers.ManyRelatedField: 'many related objects',
|
|
35
|
-
ComponentManyToManyRelatedField: 'many related objects'
|
|
37
|
+
ComponentManyToManyRelatedField: 'many related objects',
|
|
38
|
+
HiddenSerializerField: 'hidden',
|
|
36
39
|
})
|
|
37
40
|
|
|
38
41
|
def get_field_info(self, field):
|
|
Binary file
|
simo/core/forms.py
CHANGED
|
@@ -26,6 +26,15 @@ from .utils.formsets import FormsetField
|
|
|
26
26
|
from .utils.validators import validate_slaves
|
|
27
27
|
|
|
28
28
|
|
|
29
|
+
class HiddenField(forms.CharField):
|
|
30
|
+
'''
|
|
31
|
+
Hidden field used in API
|
|
32
|
+
'''
|
|
33
|
+
def __init__(self, *args, **kwargs):
|
|
34
|
+
super().__init__(widget=forms.HiddenInput())
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
29
38
|
class HubConfigForm(forms.Form):
|
|
30
39
|
name = forms.CharField(
|
|
31
40
|
label=_("Hub Name"), required=True,
|
|
@@ -278,8 +287,8 @@ class ComponentAdminForm(forms.ModelForm):
|
|
|
278
287
|
model = Component
|
|
279
288
|
fields = '__all__'
|
|
280
289
|
exclude = (
|
|
281
|
-
'gateway', 'controller_uid', 'base_type',
|
|
282
|
-
'alive', 'value_type', 'value', 'arm_status',
|
|
290
|
+
'gateway', 'controller_uid', 'base_type', 'instance_methods',
|
|
291
|
+
'alive', 'value_type', 'value', 'arm_status', 'slaves',
|
|
283
292
|
)
|
|
284
293
|
widgets = {
|
|
285
294
|
'icon': autocomplete.ModelSelect2(
|
|
@@ -291,7 +300,7 @@ class ComponentAdminForm(forms.ModelForm):
|
|
|
291
300
|
'category': autocomplete.ModelSelect2(
|
|
292
301
|
url='autocomplete-category', attrs={'data-html': True}
|
|
293
302
|
),
|
|
294
|
-
'instance_methods': PythonCode
|
|
303
|
+
#'instance_methods': PythonCode
|
|
295
304
|
}
|
|
296
305
|
|
|
297
306
|
def __init__(self, *args, **kwargs):
|
|
@@ -322,7 +331,8 @@ class ComponentAdminForm(forms.ModelForm):
|
|
|
322
331
|
main_fields = (
|
|
323
332
|
'name', 'icon', 'zone', 'category',
|
|
324
333
|
'show_in_app', 'battery_level',
|
|
325
|
-
'instance_methods',
|
|
334
|
+
#'instance_methods',
|
|
335
|
+
'value_units',
|
|
326
336
|
'alarm_category', 'arm_status',
|
|
327
337
|
)
|
|
328
338
|
base_fields = ['id', 'gateway', 'base_type', 'name']
|
|
@@ -338,7 +348,7 @@ class ComponentAdminForm(forms.ModelForm):
|
|
|
338
348
|
|
|
339
349
|
base_fields.append('show_in_app')
|
|
340
350
|
base_fields.append('control')
|
|
341
|
-
base_fields.append('instance_methods')
|
|
351
|
+
#base_fields.append('instance_methods')
|
|
342
352
|
|
|
343
353
|
fieldsets = [
|
|
344
354
|
(_("Base settings"), {'fields': base_fields}),
|
simo/core/serializers.py
CHANGED
|
@@ -2,16 +2,20 @@ import inspect
|
|
|
2
2
|
import datetime
|
|
3
3
|
import json
|
|
4
4
|
from django import forms
|
|
5
|
+
from collections import OrderedDict
|
|
5
6
|
from django.forms.utils import ErrorDict
|
|
6
7
|
from collections.abc import Iterable
|
|
7
8
|
from easy_thumbnails.files import get_thumbnailer
|
|
8
9
|
from simo.core.middleware import get_current_request
|
|
9
10
|
from rest_framework import serializers
|
|
10
|
-
from
|
|
11
|
+
from rest_framework.fields import SkipField
|
|
12
|
+
from rest_framework.relations import Hyperlink, PKOnlyObject
|
|
13
|
+
from simo.core.forms import HiddenField, FormsetField
|
|
11
14
|
from rest_framework.relations import PrimaryKeyRelatedField, ManyRelatedField
|
|
12
15
|
from .drf_braces.serializers.form_serializer import (
|
|
13
16
|
FormSerializer, FormSerializerBase, reduce_attr_dict_from_instance,
|
|
14
|
-
FORM_SERIALIZER_FIELD_MAPPING, set_form_partial_validation
|
|
17
|
+
FORM_SERIALIZER_FIELD_MAPPING, set_form_partial_validation,
|
|
18
|
+
find_matching_class_kwargs
|
|
15
19
|
)
|
|
16
20
|
from .forms import ComponentAdminForm
|
|
17
21
|
from .models import Category, Zone, Icon, ComponentHistory
|
|
@@ -60,7 +64,6 @@ class CategorySerializer(serializers.ModelSerializer):
|
|
|
60
64
|
return
|
|
61
65
|
|
|
62
66
|
|
|
63
|
-
|
|
64
67
|
class ObjectSerializerMethodField(serializers.SerializerMethodField):
|
|
65
68
|
|
|
66
69
|
def bind(self, field_name, parent):
|
|
@@ -83,6 +86,9 @@ class FormsetPrimaryKeyRelatedField(PrimaryKeyRelatedField):
|
|
|
83
86
|
|
|
84
87
|
# TODO: if form field has initial value and is required, it is serialized as not required field, howerver when trying to submit it fails with a message, that field is required.
|
|
85
88
|
|
|
89
|
+
class HiddenSerializerField(serializers.CharField):
|
|
90
|
+
pass
|
|
91
|
+
|
|
86
92
|
|
|
87
93
|
class ComponentFormsetField(FormSerializer):
|
|
88
94
|
|
|
@@ -91,6 +97,7 @@ class ComponentFormsetField(FormSerializer):
|
|
|
91
97
|
# we set it to proper formset form on __init__
|
|
92
98
|
form = forms.Form
|
|
93
99
|
field_mapping = {
|
|
100
|
+
HiddenField: HiddenSerializerField,
|
|
94
101
|
forms.ModelChoiceField: FormsetPrimaryKeyRelatedField,
|
|
95
102
|
forms.TypedChoiceField: serializers.ChoiceField,
|
|
96
103
|
forms.FloatField: serializers.FloatField,
|
|
@@ -145,10 +152,38 @@ class ComponentFormsetField(FormSerializer):
|
|
|
145
152
|
kwargs['style'] = {'form_field': form_field}
|
|
146
153
|
if serializer_field_class == FormsetPrimaryKeyRelatedField:
|
|
147
154
|
kwargs['queryset'] = form_field.queryset
|
|
155
|
+
|
|
156
|
+
attrs = find_matching_class_kwargs(form_field, serializer_field_class)
|
|
157
|
+
if 'choices' in attrs:
|
|
158
|
+
kwargs['choices'] = attrs['choices']
|
|
159
|
+
|
|
148
160
|
return kwargs
|
|
149
161
|
|
|
150
162
|
def to_representation(self, instance):
|
|
151
|
-
|
|
163
|
+
"""
|
|
164
|
+
Object instance -> Dict of primitive datatypes.
|
|
165
|
+
"""
|
|
166
|
+
ret = OrderedDict()
|
|
167
|
+
fields = self._readable_fields
|
|
168
|
+
|
|
169
|
+
for field in fields:
|
|
170
|
+
try:
|
|
171
|
+
attribute = field.get_attribute(instance)
|
|
172
|
+
except SkipField:
|
|
173
|
+
continue
|
|
174
|
+
except:
|
|
175
|
+
ret[field.field_name] = None
|
|
176
|
+
continue
|
|
177
|
+
|
|
178
|
+
check_for_none = attribute.pk if isinstance(
|
|
179
|
+
attribute, PKOnlyObject
|
|
180
|
+
) else attribute
|
|
181
|
+
if check_for_none is None:
|
|
182
|
+
ret[field.field_name] = None
|
|
183
|
+
else:
|
|
184
|
+
ret[field.field_name] = field.to_representation(attribute)
|
|
185
|
+
|
|
186
|
+
return ret
|
|
152
187
|
|
|
153
188
|
def get_form(self, data=None, **kwargs):
|
|
154
189
|
form = super().get_form(data=data, **kwargs)
|
|
@@ -178,7 +213,7 @@ class ComponentManyToManyRelatedField(serializers.Field):
|
|
|
178
213
|
super().__init__(*args, **kwargs)
|
|
179
214
|
|
|
180
215
|
def to_representation(self, value):
|
|
181
|
-
return [obj.pk for obj in value]
|
|
216
|
+
return [obj.pk for obj in value.all()]
|
|
182
217
|
|
|
183
218
|
def to_internal_value(self, data):
|
|
184
219
|
if data == [] and self.allow_blank:
|
|
@@ -188,7 +223,6 @@ class ComponentManyToManyRelatedField(serializers.Field):
|
|
|
188
223
|
|
|
189
224
|
class ComponentSerializer(FormSerializer):
|
|
190
225
|
id = ObjectSerializerMethodField()
|
|
191
|
-
controller_methods = serializers.SerializerMethodField()
|
|
192
226
|
last_change = TimestampField(read_only=True)
|
|
193
227
|
read_only = serializers.SerializerMethodField()
|
|
194
228
|
app_widget = serializers.SerializerMethodField()
|
|
@@ -204,8 +238,8 @@ class ComponentSerializer(FormSerializer):
|
|
|
204
238
|
|
|
205
239
|
class Meta:
|
|
206
240
|
form = ComponentAdminForm
|
|
207
|
-
exclude = ('instance_methods', )
|
|
208
241
|
field_mapping = {
|
|
242
|
+
HiddenField: HiddenSerializerField,
|
|
209
243
|
forms.TypedChoiceField: serializers.ChoiceField,
|
|
210
244
|
forms.FloatField: serializers.FloatField,
|
|
211
245
|
forms.SlugField: serializers.CharField,
|
|
@@ -217,7 +251,7 @@ class ComponentSerializer(FormSerializer):
|
|
|
217
251
|
def get_fields(self):
|
|
218
252
|
self.set_form_cls()
|
|
219
253
|
|
|
220
|
-
ret =
|
|
254
|
+
ret = OrderedDict()
|
|
221
255
|
|
|
222
256
|
field_mapping = reduce_attr_dict_from_instance(
|
|
223
257
|
self,
|
|
@@ -234,11 +268,11 @@ class ComponentSerializer(FormSerializer):
|
|
|
234
268
|
if field_name in getattr(self.Meta, 'exclude', []):
|
|
235
269
|
continue
|
|
236
270
|
|
|
237
|
-
# if field is already defined via declared fields
|
|
238
|
-
# skip mapping it from forms which then honors
|
|
239
|
-
# the custom validation defined on the DRF declared field
|
|
240
|
-
if field_name in ret:
|
|
241
|
-
|
|
271
|
+
# # if field is already defined via declared fields
|
|
272
|
+
# # skip mapping it from forms which then honors
|
|
273
|
+
# # the custom validation defined on the DRF declared field
|
|
274
|
+
# if field_name in ret:
|
|
275
|
+
# continue
|
|
242
276
|
|
|
243
277
|
form_field = form[field_name]
|
|
244
278
|
|
|
@@ -266,6 +300,10 @@ class ComponentSerializer(FormSerializer):
|
|
|
266
300
|
ret[field_name].initial = form_field.initial
|
|
267
301
|
ret[field_name].default = form_field.initial
|
|
268
302
|
|
|
303
|
+
for name, field in super(FormSerializerBase, self).get_fields().items():
|
|
304
|
+
if name in ret:
|
|
305
|
+
continue
|
|
306
|
+
ret[name] = field
|
|
269
307
|
return ret
|
|
270
308
|
|
|
271
309
|
def _get_field_kwargs(self, form_field, serializer_field_class):
|
|
@@ -279,6 +317,10 @@ class ComponentSerializer(FormSerializer):
|
|
|
279
317
|
kwargs['formset_field'] = form_field
|
|
280
318
|
kwargs['many'] = True
|
|
281
319
|
|
|
320
|
+
attrs = find_matching_class_kwargs(form_field, serializer_field_class)
|
|
321
|
+
if 'choices' in attrs:
|
|
322
|
+
kwargs['choices'] = form_field.choices
|
|
323
|
+
|
|
282
324
|
return kwargs
|
|
283
325
|
|
|
284
326
|
def set_form_cls(self):
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
simo/fleet/controllers.py
CHANGED
|
@@ -112,7 +112,6 @@ class DS18B20Sensor(FleeDeviceMixin, BasicSensorMixin, BaseNumericSensor):
|
|
|
112
112
|
|
|
113
113
|
|
|
114
114
|
class BaseClimateSensor(FleeDeviceMixin, BasicSensorMixin, BaseMultiSensor):
|
|
115
|
-
manual_add = False
|
|
116
115
|
app_widget = NumericSensorWidget
|
|
117
116
|
|
|
118
117
|
def __init__(self, *args, **kwargs):
|
|
@@ -195,6 +194,29 @@ class BasicOutputMixin:
|
|
|
195
194
|
class Switch(FleeDeviceMixin, BasicOutputMixin, BaseSwitch):
|
|
196
195
|
config_form = ColonelSwitchConfigForm
|
|
197
196
|
|
|
197
|
+
def signal(self, pulses):
|
|
198
|
+
'''
|
|
199
|
+
Expecting list of tuples where each item represents component value
|
|
200
|
+
followed by duration in miliseconds.
|
|
201
|
+
Maximum of 20 pulses is accepted, each pulse might not be longer than 3000ms
|
|
202
|
+
If you need anything longer than this, use on(), off() methods instead.
|
|
203
|
+
:param pulses: [(True, 200), (False, 600), (True, 200)]
|
|
204
|
+
:return: None
|
|
205
|
+
'''
|
|
206
|
+
assert len(pulses) > 0, "At least on pulse is expected"
|
|
207
|
+
assert len(pulses) <= 20, "No more than 20 pulses is accepted"
|
|
208
|
+
for i, pulse in enumerate(pulses):
|
|
209
|
+
assert isinstance(pulse[0], bool), f"{i+1}-th pulse is not boolean!"
|
|
210
|
+
assert pulse[1] <= 3000, "Pulses must not exceed 3000ms"
|
|
211
|
+
|
|
212
|
+
GatewayObjectCommand(
|
|
213
|
+
self.component.gateway,
|
|
214
|
+
Colonel(id=self.component.config['colonel']),
|
|
215
|
+
command='call', method='signal', args=[pulses],
|
|
216
|
+
id=self.component.id,
|
|
217
|
+
).publish()
|
|
218
|
+
|
|
219
|
+
|
|
198
220
|
|
|
199
221
|
class PWMOutput(FleeDeviceMixin, BasicOutputMixin, BaseDimmer):
|
|
200
222
|
name = "Dimmer"
|
|
@@ -432,6 +454,30 @@ class TTLock(FleeDeviceMixin, Lock):
|
|
|
432
454
|
command='call', method='get_fingerprints'
|
|
433
455
|
).publish()
|
|
434
456
|
|
|
457
|
+
def check_locked_status(self):
|
|
458
|
+
'''
|
|
459
|
+
Lock state is monitored by capturing adv data
|
|
460
|
+
periodically transmitted by the lock.
|
|
461
|
+
This data includes information about it's lock/unlock position
|
|
462
|
+
also if there are any new events in it that we are not yet aware of.
|
|
463
|
+
|
|
464
|
+
If anything new is observer, connection is made to the lock
|
|
465
|
+
and reported back to the system.
|
|
466
|
+
This helps to save batteries of a lock,
|
|
467
|
+
however it is not always as timed as we would want to.
|
|
468
|
+
Sometimes it can take even up to 20s for these updates to occur.
|
|
469
|
+
|
|
470
|
+
This method is here to force immediate connection to the lock
|
|
471
|
+
to check it's current status. After this method is called,
|
|
472
|
+
we might expect to receive an update within 2 seconds or less.
|
|
473
|
+
'''
|
|
474
|
+
GatewayObjectCommand(
|
|
475
|
+
self.component.gateway,
|
|
476
|
+
Colonel(id=self.component.config['colonel']),
|
|
477
|
+
id=self.component.id,
|
|
478
|
+
command='call', method='check_locked_status'
|
|
479
|
+
).publish()
|
|
480
|
+
|
|
435
481
|
def _receive_meta(self, data):
|
|
436
482
|
from simo.users.models import Fingerprint
|
|
437
483
|
if 'codes' in data:
|
simo/fleet/forms.py
CHANGED
|
@@ -14,6 +14,7 @@ from simo.core.widgets import LogOutputWidget
|
|
|
14
14
|
from simo.core.utils.easing import EASING_CHOICES
|
|
15
15
|
from simo.core.utils.validators import validate_slaves
|
|
16
16
|
from simo.core.utils.admin import AdminFormActionForm
|
|
17
|
+
from simo.core.events import GatewayObjectCommand
|
|
17
18
|
from .models import Colonel, ColonelPin, Interface
|
|
18
19
|
from .utils import INTERFACES_PINS_MAP
|
|
19
20
|
|
|
@@ -984,6 +985,18 @@ class BurglarSmokeDetectorConfigForm(ColonelComponentForm):
|
|
|
984
985
|
|
|
985
986
|
class TTLockConfigForm(ColonelComponentForm):
|
|
986
987
|
|
|
988
|
+
door_sensor = forms.ModelChoiceField(
|
|
989
|
+
Component.objects.filter(base_type='binary-sensor'),
|
|
990
|
+
required=False,
|
|
991
|
+
help_text="Quickens up lock status reporting on open/close if provided.",
|
|
992
|
+
widget=autocomplete.ModelSelect2(
|
|
993
|
+
url='autocomplete-component', attrs={'data-html': True},
|
|
994
|
+
forward=(
|
|
995
|
+
forward.Const(['binary-sensor'], 'base_type'),
|
|
996
|
+
)
|
|
997
|
+
)
|
|
998
|
+
)
|
|
999
|
+
|
|
987
1000
|
def clean(self):
|
|
988
1001
|
if not self.instance or not self.instance.pk:
|
|
989
1002
|
from .controllers import TTLock
|
|
@@ -1002,6 +1015,11 @@ class TTLockConfigForm(ColonelComponentForm):
|
|
|
1002
1015
|
if commit:
|
|
1003
1016
|
self.cleaned_data['colonel'].components.add(obj)
|
|
1004
1017
|
self.cleaned_data['colonel'].save()
|
|
1018
|
+
if self.cleaned_data['door_sensor']:
|
|
1019
|
+
GatewayObjectCommand(
|
|
1020
|
+
self.instance.gateway, self.cleaned_data['door_sensor'],
|
|
1021
|
+
command='watch_lock_sensor'
|
|
1022
|
+
).publish()
|
|
1005
1023
|
return obj
|
|
1006
1024
|
|
|
1007
1025
|
|
simo/fleet/gateways.py
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import datetime
|
|
2
2
|
import time
|
|
3
|
+
import json
|
|
3
4
|
from django.utils import timezone
|
|
5
|
+
from simo.core.models import Component
|
|
4
6
|
from simo.core.gateways import BaseObjectCommandsGatewayHandler
|
|
5
7
|
from simo.core.forms import BaseGatewayForm
|
|
6
8
|
from simo.core.models import Gateway
|
|
7
|
-
from simo.core.events import GatewayObjectCommand
|
|
9
|
+
from simo.core.events import GatewayObjectCommand, get_event_obj
|
|
8
10
|
from simo.core.utils.serialization import deserialize_form_data
|
|
9
11
|
|
|
10
12
|
|
|
13
|
+
|
|
11
14
|
class FleetGatewayHandler(BaseObjectCommandsGatewayHandler):
|
|
12
15
|
name = "SIMO.io Fleet"
|
|
13
16
|
config_form = BaseGatewayForm
|
|
@@ -18,8 +21,41 @@ class FleetGatewayHandler(BaseObjectCommandsGatewayHandler):
|
|
|
18
21
|
('push_discoveries', 6),
|
|
19
22
|
)
|
|
20
23
|
|
|
24
|
+
def run(self, exit):
|
|
25
|
+
from simo.fleet.controllers import TTLock
|
|
26
|
+
self.door_sensors_on_watch = set()
|
|
27
|
+
for lock in Component.objects.filter(controller_uid=TTLock.uid):
|
|
28
|
+
if not lock.config.get('door_sensor'):
|
|
29
|
+
continue
|
|
30
|
+
door_sensor = Component.objects.filter(
|
|
31
|
+
id=lock.config['door_sensor']
|
|
32
|
+
).first()
|
|
33
|
+
if not door_sensor:
|
|
34
|
+
continue
|
|
35
|
+
self.door_sensors_on_watch.add(door_sensor.id)
|
|
36
|
+
door_sensor.on_change(self.on_door_sensor)
|
|
37
|
+
super().run(exit)
|
|
38
|
+
|
|
39
|
+
|
|
21
40
|
def _on_mqtt_message(self, client, userdata, msg):
|
|
22
|
-
|
|
41
|
+
from simo.core.models import Component
|
|
42
|
+
payload = json.loads(msg.payload)
|
|
43
|
+
if payload.get('command') == 'watch_lock_sensor':
|
|
44
|
+
door_sensor = get_event_obj(payload, Component)
|
|
45
|
+
if not door_sensor:
|
|
46
|
+
return
|
|
47
|
+
print("Adding door sensor to lock watch!")
|
|
48
|
+
if door_sensor.id in self.door_sensors_on_watch:
|
|
49
|
+
return
|
|
50
|
+
self.door_sensors_on_watch.add(door_sensor.id)
|
|
51
|
+
door_sensor.on_change(self.on_door_sensor)
|
|
52
|
+
|
|
53
|
+
def on_door_sensor(self, sensor):
|
|
54
|
+
from simo.fleet.controllers import TTLock
|
|
55
|
+
for lock in Component.objects.filter(
|
|
56
|
+
controller_uid=TTLock.uid, config__door_sensor=sensor.id
|
|
57
|
+
):
|
|
58
|
+
lock.check_locked_status()
|
|
23
59
|
|
|
24
60
|
def look_for_updates(self):
|
|
25
61
|
from .models import Colonel
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
simo/generic/controllers.py
CHANGED
|
@@ -5,6 +5,7 @@ import datetime
|
|
|
5
5
|
import json
|
|
6
6
|
from django.core.exceptions import ValidationError
|
|
7
7
|
from django.utils import timezone
|
|
8
|
+
from django.utils.functional import cached_property
|
|
8
9
|
from django.utils.translation import gettext_lazy as _
|
|
9
10
|
from django.conf import settings
|
|
10
11
|
from django.urls import reverse_lazy
|
|
@@ -354,6 +355,20 @@ class AlarmGroup(ControllerBase):
|
|
|
354
355
|
self.component.config['stats'] = stats
|
|
355
356
|
self.component.save()
|
|
356
357
|
|
|
358
|
+
@cached_property
|
|
359
|
+
def events_map(self):
|
|
360
|
+
map = {}
|
|
361
|
+
for entry in self.component.config.get('breach_events', []):
|
|
362
|
+
if 'uid' not in entry:
|
|
363
|
+
continue
|
|
364
|
+
comp = Component.objects.filter(id=entry['component']).first()
|
|
365
|
+
if not comp:
|
|
366
|
+
continue
|
|
367
|
+
map[entry['uid']] = json.loads(json.dumps(entry))
|
|
368
|
+
map[entry['uid']].pop('uid')
|
|
369
|
+
map[entry['uid']]['component'] = comp
|
|
370
|
+
return map
|
|
371
|
+
|
|
357
372
|
|
|
358
373
|
class WeatherForecast(ControllerBase):
|
|
359
374
|
name = _("Weather Forecast")
|
simo/generic/forms.py
CHANGED
|
@@ -4,7 +4,7 @@ from django.db.models import Q
|
|
|
4
4
|
from django.utils.translation import gettext_lazy as _
|
|
5
5
|
from django.urls.base import get_script_prefix
|
|
6
6
|
from django.contrib.contenttypes.models import ContentType
|
|
7
|
-
from simo.core.forms import BaseComponentForm
|
|
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
10
|
BinarySensor, NumericSensor, MultiSensor, Switch
|
|
@@ -18,6 +18,14 @@ from simo.core.utils.form_fields import ListSelect2Widget
|
|
|
18
18
|
from simo.conf import dynamic_settings
|
|
19
19
|
|
|
20
20
|
|
|
21
|
+
ACTION_METHODS = (
|
|
22
|
+
('turn_on', "Turn ON"), ('turn_off', "Turn OFF"),
|
|
23
|
+
('play', "Play"), ('pause', "Pause"), ('stop', "Stop"),
|
|
24
|
+
('open', 'Open'), ('close', 'Close'),
|
|
25
|
+
('lock', "Lock"), ('unlock', "Unlock"),
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
21
29
|
class ScriptConfigForm(BaseComponentForm):
|
|
22
30
|
autostart = forms.BooleanField(
|
|
23
31
|
initial=True, required=False,
|
|
@@ -149,6 +157,49 @@ class ThermostatConfigForm(BaseComponentForm):
|
|
|
149
157
|
return super().save(commit)
|
|
150
158
|
|
|
151
159
|
|
|
160
|
+
class AlarmBreachEventForm(forms.Form):
|
|
161
|
+
uid = HiddenField(required=False)
|
|
162
|
+
component = forms.ModelChoiceField(
|
|
163
|
+
Component.objects.all(),
|
|
164
|
+
widget=autocomplete.ModelSelect2(
|
|
165
|
+
url='autocomplete-component', attrs={'data-html': True},
|
|
166
|
+
),
|
|
167
|
+
)
|
|
168
|
+
breach_action = forms.ChoiceField(
|
|
169
|
+
initial='turn_on', choices=ACTION_METHODS
|
|
170
|
+
)
|
|
171
|
+
disarm_action = forms.ChoiceField(
|
|
172
|
+
required=False, initial='turn_off', choices=ACTION_METHODS
|
|
173
|
+
)
|
|
174
|
+
delay = forms.IntegerField(
|
|
175
|
+
label="Delay (s)",
|
|
176
|
+
min_value=0, max_value=600, initial=0,
|
|
177
|
+
help_text="Event will not fire if alarm group is disarmed "
|
|
178
|
+
"within given timeframe of seconds after the breach."
|
|
179
|
+
)
|
|
180
|
+
prefix = 'breach_events'
|
|
181
|
+
|
|
182
|
+
def clean(self):
|
|
183
|
+
if not self.cleaned_data.get('component'):
|
|
184
|
+
return self.cleaned_data
|
|
185
|
+
if not self.cleaned_data.get('breach_action'):
|
|
186
|
+
return self.cleaned_data
|
|
187
|
+
component = self.cleaned_data.get('component')
|
|
188
|
+
if not hasattr(component, self.cleaned_data['breach_action']):
|
|
189
|
+
self.add_error(
|
|
190
|
+
'breach_action',
|
|
191
|
+
f"{component} has no {self.cleaned_data['breach_action']} action!"
|
|
192
|
+
)
|
|
193
|
+
if self.cleaned_data.get('disarm_action'):
|
|
194
|
+
if not hasattr(component, self.cleaned_data['disarm_action']):
|
|
195
|
+
self.add_error(
|
|
196
|
+
'disarm_action',
|
|
197
|
+
f"{component} has no "
|
|
198
|
+
f"{self.cleaned_data['disarm_action']} action!"
|
|
199
|
+
)
|
|
200
|
+
return self.cleaned_data
|
|
201
|
+
|
|
202
|
+
|
|
152
203
|
# TODO: create control widget for admin use.
|
|
153
204
|
class AlarmGroupConfigForm(BaseComponentForm):
|
|
154
205
|
components = forms.ModelMultipleChoiceField(
|
|
@@ -167,11 +218,40 @@ class AlarmGroupConfigForm(BaseComponentForm):
|
|
|
167
218
|
required=False,
|
|
168
219
|
help_text="Defines if this is your main/top global alarm group."
|
|
169
220
|
)
|
|
221
|
+
arming_locks = forms.ModelMultipleChoiceField(
|
|
222
|
+
Component.objects.filter(base_type='lock'),
|
|
223
|
+
label="Arming locks", required=False,
|
|
224
|
+
widget=autocomplete.ModelSelect2Multiple(
|
|
225
|
+
url='autocomplete-component', attrs={'data-html': True},
|
|
226
|
+
forward=(
|
|
227
|
+
forward.Const(['lock'], 'base_type'),
|
|
228
|
+
)
|
|
229
|
+
),
|
|
230
|
+
help_text="Alarm group will get armed automatically whenever "
|
|
231
|
+
"any of assigned locks get's locked. "
|
|
232
|
+
)
|
|
233
|
+
disarming_locks = forms.ModelMultipleChoiceField(
|
|
234
|
+
Component.objects.filter(base_type='lock'),
|
|
235
|
+
label="Disarming locks", required=False,
|
|
236
|
+
widget=autocomplete.ModelSelect2Multiple(
|
|
237
|
+
url='autocomplete-component', attrs={'data-html': True},
|
|
238
|
+
forward=(
|
|
239
|
+
forward.Const(['lock'], 'base_type'),
|
|
240
|
+
)
|
|
241
|
+
),
|
|
242
|
+
help_text="Alarm group will be disarmed automatically whenever "
|
|
243
|
+
"any of assigned locks get's unlocked. "
|
|
244
|
+
)
|
|
170
245
|
notify_on_breach = forms.IntegerField(
|
|
171
246
|
required=False, min_value=0,
|
|
172
|
-
help_text="Notify active users if "
|
|
173
|
-
"not disarmed within given number of seconds "
|
|
174
|
-
"
|
|
247
|
+
help_text="Notify active users on breach if "
|
|
248
|
+
"not disarmed within given number of seconds. <br>"
|
|
249
|
+
"Leave this empty to disable breach notifications."
|
|
250
|
+
)
|
|
251
|
+
breach_events = FormsetField(
|
|
252
|
+
formset_factory(
|
|
253
|
+
AlarmBreachEventForm, can_delete=True, can_order=True, extra=0
|
|
254
|
+
), label='Breach events'
|
|
175
255
|
)
|
|
176
256
|
has_alarm = False
|
|
177
257
|
|
|
@@ -194,6 +274,14 @@ class AlarmGroupConfigForm(BaseComponentForm):
|
|
|
194
274
|
self.fields['is_main'].widget.attrs['disabled'] = 'disabled'
|
|
195
275
|
|
|
196
276
|
|
|
277
|
+
def clean_breach_events(self):
|
|
278
|
+
events = self.cleaned_data['breach_events']
|
|
279
|
+
for i, cont in enumerate(events):
|
|
280
|
+
if not cont.get('uid'):
|
|
281
|
+
cont['uid'] = get_random_string(6)
|
|
282
|
+
return events
|
|
283
|
+
|
|
284
|
+
|
|
197
285
|
def recurse_check_alarm_groups(self, components, start_comp=None):
|
|
198
286
|
for comp in components:
|
|
199
287
|
check_cmp = start_comp if start_comp else comp
|
|
@@ -476,16 +564,8 @@ class StateSelectForm(BaseComponentForm):
|
|
|
476
564
|
)
|
|
477
565
|
|
|
478
566
|
|
|
479
|
-
ACTION_METHODS = (
|
|
480
|
-
('turn_on', "Turn ON"), ('turn_off', "Turn OFF"),
|
|
481
|
-
('play', "Play"), ('pause', "Pause"), ('stop', "Stop"),
|
|
482
|
-
('open', 'Open'), ('close', 'Close'),
|
|
483
|
-
('lock', "Lock"), ('unlock', "Unlock"),
|
|
484
|
-
)
|
|
485
|
-
|
|
486
|
-
|
|
487
567
|
class AlarmClockEventForm(forms.Form):
|
|
488
|
-
uid =
|
|
568
|
+
uid = HiddenField(required=False)
|
|
489
569
|
enabled = forms.BooleanField(initial=True)
|
|
490
570
|
name = forms.CharField(max_length=30)
|
|
491
571
|
component = forms.ModelChoiceField(
|
simo/generic/gateways.py
CHANGED
|
@@ -5,6 +5,7 @@ import json
|
|
|
5
5
|
import time
|
|
6
6
|
import multiprocessing
|
|
7
7
|
import threading
|
|
8
|
+
import traceback
|
|
8
9
|
from django.conf import settings
|
|
9
10
|
from django.utils import timezone
|
|
10
11
|
from django.db import connection as db_connection
|
|
@@ -165,7 +166,8 @@ class GenericGatewayHandler(BaseObjectCommandsGatewayHandler):
|
|
|
165
166
|
('watch_thermostats', 60),
|
|
166
167
|
('watch_alarm_clocks', 30),
|
|
167
168
|
('watch_scripts', 10),
|
|
168
|
-
('watch_watering', 60)
|
|
169
|
+
('watch_watering', 60),
|
|
170
|
+
('watch_alarm_events', 1),
|
|
169
171
|
)
|
|
170
172
|
|
|
171
173
|
def watch_thermostats(self):
|
|
@@ -441,6 +443,27 @@ class GenericGatewayHandler(BaseObjectCommandsGatewayHandler):
|
|
|
441
443
|
else:
|
|
442
444
|
switch.turn_off()
|
|
443
445
|
|
|
446
|
+
def watch_alarm_events(self):
|
|
447
|
+
from .controllers import AlarmGroup
|
|
448
|
+
for alarm in Component.objects.filter(
|
|
449
|
+
controller_uid=AlarmGroup.uid, value='breached',
|
|
450
|
+
meta__breach_start__gt=0
|
|
451
|
+
):
|
|
452
|
+
for uid, event in alarm.controller.events_map.items():
|
|
453
|
+
if uid in alarm.meta.get('events_triggered', []):
|
|
454
|
+
continue
|
|
455
|
+
if time.time() - alarm.meta['breach_start'] < event['delay']:
|
|
456
|
+
continue
|
|
457
|
+
try:
|
|
458
|
+
getattr(event['component'], event['breach_action'])()
|
|
459
|
+
except Exception:
|
|
460
|
+
print(traceback.format_exc(), file=sys.stderr)
|
|
461
|
+
if not alarm.meta.get('events_triggered'):
|
|
462
|
+
alarm.meta['events_triggered'] = [uid]
|
|
463
|
+
else:
|
|
464
|
+
alarm.meta['events_triggered'].append(uid)
|
|
465
|
+
alarm.save(update_fields=['meta'])
|
|
466
|
+
|
|
444
467
|
|
|
445
468
|
class DummyGatewayHandler(BaseObjectCommandsGatewayHandler):
|
|
446
469
|
name = "Dummy"
|
simo/generic/models.py
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import sys
|
|
3
|
+
import traceback
|
|
1
4
|
from threading import Timer
|
|
2
5
|
from django.db.models.signals import pre_save, post_save, post_delete
|
|
3
6
|
from django.dispatch import receiver
|
|
@@ -19,7 +22,6 @@ def handle_alarm_groups(sender, instance, *args, **kwargs):
|
|
|
19
22
|
for alarm_group in Component.objects.filter(
|
|
20
23
|
controller_uid=AlarmGroup.uid,
|
|
21
24
|
config__components__contains=instance.id,
|
|
22
|
-
config__notify_on_breach__gt=-1
|
|
23
25
|
).exclude(value='disarmed'):
|
|
24
26
|
stats = {
|
|
25
27
|
'disarmed': 0, 'pending-arm': 0, 'armed': 0, 'breached': 0
|
|
@@ -57,26 +59,44 @@ def handle_alarm_groups(sender, instance, *args, **kwargs):
|
|
|
57
59
|
'alarm', str(alarm_group_component), body,
|
|
58
60
|
component=alarm_group_component
|
|
59
61
|
)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
if alarm_group.config.get('notify_on_breach') is not None:
|
|
63
|
+
t = Timer(
|
|
64
|
+
# give it one second to finish with other db processes.
|
|
65
|
+
alarm_group.config['notify_on_breach'] + 1,
|
|
66
|
+
notify_users_security_breach, [alarm_group.id]
|
|
67
|
+
)
|
|
68
|
+
t.start()
|
|
66
69
|
alarm_group_value = 'breached'
|
|
67
70
|
else:
|
|
68
71
|
alarm_group_value = 'pending-arm'
|
|
72
|
+
|
|
69
73
|
alarm_group.controller.set(alarm_group_value)
|
|
70
74
|
|
|
71
75
|
|
|
72
|
-
@receiver(
|
|
73
|
-
def
|
|
74
|
-
if not created:
|
|
75
|
-
return
|
|
76
|
+
@receiver(pre_save, sender=Component)
|
|
77
|
+
def manage_alarm_groups(sender, instance, *args, **kwargs):
|
|
76
78
|
if instance.controller_uid != AlarmGroup.uid:
|
|
77
79
|
return
|
|
78
|
-
|
|
79
|
-
|
|
80
|
+
|
|
81
|
+
if 'value' not in instance.get_dirty_fields():
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
if instance.value == 'breached':
|
|
85
|
+
instance.meta['breach_start'] = time.time()
|
|
86
|
+
instance.meta['events_triggered'] = []
|
|
87
|
+
elif instance.get_dirty_fields()['value'] == 'breached' \
|
|
88
|
+
and instance.value == 'disarmed':
|
|
89
|
+
instance.meta['breach_start'] = None
|
|
90
|
+
for event_uid in instance.meta.get('events_triggered', []):
|
|
91
|
+
event = instance.controller.events_map.get(event_uid)
|
|
92
|
+
if not event:
|
|
93
|
+
continue
|
|
94
|
+
if not event.get('disarm_action'):
|
|
95
|
+
continue
|
|
96
|
+
try:
|
|
97
|
+
getattr(event['component'], event['disarm_action'])()
|
|
98
|
+
except Exception:
|
|
99
|
+
print(traceback.format_exc(), file=sys.stderr)
|
|
80
100
|
|
|
81
101
|
|
|
82
102
|
@receiver(post_delete, sender=Component)
|
|
@@ -91,3 +111,24 @@ def clear_alarm_group_config_on_component_delete(
|
|
|
91
111
|
id for id in ag.config.get('components', []) if id != instance.id
|
|
92
112
|
]
|
|
93
113
|
ag.save(update_fields=['config'])
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@receiver(post_save, sender=Component)
|
|
117
|
+
def bind_controlling_locks_to_alarm_groups(sender, instance, *args, **kwargs):
|
|
118
|
+
if instance.base_type != 'lock':
|
|
119
|
+
return
|
|
120
|
+
if 'value' not in instance.get_dirty_fields():
|
|
121
|
+
return
|
|
122
|
+
if instance.value == 'locked':
|
|
123
|
+
for ag in Component.objects.filter(
|
|
124
|
+
base_type=AlarmGroup.base_type,
|
|
125
|
+
config__arming_locks__contains=instance.id
|
|
126
|
+
):
|
|
127
|
+
ag.arm()
|
|
128
|
+
elif instance.value == 'unlocked':
|
|
129
|
+
for ag in Component.objects.filter(
|
|
130
|
+
base_type=AlarmGroup.base_type,
|
|
131
|
+
config__arming_locks__contains=instance.id
|
|
132
|
+
):
|
|
133
|
+
ag.disarm()
|
|
134
|
+
|
|
@@ -25,10 +25,10 @@ simo/_hub_template/hub/settings.py,sha256=4QhvhbtLRxHvAntwqG_qeAAtpDUqKvN4jzw9u3
|
|
|
25
25
|
simo/_hub_template/hub/supervisor.conf,sha256=IY3fdK0fDD2eAothB0n54xhjQj8LYoXIR96-Adda5Z8,1353
|
|
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
|
-
simo/core/admin.py,sha256=
|
|
28
|
+
simo/core/admin.py,sha256=5QFmf7ZOqkSEba2Fo-UfJp5dVe-vhKUtOUlAnfQGhno,17634
|
|
29
29
|
simo/core/api.py,sha256=OVvv3a7KYJGbuUgSOfWzPg8hoCCRhdDejQ2kVs0H2p8,23803
|
|
30
30
|
simo/core/api_auth.py,sha256=_3hG4e1eLKrcRnSAOB_xTL6cwtOJ2_7JS7GZU_iqTgA,1251
|
|
31
|
-
simo/core/api_meta.py,sha256=
|
|
31
|
+
simo/core/api_meta.py,sha256=dZkz7z-7GaMPVAsfQxOYCvpYaMPx_v2zynbY6JM8oD8,3477
|
|
32
32
|
simo/core/app_widgets.py,sha256=EEQOto3fGR0syDqpJE38tQrx8DoTTyg26nF5kYzHY38,2018
|
|
33
33
|
simo/core/auto_urls.py,sha256=0gu-IL7PHobrmKW6ksffiOkAYu-aIorykWdxRNtwGYo,1194
|
|
34
34
|
simo/core/autocomplete_views.py,sha256=JT5LA2_Wtr60XYSAIqaXFKFYPjrmkEf6yunXD9y2zco,4022
|
|
@@ -38,7 +38,7 @@ simo/core/controllers.py,sha256=2D7YCLktx7a-4tn80DenQP2CdY0dc2bWg6IRU69YTZ8,2724
|
|
|
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=
|
|
41
|
+
simo/core/forms.py,sha256=H66FJCX_OJ8ADTfFHRLLl6xEg5AsaFH59rbd2rCE2c8,21711
|
|
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
|
|
@@ -46,7 +46,7 @@ simo/core/middleware.py,sha256=64PYjnyRnYf4sgMvPfR0oQqf9UEtxUwnhJe3RV6z_HI,2040
|
|
|
46
46
|
simo/core/models.py,sha256=j5AymbJFt5HOIOYsHJ8UUKhb1TvIoqgH0T1y3BeGJuM,19408
|
|
47
47
|
simo/core/permissions.py,sha256=UmFjGPDWtAUbaWxJsWORb2q6BREHqndv9mkSIpnmdLk,1379
|
|
48
48
|
simo/core/routing.py,sha256=X1_IHxyA-_Q7hw1udDoviVP4_FSBDl8GYETTC2zWTbY,499
|
|
49
|
-
simo/core/serializers.py,sha256=
|
|
49
|
+
simo/core/serializers.py,sha256=dY2R9KPvmmwMP4Up_6AHM6lf0MY3BoON8Dn8ixFubAI,17118
|
|
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
|
|
@@ -56,10 +56,10 @@ 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
|
-
simo/core/__pycache__/admin.cpython-38.pyc,sha256=
|
|
59
|
+
simo/core/__pycache__/admin.cpython-38.pyc,sha256=D9Hu5xMkclP6xFjEOLB-mBOe0ZMuCF10uilsROKkvmM,13446
|
|
60
60
|
simo/core/__pycache__/api.cpython-38.pyc,sha256=W-CdACWTu_ebqH52riswadzT7z2mNRZJU8HngqbYXzw,18874
|
|
61
61
|
simo/core/__pycache__/api_auth.cpython-38.pyc,sha256=5UTBr3rDMERAfc0OuOVDwGeQkt6Q7GLBtZJAMBse1sg,1712
|
|
62
|
-
simo/core/__pycache__/api_meta.cpython-38.pyc,sha256=
|
|
62
|
+
simo/core/__pycache__/api_meta.cpython-38.pyc,sha256=7dDi_Aay7T4eSNYmEItMlb7yU91-5_pzEVg8GXXf4Qc,2783
|
|
63
63
|
simo/core/__pycache__/app_widgets.cpython-38.pyc,sha256=9Es2wZNduzUJv-jZ_HX0-L3vqwpXWBbseEwoC5K6b-w,3465
|
|
64
64
|
simo/core/__pycache__/auto_urls.cpython-38.pyc,sha256=SVl4fF0-yiq7e9gt08jIM6_rL4JYcR0cNHzR9jCEi1M,931
|
|
65
65
|
simo/core/__pycache__/autocomplete_views.cpython-38.pyc,sha256=hJ6JILI1LqrAtpQMvxnLvljGdW1v1gpvBsD79vFkZ58,3972
|
|
@@ -69,7 +69,7 @@ simo/core/__pycache__/controllers.cpython-38.pyc,sha256=-NjuX7iGheE_ZMqkZ6g4ZnnV
|
|
|
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=
|
|
72
|
+
simo/core/__pycache__/forms.cpython-38.pyc,sha256=ikU7wTC6vxzsCJJul0VD5qFJlpXHvvksSkwGun8tYVM,18187
|
|
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
|
|
@@ -77,7 +77,7 @@ simo/core/__pycache__/middleware.cpython-38.pyc,sha256=bGOFJNEhJeLbpsZp8LYn1VA3p
|
|
|
77
77
|
simo/core/__pycache__/models.cpython-38.pyc,sha256=Gm36LWRxswvWiB3Wz0F7g32ZVXugh7chSSBz1lgBPZs,16995
|
|
78
78
|
simo/core/__pycache__/permissions.cpython-38.pyc,sha256=uygjPbfRQiEzyo5-McCxsuMDJLbDYO_TLu55U7bJbR0,1809
|
|
79
79
|
simo/core/__pycache__/routing.cpython-38.pyc,sha256=3T3FPJ8Cn99xZCGvMyg2xjl7al-Shm9CelbSpkJtNP8,599
|
|
80
|
-
simo/core/__pycache__/serializers.cpython-38.pyc,sha256=
|
|
80
|
+
simo/core/__pycache__/serializers.cpython-38.pyc,sha256=07FzXZGZBEHlx1vboZThhU98WpmJDSbcQP3uYWNKMDw,16551
|
|
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
|
|
@@ -116,7 +116,7 @@ simo/core/drf_braces/serializers/enforce_validation_serializer.py,sha256=CQnnx8E
|
|
|
116
116
|
simo/core/drf_braces/serializers/form_serializer.py,sha256=CacYpFHLbBFT9Cyxuiya4ZpAH64m_D4oyUk-JgiyYDc,14818
|
|
117
117
|
simo/core/drf_braces/serializers/swapping.py,sha256=8gQerjgEzFx9nzRFKfy66REA51GOlwX17NsmhWSiGgo,1790
|
|
118
118
|
simo/core/drf_braces/serializers/__pycache__/__init__.cpython-38.pyc,sha256=tyqVhHyZyg_OeWz9jptQ7Ya4M-AtUq8EJDLVrRbgUow,181
|
|
119
|
-
simo/core/drf_braces/serializers/__pycache__/form_serializer.cpython-38.pyc,sha256=
|
|
119
|
+
simo/core/drf_braces/serializers/__pycache__/form_serializer.cpython-38.pyc,sha256=9_RxMMXVE2qQgShX4797uj_4nJPx9JQezwEn-g4c8j4,13029
|
|
120
120
|
simo/core/drf_braces/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
121
121
|
simo/core/drf_braces/tests/test_mixins.py,sha256=PlGkHrC8-lui2CcRu3PS3fKqEAP5lzdbzJFXAMu1CF8,4065
|
|
122
122
|
simo/core/drf_braces/tests/test_parsers.py,sha256=Sv6v6TWTnup72a-gXely5g7x-n8EtOc_uhYF95YZBK8,2171
|
|
@@ -10168,9 +10168,9 @@ simo/fleet/api.py,sha256=Hxn84xI-Q77HxjINgRbjSJQOv9jii4OL20LxK0VSrS8,2499
|
|
|
10168
10168
|
simo/fleet/auto_urls.py,sha256=X04oKJWA48wFW5iXg3PPROY2KDdHn_a99orQSE28QC4,518
|
|
10169
10169
|
simo/fleet/base_types.py,sha256=wL9RVkHr0gA7HI1wZq0pruGEIgvQqpfnCL4cC3ywsvw,102
|
|
10170
10170
|
simo/fleet/ble.py,sha256=eHA_9ABjbmH1vUVCv9hiPXQL2GZZSEVwfO0xyI1S0nI,1081
|
|
10171
|
-
simo/fleet/controllers.py,sha256=
|
|
10172
|
-
simo/fleet/forms.py,sha256=
|
|
10173
|
-
simo/fleet/gateways.py,sha256=
|
|
10171
|
+
simo/fleet/controllers.py,sha256=rTxRFf-LKWAZxzixrsLZHHm51BmMx9a1PLdgf6inlNM,20533
|
|
10172
|
+
simo/fleet/forms.py,sha256=rLymJ2ovNgmwdKYJtxTfjuIm5Q4p3GLkIb7-Yo2DmS4,36375
|
|
10173
|
+
simo/fleet/gateways.py,sha256=KV5i5fxXIrlK-k6zyEkk83x11GJt-ELQ0npb4Ac83cM,3693
|
|
10174
10174
|
simo/fleet/managers.py,sha256=XOpDOA9L-f_550TNSyXnJbun2EmtGz1TenVTMlUSb8E,807
|
|
10175
10175
|
simo/fleet/models.py,sha256=J-rnn7Ew-7s3646NNRVY947Sbz21mUD_nBHtuHAKXds,14160
|
|
10176
10176
|
simo/fleet/routing.py,sha256=cofGsVWXMfPDwsJ6HM88xxtRxHwERhJ48Xyxc8mxg5o,149
|
|
@@ -10184,9 +10184,9 @@ simo/fleet/__pycache__/api.cpython-38.pyc,sha256=rL9fb7cCQatyFvXyKmlNOKmxVo8vHYe
|
|
|
10184
10184
|
simo/fleet/__pycache__/auto_urls.cpython-38.pyc,sha256=SqyTuaz_kEBvx-bL46SclsZEEP5RFh6U6TGKyXDdiOE,565
|
|
10185
10185
|
simo/fleet/__pycache__/base_types.cpython-38.pyc,sha256=deyPwjpT6xZiFxBGFnj5b7R-lbdOTh2krgpJhrcGVhc,274
|
|
10186
10186
|
simo/fleet/__pycache__/ble.cpython-38.pyc,sha256=Nrof9w7cm4OlpFWHeVnmvvanh2_oF9oQ3TknJiV93-0,1267
|
|
10187
|
-
simo/fleet/__pycache__/controllers.cpython-38.pyc,sha256=
|
|
10188
|
-
simo/fleet/__pycache__/forms.cpython-38.pyc,sha256
|
|
10189
|
-
simo/fleet/__pycache__/gateways.cpython-38.pyc,sha256=
|
|
10187
|
+
simo/fleet/__pycache__/controllers.cpython-38.pyc,sha256=l9bz18Qp33C12TJOKPSn9vIXnlBKnBusODNk7Fg64qA,18103
|
|
10188
|
+
simo/fleet/__pycache__/forms.cpython-38.pyc,sha256=-Sbb1kBxb246hV8A2ESlR4LVEISJNedVFiOK1IzRt6c,25954
|
|
10189
|
+
simo/fleet/__pycache__/gateways.cpython-38.pyc,sha256=YAcgTOqJbtjGI03lvEcU6keFfrwAHkObVmErYzfRvjk,3569
|
|
10190
10190
|
simo/fleet/__pycache__/managers.cpython-38.pyc,sha256=8uz-xpUiqbGDgXIZ_XRZtFb-Tju6NGxflGg-Ee4Yo6k,1310
|
|
10191
10191
|
simo/fleet/__pycache__/models.cpython-38.pyc,sha256=S9pPqRjIxASXahoIOkkjQX7cBwjkdu4d2nXMju0-Cf8,12283
|
|
10192
10192
|
simo/fleet/__pycache__/routing.cpython-38.pyc,sha256=aPrCmxFKVyB8R8ZbJDwdPdFfvT7CvobovvZeq_mqRgY,314
|
|
@@ -10261,25 +10261,25 @@ simo/fleet/migrations/__pycache__/0029_alter_i2cinterface_scl_pin_and_more.cpyth
|
|
|
10261
10261
|
simo/fleet/migrations/__pycache__/0030_colonelpin_label_alter_colonel_type.cpython-38.pyc,sha256=sfglSDxXLKJ0qE8Dl3MYjv5hbszpDtb9CDxatoEzPSw,853
|
|
10262
10262
|
simo/fleet/migrations/__pycache__/0031_alter_colonel_type.cpython-38.pyc,sha256=zXX254ZgEE4uSV8xnLdH9DM3qy-ICmbrT05i0Q287bU,733
|
|
10263
10263
|
simo/fleet/migrations/__pycache__/0032_auto_20240415_0736.cpython-38.pyc,sha256=QD3JNIDQhzseXKLRYysYY3Q9_vDaurIhlWBcri83FMw,1655
|
|
10264
|
-
simo/fleet/migrations/__pycache__/0033_auto_20240415_0736.cpython-38.pyc,sha256=
|
|
10264
|
+
simo/fleet/migrations/__pycache__/0033_auto_20240415_0736.cpython-38.pyc,sha256=rZK8jUEeuXM7BZ7XCl0RHXXaGakzd_WcuFxmPjp5F_s,1046
|
|
10265
10265
|
simo/fleet/migrations/__pycache__/0034_auto_20240418_0735.cpython-38.pyc,sha256=FEd_tw1GVVrRYd44Fdx1Yf-rsVIebHLel7jlv434A_E,924
|
|
10266
10266
|
simo/fleet/migrations/__pycache__/__init__.cpython-38.pyc,sha256=5k1KW0jeSDzw6RnVPRq4CaO13Lg7M0F-pxA_gqqZ6Mg,170
|
|
10267
10267
|
simo/generic/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10268
10268
|
simo/generic/app_widgets.py,sha256=E_pnpA1hxMIhenRCrHoQ5cik06jm2BAHCkl_eo-OudU,1264
|
|
10269
10269
|
simo/generic/base_types.py,sha256=djymox_boXTHX1BTTCLXrCH7ED-uAsV_idhaDOc3OLI,409
|
|
10270
|
-
simo/generic/controllers.py,sha256=
|
|
10271
|
-
simo/generic/forms.py,sha256=
|
|
10272
|
-
simo/generic/gateways.py,sha256=
|
|
10273
|
-
simo/generic/models.py,sha256=
|
|
10270
|
+
simo/generic/controllers.py,sha256=WYuOUzDWvkYRaTvlbdGy_qmwp1o_ohqKDfV7OrOq2QU,52218
|
|
10271
|
+
simo/generic/forms.py,sha256=lrdGgOuFxizUu2l5PbW8CUiI0V_L8xHl2pN_kZB9238,23160
|
|
10272
|
+
simo/generic/gateways.py,sha256=b3tQ2bAkDVYXCF5iZi2yi-6nZAM8WmHE9ICwxMyR0to,17034
|
|
10273
|
+
simo/generic/models.py,sha256=qRwZq92_suejbXSPTqbjIfICT0Y_ABywwYPa8DJ2V8A,5053
|
|
10274
10274
|
simo/generic/routing.py,sha256=elQVZmgnPiieEuti4sJ7zITk1hlRxpgbotcutJJgC60,228
|
|
10275
10275
|
simo/generic/socket_consumers.py,sha256=NfTQGYtVAc864IoogZRxf_0xpDPM0eMCWn0SlKA5P7Y,1751
|
|
10276
10276
|
simo/generic/__pycache__/__init__.cpython-38.pyc,sha256=mLu54WS9KIl-pHwVCBKpsDFIlOqml--JsOVzAUHg6cU,161
|
|
10277
10277
|
simo/generic/__pycache__/app_widgets.cpython-38.pyc,sha256=0IoKRG9n1tkNRRkrqAeOQwWBPd_33u98JBcVtMVVCio,2374
|
|
10278
10278
|
simo/generic/__pycache__/base_types.cpython-38.pyc,sha256=ptw6axyAqemZA35oa6vzr7EihzvbhW9w7Y-G6kfDedU,555
|
|
10279
|
-
simo/generic/__pycache__/controllers.cpython-38.pyc,sha256=
|
|
10280
|
-
simo/generic/__pycache__/forms.cpython-38.pyc,sha256=
|
|
10281
|
-
simo/generic/__pycache__/gateways.cpython-38.pyc,sha256=
|
|
10282
|
-
simo/generic/__pycache__/models.cpython-38.pyc,sha256=
|
|
10279
|
+
simo/generic/__pycache__/controllers.cpython-38.pyc,sha256=e0bvgyePgJbIs1omBq0TRPlVSKar2sK_JbUKqDRj7mY,33235
|
|
10280
|
+
simo/generic/__pycache__/forms.cpython-38.pyc,sha256=IOiN9D0NVn8e8OOieawG0QjSTZDcmFhGsf2i6oWaj5A,17430
|
|
10281
|
+
simo/generic/__pycache__/gateways.cpython-38.pyc,sha256=a4lLIMPyxm9tNzIqorXHIPZFVTcXlPsM1ycJMghxcHA,12673
|
|
10282
|
+
simo/generic/__pycache__/models.cpython-38.pyc,sha256=j4jKYVTM4XUfFzxIaRFhYx0oRSYzDRn8YVQyCteEhtc,3881
|
|
10283
10283
|
simo/generic/__pycache__/routing.cpython-38.pyc,sha256=xtxTUTBTdivzFyA5Wh7k-hUj1WDO_FiRq6HYXdbr9Ks,382
|
|
10284
10284
|
simo/generic/__pycache__/socket_consumers.cpython-38.pyc,sha256=piFHces0J9QuXu_CNBCQCYjoZEeoaxyVjLfJ9KaR8C8,1898
|
|
10285
10285
|
simo/generic/static/weather_icons/01d@2x.png,sha256=TZfWi6Rfddb2P-oldWWcjUiuCHiU9Yrc5hyrQAhF26I,948
|
|
@@ -10444,8 +10444,8 @@ simo/users/templates/invitations/expired_msg.html,sha256=47DEQpj8HBSa-_TImW-5JCe
|
|
|
10444
10444
|
simo/users/templates/invitations/expired_suggestion.html,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10445
10445
|
simo/users/templates/invitations/taken_msg.html,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10446
10446
|
simo/users/templates/invitations/taken_suggestion.html,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10447
|
-
simo-2.0.
|
|
10448
|
-
simo-2.0.
|
|
10449
|
-
simo-2.0.
|
|
10450
|
-
simo-2.0.
|
|
10451
|
-
simo-2.0.
|
|
10447
|
+
simo-2.0.13.dist-info/LICENSE.md,sha256=M7wm1EmMGDtwPRdg7kW4d00h1uAXjKOT3HFScYQMeiE,34916
|
|
10448
|
+
simo-2.0.13.dist-info/METADATA,sha256=rwjjG5hKGMgEGunNKlhYByM46Kepk1OFgrYTcoQ5bcg,1700
|
|
10449
|
+
simo-2.0.13.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
10450
|
+
simo-2.0.13.dist-info/top_level.txt,sha256=GmS1hrAbpVqn9OWZh6UX82eIOdRLgYA82RG9fe8v4Rs,5
|
|
10451
|
+
simo-2.0.13.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|