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.

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 ComponentManyToManyRelatedField
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):
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
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
- pass
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
@@ -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
- "after the breached."
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 = forms.CharField(widget=forms.HiddenInput(), required=False)
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
- t = Timer(
61
- # give it one second to finish with other db processes.
62
- alarm_group.config['notify_on_breach'] + 1,
63
- notify_users_security_breach, [alarm_group.id]
64
- )
65
- t.start()
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(post_save, sender=Component)
73
- def set_initial_alarm_group_stats(sender, instance, created, *args, **kwargs):
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
- if instance.controller:
79
- instance.controller.refresh_status()
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
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: simo
3
- Version: 2.0.11
3
+ Version: 2.0.12
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
@@ -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=li4KLFl6xTYOoD8pgS8PYcjqTZlzhXI0reAkt06PZFA,3404
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=OQTPuacid9AoEvZ86G9p6ILuviqJCtSDuwk2WlLj43E,21484
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=miEh2Sd47KmB0viyNlwrEG_1kxYiTUrvQQvisEoLYuA,15759
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=bdk1PG7FuRfFwcvQtF103H0kQrx6Vt0JRiepktWEPh8,2730
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=KH1SBm1NaDv99yHONenWWJcd4XSIqQ5tqaF8LSoeDws,17843
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=tJf8jV9pPJXnJjUPXCc103wQxC5ZrZaC9SlIR41JomQ,15707
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=ko0jgtrD9XeMBwIN13gF3bepei_LL5FZgoI5GiiT3TY,13029
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=8kLEGNrZ3v5H_X9t8CUi6E6g8hTVohILHxgMSVSrGgM,18515
10172
- simo/fleet/forms.py,sha256=lWJ9luZiRJD4yLQCdCMmkvezk-vY3yVb3q7Y5C4r9qc,35649
10173
- simo/fleet/gateways.py,sha256=EbSxv1DGSuv8MT7MmpZxKgV041umOteFW2Xo4LybzWI,2284
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=3zVvD2Sp26JJfU_nQm8oe3O12-xXBQC7EKlaOpidDbw,16180
10188
- simo/fleet/__pycache__/forms.cpython-38.pyc,sha256=pHgx8ulx_2v0L0bkUHaW13Tn9KcfOP6ROhgHXPVvKJE,25510
10189
- simo/fleet/__pycache__/gateways.cpython-38.pyc,sha256=xg7fW5JusYwl6epn5nEjxQJlFOyQ18SVybOaRBcpwl8,2418
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=zN3KrYHzE40Wfnt48zp7AGCIa8Jj9-fPXxo3adZIxwo,1046
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=Mn4jmOx-1Yb5rAZb9ZcfMcYBMf7r61NQV2p8JgxbYvQ,51675
10271
- simo/generic/forms.py,sha256=p-gzH1z7J0CV8bKMmWTQluzpYmgc7fF1aEHL7eL-fJI,20075
10272
- simo/generic/gateways.py,sha256=L3obiClEomiRbWfDu_LrMbhH9r7hUH4e0oBeLMXghXw,16027
10273
- simo/generic/models.py,sha256=d00Q-UXtt7mG9MdPpZ3mXnxKjwlTI2FgCEklZpGBk7s,3629
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=3U3W-EgpKdqh7MN94ZaoCEGvY4SSq2zsTptJoZbysgM,32795
10280
- simo/generic/__pycache__/forms.cpython-38.pyc,sha256=Uvh-YQxcN-0RCinRi13fyITM1Jxm6FwYQyXs3OLNruA,15699
10281
- simo/generic/__pycache__/gateways.cpython-38.pyc,sha256=syQzoWdRGcU4kKRMC8HfbQ9VQiGxMWkzYwJDqQcfDwg,11940
10282
- simo/generic/__pycache__/models.cpython-38.pyc,sha256=ItjBjJaioJttzSUJsHCKV4xiqH6QX90kRw1C_Xx7mMk,3065
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.11.dist-info/LICENSE.md,sha256=M7wm1EmMGDtwPRdg7kW4d00h1uAXjKOT3HFScYQMeiE,34916
10448
- simo-2.0.11.dist-info/METADATA,sha256=Jht-Iw22NzZexiiYzADTUYitoe7_ld7aSBHpOZPpylc,1700
10449
- simo-2.0.11.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
10450
- simo-2.0.11.dist-info/top_level.txt,sha256=GmS1hrAbpVqn9OWZh6UX82eIOdRLgYA82RG9fe8v4Rs,5
10451
- simo-2.0.11.dist-info/RECORD,,
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