simo 2.8.3__py3-none-any.whl → 2.8.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of simo might be problematic. Click here for more details.

Files changed (39) hide show
  1. simo/__pycache__/settings.cpython-312.pyc +0 -0
  2. simo/automation/__pycache__/gateways.cpython-312.pyc +0 -0
  3. simo/automation/gateways.py +40 -31
  4. simo/backups/__pycache__/tasks.cpython-312.pyc +0 -0
  5. simo/backups/tasks.py +1 -1
  6. simo/core/__pycache__/controllers.cpython-312.pyc +0 -0
  7. simo/core/__pycache__/models.cpython-312.pyc +0 -0
  8. simo/core/controllers.py +2 -0
  9. simo/core/management/_hub_template/hub/supervisor.conf +1 -0
  10. simo/core/models.py +1 -0
  11. simo/core/templates/admin/component_history.html +2 -2
  12. simo/fleet/__pycache__/admin.cpython-312.pyc +0 -0
  13. simo/fleet/__pycache__/apps.cpython-312.pyc +0 -0
  14. simo/fleet/__pycache__/models.cpython-312.pyc +0 -0
  15. simo/fleet/__pycache__/socket_consumers.cpython-312.pyc +0 -0
  16. simo/fleet/__pycache__/tasks.cpython-312.pyc +0 -0
  17. simo/fleet/admin.py +24 -3
  18. simo/fleet/apps.py +10 -0
  19. simo/fleet/models.py +12 -1
  20. simo/fleet/socket_consumers.py +31 -7
  21. simo/fleet/tasks.py +13 -0
  22. simo/fleet/templates/admin/colonel_history.html +5 -0
  23. simo/generic/__pycache__/forms.cpython-312.pyc +0 -0
  24. simo/generic/__pycache__/gateways.cpython-312.pyc +0 -0
  25. simo/generic/__pycache__/models.cpython-312.pyc +0 -0
  26. simo/generic/__pycache__/tasks.cpython-312.pyc +0 -0
  27. simo/generic/forms.py +6 -0
  28. simo/generic/gateways.py +0 -38
  29. simo/generic/models.py +41 -37
  30. simo/generic/tasks.py +80 -0
  31. simo/notifications/__pycache__/utils.cpython-312.pyc +0 -0
  32. simo/settings.py +25 -1
  33. simo/users/__pycache__/tasks.cpython-312.pyc +0 -0
  34. {simo-2.8.3.dist-info → simo-2.8.5.dist-info}/METADATA +1 -1
  35. {simo-2.8.3.dist-info → simo-2.8.5.dist-info}/RECORD +39 -30
  36. {simo-2.8.3.dist-info → simo-2.8.5.dist-info}/LICENSE.md +0 -0
  37. {simo-2.8.3.dist-info → simo-2.8.5.dist-info}/WHEEL +0 -0
  38. {simo-2.8.3.dist-info → simo-2.8.5.dist-info}/entry_points.txt +0 -0
  39. {simo-2.8.3.dist-info → simo-2.8.5.dist-info}/top_level.txt +0 -0
Binary file
@@ -6,7 +6,6 @@ import json
6
6
  import time
7
7
  import multiprocessing
8
8
  import threading
9
- import traceback
10
9
  from django.conf import settings
11
10
  from django.utils import timezone
12
11
  from django.db import connection as db_connection
@@ -43,10 +42,7 @@ class ScriptRunHandler(multiprocessing.Process):
43
42
  def run(self):
44
43
  db_connection.connect()
45
44
  self.component = Component.objects.get(id=self.component_id)
46
- try:
47
- tz = pytz.timezone(self.component.zone.instance.timezone)
48
- except:
49
- tz = pytz.timezone('UTC')
45
+ tz = pytz.timezone(self.component.zone.instance.timezone)
50
46
  timezone.activate(tz)
51
47
  introduce_instance(self.component.zone.instance)
52
48
  self.logger = get_component_logger(self.component)
@@ -235,8 +231,10 @@ class AutomationsGatewayHandler(GatesHandler, BaseObjectCommandsGatewayHandler):
235
231
  def watch_scripts(self):
236
232
  drop_current_instance()
237
233
  # observe running scripts and drop the ones that are no longer alive
238
- dead_scripts = False
239
- for id, process in list(self.running_scripts.items()):
234
+ for id, data in list(self.running_scripts.items()):
235
+ if time.time() - data['start_time'] < 5:
236
+ continue
237
+ process = data['proc']
240
238
 
241
239
  comp = Component.objects.filter(id=id).first()
242
240
  if comp and comp.value == 'finished':
@@ -252,12 +250,18 @@ class AutomationsGatewayHandler(GatesHandler, BaseObjectCommandsGatewayHandler):
252
250
  self.running_scripts.pop(id)
253
251
  continue
254
252
  else:
253
+ # it as been observed that is_alive might sometimes report false
254
+ # however the process is actually still running
255
+ process.kill()
255
256
  self.last_death = time.time()
256
257
  self.running_scripts.pop(id, None) # no longer running for sure!
257
258
  if not comp or comp.value != 'running':
258
259
  continue
259
260
 
260
261
  if id not in self.terminating_scripts: # was not intentionaly terminated
262
+ if comp:
263
+ tz = pytz.timezone(comp.zone.instance.timezone)
264
+ timezone.activate(tz)
261
265
  logger = get_component_logger(comp)
262
266
  logger.log(logging.INFO, "-------DEAD!-------")
263
267
  comp.value = 'error'
@@ -348,30 +352,35 @@ class AutomationsGatewayHandler(GatesHandler, BaseObjectCommandsGatewayHandler):
348
352
 
349
353
  def start_script(self, component):
350
354
  print("START SCRIPT %s" % str(component))
355
+
351
356
  if component.id in self.running_scripts:
352
- if component.value in ('finished', 'error', 'stopped'):
353
- try:
354
- self.running_scripts[component.id].kill()
355
- except:
356
- pass
357
- self.running_scripts.pop(component.id, None)
358
- elif component.id not in self.terminating_scripts \
359
- and self.running_scripts[component.id].is_alive():
357
+ # it appears that the script is running and is perfectly healthy
358
+ # so we make sure it has correct value, do nothing else and return!
359
+ if component.id not in self.terminating_scripts \
360
+ and self.running_scripts[component.id]['proc'].is_alive():
360
361
  if component.value != 'running':
361
362
  component.value = 'running'
362
363
  component.save()
363
364
  return
364
- else:
365
- try:
366
- self.running_scripts[component.id].kill()
367
- except:
368
- pass
369
365
 
370
- self.running_scripts[component.id] = ScriptRunHandler(
371
- component.id, multiprocessing.Event(),
372
- daemon=True
366
+ # script is in terminating state or is no longer alive
367
+ # since starting of a new script was requested, we kill it viciously
368
+ # and continue on!
369
+ try:
370
+ self.running_scripts[component.id]['proc'].kill()
371
+ except:
372
+ pass
373
+ self.running_scripts.pop(component.id, None)
374
+
375
+
376
+ process = ScriptRunHandler(
377
+ component.id, multiprocessing.Event(), daemon=True
373
378
  )
374
- self.running_scripts[component.id].start()
379
+ process.start()
380
+ self.running_scripts[component.id] = {
381
+ 'proc': process, 'start_time': time.time()
382
+ }
383
+
375
384
 
376
385
  def stop_script(self, component, stop_status='stopped'):
377
386
  self.terminating_scripts.add(component.id)
@@ -389,17 +398,17 @@ class AutomationsGatewayHandler(GatesHandler, BaseObjectCommandsGatewayHandler):
389
398
  elif stop_status == 'stopped':
390
399
  logger.log(logging.INFO, "-------STOP-------")
391
400
 
392
- if self.running_scripts[component.id].exit_in_use.is_set()\
393
- and not self.running_scripts[component.id].exin_in_use_fail.is_set():
394
- self.running_scripts[component.id].exit_event.set()
401
+ if self.running_scripts[component.id]['proc'].exit_in_use.is_set()\
402
+ and not self.running_scripts[component.id]['proc'].exin_in_use_fail.is_set():
403
+ self.running_scripts[component.id]['proc'].exit_event.set()
395
404
  else:
396
- self.running_scripts[component.id].terminate()
405
+ self.running_scripts[component.id]['proc'].terminate()
397
406
 
398
407
  def kill():
399
408
  start = time.time()
400
409
  terminated = False
401
410
  while start > time.time() - 2:
402
- if not self.running_scripts[component.id].is_alive():
411
+ if not self.running_scripts[component.id]['proc'].is_alive():
403
412
  terminated = True
404
413
  break
405
414
  time.sleep(0.1)
@@ -408,12 +417,12 @@ class AutomationsGatewayHandler(GatesHandler, BaseObjectCommandsGatewayHandler):
408
417
  logger.log(logging.INFO, "-------GATEWAY KILL-------")
409
418
  else:
410
419
  logger.log(logging.INFO, "-------KILL!-------")
411
- self.running_scripts[component.id].kill()
420
+ self.running_scripts[component.id]['proc'].kill()
412
421
 
413
422
  component.set(stop_status)
414
423
  self.terminating_scripts.remove(component.id)
415
424
  # making sure it's fully killed along with it's child processes
416
- self.running_scripts[component.id].kill()
425
+ self.running_scripts[component.id]['proc'].kill()
417
426
  self.running_scripts.pop(component.id, None)
418
427
  logger.handlers = []
419
428
 
simo/backups/tasks.py CHANGED
@@ -315,7 +315,7 @@ def perform_backup():
315
315
 
316
316
  if other_month_folders:
317
317
  # delete old backups to free up at least 20G of space
318
- while shutil.disk_usage('/media/backup').free < 20 * 1024 * 1024 * 1024:
318
+ while shutil.disk_usage(sd_mountpoint).free < 20 * 1024 * 1024 * 1024:
319
319
  remove_folder = other_month_folders.pop()[0]
320
320
  print(f"REMOVE: {remove_folder}")
321
321
  shutil.rmtree(remove_folder)
simo/core/controllers.py CHANGED
@@ -289,6 +289,8 @@ class ControllerBase(ABC):
289
289
  actor = self._get_actor(value)
290
290
  if not actor:
291
291
  actor = get_current_user()
292
+ if actor:
293
+ introduce_user(actor)
292
294
 
293
295
  self.component.refresh_from_db()
294
296
  if value != self.component.value:
@@ -44,6 +44,7 @@ autostart=true
44
44
  autorestart=true
45
45
  stopwaitsecs=15
46
46
  killasgroup=true
47
+ environment=PYTHONUNBUFFERED=1
47
48
 
48
49
 
49
50
  [program:simo-gateways]
simo/core/models.py CHANGED
@@ -14,6 +14,7 @@ from django.conf import settings
14
14
  from django.core.files.storage import FileSystemStorage
15
15
  from timezone_utils.choices import ALL_TIMEZONES_CHOICES
16
16
  from location_field.models.plain import PlainLocationField
17
+ from actstream import action # do not delete from here!
17
18
  from model_utils import FieldTracker
18
19
  from dirtyfields import DirtyFieldsMixin
19
20
  from simo.core.utils.mixins import SimoAdminMixin
@@ -2,7 +2,7 @@
2
2
  <h3>Value:</h3>
3
3
  <ul style="margin-left: 1.5em;">
4
4
  {% for history_item in value_history %}
5
- <li>[{{ history_item.user }}] {{ history_item.date }} - <strong>{{ history_item.value }}</strong></li>
5
+ <li>[{{ history_item.user }}] {{ history_item.date|date:"Y-m-d H:i:s" }} - <strong>{{ history_item.value }}</strong></li>
6
6
  {% endfor %}
7
7
  </ul>
8
8
  {% endif %}
@@ -12,7 +12,7 @@
12
12
  <h3>Security:</h3>
13
13
  <ul style="margin-left: 1.5em;">
14
14
  {% for history_item in arm_status_history %}
15
- <li>[{{ history_item.user }}] {{ history_item.date }} - <strong>{{ history_item.value }}</strong></li>
15
+ <li>[{{ history_item.user }}] {{ history_item.date|date:"Y-m-d H:i:s" }} - <strong>{{ history_item.value }}</strong></li>
16
16
  {% endfor %}
17
17
  </ul>
18
18
  {% endif %}
simo/fleet/admin.py CHANGED
@@ -1,4 +1,5 @@
1
1
  from threading import Timer
2
+ from actstream.models import actor_stream
2
3
  from django.contrib import admin
3
4
  from django.utils.safestring import mark_safe
4
5
  from django.template.loader import render_to_string
@@ -60,10 +61,8 @@ class ColonelAdmin(admin.ModelAdmin):
60
61
  readonly_fields = (
61
62
  'type', 'uid', 'connected', 'last_seen',
62
63
  'firmware_version', 'newer_firmware_available',
64
+ 'history',
63
65
  )
64
- fields = (
65
- 'name', 'instance', 'enabled', 'firmware_auto_update'
66
- ) + readonly_fields + ('pwm_frequency', 'logs_stream', 'log', )
67
66
 
68
67
  actions = (
69
68
  'check_for_upgrade', 'update_firmware', 'update_config', 'restart',
@@ -73,6 +72,19 @@ class ColonelAdmin(admin.ModelAdmin):
73
72
 
74
73
  inlines = InterfaceInline, ColonelPinsInline
75
74
 
75
+ fieldsets = (
76
+ ("", {'fields': (
77
+ 'name', 'instance', 'enabled', 'firmware_auto_update',
78
+ 'type', 'uid', 'connected', 'last_seen',
79
+ 'firmware_version', 'newer_firmware_available',
80
+ 'pwm_frequency', 'logs_stream', 'log'
81
+ )}),
82
+ ("History", {
83
+ 'fields': ('history',),
84
+ 'classes': ('collapse',),
85
+ }),
86
+ )
87
+
76
88
  def get_queryset(self, request):
77
89
  qs = super().get_queryset(request)
78
90
  return qs.filter(instance=get_current_instance())
@@ -165,6 +177,15 @@ class ColonelAdmin(admin.ModelAdmin):
165
177
  return mark_safe('<img src="%s" alt="True">' % static('admin/img/icon-yes.svg'))
166
178
  return mark_safe('<img src="%s" alt="False">' % static('admin/img/icon-no.svg'))
167
179
 
180
+ def history(self, obj):
181
+ if not obj:
182
+ return ''
183
+ actions = actor_stream(obj)[:100]
184
+ if not len(actions):
185
+ return ''
186
+ return render_to_string(
187
+ 'admin/colonel_history.html', {'actions': actor_stream(obj)[:100]}
188
+ )
168
189
 
169
190
  @admin.register(Interface)
170
191
  class InterfaceAdmin(admin.ModelAdmin):
simo/fleet/apps.py ADDED
@@ -0,0 +1,10 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class SIMOFleetAppConfig(AppConfig):
5
+ name = 'simo.fleet'
6
+
7
+ def ready(self):
8
+ from actstream import registry
9
+ from simo.fleet.models import Colonel
10
+ registry.register(Colonel)
simo/fleet/models.py CHANGED
@@ -1,5 +1,6 @@
1
1
  import requests
2
2
  import time
3
+ from actstream import action
3
4
  from django.core.exceptions import ValidationError
4
5
  from django.db import transaction
5
6
  from django.db import models
@@ -281,6 +282,17 @@ def after_colonel_save(sender, instance, created, *args, **kwargs):
281
282
  Interface.objects.create(colonel=instance, no=1)
282
283
  Interface.objects.create(colonel=instance, no=2)
283
284
 
285
+ if 'socket_connected' in instance.get_dirty_fields():
286
+ if instance.socket_connected:
287
+ verb = 'connected'
288
+ else:
289
+ verb = 'disconnected'
290
+ action.send(
291
+ instance, target=instance, verb=verb,
292
+ instance_id=instance.instance.id,
293
+ action_type='colonel_status', value=verb
294
+ )
295
+
284
296
 
285
297
  @receiver(post_save, sender=Component)
286
298
  def post_component_save(sender, instance, created, *args, **kwargs):
@@ -472,7 +484,6 @@ def post_interface_save(sender, instance, created, *args, **kwargs):
472
484
 
473
485
 
474
486
 
475
-
476
487
  @receiver(post_delete, sender=Interface)
477
488
  def post_interface_delete(sender, instance, *args, **kwargs):
478
489
  with transaction.atomic():
@@ -5,6 +5,7 @@ import pytz
5
5
  import traceback
6
6
  import sys
7
7
  import zlib
8
+ import time
8
9
  from django.db import transaction
9
10
  from logging.handlers import RotatingFileHandler
10
11
  from django.utils import timezone
@@ -27,10 +28,15 @@ from .controllers import TTLock
27
28
 
28
29
  @capture_socket_errors
29
30
  class FleetConsumer(AsyncWebsocketConsumer):
30
- colonel = None
31
- colonel_logger = None
32
- connected = False
33
- mqtt_client = None
31
+
32
+
33
+ def __init__(self, *args, **kwargs):
34
+ super().__init__(*args, **kwargs)
35
+ self.colonel = None
36
+ self.colonel_logger = None
37
+ self.connected = False
38
+ self.mqtt_client = None
39
+ self.last_seen = 0
34
40
 
35
41
 
36
42
  async def disconnect(self, code):
@@ -189,10 +195,26 @@ class FleetConsumer(AsyncWebsocketConsumer):
189
195
  self.colonel.minor_upgrade_available
190
196
  )
191
197
 
192
- await asyncio.sleep(10)
198
+ await asyncio.sleep(2)
199
+
193
200
  # Default pinging system sometimes get's lost somewhere,
194
- # therefore we use our own to ensure connection
201
+ # therefore we use our own to ensure connection and understand if
202
+ # colonel is connected or not
203
+
195
204
  await self.send_data({'command': 'ping'})
205
+ ping_start = time.time()
206
+ await asyncio.sleep(0.1)
207
+
208
+ while ping_start > self.last_seen:
209
+ if time.time() - ping_start > 3:
210
+ def disconnect_socket():
211
+ self.colonel.socket_connected = False
212
+ self.colonel.save(update_fields=['socket_connected'])
213
+ await sync_to_async(disconnect_socket, thread_sensitive=True)()
214
+ break
215
+ await asyncio.sleep(0.1)
216
+
217
+
196
218
 
197
219
  async def firmware_update(self, to_version):
198
220
  print("Firmware update: ", str(self.colonel))
@@ -366,8 +388,9 @@ class FleetConsumer(AsyncWebsocketConsumer):
366
388
  drop_current_instance()
367
389
  try:
368
390
  if text_data:
369
- print(f"{self.colonel}: {text_data}")
370
391
  data = json.loads(text_data)
392
+ if 'ping' not in data:
393
+ print(f"{self.colonel}: {text_data}")
371
394
  if 'get_config' in data:
372
395
  config = await self.get_config_data()
373
396
  print("Send config: ", config)
@@ -453,6 +476,7 @@ class FleetConsumer(AsyncWebsocketConsumer):
453
476
 
454
477
 
455
478
  async def log_colonel_connected(self):
479
+ self.last_seen = time.time()
456
480
 
457
481
  def save_last_seen():
458
482
  self.colonel.socket_connected = True
simo/fleet/tasks.py CHANGED
@@ -5,6 +5,18 @@ from simo.core.middleware import drop_current_instance
5
5
  from celeryc import celery_app
6
6
 
7
7
 
8
+ @celery_app.task
9
+ def check_colonels_connected():
10
+ from .models import Colonel
11
+ drop_current_instance()
12
+ for lost_colonel in Colonel.objects.filter(
13
+ socket_connected=True,
14
+ last_seen__lt=timezone.now() - datetime.timedelta(seconds=20)
15
+ ):
16
+ lost_colonel.socket_connected = False
17
+ lost_colonel.save()
18
+
19
+
8
20
  @celery_app.task
9
21
  def check_colonel_components_alive():
10
22
  from simo.core.models import Component
@@ -24,4 +36,5 @@ def check_colonel_components_alive():
24
36
 
25
37
  @celery_app.on_after_finalize.connect
26
38
  def setup_periodic_tasks(sender, **kwargs):
39
+ sender.add_periodic_task(10, check_colonels_connected.s())
27
40
  sender.add_periodic_task(20, check_colonel_components_alive.s())
@@ -0,0 +1,5 @@
1
+ <ul style="margin-left: 1.5em;">
2
+ {% for action in actions %}
3
+ <li>{{ action.timestamp|date:"Y-m-d H:i:s" }} - <strong>{{ action.verb }}</strong></li>
4
+ {% endfor %}
5
+ </ul>
simo/generic/forms.py CHANGED
@@ -157,6 +157,11 @@ class ThermostatConfigForm(BaseComponentForm):
157
157
 
158
158
  class AlarmBreachEventForm(forms.Form):
159
159
  uid = HiddenField(required=False)
160
+ threshold = forms.IntegerField(
161
+ label="Number of sensors breached",
162
+ min_value=1, max_value=5, initial=1,
163
+ help_text="React when at least given amount of sensors were breached."
164
+ )
160
165
  component = Select2ModelChoiceField(
161
166
  queryset=Component.objects.all(),
162
167
  url='autocomplete-component',
@@ -173,6 +178,7 @@ class AlarmBreachEventForm(forms.Form):
173
178
  help_text="Event will not fire if alarm group is disarmed "
174
179
  "within given timeframe of seconds after the breach."
175
180
  )
181
+
176
182
  prefix = 'breach_events'
177
183
 
178
184
  def clean(self):
simo/generic/gateways.py CHANGED
@@ -185,8 +185,6 @@ class GenericGatewayHandler(
185
185
  ('watch_thermostats', 60),
186
186
  ('watch_alarm_clocks', 30),
187
187
  ('watch_watering', 60),
188
- ('watch_alarm_events', 1),
189
- ('watch_timers', 1),
190
188
  ('watch_main_states', 60),
191
189
  ('watch_groups', 60)
192
190
  )
@@ -328,42 +326,6 @@ class GenericGatewayHandler(
328
326
  other_group.refresh_status()
329
327
 
330
328
 
331
- def watch_alarm_events(self):
332
- from .controllers import AlarmGroup
333
- drop_current_instance()
334
- for alarm in Component.objects.filter(
335
- controller_uid=AlarmGroup.uid, value='breached',
336
- meta__breach_start__gt=0
337
- ):
338
- for uid, event in alarm.controller.events_map.items():
339
- if uid in alarm.meta.get('events_triggered', []):
340
- continue
341
- if time.time() - alarm.meta['breach_start'] < event['delay']:
342
- continue
343
- try:
344
- getattr(event['component'], event['breach_action'])()
345
- except Exception:
346
- print(traceback.format_exc(), file=sys.stderr)
347
- if not alarm.meta.get('events_triggered'):
348
- alarm.meta['events_triggered'] = [uid]
349
- else:
350
- alarm.meta['events_triggered'].append(uid)
351
- alarm.save(update_fields=['meta'])
352
-
353
- def watch_timers(self):
354
- drop_current_instance()
355
- for component in Component.objects.filter(
356
- meta__timer_to__gt=0
357
- ).filter(meta__timer_to__lt=time.time()):
358
- component.meta['timer_to'] = 0
359
- component.meta['timer_start'] = 0
360
- component.save()
361
- try:
362
- component.controller._on_timer_end()
363
- except Exception as e:
364
- print(traceback.format_exc(), file=sys.stderr)
365
-
366
-
367
329
  def set_get_day_evening_night_morning(self, state):
368
330
  if state.value not in ('day', 'night', 'evening', 'morning'):
369
331
  return
simo/generic/models.py CHANGED
@@ -1,12 +1,14 @@
1
1
  import time
2
2
  import sys
3
3
  import traceback
4
- from threading import Timer
5
4
  from django.db.models.signals import pre_save, post_save, post_delete
6
5
  from django.dispatch import receiver
7
- from simo.core.models import Instance, Component
6
+ from simo.core.models import Component
8
7
  from simo.users.models import InstanceUser
9
-
8
+ from .tasks import (
9
+ notify_users_on_alarm_group_breach,
10
+ fire_breach_events
11
+ )
10
12
 
11
13
 
12
14
  @receiver(post_save, sender=Component)
@@ -21,7 +23,7 @@ def handle_alarm_groups(sender, instance, *args, **kwargs):
21
23
 
22
24
  from .controllers import AlarmGroup
23
25
 
24
- for alarm_group in Component.objects.filter(
26
+ for ag in Component.objects.filter(
25
27
  controller_uid=AlarmGroup.uid,
26
28
  config__components__contains=instance.id,
27
29
  ).exclude(value='disarmed'):
@@ -30,48 +32,44 @@ def handle_alarm_groups(sender, instance, *args, **kwargs):
30
32
  }
31
33
  stats[instance.arm_status] += 1
32
34
  for slave in Component.objects.filter(
33
- pk__in=alarm_group.config['components'],
35
+ pk__in=ag.config['components'],
34
36
  ).exclude(pk=instance.pk):
35
37
  stats[slave.arm_status] += 1
36
- alarm_group.config['stats'] = stats
37
- alarm_group.save(update_fields=['config'])
38
38
 
39
- if stats['disarmed'] == len(alarm_group.config['components']):
39
+ print(f"STATS OF {ag} are: {stats}")
40
+ ag.config['stats'] = stats
41
+
42
+
43
+ if stats['disarmed'] == len(ag.config['components']):
40
44
  alarm_group_value = 'disarmed'
41
- elif stats['armed'] == len(alarm_group.config['components']):
45
+ elif stats['armed'] == len(ag.config['components']):
42
46
  alarm_group_value = 'armed'
43
47
  elif stats['breached']:
44
- if alarm_group.value != 'breached':
45
- def notify_users_security_breach(alarm_group_component_id):
46
- alarm_group_component = Component.objects.filter(
47
- id=alarm_group_component_id, value='breached'
48
- ).first()
49
- if not alarm_group_component:
50
- return
51
- breached_components = Component.objects.filter(
52
- pk__in=alarm_group_component.config['components'],
53
- arm_status='breached'
54
- )
55
- body = "Security Breach! " + '; '.join(
56
- [str(c) for c in breached_components]
57
- )
58
- from simo.notifications.utils import notify_users
59
- notify_users(
60
- 'alarm', str(alarm_group_component), body,
61
- component=alarm_group_component
62
- )
63
- if alarm_group.config.get('notify_on_breach') is not None:
64
- t = Timer(
65
- # give it one second to finish with other db processes.
66
- alarm_group.config['notify_on_breach'] + 1,
67
- notify_users_security_breach, [alarm_group.id]
68
- )
69
- t.start()
70
48
  alarm_group_value = 'breached'
71
49
  else:
72
50
  alarm_group_value = 'pending-arm'
73
51
 
74
- alarm_group.controller.set(alarm_group_value)
52
+ print(f"{ag} value: {alarm_group_value}")
53
+
54
+ if alarm_group_value == 'breached' and instance.arm_status == 'breached':
55
+ if ag.value != 'breached':
56
+ ag.meta['breach_times'] = [time.time()]
57
+ else:
58
+ ag.meta['breach_times'].append(time.time())
59
+
60
+ ag.save(update_fields=['meta', 'config'])
61
+ ag.controller.set(alarm_group_value)
62
+
63
+ if alarm_group_value == 'breached' and instance.arm_status == 'breached':
64
+ for event in ag.config['breach_events']:
65
+ if event['uid'] in ag.meta.get('events_triggered', []):
66
+ continue
67
+ threshold = event.get('threshold', 1)
68
+ if len(ag.meta['breach_times']) < threshold:
69
+ continue
70
+ fire_breach_events.apply_async(
71
+ args=[ag.id], countdown=event['delay']
72
+ )
75
73
 
76
74
 
77
75
  @receiver(pre_save, sender=Component)
@@ -85,8 +83,14 @@ def manage_alarm_groups(sender, instance, *args, **kwargs):
85
83
  return
86
84
 
87
85
  if instance.value == 'breached':
88
- instance.meta['breach_start'] = time.time()
89
86
  instance.meta['events_triggered'] = []
87
+
88
+ if instance.config.get('notify_on_breach') is not None:
89
+ notify_users_on_alarm_group_breach.apply_async(
90
+ args=[instance.id],
91
+ countdown=instance.config['notify_on_breach']
92
+ )
93
+
90
94
  elif instance.get_dirty_fields()['value'] == 'breached' \
91
95
  and instance.value == 'disarmed':
92
96
  instance.meta['breach_start'] = None
simo/generic/tasks.py ADDED
@@ -0,0 +1,80 @@
1
+ import sys, traceback, time
2
+ from celeryc import celery_app
3
+ from simo.core.middleware import drop_current_instance, drop_current_instance
4
+
5
+
6
+ @celery_app.task
7
+ def notify_users_on_alarm_group_breach(ag_id):
8
+ from simo.core.models import Component
9
+ drop_current_instance()
10
+ ag = Component.objects.filter(id=ag_id).first()
11
+ if not ag:
12
+ return
13
+ if ag.value != 'breached':
14
+ # no longer breached, somebody disarmed it,
15
+ # no need to send any notifications
16
+ return
17
+
18
+ breached_components = Component.objects.filter(
19
+ pk__in=ag.config['components'],
20
+ arm_status='breached'
21
+ )
22
+ body = "Security Breach! " + '; '.join(
23
+ [str(c) for c in breached_components]
24
+ )
25
+ from simo.notifications.utils import notify_users
26
+ notify_users(
27
+ 'alarm', str(ag), body, component=ag,
28
+ instance=ag.zone.instance
29
+ )
30
+
31
+
32
+ @celery_app.task
33
+ def fire_breach_events(ag_id):
34
+ from simo.core.models import Component
35
+ drop_current_instance()
36
+ ag = Component.objects.filter(id=ag_id).first()
37
+ if not ag:
38
+ return
39
+ if ag.value != 'breached':
40
+ # no longer breached, somebody disarmed it,
41
+ # no need to send any notifications
42
+ return
43
+ for uid, event in ag.controller.events_map.items():
44
+ if uid in ag.meta.get('events_triggered', []):
45
+ continue
46
+ threshold = event.get('threshold', 1)
47
+ if len(ag.meta['breach_times']) < threshold:
48
+ continue
49
+ if time.time() - ag.meta['breach_times'][threshold - 1] < event['delay']:
50
+ continue
51
+ try:
52
+ getattr(event['component'], event['breach_action'])()
53
+ except Exception:
54
+ print(traceback.format_exc(), file=sys.stderr)
55
+ if not ag.meta.get('events_triggered'):
56
+ ag.meta['events_triggered'] = [uid]
57
+ else:
58
+ ag.meta['events_triggered'].append(uid)
59
+ ag.save(update_fields=['meta'])
60
+
61
+
62
+ @celery_app.task
63
+ def watch_timers():
64
+ from simo.core.models import Component
65
+ drop_current_instance()
66
+ for component in Component.objects.filter(
67
+ meta__timer_to__gt=0
68
+ ).filter(meta__timer_to__lt=time.time()):
69
+ component.meta['timer_to'] = 0
70
+ component.meta['timer_start'] = 0
71
+ component.save()
72
+ try:
73
+ component.controller._on_timer_end()
74
+ except Exception as e:
75
+ print(traceback.format_exc(), file=sys.stderr)
76
+
77
+
78
+ @celery_app.on_after_finalize.connect
79
+ def setup_periodic_tasks(sender, **kwargs):
80
+ sender.add_periodic_task(1, watch_timers.s())
simo/settings.py CHANGED
@@ -3,6 +3,7 @@ Django settings for SIMO.io project.
3
3
  """
4
4
  import sys
5
5
  import os
6
+ import datetime
6
7
 
7
8
  # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
8
9
  BASE_DIR = '/etc/SIMO'
@@ -288,4 +289,27 @@ ACTSTREAM_SETTINGS = {
288
289
  'USE_PREFETCH': True,
289
290
  'USE_JSONFIELD': True,
290
291
  'GFK_FETCH_DEPTH': 1,
291
- }
292
+ }
293
+
294
+
295
+ DATETIME_FORMAT = 'Y-m-d H:i:s'
296
+
297
+
298
+ class TimestampedStream:
299
+ """Adds timestamps to all the prints"""
300
+
301
+ def __init__(self, stream):
302
+ self.stream = stream
303
+
304
+ def write(self, data):
305
+ if data != '\n':
306
+ timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
307
+ self.stream.write(f'[{timestamp}] {data}')
308
+ else:
309
+ self.stream.write(data)
310
+
311
+ def flush(self):
312
+ self.stream.flush()
313
+
314
+
315
+ sys.stdout = TimestampedStream(sys.stdout)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: simo
3
- Version: 2.8.3
3
+ Version: 2.8.5
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
@@ -2,7 +2,7 @@ simo/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
2
2
  simo/asgi.py,sha256=L8CUVZLM32IMzWDZ4IShdDN-m69t7oxAUeHods4-xNM,822
3
3
  simo/celeryc.py,sha256=eab7_e9rw0c__DCeoUFUh_tjAGVlulxVrk75BaJf57Q,1512
4
4
  simo/conf.py,sha256=H2BhXAV8MEDVXF8AbkaLSfR4ULd-9_bS4bnhE5sE5fg,112
5
- simo/settings.py,sha256=CjHxwQvyljm0qVOCHvi6gdMweDzEqAedXTmGMJ6rE1A,7009
5
+ simo/settings.py,sha256=C04iruyTTGqrcYuZjW_EN7V8MNPWNnsde4dZiQ3tr4Y,7523
6
6
  simo/urls.py,sha256=d8g-wN0Xr2PVIV8RZl_h_PMN9KGZNIE9to2hQj1p1TU,2497
7
7
  simo/__pycache__/__init__.cpython-312.pyc,sha256=a12_Zr7kC5DXzcFxA5eMu-TiSU5xbdF5cdKq-gwc3x0,159
8
8
  simo/__pycache__/__init__.cpython-38.pyc,sha256=j81de0BqHMr6bs0C7cuYrXl7HwtK_vv8hDEtAdSwDJc,153
@@ -12,7 +12,7 @@ simo/__pycache__/celeryc.cpython-312.pyc,sha256=MQlG6VeEdCpXPpiiAwZavriWKwHbhwTM
12
12
  simo/__pycache__/celeryc.cpython-38.pyc,sha256=eSRoaKwfYlxVaxAiwqpQ2ndEcx7W-VpZtbxRFSV8UYg,1653
13
13
  simo/__pycache__/conf.cpython-312.pyc,sha256=q63YJWqaaaQLz3qXW8clENjvH1zUfY_k34_m56n5gRY,320
14
14
  simo/__pycache__/conf.cpython-38.pyc,sha256=MYP2yk3ULxiYwZsZR6tCLjKnU-z03A3avzQzIn66y3k,273
15
- simo/__pycache__/settings.cpython-312.pyc,sha256=PE2CUT2j6Uj3xB5LpEEyw8q9MQLsoZPRgcOsUDh9y8o,6688
15
+ simo/__pycache__/settings.cpython-312.pyc,sha256=lrMjm4OihDlNrull26G_TaWUEJkFbo4Po5ir5efSRU4,7886
16
16
  simo/__pycache__/settings.cpython-38.pyc,sha256=4w3ds3D9S78zbsovXsXC05PYBAafDrtsOhX14FT0YyE,6149
17
17
  simo/__pycache__/urls.cpython-312.pyc,sha256=mIg_YD7zgjmIzfWzpGpikMqanGKP2O-iuA1ixqQngnc,3689
18
18
  simo/__pycache__/urls.cpython-38.pyc,sha256=u0x6EqT8S1YfDOSPgbI8Kf-RDlveY9OV-EDXMYKAQ7w,2125
@@ -21,7 +21,7 @@ simo/automation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
21
  simo/automation/app_widgets.py,sha256=gaqImMZjuMHm7nIb9a4D-Y3qipz_WhSPAHXcwGx4Uzs,199
22
22
  simo/automation/controllers.py,sha256=66aGjlfgigcePXiLmDkbVrxbm-Z26klMKIiOvaMH71k,11545
23
23
  simo/automation/forms.py,sha256=UWnkxw8pILPK0smRPTo4SLgsZl78zOySx7JIc30Bgtk,10228
24
- simo/automation/gateways.py,sha256=MAzv-i8BemhqfNY2NTYYlyCGPfctyNh4_dXyIZYJRSE,15848
24
+ simo/automation/gateways.py,sha256=8qgBDhkM_xYGmeEbnH5WXs5m3ihMp8Yzx7yj6XG8DNM,16382
25
25
  simo/automation/helpers.py,sha256=iP-fxxB8HsFQy3k2CjFubu86aMqvWgmh-p24DiyOrek,4330
26
26
  simo/automation/models.py,sha256=zt-jkzyq5ddqGT864OkJzCsvov2vZ0nO4ez3hAeZkXg,934
27
27
  simo/automation/serializers.py,sha256=PjyFrjdPK1mBsgbNhyqMi9SWzcymqTa742ipy0LhAN4,1996
@@ -34,7 +34,7 @@ simo/automation/__pycache__/controllers.cpython-312.pyc,sha256=QV63g3UlpMAA-yCaZ
34
34
  simo/automation/__pycache__/controllers.cpython-38.pyc,sha256=CL-0Tq9B4-E36fYfWT1XEBTq1dkq1W8003f6MrBnQU0,8391
35
35
  simo/automation/__pycache__/forms.cpython-312.pyc,sha256=63rU0rWZk-Rz5qoMZiXl743WPc9NVm5d8bSd8w52T4E,12347
36
36
  simo/automation/__pycache__/forms.cpython-38.pyc,sha256=cpA5hA2Iz3JsPC0Dq01ki1I7S9c5DKRcXveHApI1dJo,7772
37
- simo/automation/__pycache__/gateways.cpython-312.pyc,sha256=j4bG9KqBcCB8JD59frTl9VtP8n04mUPF_bd0vInDZfs,22223
37
+ simo/automation/__pycache__/gateways.cpython-312.pyc,sha256=mQWn7s8qqF-CIw9Zn80RbbaFJjTiCdaN_grMsfwa-nY,22405
38
38
  simo/automation/__pycache__/gateways.cpython-38.pyc,sha256=nHujqChMCqqxHbZezP3MisavjKDhczqzFGurO10h-lc,11113
39
39
  simo/automation/__pycache__/helpers.cpython-312.pyc,sha256=aDFtzBE72szi4gzVxK_NiAEC__wCmdztw0UKu2lVU58,5853
40
40
  simo/automation/__pycache__/helpers.cpython-38.pyc,sha256=fNjSyn4Mfq7-JQx-bdsnj-rSxgu20dVJ9-5ZEMT6yiM,3627
@@ -61,7 +61,7 @@ simo/backups/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
61
61
  simo/backups/admin.py,sha256=cEakxnQlOHvUf8LdBdekXpDAvnqPoVN8y7pnN3WK29A,2487
62
62
  simo/backups/dynamic_settings.py,sha256=Q52RLa3UQsmAhqkwR16cM6pbBnIbXqmVQ2oIUP2ZVD0,416
63
63
  simo/backups/models.py,sha256=-tgILgkqmBEuxBwoymKZN1a0UVQzmJvqWrIGYMMFDaQ,695
64
- simo/backups/tasks.py,sha256=Vz6hJGWzlfaHtXRkLMhCg8e3oH80aQtgc3YfDsRLpgg,13372
64
+ simo/backups/tasks.py,sha256=UtWMsHONhL_rBAedCk7Mirbp0sZ-glF49bnR2mdTM4U,13370
65
65
  simo/backups/__pycache__/__init__.cpython-312.pyc,sha256=wl1RXj84eHmQ_WAvVl7oahd1qa8ezF1HcwRQzho7DyU,167
66
66
  simo/backups/__pycache__/__init__.cpython-38.pyc,sha256=vzOf-JIMeZ6P85FyvTpYev3mscFosUy-SJTshcQbOHU,161
67
67
  simo/backups/__pycache__/admin.cpython-312.pyc,sha256=4rXygr4247idEgFvadmM4L7-1rRChe7zgoGYTzo2UGI,3863
@@ -70,6 +70,7 @@ simo/backups/__pycache__/dynamic_settings.cpython-312.pyc,sha256=Sf8IorKxo6r3R0Q
70
70
  simo/backups/__pycache__/dynamic_settings.cpython-38.pyc,sha256=51gJFjn_XqQBRoHeubo6ppb9pNuFQKI5hAR0ms9flE8,731
71
71
  simo/backups/__pycache__/models.cpython-312.pyc,sha256=45nfTQzX8YuKI67KEe0nAdlZEQHAZjJu44NUr4PC5WI,1746
72
72
  simo/backups/__pycache__/models.cpython-38.pyc,sha256=zclX7HwOT4_izweyKNQ8LmgSHb3hNcYcfsSiwwfQoLY,1220
73
+ simo/backups/__pycache__/tasks.cpython-312.pyc,sha256=Vg95hEynVXVA89bkG0BN5Cw6KkuVCV4JCiKLsd7xI_8,18369
73
74
  simo/backups/__pycache__/tasks.cpython-38.pyc,sha256=bKz_Rxt_H0lC0d9_4Dxqv7cirQDSH9LVurZZC0LU94s,9179
74
75
  simo/backups/migrations/0001_initial.py,sha256=0LzCusTUyYf61ksiepdnqXIuYYNTNd_djh_Wa484HA0,770
75
76
  simo/backups/migrations/0002_backuplog_backup_level_backup_size.py,sha256=w9T9MQWuecy91OZE1fMExriwPuXA8HMPKsPwXhmC8_k,1023
@@ -97,7 +98,7 @@ simo/core/auto_urls.py,sha256=FqKhH0fF7cGO6P2YrjblwG4JA2UkVXj3lreJUOB2Jq4,1194
97
98
  simo/core/autocomplete_views.py,sha256=x3MKOZvXYS3xVQ-V1S7Liv_U5bxr-uc0gePa85wv5nA,4561
98
99
  simo/core/base_types.py,sha256=WypW8hTfzveuTQtruGjLYAGQZIuczxTlW-SdRk3iQug,666
99
100
  simo/core/context.py,sha256=LKw1I4iIRnlnzoTCuSLLqDX7crHdBnMo3hjqYvVmzFc,1557
100
- simo/core/controllers.py,sha256=Cy0-F4QEo-RbVkRpdfvl0sah9cUs4InP1T8shojaiKY,37355
101
+ simo/core/controllers.py,sha256=4u67VNwU4Zts64aVddqhoPt9ZRbpTmAHPCdu-pFfIX0,37407
101
102
  simo/core/dynamic_settings.py,sha256=bUs58XEZOCIEhg1TigR3LmYggli13KMryBZ9pC7ugAQ,1872
102
103
  simo/core/events.py,sha256=1_KIk5pJqdLPRQlCQ9xSyALst2Cn0b2lAEAJ3QjwIjE,4801
103
104
  simo/core/filters.py,sha256=6wbn8C2WvKTTjtfMwwLBp2Fib1V0-DMpS4iqJd6jJQo,2540
@@ -107,7 +108,7 @@ simo/core/gateways.py,sha256=Y2BME6zSyeUq_e-hzEUF6gErCUCP6nFxedkLZKiLVOo,4141
107
108
  simo/core/loggers.py,sha256=EBdq23gTQScVfQVH-xeP90-wII2DQFDjoROAW6ggUP4,1645
108
109
  simo/core/managers.py,sha256=Ampwe5K7gfE6IJULNCV35V8ysmMOdS_wz7mRzfaLZUw,3014
109
110
  simo/core/middleware.py,sha256=zX6N4P_KR7gG8N2-NcR7jKtuCEhCGRh51g4EktAhP7w,3272
110
- simo/core/models.py,sha256=YjcTcBzesXwR5akdRXjM7wI2e1pYn379loBSaDWdDJM,23696
111
+ simo/core/models.py,sha256=Nw-XT0v0y5pBYlKKhzoI68HiEMMTp7fqZQZgKmeOW7Q,23752
111
112
  simo/core/permissions.py,sha256=Ef4NO7QwwDd3Z-v61R0BeCBXxTOJz9qBvzRTIB5tHwI,2943
112
113
  simo/core/routing.py,sha256=X1_IHxyA-_Q7hw1udDoviVP4_FSBDl8GYETTC2zWTbY,499
113
114
  simo/core/serializers.py,sha256=adqe8oYP5f7hGFXcxa-Zmce2KOwy3hZOaWE3MaAFtpM,23157
@@ -141,7 +142,7 @@ simo/core/__pycache__/base_types.cpython-312.pyc,sha256=Lnq2NL9B5hfwJARJYC447Rdv
141
142
  simo/core/__pycache__/base_types.cpython-38.pyc,sha256=CX-qlF7CefRi_mCE954wYa9rUFR88mOl6g7fybDRu7g,803
142
143
  simo/core/__pycache__/context.cpython-312.pyc,sha256=8rsN2Er-Sx3rrVmO0Gk4cem3euGh0kTELXj667GGZ5E,2193
143
144
  simo/core/__pycache__/context.cpython-38.pyc,sha256=NlTHt2GvXxA21AhBkeyOLfRFUuXw7wmwqyNhhcDl2cw,1373
144
- simo/core/__pycache__/controllers.cpython-312.pyc,sha256=4QDs4cfrxPLcSnJLZFNn0XsGyvAy4GzNCVMr-qljNbM,52855
145
+ simo/core/__pycache__/controllers.cpython-312.pyc,sha256=pNjKIaSvzEWJlI6_yQdmakyFYNgrxv6dyexVvoScUj4,52897
145
146
  simo/core/__pycache__/controllers.cpython-38.pyc,sha256=LtrQQ8egOIOuQbAckeM-z8OfbzS4W8VQ3vBnryAm3iU,32086
146
147
  simo/core/__pycache__/dynamic_settings.cpython-312.pyc,sha256=WUZ6XF4kZb6zPf541PkKmiQaBIw-r5C6F3EUUZiTEnE,3331
147
148
  simo/core/__pycache__/dynamic_settings.cpython-38.pyc,sha256=wGpnscX1DxFpRl54MQURhjz2aD3NJohSzw9JCFnzh2Y,2384
@@ -161,7 +162,7 @@ simo/core/__pycache__/managers.cpython-312.pyc,sha256=RpeR1Z0GtSZht4_a4iDfTU_E8P
161
162
  simo/core/__pycache__/managers.cpython-38.pyc,sha256=6RTIxyjOgpQGtAqcUyE2vFPS09w1V5Wmd_vOV7rHRRI,3370
162
163
  simo/core/__pycache__/middleware.cpython-312.pyc,sha256=2VhHTVY-rdPNqNX0wst2ioVbHD5uMqHkY-tpujLdpH0,4195
163
164
  simo/core/__pycache__/middleware.cpython-38.pyc,sha256=SgTLFNkKxvJ62hevSAVNZHgHdG_u2p7AZBhrj-jfFPs,2649
164
- simo/core/__pycache__/models.cpython-312.pyc,sha256=CVESJOD2imhwbhZ6i0KIs3M4equBtHT-TApdqZ10uoE,31932
165
+ simo/core/__pycache__/models.cpython-312.pyc,sha256=z6RR7Rv4POCpVl5acn2VHVhLLRh59YhO1jq_JSO8CmI,31960
165
166
  simo/core/__pycache__/models.cpython-38.pyc,sha256=A4JsWsDMBaQ_U5sV5cX0c_Uox9mP5fAqn_712EjfNS4,19605
166
167
  simo/core/__pycache__/permissions.cpython-312.pyc,sha256=yqG6t9NZZtL30Hr7razjiG6JDGKiz0Qjcjxgv1C93vM,4450
167
168
  simo/core/__pycache__/permissions.cpython-38.pyc,sha256=UdtxCTXPEbe99vgZOfRz9wfKSYvUn9hSRbpIV9CJSyI,2988
@@ -252,7 +253,7 @@ simo/core/management/_hub_template/hub/celeryc.py,sha256=3ksDXftIZKJ4Cq9WNKJERdZ
252
253
  simo/core/management/_hub_template/hub/manage.py,sha256=PNNlw3EVeIJDgkG0l-klqoxsKWfTYWG9jzRG0upmAaI,620
253
254
  simo/core/management/_hub_template/hub/nginx.conf,sha256=40hvXL42MeiqqkLURNcDQsRudv1dNFLJnvb2-Y3RCkk,2394
254
255
  simo/core/management/_hub_template/hub/settings.py,sha256=4QhvhbtLRxHvAntwqG_qeAAtpDUqKvN4jzw9u3vqff8,361
255
- simo/core/management/_hub_template/hub/supervisor.conf,sha256=-xFtt7cVyY-SCZ_6Z4yI1-1LNtd0f0EAM9zB5VbY9i8,2362
256
+ simo/core/management/_hub_template/hub/supervisor.conf,sha256=11c39qVa1V5qf6OOK2PJhqKJXi5wfMhQNwd-TdWq2yA,2393
256
257
  simo/core/management/_hub_template/hub/urls.py,sha256=Ydm-1BkYAzWeEF-MKSDIFf-7aE4qNLPm48-SA51XgJQ,25
257
258
  simo/core/management/_hub_template/hub/wsgi.py,sha256=Lo-huLHnMDTxSmMBOodVFMWBls9poddrV2KRzXU0xGo,280
258
259
  simo/core/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -10307,7 +10308,7 @@ simo/core/templates/admin/action_intermediate_form.html,sha256=rOWF3UDv-SQZUfOzp
10307
10308
  simo/core/templates/admin/base.html,sha256=yACgUEp8VLmvWwlLM0ygaeorWdn6ykX-7NkXFz3iZAI,6281
10308
10309
  simo/core/templates/admin/clearable_easy_thumbnails_widget.html,sha256=Gh0z_KIEt3QC53HKJ_3QiPt9nIFCvwAx1gCny-dPHOM,704
10309
10310
  simo/core/templates/admin/component_change_list.html,sha256=N3seVjTkIRXIXKGsleB7oWUnPlMRXDIqOPqfEyq4PNE,3320
10310
- simo/core/templates/admin/component_history.html,sha256=xiACt0w7EPrdmTOsi5XO07O-8LcL_XpCu9pSynmNNpI,541
10311
+ simo/core/templates/admin/component_history.html,sha256=7vkWSDu5xugkrb79IffJ-CZts9h2EMZr9Gi4tqHBc0I,579
10311
10312
  simo/core/templates/admin/formset_widget.html,sha256=NAonHNLyicsoE4Iir3My4AlVSz4Q7-1OA19zgsIOjJA,3563
10312
10313
  simo/core/templates/admin/index.html,sha256=AJpt1FA4g6IAFQA99-3glJ88RmoWJ0YXBIz_DUt-R14,4015
10313
10314
  simo/core/templates/admin/item_name_display.html,sha256=Y3zk-rc-y8_6dC6_WrSy6tx6Z-6IZzZ5w3a1hMxlpz4,357
@@ -10403,8 +10404,9 @@ simo/core/utils/__pycache__/type_constants.cpython-38.pyc,sha256=ERC5U7T5pThjLrs
10403
10404
  simo/core/utils/__pycache__/validators.cpython-312.pyc,sha256=w8-XwptooefG-7gIIngVt1WWssIqNU3P1jPHBoYSYH4,1879
10404
10405
  simo/core/utils/__pycache__/validators.cpython-38.pyc,sha256=gjeBOjL_keMoRDjdn8v-3F3wcjPIT3Xx5KpTalo0e-Y,1247
10405
10406
  simo/fleet/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10406
- simo/fleet/admin.py,sha256=3udiNaNoL37699SN-rQqjvvZ0lO2pi2zvg9SqZPp5R0,6403
10407
+ simo/fleet/admin.py,sha256=J_kiwYIGMTxnRuytl_YoM7Pxrj5LzlN9xC8E7Ag5yvQ,7006
10407
10408
  simo/fleet/api.py,sha256=rJwAYJvp0uslW7O6Q4XOpOa8hfCdy3keUD7b3UNL43E,3424
10409
+ simo/fleet/apps.py,sha256=je8mRXMcRq4lABQZlyF2G2hOCkBUicR9I2jvrLDA8eI,238
10408
10410
  simo/fleet/auto_urls.py,sha256=vrfrooPyY4pDuQjya-eLxCgZldfhwbEeEiXa7diO_CY,847
10409
10411
  simo/fleet/base_types.py,sha256=wL9RVkHr0gA7HI1wZq0pruGEIgvQqpfnCL4cC3ywsvw,102
10410
10412
  simo/fleet/ble.py,sha256=eHA_9ABjbmH1vUVCv9hiPXQL2GZZSEVwfO0xyI1S0nI,1081
@@ -10412,19 +10414,20 @@ simo/fleet/controllers.py,sha256=HV6onYd11YAc_f_8NFHAbv5IJZUEd1bDGTRMQtfFe-g,289
10412
10414
  simo/fleet/forms.py,sha256=VcqATlX-omZruUgUC2fJpeLNUGSR-szmf36_M3aa5dg,66850
10413
10415
  simo/fleet/gateways.py,sha256=C7dyapWDlJ5erYPNLkSoH50I8kj0lIXicSno0_CrdXc,5783
10414
10416
  simo/fleet/managers.py,sha256=ZNeHFSkF5kzsl9E1DCBevOW6kXJlD6kw0LU4B-JMOG8,828
10415
- simo/fleet/models.py,sha256=zPplx_v64nfKBmb-nCb74aCVtEeY3m3SjEy-VhbnydU,17511
10417
+ simo/fleet/models.py,sha256=lPiIurCNK--4mPFsg_QLo6GHbpYfE8Aju_G4VAKsRLM,17895
10416
10418
  simo/fleet/routing.py,sha256=cofGsVWXMfPDwsJ6HM88xxtRxHwERhJ48Xyxc8mxg5o,149
10417
10419
  simo/fleet/serializers.py,sha256=PQnjp7LaEpMts-om2OPV5XOU9ut6KFWiePPDCXK0M98,2679
10418
- simo/fleet/socket_consumers.py,sha256=3Ayry0PJgP5K2V7c-4a11QFOxfM8lOSXfIVhaBB8wTM,18820
10419
- simo/fleet/tasks.py,sha256=NX_opj-rbkK9eeQMeRNwHj_ZJ0LC4rYOZovMg9_r0kA,904
10420
+ simo/fleet/socket_consumers.py,sha256=fFsnpAvItGT1tYbkAkpTxBjG5v0cT-o-fmQRM9S1wsg,19630
10421
+ simo/fleet/tasks.py,sha256=VSY0cMFIs7Ocjz0_HwRLp-yaDdBl1T8U9T-7b8Ggegc,1308
10420
10422
  simo/fleet/utils.py,sha256=wNJvURzLP3-aho3D3rfg07N9kWCaMIw5gOsmeeO9Nlg,4740
10421
10423
  simo/fleet/views.py,sha256=3F8im6BsSOaK3KEuBNESE4sDbS_dWHYaOdhTR4cCLjE,5189
10422
10424
  simo/fleet/__pycache__/__init__.cpython-312.pyc,sha256=-BZyG4uq87W18Ra1pKTjSzDgiLEAcw254NqdRX-vSJo,165
10423
10425
  simo/fleet/__pycache__/__init__.cpython-38.pyc,sha256=pIZE7EL6-cuJ3pQtaSwjKLrKLsTYelp1k9sRhXKLh6s,159
10424
- simo/fleet/__pycache__/admin.cpython-312.pyc,sha256=rzG80aTmBCD-at2TNa6noEVx6vyz5R0YFOHVS-q3OgA,9351
10426
+ simo/fleet/__pycache__/admin.cpython-312.pyc,sha256=8TOuku1Atv-LyaVi4OICTc43iBFPgxCjrgjA9q6CN1s,9890
10425
10427
  simo/fleet/__pycache__/admin.cpython-38.pyc,sha256=iweeu5AkaggBhQntP6-VF_eEodkNc6E7zKy0VjfwC2o,6652
10426
10428
  simo/fleet/__pycache__/api.cpython-312.pyc,sha256=OM7s1FqGqTC2vwOOto0GhZrObEnI58SxMcTWaPtHD4Y,5892
10427
10429
  simo/fleet/__pycache__/api.cpython-38.pyc,sha256=rZ1mkfkaMBEXhi9sw_jTKdk2CPJhBNxoImtjQ3Rf1VY,4016
10430
+ simo/fleet/__pycache__/apps.cpython-312.pyc,sha256=S8OK4R0W9VbNfD4Nos_CybjZ3AXS8CxvRaRUJO57xQA,707
10428
10431
  simo/fleet/__pycache__/auto_urls.cpython-312.pyc,sha256=32yXKNoqMxNgYvsspUgx1A84LVQqr8LP4BRvURNCgeY,1026
10429
10432
  simo/fleet/__pycache__/auto_urls.cpython-38.pyc,sha256=jHsvfwAumiBusr91QK1-qC-nmpPEC3r2uMGG8g0fABE,769
10430
10433
  simo/fleet/__pycache__/base_types.cpython-312.pyc,sha256=cq-Pnje7FoMP608U_L_gsCfY_JMY23z87Uao-AxjNBw,304
@@ -10438,14 +10441,15 @@ simo/fleet/__pycache__/gateways.cpython-312.pyc,sha256=ZZGBAH2w9YmFvSrajZY8fUXd_
10438
10441
  simo/fleet/__pycache__/gateways.cpython-38.pyc,sha256=MIpXuGWitGNdsxJ99fWvMXJ6sVE96ac7iR4K4aM4Sds,5148
10439
10442
  simo/fleet/__pycache__/managers.cpython-312.pyc,sha256=sgcaERbhjilkFDCPqc8YZwSiEfRkXpufe0qDPqgLOiU,1733
10440
10443
  simo/fleet/__pycache__/managers.cpython-38.pyc,sha256=Vmm23zoQnS3-uS5_WJt2n3wtjhLiEhLWaYxXJCU6Gts,1339
10441
- simo/fleet/__pycache__/models.cpython-312.pyc,sha256=lnSAUceAf0DAo2Gqs3dPjZf37MSPtqkljOXG5A5up0I,24994
10444
+ simo/fleet/__pycache__/models.cpython-312.pyc,sha256=OkCAXSrrMaxfsclJQV0yhlcBFerKV-xA7FQfR-2e7e0,25400
10442
10445
  simo/fleet/__pycache__/models.cpython-38.pyc,sha256=AXk1Q_nnHDXirHYgM3EW5pLsrR2CaPWk4EuvGCuDUpI,14131
10443
10446
  simo/fleet/__pycache__/routing.cpython-312.pyc,sha256=vafYpGAtYc2NYxBQObMX6eIZfVZflOYgzjYv0SL1jAQ,385
10444
10447
  simo/fleet/__pycache__/routing.cpython-38.pyc,sha256=aPrCmxFKVyB8R8ZbJDwdPdFfvT7CvobovvZeq_mqRgY,314
10445
10448
  simo/fleet/__pycache__/serializers.cpython-312.pyc,sha256=reKKBMohl7vi7iJ6wjRbOBoFn7J9ny8EE9wv5spBHYM,4912
10446
10449
  simo/fleet/__pycache__/serializers.cpython-38.pyc,sha256=l_FzORWCM1hcSZV0AaGRO-p0CMTcEfqnLGgbn2IVvI0,3648
10447
- simo/fleet/__pycache__/socket_consumers.cpython-312.pyc,sha256=1ZEKGEL8FDeALU-BLlap9rjHZKgc_FDtLr0INSWspGo,27221
10450
+ simo/fleet/__pycache__/socket_consumers.cpython-312.pyc,sha256=r2n7h89t1HgI16UdUsaR9nrFFPgb-b2z9emn2eMdPZQ,28536
10448
10451
  simo/fleet/__pycache__/socket_consumers.cpython-38.pyc,sha256=lEC1SkY_KgRY0QoBUMPjnbFwSa7qmCf-4eNQ45hAy68,14141
10452
+ simo/fleet/__pycache__/tasks.cpython-312.pyc,sha256=1XDir2GWxl7c17n0ZuCxOFKsLjaCqXgqVT4TcDvzhm8,1817
10449
10453
  simo/fleet/__pycache__/tasks.cpython-38.pyc,sha256=RoNxL2WUiW67s9O9DjaYVVjCBSZu2nje0Qn9FJkWVS0,1116
10450
10454
  simo/fleet/__pycache__/utils.cpython-312.pyc,sha256=mrICzm_po8QGUaRddn0TvBp5RDLBe1Jklg7xZr0YduM,4995
10451
10455
  simo/fleet/__pycache__/utils.cpython-38.pyc,sha256=obUd-X2Y-ybx4icqUWq_qwIxrP9yyarJjexWAfO4MTI,3344
@@ -10586,17 +10590,19 @@ simo/fleet/migrations/__pycache__/0044_auto_20241210_0707.cpython-312.pyc,sha256
10586
10590
  simo/fleet/migrations/__pycache__/0044_auto_20241210_0707.cpython-38.pyc,sha256=M99PMBDgOdS_0KIEhlUamHWeTVCTdIHFXIvYTRX15Z8,1029
10587
10591
  simo/fleet/migrations/__pycache__/__init__.cpython-312.pyc,sha256=1rujN3qD3L0Q2MRB-gxwRKyShgUTX9NBpDGaIl42ozU,176
10588
10592
  simo/fleet/migrations/__pycache__/__init__.cpython-38.pyc,sha256=5k1KW0jeSDzw6RnVPRq4CaO13Lg7M0F-pxA_gqqZ6Mg,170
10593
+ simo/fleet/templates/admin/colonel_history.html,sha256=YfA6LDVExk1sAWhBuiCLA6vb3XcBNN7_fpJNZzGFtB0,169
10589
10594
  simo/fleet/templates/fleet/controllers_info/Button.md,sha256=GIuxqG617174NEtpPeCGVocxO4YMe7-CacgVSu_L5-E,739
10590
10595
  simo/fleet/templates/fleet/controllers_info/ENS160AirQualitySensor.md,sha256=3LSTY9YPFuVPIbVsYCAifcotrXJcOXl2k774_vo6nAE,770
10591
10596
  simo/generic/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10592
10597
  simo/generic/app_widgets.py,sha256=y8W3jR76Hh26O9pPQyg2SophMbYIOtAWD33MPKbB8Mg,856
10593
10598
  simo/generic/base_types.py,sha256=u3SlfpNYaCwkVBwomWgso4ODzL71ay9MhiAW-bxgnDU,341
10594
10599
  simo/generic/controllers.py,sha256=NZmrJ1BYzAwHJ1vN-OMvqnH13ihvjUhZqdxq-l__1Jk,46268
10595
- simo/generic/forms.py,sha256=yCEmOy6Sm5moCh-9UTzqLDiI5xmzfm5LwEhZ2r9KRbw,26054
10596
- simo/generic/gateways.py,sha256=d7sbt_GeeGGwp2hoD-FZNuyM6fNvEU51kiZyznZYcxg,17589
10597
- simo/generic/models.py,sha256=Adq7ipWK-renxJlNW-SZnAq2oGEOwKx8EdUWaKnfcVQ,7597
10600
+ simo/generic/forms.py,sha256=1Gt3FycP4Da8LbtOuOGTpLSf2dCDEE-Sdr_X3qlA1aw,26265
10601
+ simo/generic/gateways.py,sha256=kjqGMjlLXlGNreKLQtHoSnw4xan1IB8-KF8AqiZ82T0,16057
10602
+ simo/generic/models.py,sha256=59fkYowOX0imviIhA6uwupvuharrpBykmBm674rJNoI,7279
10598
10603
  simo/generic/routing.py,sha256=elQVZmgnPiieEuti4sJ7zITk1hlRxpgbotcutJJgC60,228
10599
10604
  simo/generic/socket_consumers.py,sha256=qesKZVhI56Kh7vdIUDD3hzDUi0FcXwIfcmE_a3YS6JQ,1772
10605
+ simo/generic/tasks.py,sha256=5jhi7Jv7lfaM3T8GArWKaDqQfuvdBsKqz5obN6NVUqk,2570
10600
10606
  simo/generic/__pycache__/__init__.cpython-312.pyc,sha256=-34GwswSg1zc1becA8lPwUpnd4ek2IoduCsIUr0KTNo,167
10601
10607
  simo/generic/__pycache__/__init__.cpython-38.pyc,sha256=mLu54WS9KIl-pHwVCBKpsDFIlOqml--JsOVzAUHg6cU,161
10602
10608
  simo/generic/__pycache__/app_widgets.cpython-312.pyc,sha256=ywoKk91YSEZxpyt9haG509_c0G9DMJVpae_y1iiZxJU,1937
@@ -10605,16 +10611,17 @@ simo/generic/__pycache__/base_types.cpython-312.pyc,sha256=h8Mwu49i-zmwTbL33JaLJ
10605
10611
  simo/generic/__pycache__/base_types.cpython-38.pyc,sha256=aV5NdIuvXR-ItKpI__MwcyPZHD6Z882TFdgYkPCkr1I,493
10606
10612
  simo/generic/__pycache__/controllers.cpython-312.pyc,sha256=TgHV-dNMJqYuFOgeJTxeh4Mihb4lHnfhid0CmdhZCR4,52737
10607
10613
  simo/generic/__pycache__/controllers.cpython-38.pyc,sha256=jJjwKVaDYyazrRGNjUFoY74nr_jX_DEnsC9KjyxZCgc,30427
10608
- simo/generic/__pycache__/forms.cpython-312.pyc,sha256=duBW82wI9zfyzPnIk4hLs_vx5Rfk4vSCh9n7cSNbgcc,34681
10614
+ simo/generic/__pycache__/forms.cpython-312.pyc,sha256=nTyrgmEmOrgORJSOxUI_i2O7wVUXCQL9MYhaT76Jmew,34869
10609
10615
  simo/generic/__pycache__/forms.cpython-38.pyc,sha256=k8lz3taXdWAg5P9jcnw66mWH51pCc4SOsg32kVEtBCg,19416
10610
- simo/generic/__pycache__/gateways.cpython-312.pyc,sha256=-ifxm5kYUvp6tRw3xr1EO7jCEXQ9_aQG3OdxAQvxHBg,21758
10616
+ simo/generic/__pycache__/gateways.cpython-312.pyc,sha256=ixeotxPeqHkINziDnnkymlKR2wYMb5UR2sxiioMQxhY,19309
10611
10617
  simo/generic/__pycache__/gateways.cpython-38.pyc,sha256=GIeMT51oZU2OCFD4eUDFdSRRYE0Qf14AcOr_gdUqG94,12705
10612
- simo/generic/__pycache__/models.cpython-312.pyc,sha256=xriUzjkaM2Y4mT3jo2OPK-XGBroBBSFJfLqK0jMA4MA,10200
10618
+ simo/generic/__pycache__/models.cpython-312.pyc,sha256=ggaeX6BQa-0-KG50HadpRCWeW84Fbog0muT2gBkqLNQ,10190
10613
10619
  simo/generic/__pycache__/models.cpython-38.pyc,sha256=MZpum7syAFxuulf47K7gtUlJJ7xRD-IBUBAwUM1ZRnw,5825
10614
10620
  simo/generic/__pycache__/routing.cpython-312.pyc,sha256=_wQPZeAgwlGtnafw9VcabgqjyJxzDFywHBIFbGhzYRE,452
10615
10621
  simo/generic/__pycache__/routing.cpython-38.pyc,sha256=xtxTUTBTdivzFyA5Wh7k-hUj1WDO_FiRq6HYXdbr9Ks,382
10616
10622
  simo/generic/__pycache__/socket_consumers.cpython-312.pyc,sha256=zERTr2bHXLKSXCoIov6MaFRrgfeS1A0IHCXbaPfVvK4,2814
10617
10623
  simo/generic/__pycache__/socket_consumers.cpython-38.pyc,sha256=FaVCf_uJI2uwj1Zz-jwsOXou65oV9gFCIB8e-YKquRk,1662
10624
+ simo/generic/__pycache__/tasks.cpython-312.pyc,sha256=zEyNpFVmEJoZdeYKNZ7cEmPtIyZMTwEqzZJZZwMYl-o,4494
10618
10625
  simo/generic/migrations/0001_initial.py,sha256=7FpPcfpRU5ya0b8s2KbxR5a3npf92YruvZltUybjzys,676
10619
10626
  simo/generic/migrations/0002_auto_20241126_0726.py,sha256=SX38JwP732QooOm5HM1Xo7Th_Mv_6YZloT3eozULOhs,922
10620
10627
  simo/generic/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -10719,6 +10726,7 @@ simo/notifications/__pycache__/models.cpython-312.pyc,sha256=VKa2ZQxzBH22YAUlsmh
10719
10726
  simo/notifications/__pycache__/models.cpython-38.pyc,sha256=PoqLuOnlaAWQQ-20AtqhvAlLSkakPmdn7J7wGvHNW3g,2449
10720
10727
  simo/notifications/__pycache__/serializers.cpython-312.pyc,sha256=on-lu0Sj7vEl0_-2ul19Yk59gYHIFESXZKpA9RKNewQ,1226
10721
10728
  simo/notifications/__pycache__/serializers.cpython-38.pyc,sha256=7-eRGKYuQ4g1SpKOMpz17SIiu1HmaMoYv-cJbaO9QGA,1028
10729
+ simo/notifications/__pycache__/utils.cpython-312.pyc,sha256=o-CnxcAtX7tHo2CdGBKjLTd1OU3z57zUCaWlYzEuv88,2423
10722
10730
  simo/notifications/__pycache__/utils.cpython-38.pyc,sha256=4ZnI-pmWji84EXBkPrl4ir1kGjfanO4bH5--bLNRxCA,1648
10723
10731
  simo/notifications/migrations/0001_initial.py,sha256=Zh69AQ-EKlQKfqfnMDVRcxvo1MxRY-TFLCdnNcgqi6g,2003
10724
10732
  simo/notifications/migrations/0002_notification_instance.py,sha256=B3msbMeKvsuq-V7gvRADRjj5PFLayhi3pQvHZjqzO5g,563
@@ -10777,6 +10785,7 @@ simo/users/__pycache__/sso_urls.cpython-312.pyc,sha256=FQLOFu310j7pOhTNlg2wyUybq
10777
10785
  simo/users/__pycache__/sso_urls.cpython-38.pyc,sha256=uAwDozpOmrhUald-8tOHANILXkH7-TI8fNYXOtPkSY8,402
10778
10786
  simo/users/__pycache__/sso_views.cpython-312.pyc,sha256=FZGDRdt7pb8qROe8ZAEGSTfeZtLCGNBvQiXpDtrc4dE,6011
10779
10787
  simo/users/__pycache__/sso_views.cpython-38.pyc,sha256=PLRF6FYCxRhnmgnN_gUS-pdQlH7lofLU1Xhgw3vDO_Y,4019
10788
+ simo/users/__pycache__/tasks.cpython-312.pyc,sha256=cu-VV1HWXToHR875DFC2E9_x1cn1mD5mXYqltk0QOuw,1955
10780
10789
  simo/users/__pycache__/tasks.cpython-38.pyc,sha256=XLMKt3suT7BlcXrJZoH9ZIhhtBuqyiW4lsOB9IbBkko,1225
10781
10790
  simo/users/__pycache__/utils.cpython-312.pyc,sha256=dJtfFncpI2QtTSZoUUNCfUsDoofOOfVtMRXNm5ZXC20,3515
10782
10791
  simo/users/__pycache__/utils.cpython-38.pyc,sha256=cgxUwEaBgxT360mw4E03J7u4vi3dhw6K3n7-8WNvyFU,1888
@@ -10924,9 +10933,9 @@ simo/users/templates/invitations/expired_msg.html,sha256=47DEQpj8HBSa-_TImW-5JCe
10924
10933
  simo/users/templates/invitations/expired_suggestion.html,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10925
10934
  simo/users/templates/invitations/taken_msg.html,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10926
10935
  simo/users/templates/invitations/taken_suggestion.html,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10927
- simo-2.8.3.dist-info/LICENSE.md,sha256=M7wm1EmMGDtwPRdg7kW4d00h1uAXjKOT3HFScYQMeiE,34916
10928
- simo-2.8.3.dist-info/METADATA,sha256=e73yT_ZPrUBSQ9WeaC3C_Ha7RqPRz8oRME8ayRXL1X4,2008
10929
- simo-2.8.3.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
10930
- simo-2.8.3.dist-info/entry_points.txt,sha256=S9PwnUYmTSW7681GKDCxUbL0leRJIaRk6fDQIKgbZBA,135
10931
- simo-2.8.3.dist-info/top_level.txt,sha256=GmS1hrAbpVqn9OWZh6UX82eIOdRLgYA82RG9fe8v4Rs,5
10932
- simo-2.8.3.dist-info/RECORD,,
10936
+ simo-2.8.5.dist-info/LICENSE.md,sha256=M7wm1EmMGDtwPRdg7kW4d00h1uAXjKOT3HFScYQMeiE,34916
10937
+ simo-2.8.5.dist-info/METADATA,sha256=J6rygMMxiXfeOMVZEhetZjpGJKHF_m2obJCRJ5D7y00,2008
10938
+ simo-2.8.5.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
10939
+ simo-2.8.5.dist-info/entry_points.txt,sha256=S9PwnUYmTSW7681GKDCxUbL0leRJIaRk6fDQIKgbZBA,135
10940
+ simo-2.8.5.dist-info/top_level.txt,sha256=GmS1hrAbpVqn9OWZh6UX82eIOdRLgYA82RG9fe8v4Rs,5
10941
+ simo-2.8.5.dist-info/RECORD,,
File without changes