simo 2.0.11__py3-none-any.whl → 2.0.12__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__/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/api_meta.py +5 -2
- simo/core/drf_braces/serializers/__pycache__/form_serializer.cpython-38.pyc +0 -0
- simo/core/forms.py +11 -2
- simo/core/serializers.py +19 -3
- 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 +24 -0
- 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.12.dist-info}/METADATA +1 -1
- {simo-2.0.11.dist-info → simo-2.0.12.dist-info}/RECORD +27 -27
- {simo-2.0.11.dist-info → simo-2.0.12.dist-info}/LICENSE.md +0 -0
- {simo-2.0.11.dist-info → simo-2.0.12.dist-info}/WHEEL +0 -0
- {simo-2.0.11.dist-info → simo-2.0.12.dist-info}/top_level.txt +0 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
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,7 +287,7 @@ class ComponentAdminForm(forms.ModelForm):
|
|
|
278
287
|
model = Component
|
|
279
288
|
fields = '__all__'
|
|
280
289
|
exclude = (
|
|
281
|
-
'gateway', 'controller_uid', 'base_type',
|
|
290
|
+
'gateway', 'controller_uid', 'base_type', 'instance_methods'
|
|
282
291
|
'alive', 'value_type', 'value', 'arm_status',
|
|
283
292
|
)
|
|
284
293
|
widgets = {
|
|
@@ -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):
|
simo/core/serializers.py
CHANGED
|
@@ -2,16 +2,18 @@ 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 simo.core.forms import FormsetField
|
|
11
|
+
from simo.core.forms import HiddenField, FormsetField
|
|
11
12
|
from rest_framework.relations import PrimaryKeyRelatedField, ManyRelatedField
|
|
12
13
|
from .drf_braces.serializers.form_serializer import (
|
|
13
14
|
FormSerializer, FormSerializerBase, reduce_attr_dict_from_instance,
|
|
14
|
-
FORM_SERIALIZER_FIELD_MAPPING, set_form_partial_validation
|
|
15
|
+
FORM_SERIALIZER_FIELD_MAPPING, set_form_partial_validation,
|
|
16
|
+
find_matching_class_kwargs
|
|
15
17
|
)
|
|
16
18
|
from .forms import ComponentAdminForm
|
|
17
19
|
from .models import Category, Zone, Icon, ComponentHistory
|
|
@@ -60,7 +62,6 @@ class CategorySerializer(serializers.ModelSerializer):
|
|
|
60
62
|
return
|
|
61
63
|
|
|
62
64
|
|
|
63
|
-
|
|
64
65
|
class ObjectSerializerMethodField(serializers.SerializerMethodField):
|
|
65
66
|
|
|
66
67
|
def bind(self, field_name, parent):
|
|
@@ -83,6 +84,9 @@ class FormsetPrimaryKeyRelatedField(PrimaryKeyRelatedField):
|
|
|
83
84
|
|
|
84
85
|
# 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
86
|
|
|
87
|
+
class HiddenSerializerField(serializers.CharField):
|
|
88
|
+
pass
|
|
89
|
+
|
|
86
90
|
|
|
87
91
|
class ComponentFormsetField(FormSerializer):
|
|
88
92
|
|
|
@@ -91,6 +95,7 @@ class ComponentFormsetField(FormSerializer):
|
|
|
91
95
|
# we set it to proper formset form on __init__
|
|
92
96
|
form = forms.Form
|
|
93
97
|
field_mapping = {
|
|
98
|
+
HiddenField: HiddenSerializerField,
|
|
94
99
|
forms.ModelChoiceField: FormsetPrimaryKeyRelatedField,
|
|
95
100
|
forms.TypedChoiceField: serializers.ChoiceField,
|
|
96
101
|
forms.FloatField: serializers.FloatField,
|
|
@@ -145,6 +150,11 @@ class ComponentFormsetField(FormSerializer):
|
|
|
145
150
|
kwargs['style'] = {'form_field': form_field}
|
|
146
151
|
if serializer_field_class == FormsetPrimaryKeyRelatedField:
|
|
147
152
|
kwargs['queryset'] = form_field.queryset
|
|
153
|
+
|
|
154
|
+
attrs = find_matching_class_kwargs(form_field, serializer_field_class)
|
|
155
|
+
if 'choices' in attrs:
|
|
156
|
+
kwargs['choices'] = attrs['choices']
|
|
157
|
+
|
|
148
158
|
return kwargs
|
|
149
159
|
|
|
150
160
|
def to_representation(self, instance):
|
|
@@ -206,6 +216,7 @@ class ComponentSerializer(FormSerializer):
|
|
|
206
216
|
form = ComponentAdminForm
|
|
207
217
|
exclude = ('instance_methods', )
|
|
208
218
|
field_mapping = {
|
|
219
|
+
HiddenField: HiddenSerializerField,
|
|
209
220
|
forms.TypedChoiceField: serializers.ChoiceField,
|
|
210
221
|
forms.FloatField: serializers.FloatField,
|
|
211
222
|
forms.SlugField: serializers.CharField,
|
|
@@ -266,6 +277,7 @@ class ComponentSerializer(FormSerializer):
|
|
|
266
277
|
ret[field_name].initial = form_field.initial
|
|
267
278
|
ret[field_name].default = form_field.initial
|
|
268
279
|
|
|
280
|
+
print("Fields: ", ret)
|
|
269
281
|
return ret
|
|
270
282
|
|
|
271
283
|
def _get_field_kwargs(self, form_field, serializer_field_class):
|
|
@@ -279,6 +291,10 @@ class ComponentSerializer(FormSerializer):
|
|
|
279
291
|
kwargs['formset_field'] = form_field
|
|
280
292
|
kwargs['many'] = True
|
|
281
293
|
|
|
294
|
+
attrs = find_matching_class_kwargs(form_field, serializer_field_class)
|
|
295
|
+
if 'choices' in attrs:
|
|
296
|
+
kwargs['choices'] = form_field.choices
|
|
297
|
+
|
|
282
298
|
return kwargs
|
|
283
299
|
|
|
284
300
|
def set_form_cls(self):
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
simo/fleet/controllers.py
CHANGED
|
@@ -432,6 +432,30 @@ class TTLock(FleeDeviceMixin, Lock):
|
|
|
432
432
|
command='call', method='get_fingerprints'
|
|
433
433
|
).publish()
|
|
434
434
|
|
|
435
|
+
def check_locked_status(self):
|
|
436
|
+
'''
|
|
437
|
+
Lock state is monitored by capturing adv data
|
|
438
|
+
periodically transmitted by the lock.
|
|
439
|
+
This data includes information about it's lock/unlock position
|
|
440
|
+
also if there are any new events in it that we are not yet aware of.
|
|
441
|
+
|
|
442
|
+
If anything new is observer, connection is made to the lock
|
|
443
|
+
and reported back to the system.
|
|
444
|
+
This helps to save batteries of a lock,
|
|
445
|
+
however it is not always as timed as we would want to.
|
|
446
|
+
Sometimes it can take even up to 20s for these updates to occur.
|
|
447
|
+
|
|
448
|
+
This method is here to force immediate connection to the lock
|
|
449
|
+
to check it's current status. After this method is called,
|
|
450
|
+
we might expect to receive an update within 2 seconds or less.
|
|
451
|
+
'''
|
|
452
|
+
GatewayObjectCommand(
|
|
453
|
+
self.component.gateway,
|
|
454
|
+
Colonel(id=self.component.config['colonel']),
|
|
455
|
+
id=self.component.id,
|
|
456
|
+
command='call', method='check_locked_status'
|
|
457
|
+
).publish()
|
|
458
|
+
|
|
435
459
|
def _receive_meta(self, data):
|
|
436
460
|
from simo.users.models import Fingerprint
|
|
437
461
|
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
|
+
|
|
@@ -28,7 +28,7 @@ simo/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
28
28
|
simo/core/admin.py,sha256=MoYz7B5YaTkF2JeacaqsR177N2IagKXc-rG0FTXjJ1w,17622
|
|
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=QSBxiVD-mHG6b5M9ALvSw4mKTzZb3e0TY_m7bqD152A,21686
|
|
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=aiFddwjss4Zwf05S1bMcDIePNUeBDPyTtJabNrZB26c,16351
|
|
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
|
|
@@ -59,7 +59,7 @@ simo/core/__pycache__/__init__.cpython-38.pyc,sha256=y0IW37wBUIGa3Eh_ZG28pRqHKoL
|
|
|
59
59
|
simo/core/__pycache__/admin.cpython-38.pyc,sha256=GP3lRW8djW3w_k5jgzs_fuW7j8wnmeysZcDtm5RFnD0,13442
|
|
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=Wn5ZNCT5nqYUQUghSbdppYa1v4Fd8tuEcZAYDbae8wo,18215
|
|
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=8N_iZ3V8E5E-el3OQQRAMANJvNrYLGWb7hy91IHVtGc,16136
|
|
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=SypK2i_-0iNNVDr9CWtjFVUmFjC81mtwpAwRUI_xH-A,19561
|
|
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=H7RNRVGyT8ZNW3Z32sCePbALxq1huzwOroLPak2kLck,17163
|
|
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.12.dist-info/LICENSE.md,sha256=M7wm1EmMGDtwPRdg7kW4d00h1uAXjKOT3HFScYQMeiE,34916
|
|
10448
|
+
simo-2.0.12.dist-info/METADATA,sha256=WO2Mt2Kexp6wdYCPQ_aw7e4ebWdaCi8ZET12OdoZIs4,1700
|
|
10449
|
+
simo-2.0.12.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
|
10450
|
+
simo-2.0.12.dist-info/top_level.txt,sha256=GmS1hrAbpVqn9OWZh6UX82eIOdRLgYA82RG9fe8v4Rs,5
|
|
10451
|
+
simo-2.0.12.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|