simo 2.0.1__py3-none-any.whl → 2.0.3__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/controllers.py CHANGED
@@ -762,6 +762,12 @@ class Lock(Switch):
762
762
  app_widget = LockWidget
763
763
  admin_widget_template = 'admin/controller_widgets/lock.html'
764
764
 
765
+ UNLOCKED = 0
766
+ LOCKED = 1
767
+ LOCKING = 2
768
+ UNLOCKING = 3
769
+ FAULT = 4
770
+
765
771
  def lock(self):
766
772
  self.turn_on()
767
773
 
@@ -769,11 +775,20 @@ class Lock(Switch):
769
775
  self.turn_off()
770
776
 
771
777
  def _receive_from_device(self, value, is_alive=True):
772
- if type(value) in (int, bool):
778
+ if type(value) == bool:
773
779
  if value:
774
780
  value = 'locked'
775
781
  else:
776
782
  value = 'unlocked'
783
+ if type(value) == int:
784
+ values_map = {
785
+ self.UNLOCKED: 'unlocked',
786
+ self.LOCKED: 'locked',
787
+ self.LOCKING: 'locking',
788
+ self.UNLOCKING: 'unlocking',
789
+ self.FAULT: 'fault'
790
+ }
791
+ value = values_map.get(value, 'fault')
777
792
  return super()._receive_from_device(value, is_alive=is_alive)
778
793
 
779
794
  def _validate_val(self, value, occasion=None):
@@ -7,6 +7,7 @@ from django.conf import settings
7
7
  from django.contrib.contenttypes.models import ContentType
8
8
  from channels.generic.websocket import AsyncWebsocketConsumer, WebsocketConsumer
9
9
  from simo.core.events import ObjectChangeEvent, get_event_obj
10
+ from simo.core.utils.logs import capture_socket_errors
10
11
  import paho.mqtt.client as mqtt
11
12
  from simo.users.middleware import introduce
12
13
  from simo.core.models import Component, Gateway
@@ -14,6 +15,7 @@ from simo.core.utils.model_helpers import get_log_file_path
14
15
  from simo.core.middleware import introduce_instance
15
16
 
16
17
 
18
+ @capture_socket_errors
17
19
  class SIMOWebsocketConsumer(WebsocketConsumer):
18
20
  headers = {}
19
21
 
@@ -24,6 +26,7 @@ class SIMOWebsocketConsumer(WebsocketConsumer):
24
26
  }
25
27
 
26
28
 
29
+ @capture_socket_errors
27
30
  class LogConsumer(AsyncWebsocketConsumer):
28
31
  log_file = None
29
32
  in_error = False
@@ -123,6 +126,7 @@ class LogConsumer(AsyncWebsocketConsumer):
123
126
  self.log_file = None
124
127
 
125
128
 
129
+ @capture_socket_errors
126
130
  class GatewayController(SIMOWebsocketConsumer):
127
131
  gateway = None
128
132
  _mqtt_client = None
@@ -178,6 +182,7 @@ class GatewayController(SIMOWebsocketConsumer):
178
182
  pass
179
183
 
180
184
 
185
+ @capture_socket_errors
181
186
  class ComponentController(SIMOWebsocketConsumer):
182
187
  component = None
183
188
  send_value = False
simo/core/utils/logs.py CHANGED
@@ -1,4 +1,10 @@
1
1
  import logging
2
+ from functools import wraps
3
+ from inspect import iscoroutinefunction
4
+ from logging import getLogger
5
+ from channels.exceptions import AcceptConnection, DenyConnection, StopConsumer
6
+
7
+ logger = getLogger()
2
8
 
3
9
 
4
10
  class StreamToLogger(object):
@@ -28,3 +34,31 @@ class StreamToLogger(object):
28
34
  if self.linebuf != '':
29
35
  self.logger.log(self.log_level, self.linebuf.rstrip())
30
36
  self.linebuf = ''
37
+
38
+
39
+
40
+
41
+ def propagate_exceptions(func):
42
+ async def wrapper(*args, **kwargs): # we're wrapping an async function
43
+ try:
44
+ return await func(*args, **kwargs)
45
+ except (AcceptConnection, DenyConnection, StopConsumer): # these are handled by channels
46
+ raise
47
+ except Exception as exception: # any other exception
48
+ # avoid logging the same exception multiple times
49
+ if not getattr(exception, "caught", False):
50
+ setattr(exception, "caught", True)
51
+ logger.error(
52
+ "Exception occurred in {}:".format(func.__qualname__),
53
+ exc_info=exception,
54
+ )
55
+ raise # propagate the exception
56
+ return wraps(func)(wrapper)
57
+
58
+
59
+ def capture_socket_errors(consumer_class):
60
+ for method_name, method in list(consumer_class.__dict__.items()):
61
+ if iscoroutinefunction(method): # an async method
62
+ # wrap the method with a decorator that propagate exceptions
63
+ setattr(consumer_class, method_name, propagate_exceptions(method))
64
+ return consumer_class
simo/fleet/admin.py CHANGED
@@ -1,3 +1,4 @@
1
+ from threading import Timer
1
2
  from django.contrib import admin
2
3
  from django.utils.safestring import mark_safe
3
4
  from django.template.loader import render_to_string
@@ -78,6 +79,15 @@ class ColonelAdmin(admin.ModelAdmin):
78
79
  return qs
79
80
  return qs.filter(instance__in=request.user.instances)
80
81
 
82
+ def save_model(self, request, obj, form, change):
83
+ super().save_model(request, obj, form, change)
84
+ # give it one second to finish up with atomic transaction and
85
+ # send update_config command.
86
+ def update_colonel_config(colonel):
87
+ colonel.update_config()
88
+ Timer(1, update_colonel_config, [obj]).start()
89
+
90
+
81
91
  def has_add_permission(self, request):
82
92
  return False
83
93
 
simo/fleet/controllers.py CHANGED
@@ -308,6 +308,8 @@ class TTLock(FleeDeviceMixin, Lock):
308
308
  @classmethod
309
309
  def _process_discovery(cls, started_with, data):
310
310
  if data['discover-ttlock'] == 'fail':
311
+ if data['result'] == 0:
312
+ return {'error': 'Internal Colonel error. See Colonel logs.'}
311
313
  if data['result'] == 1:
312
314
  return {'error': 'TTLock not found.'}
313
315
  elif data['result'] == 2:
@@ -7,7 +7,7 @@ from django.db import migrations
7
7
 
8
8
  def forwards_func(apps, schema_editor):
9
9
  from simo.fleet.utils import GPIO_PINS
10
- from django.contrib.contenttypes.models import ContentType
10
+ #from django.contrib.contenttypes.models import ContentType
11
11
 
12
12
  Colonel = apps.get_model('fleet', "Colonel")
13
13
  ColonelPin = apps.get_model("fleet", "ColonelPin")
@@ -26,18 +26,18 @@ def forwards_func(apps, schema_editor):
26
26
  )
27
27
 
28
28
  for i2c in colonel.i2c_interfaces.all():
29
- ct = ContentType.objects.get_for_model(i2c)
29
+ #ct = ContentType.objects.get_for_model(i2c)
30
30
 
31
31
  scl_pin = new_pins[colonel.id][i2c.scl_pin_no]
32
- scl_pin.occupied_by_content_type = ct
33
- scl_pin.occupied_by_id = i2c.id
32
+ #scl_pin.occupied_by_content_type = ct
33
+ #scl_pin.occupied_by_id = i2c.id
34
34
  scl_pin.save()
35
35
  i2c.scl_pin = scl_pin
36
36
  i2c.scl_pin.save()
37
37
 
38
38
  sda_pin = new_pins[colonel.id][i2c.sda_pin_no]
39
- sda_pin.occupied_by_content_type = ct
40
- sda_pin.occupied_by_id = i2c.id
39
+ #sda_pin.occupied_by_content_type = ct
40
+ #sda_pin.occupied_by_id = i2c.id
41
41
  sda_pin.save()
42
42
 
43
43
  i2c.sda_pin = sda_pin
@@ -48,7 +48,7 @@ def forwards_func(apps, schema_editor):
48
48
  for comp in Component.objects.filter(
49
49
  controller_uid__startswith='simo.fleet.'
50
50
  ):
51
- ct = ContentType.objects.get_for_model(comp)
51
+ #ct = ContentType.objects.get_for_model(comp)
52
52
  try:
53
53
  colonel = Colonel.objects.get(id=comp.config['colonel'])
54
54
  except Exception as e:
@@ -64,8 +64,8 @@ def forwards_func(apps, schema_editor):
64
64
 
65
65
  pin = new_pins[colonel.id][comp.config['pin_no']]
66
66
  comp.config['pin'] = pin.pk
67
- pin.occupied_by_content_type = ct
68
- pin.occupied_by_id = comp.id
67
+ #pin.occupied_by_content_type = ct
68
+ #pin.occupied_by_id = comp.id
69
69
  pin.save()
70
70
  if 'power_pin' in comp.config:
71
71
  comp.config['power_pin_no'] = comp.config.pop('power_pin')
@@ -77,8 +77,8 @@ def forwards_func(apps, schema_editor):
77
77
 
78
78
  pin = new_pins[colonel.id][comp.config['power_pin_no']]
79
79
  comp.config['power_pin'] = pin.pk
80
- pin.occupied_by_content_type = ct
81
- pin.occupied_by_id = comp.id
80
+ #pin.occupied_by_content_type = ct
81
+ #pin.occupied_by_id = comp.id
82
82
  pin.save()
83
83
  if 'sensor_pin' in comp.config:
84
84
  comp.config['sensor_pin_no'] = comp.config.pop('sensor_pin')
@@ -90,8 +90,8 @@ def forwards_func(apps, schema_editor):
90
90
 
91
91
  pin = new_pins[colonel.id][comp.config['sensor_pin_no']]
92
92
  comp.config['sensor_pin'] = pin.pk
93
- pin.occupied_by_content_type = ct
94
- pin.occupied_by_id = comp.id
93
+ #pin.occupied_by_content_type = ct
94
+ #pin.occupied_by_id = comp.id
95
95
  pin.save()
96
96
  if 'output_pin' in comp.config:
97
97
  comp.config['output_pin_no'] = comp.config.pop('output_pin')
@@ -103,8 +103,8 @@ def forwards_func(apps, schema_editor):
103
103
 
104
104
  pin = new_pins[colonel.id][comp.config['output_pin_no']]
105
105
  comp.config['output_pin'] = pin.pk
106
- pin.occupied_by_content_type = ct
107
- pin.occupied_by_id = comp.id
106
+ #pin.occupied_by_content_type = ct
107
+ #pin.occupied_by_id = comp.id
108
108
  pin.save()
109
109
  if 'open_pin' in comp.config:
110
110
  comp.config['open_pin_no'] = comp.config.pop('open_pin')
@@ -116,8 +116,8 @@ def forwards_func(apps, schema_editor):
116
116
 
117
117
  pin = new_pins[colonel.id][comp.config['open_pin_no']]
118
118
  comp.config['open_pin'] = pin.pk
119
- pin.occupied_by_content_type = ct
120
- pin.occupied_by_id = comp.id
119
+ #pin.occupied_by_content_type = ct
120
+ #pin.occupied_by_id = comp.id
121
121
  pin.save()
122
122
  if 'close_pin' in comp.config:
123
123
  comp.config['close_pin_no'] = comp.config.pop('close_pin')
@@ -129,8 +129,8 @@ def forwards_func(apps, schema_editor):
129
129
 
130
130
  pin = new_pins[colonel.id][comp.config['close_pin_no']]
131
131
  comp.config['close_pin'] = pin.pk
132
- pin.occupied_by_content_type = ct
133
- pin.occupied_by_id = comp.id
132
+ #pin.occupied_by_content_type = ct
133
+ #pin.occupied_by_id = comp.id
134
134
  pin.save()
135
135
 
136
136
  if 'controls' in comp.config:
@@ -146,8 +146,8 @@ def forwards_func(apps, schema_editor):
146
146
  pin = new_pins[colonel.id][updated_controls['pin_no']]
147
147
  updated_controls['pin'] = pin.pk
148
148
 
149
- pin.occupied_by_content_type = ct
150
- pin.occupied_by_id = comp.id
149
+ #pin.occupied_by_content_type = ct
150
+ #pin.occupied_by_id = comp.id
151
151
  pin.save()
152
152
 
153
153
  comp.config['controls'][i] = updated_controls
simo/fleet/models.py CHANGED
@@ -82,8 +82,12 @@ class Colonel(DirtyFieldsMixin, models.Model):
82
82
  occupied_pins = models.JSONField(default=dict, blank=True)
83
83
 
84
84
  logs_stream = models.BooleanField(
85
- default=False, help_text="Might cause unnecessary overhead. "
86
- "Better to leave this off if things are running smoothly."
85
+ default=False, help_text="ATENTION! Causes serious overhead and "
86
+ "significantly degrades the lifespan of a chip "
87
+ "due to a lot of writes to the memory. "
88
+ "It also causes Colonel websocket to run out of memory "
89
+ "and reset if a lot of data is being transmitted. "
90
+ "Leave this off, unleess you know what you are doing!"
87
91
  )
88
92
  pwm_frequency = models.IntegerField(default=1, choices=(
89
93
  (0, "3kHz"), (1, "22kHz")
@@ -161,10 +165,8 @@ class Colonel(DirtyFieldsMixin, models.Model):
161
165
  @transaction.atomic
162
166
  def rebuild_occupied_pins(self):
163
167
  for pin in ColonelPin.objects.filter(colonel=self):
164
- if isinstance(pin.occupied_by, Component):
165
- pin.occupied_by_content_type = None
166
- pin.occupied_by_id = None
167
- pin.save()
168
+ pin.occupied_by = None
169
+ pin.save()
168
170
 
169
171
  for component in self.components.all():
170
172
  try:
@@ -176,6 +178,12 @@ class Colonel(DirtyFieldsMixin, models.Model):
176
178
  pin.occupied_by = component
177
179
  pin.save()
178
180
 
181
+ for interface in self.i2c_interfaces.all():
182
+ interface.sda_pin.occupied_by = interface
183
+ interface.sda_pin.save()
184
+ interface.scl_pin.occupied_by = interface
185
+ interface.scl_pin.save()
186
+
179
187
 
180
188
  @transaction.atomic()
181
189
  def move_to(self, other_colonel):
@@ -190,13 +198,11 @@ class Colonel(DirtyFieldsMixin, models.Model):
190
198
  self.components.remove(component)
191
199
  other_colonel.components.add(component)
192
200
 
193
- self.rebuild_occupied_pins()
194
- other_colonel.rebuild_occupied_pins()
195
-
196
201
  other_colonel.i2c_interfaces.all().delete()
197
202
 
198
203
  for i2c_interface in self.i2c_interfaces.all():
199
204
  I2CInterface.objects.create(
205
+ no=i2c_interface.no,
200
206
  colonel=other_colonel, name=i2c_interface.name,
201
207
  freq=i2c_interface.freq,
202
208
  scl_pin=ColonelPin.objects.get(
@@ -207,6 +213,8 @@ class Colonel(DirtyFieldsMixin, models.Model):
207
213
  ),
208
214
  )
209
215
 
216
+ self.rebuild_occupied_pins()
217
+ other_colonel.rebuild_occupied_pins()
210
218
  self.update_config()
211
219
  other_colonel.update_config()
212
220
 
@@ -263,10 +271,7 @@ class ColonelPin(models.Model):
263
271
 
264
272
  @receiver(post_save, sender=Colonel)
265
273
  def after_colonel_save(sender, instance, created, *args, **kwargs):
266
- if not created:
267
- return
268
-
269
- def after_update():
274
+ if created:
270
275
  for no, data in GPIO_PINS.get(instance.type).items():
271
276
  ColonelPin.objects.get_or_create(
272
277
  colonel=instance, no=no,
@@ -280,9 +285,6 @@ def after_colonel_save(sender, instance, created, *args, **kwargs):
280
285
  scl_pin=ColonelPin.objects.get(colonel=instance, no=4),
281
286
  sda_pin=ColonelPin.objects.get(colonel=instance, no=15),
282
287
  )
283
- instance.update_config()
284
-
285
- transaction.on_commit(after_update)
286
288
 
287
289
 
288
290
  @receiver(pre_delete, sender=Component)
simo/fleet/serializers.py CHANGED
@@ -45,3 +45,8 @@ class ColonelSerializer(serializers.ModelSerializer):
45
45
  for pin in obj.pins.all():
46
46
  result.append(ColonelPinSerializer(pin).data)
47
47
  return result
48
+
49
+ def update(self, instance, validated_data):
50
+ instance = super().update(instance, validated_data)
51
+ instance.update_config()
52
+ return instance
@@ -13,15 +13,18 @@ import paho.mqtt.client as mqtt
13
13
  from channels.generic.websocket import AsyncWebsocketConsumer
14
14
  from asgiref.sync import sync_to_async
15
15
  from simo.core.utils.model_helpers import get_log_file_path
16
+ from simo.core.utils.logs import capture_socket_errors
16
17
  from simo.core.events import GatewayObjectCommand, get_event_obj
17
18
  from simo.core.models import Gateway, Instance, Component
18
19
  from simo.conf import dynamic_settings
19
20
  from simo.users.models import Fingerprint
21
+
20
22
  from .gateways import FleetGatewayHandler
21
23
  from .models import Colonel
22
24
  from .controllers import TTLock
23
25
 
24
26
 
27
+ @capture_socket_errors
25
28
  class FleetConsumer(AsyncWebsocketConsumer):
26
29
  colonel = None
27
30
  colonel_logger = None
@@ -345,106 +348,110 @@ class FleetConsumer(AsyncWebsocketConsumer):
345
348
 
346
349
 
347
350
  async def receive(self, text_data=None, bytes_data=None):
348
- if text_data:
349
- print(f"{self.colonel}: {text_data}")
350
- data = json.loads(text_data)
351
- if 'get_config' in data:
352
- config = await self.get_config_data()
353
- print("Send config: ", config)
354
- await self.send_data({
355
- 'command': 'set_config', 'data': config
356
- }, compress=True)
357
- elif 'comp' in data:
358
- try:
351
+ try:
352
+ if text_data:
353
+ print(f"{self.colonel}: {text_data}")
354
+ data = json.loads(text_data)
355
+ if 'get_config' in data:
356
+ config = await self.get_config_data()
357
+ print("Send config: ", config)
358
+ await self.send_data({
359
+ 'command': 'set_config', 'data': config
360
+ }, compress=True)
361
+ elif 'comp' in data:
359
362
  try:
360
- id=int(data['comp'])
361
- except:
362
- return
363
-
364
- component = await sync_to_async(
365
- Component.objects.get, thread_sensitive=True
366
- )(id=id)
367
-
368
- if 'val' in data:
369
- def receive_val(val):
370
- if data.get('actor'):
371
- fingerprint = Fingerprint.objects.filter(
372
- value=f"ttlock-{component.id}-{data.get('actor')}",
373
- ).first()
374
- component.change_init_fingerprint = fingerprint
375
- component.controller._receive_from_device(
376
- val, bool(data.get('alive'))
377
- )
378
- await sync_to_async(
379
- receive_val, thread_sensitive=True
380
- )(data['val'])
381
-
382
- if 'options' in data:
383
- def receive_options(val):
384
- component.meta['options'] = val
385
- component.save()
386
- await sync_to_async(
387
- receive_options, thread_sensitive=True
388
- )(data['options'])
389
-
390
- if 'codes' in data and component.controller_uid == TTLock.uid:
391
- def save_codes(codes):
392
- component.meta['codes'] = codes
393
- for code in codes:
394
- Fingerprint.objects.get_or_create(
395
- value=f"ttlock-{component.id}-code-{str(code)}",
396
- defaults={'type': "TTLock code"}
397
- )
398
- component.save()
399
- await sync_to_async(
400
- save_codes, thread_sensitive=True
401
- )(data['codes'])
402
- if 'fingerprints' in data and component.controller_uid == TTLock.uid:
403
- def save_codes(codes):
404
- component.meta['fingerprints'] = codes
405
- for code in codes:
406
- Fingerprint.objects.get_or_create(
407
- value=f"ttlock-{component.id}-finger-{str(code)}",
408
- defaults={'type': "TTLock Fingerprint"}
363
+ try:
364
+ id=int(data['comp'])
365
+ except:
366
+ return
367
+
368
+ component = await sync_to_async(
369
+ Component.objects.get, thread_sensitive=True
370
+ )(id=id)
371
+
372
+ if 'val' in data:
373
+ def receive_val(val):
374
+ if data.get('actor'):
375
+ fingerprint = Fingerprint.objects.filter(
376
+ value=f"ttlock-{component.id}-{data.get('actor')}",
377
+ ).first()
378
+ component.change_init_fingerprint = fingerprint
379
+ component.controller._receive_from_device(
380
+ val, bool(data.get('alive'))
409
381
  )
410
- component.save()
411
- await sync_to_async(
412
- save_codes, thread_sensitive=True
413
- )(data['fingerprints'])
414
-
415
- except Exception as e:
416
- print(traceback.format_exc(), file=sys.stderr)
417
-
418
- elif 'discover-ttlock' in data:
419
- def process_discovery_result():
420
- self.gateway.refresh_from_db()
421
- if self.gateway.discovery.get('finished'):
422
- return Component.objects.filter(
423
- meta__finalization_data__temp_id=data['result']['id']
424
- ).first()
425
- try:
426
- self.gateway.process_discovery(data)
382
+ await sync_to_async(
383
+ receive_val, thread_sensitive=True
384
+ )(data['val'])
385
+
386
+ if 'options' in data:
387
+ def receive_options(val):
388
+ component.meta['options'] = val
389
+ component.save()
390
+ await sync_to_async(
391
+ receive_options, thread_sensitive=True
392
+ )(data['options'])
393
+
394
+ if 'codes' in data and component.controller_uid == TTLock.uid:
395
+ def save_codes(codes):
396
+ component.meta['codes'] = codes
397
+ for code in codes:
398
+ Fingerprint.objects.get_or_create(
399
+ value=f"ttlock-{component.id}-code-{str(code)}",
400
+ defaults={'type': "TTLock code"}
401
+ )
402
+ component.save()
403
+ await sync_to_async(
404
+ save_codes, thread_sensitive=True
405
+ )(data['codes'])
406
+ if 'fingerprints' in data and component.controller_uid == TTLock.uid:
407
+ def save_codes(codes):
408
+ component.meta['fingerprints'] = codes
409
+ for code in codes:
410
+ Fingerprint.objects.get_or_create(
411
+ value=f"ttlock-{component.id}-finger-{str(code)}",
412
+ defaults={'type': "TTLock Fingerprint"}
413
+ )
414
+ component.save()
415
+ await sync_to_async(
416
+ save_codes, thread_sensitive=True
417
+ )(data['fingerprints'])
418
+
427
419
  except Exception as e:
428
420
  print(traceback.format_exc(), file=sys.stderr)
429
- self.gateway.finish_discovery()
430
421
 
431
- finished_comp = await sync_to_async(
432
- process_discovery_result, thread_sensitive=True
433
- )()
434
- if finished_comp:
435
- await self.send_data({
436
- 'command': 'finalize',
437
- 'data': finished_comp.meta['finalization_data']
438
- })
422
+ elif 'discover-ttlock' in data:
423
+ def process_discovery_result():
424
+ self.gateway.refresh_from_db()
425
+ if self.gateway.discovery.get('finished'):
426
+ return Component.objects.filter(
427
+ meta__finalization_data__temp_id=data['result']['id']
428
+ ).first()
429
+ try:
430
+ self.gateway.process_discovery(data)
431
+ except Exception as e:
432
+ print(traceback.format_exc(), file=sys.stderr)
433
+ self.gateway.finish_discovery()
434
+
435
+ finished_comp = await sync_to_async(
436
+ process_discovery_result, thread_sensitive=True
437
+ )()
438
+ if finished_comp:
439
+ await self.send_data({
440
+ 'command': 'finalize',
441
+ 'data': finished_comp.meta['finalization_data']
442
+ })
439
443
 
440
- elif bytes_data:
441
- if not self.colonel_logger:
442
- await self.start_logger()
444
+ elif bytes_data:
445
+ if not self.colonel_logger:
446
+ await self.start_logger()
443
447
 
444
- for logline in bytes_data.decode(errors='replace').split('\n'):
445
- self.colonel_logger.log(logging.INFO, logline)
448
+ for logline in bytes_data.decode(errors='replace').split('\n'):
449
+ self.colonel_logger.log(logging.INFO, logline)
450
+
451
+ await self.log_colonel_connected()
452
+ except Exception as e:
453
+ print(traceback.format_exc(), file=sys.stderr)
446
454
 
447
- await self.log_colonel_connected()
448
455
 
449
456
 
450
457
  async def log_colonel_connected(self):
simo/generic/forms.py CHANGED
@@ -167,6 +167,12 @@ class AlarmGroupConfigForm(BaseComponentForm):
167
167
  required=False,
168
168
  help_text="Defines if this is your main/top global alarm group."
169
169
  )
170
+ notify_on_breach = forms.IntegerField(
171
+ required=False, min_value=0,
172
+ help_text="Notify active users if "
173
+ "not disarmed within given number of seconds "
174
+ "after the breached."
175
+ )
170
176
  has_alarm = False
171
177
 
172
178
 
simo/generic/models.py CHANGED
@@ -1,6 +1,4 @@
1
- from django.db import models
2
1
  from threading import Timer
3
- from django.utils.translation import gettext_lazy as _
4
2
  from django.db.models.signals import pre_save, post_save, post_delete
5
3
  from django.dispatch import receiver
6
4
  from simo.core.models import Instance, Component
@@ -20,7 +18,8 @@ def handle_alarm_groups(sender, instance, *args, **kwargs):
20
18
 
21
19
  for alarm_group in Component.objects.filter(
22
20
  controller_uid=AlarmGroup.uid,
23
- config__components__contains=instance.id
21
+ config__components__contains=instance.id,
22
+ config__notify_on_breach__gt=-1
24
23
  ).exclude(value='disarmed'):
25
24
  stats = {
26
25
  'disarmed': 0, 'pending-arm': 0, 'armed': 0, 'breached': 0
@@ -33,7 +32,6 @@ def handle_alarm_groups(sender, instance, *args, **kwargs):
33
32
  alarm_group.config['stats'] = stats
34
33
  alarm_group.save(update_fields=['config'])
35
34
 
36
- alarm_group_value = alarm_group.value
37
35
  if stats['disarmed'] == len(alarm_group.config['components']):
38
36
  alarm_group_value = 'disarmed'
39
37
  elif stats['armed'] == len(alarm_group.config['components']):
@@ -41,9 +39,11 @@ def handle_alarm_groups(sender, instance, *args, **kwargs):
41
39
  elif stats['breached']:
42
40
  if alarm_group.value != 'breached':
43
41
  def notify_users_security_breach(alarm_group_component_id):
44
- from simo.notifications.utils import notify_users
45
- alarm_group_component = Component.objects.get(
46
- id=alarm_group_component_id)
42
+ alarm_group_component = Component.objects.filter(
43
+ id=alarm_group_component_id, value='breached'
44
+ ).first()
45
+ if not alarm_group_component:
46
+ return
47
47
  breached_components = Component.objects.filter(
48
48
  pk__in=alarm_group_component.config['components'],
49
49
  arm_status='breached'
@@ -51,16 +51,22 @@ def handle_alarm_groups(sender, instance, *args, **kwargs):
51
51
  body = "Security Breach! " + '; '.join(
52
52
  [str(c) for c in breached_components]
53
53
  )
54
+ from simo.notifications.utils import notify_users
54
55
  notify_users(
56
+ alarm_group_component.zone.instance,
55
57
  'alarm', str(alarm_group_component), body,
56
58
  component=alarm_group_component
57
59
  )
58
- t = Timer(1, notify_users_security_breach, [alarm_group.id])
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
+ )
59
65
  t.start()
60
66
  alarm_group_value = 'breached'
61
67
  else:
62
68
  alarm_group_value = 'pending-arm'
63
- alarm_group.set(alarm_group_value)
69
+ alarm_group.controller.set(alarm_group_value)
64
70
 
65
71
 
66
72
  @receiver(post_save, sender=Component)
@@ -14,7 +14,7 @@ def notify_users(instance, severity, title, body=None, component=None, users=Non
14
14
  )
15
15
  if not users:
16
16
  users = User.objects.filter(
17
- instance_roles__instnace=instance,
17
+ instance_roles__instance=instance,
18
18
  instance_roles__is_active=True
19
19
  )
20
20
  for user in users:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: simo
3
- Version: 2.0.1
3
+ Version: 2.0.3
4
4
  Summary: Smart Home on Steroids!
5
5
  Author-email: Simanas Venčkauskas <simanas@simo.io>
6
6
  Project-URL: Homepage, https://simo.io
@@ -34,7 +34,7 @@ simo/core/auto_urls.py,sha256=0gu-IL7PHobrmKW6ksffiOkAYu-aIorykWdxRNtwGYo,1194
34
34
  simo/core/autocomplete_views.py,sha256=JT5LA2_Wtr60XYSAIqaXFKFYPjrmkEf6yunXD9y2zco,4022
35
35
  simo/core/base_types.py,sha256=yqbIZqBksrAkEuHRbt6iExwPDDy0K5II2NzRCkmOvMU,589
36
36
  simo/core/context.py,sha256=98PXAMie43faRVBFkOG22uNpvGRNprcGhzjBFkrxaRY,1367
37
- simo/core/controllers.py,sha256=bxJRpmREw8CsgDAiuVvpOl8HxHhZqrRM1qXTF_8afV4,26617
37
+ simo/core/controllers.py,sha256=7M28j0I2Eh-Q7jIXZ7FMkNQoA7xluu67NSXhJXaW4gs,27018
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
@@ -48,7 +48,7 @@ simo/core/permissions.py,sha256=UmFjGPDWtAUbaWxJsWORb2q6BREHqndv9mkSIpnmdLk,1379
48
48
  simo/core/routing.py,sha256=X1_IHxyA-_Q7hw1udDoviVP4_FSBDl8GYETTC2zWTbY,499
49
49
  simo/core/serializers.py,sha256=bkfXZgUzbXZOrJY69VIevBHNLWRd7DmgyFRh4arr-gs,15810
50
50
  simo/core/signal_receivers.py,sha256=EZ8NSYZxUgSaLS16YZdK7T__l8dl0joMRllOxx5PUt4,2809
51
- simo/core/socket_consumers.py,sha256=R8zOkbZvsNf19NU9gz-2HzbTzpocb-j-jTGoC2KZxak,9488
51
+ simo/core/socket_consumers.py,sha256=n7VE2Fvqt4iEAYLTRbTPOcI-7tszMAADu7gimBxB-Fg,9635
52
52
  simo/core/storage.py,sha256=YlxmdRs-zhShWtFKgpJ0qp2NDBuIkJGYC1OJzqkbttQ,572
53
53
  simo/core/tasks.py,sha256=se27V-noW02v4ZY2PMv0AJkXNsY3NtJ4G43__KLW7Kg,11005
54
54
  simo/core/todos.py,sha256=eYVXfLGiapkxKK57XuviSNe3WsUYyIWZ0hgQJk7ThKo,665
@@ -10110,7 +10110,7 @@ simo/core/utils/form_fields.py,sha256=UOzYdPd71qgCw1H3qH01u85YjrOlETPJAHOJrZKhyD
10110
10110
  simo/core/utils/form_widgets.py,sha256=Zxn9jJqPle9Q_BKNJnyTDn7MosYwNp1TFu5LoKs0bfc,408
10111
10111
  simo/core/utils/formsets.py,sha256=1u34QGZ2P67cxZD2uUJS3lAf--E8XsiiqFmZ4P41Vw4,6463
10112
10112
  simo/core/utils/helpers.py,sha256=TOWy3slspaEYEhe9zDcb0RgzHUYslF6LZDlrWPGSqUI,3791
10113
- simo/core/utils/logs.py,sha256=JBUHhnm9Cn81csrQ4xHbujBFRL9YWulMwUj_zJPNvyw,1057
10113
+ simo/core/utils/logs.py,sha256=Zn9JQxqCH9Odx2J1BWT84nFCfkJ4Z4p5X8psdll7hNc,2366
10114
10114
  simo/core/utils/mixins.py,sha256=X6kUPKAi_F-uw7tgm8LEaYalBXpvDA-yrLNFCGr2rks,259
10115
10115
  simo/core/utils/model_helpers.py,sha256=3IzJeOvBoYdUJVXCJkY20npOZXPjNPAiEFvuT0OPhwA,884
10116
10116
  simo/core/utils/relay.py,sha256=i1xy_nPTgY5Xn0l2W4lNI3xeVUpDQTUUfV3M8h2DeBg,457
@@ -10120,18 +10120,18 @@ simo/core/utils/validators.py,sha256=FRO6_K5HAO1OaC-LosApZjh-W3EA-IJZ53HnwEJgqiI
10120
10120
  simo/core/utils/__pycache__/api.cpython-38.pyc,sha256=CuJq9GKQC8gbeCxmH2wQHZUmkIihVILSEIAoVYCFwH0,1575
10121
10121
  simo/core/utils/__pycache__/serialization.cpython-38.pyc,sha256=zOo2M97bAC0Ed-iiNoVtcHvgdAYR8RwrF2bUwuf8Pus,1145
10122
10122
  simo/fleet/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10123
- simo/fleet/admin.py,sha256=sN16en2sKYbApCHkitjOq6Fc5xagBTRSQ8tNCiVa1r0,5317
10123
+ simo/fleet/admin.py,sha256=Vxp3yTNRVoDif_hwwBW3weeMNakQ3iZo-GC0VvdScfQ,5699
10124
10124
  simo/fleet/api.py,sha256=Hxn84xI-Q77HxjINgRbjSJQOv9jii4OL20LxK0VSrS8,2499
10125
10125
  simo/fleet/auto_urls.py,sha256=gAXTWUvsWkQHRdZGM_W_5iJBEsM4lY063kIx3f5LUqs,578
10126
10126
  simo/fleet/ble.py,sha256=eHA_9ABjbmH1vUVCv9hiPXQL2GZZSEVwfO0xyI1S0nI,1081
10127
- simo/fleet/controllers.py,sha256=ELYfj2o1xrceg2tcprv6ofHA4WtYaWrFmgiIAthnCSw,13885
10127
+ simo/fleet/controllers.py,sha256=N8Qzdp2RPFrpZ_l9O4u8VjHWoY_WTWGg76s3V3oJqEs,13999
10128
10128
  simo/fleet/forms.py,sha256=UGj1mK2Zbl2LRlvLtEDObeGfC2wcuHleRbePo1_Vx6I,34972
10129
10129
  simo/fleet/gateways.py,sha256=xFsmF_SXYXK_kMJOCHkiInPJ_0VcPWz-kJDoMup2lT8,1576
10130
10130
  simo/fleet/managers.py,sha256=kpfvvfdH4LDxddIBDpdAb5gsVk8Gb0-L9biFcj9OFPs,807
10131
- simo/fleet/models.py,sha256=iLIdmcWTr_j8R7wTHph3fAExek8UH4S1HJyJ-asTNgw,12420
10131
+ simo/fleet/models.py,sha256=Ro0ZkYB3a7ZhczVQOxjAobCRECIdN0Nj0yb5EBybvW0,12809
10132
10132
  simo/fleet/routing.py,sha256=cofGsVWXMfPDwsJ6HM88xxtRxHwERhJ48Xyxc8mxg5o,149
10133
- simo/fleet/serializers.py,sha256=lVxqb4ldmJ5bUIklqeet5AIQak2Fbp_Tx5uKcP0eqmQ,1339
10134
- simo/fleet/socket_consumers.py,sha256=HbkrV0i1TwBC38otu_2lzN6IlBdyZHVdXIVhU4y-YCM,18872
10133
+ simo/fleet/serializers.py,sha256=zEpXAXxjk4Rf1JhlNnLTrs20qJggqjvIySbeHVo4Tt4,1505
10134
+ simo/fleet/socket_consumers.py,sha256=o8yr27AYxKFStQGyXZrk7PP1P2fUgSjsWp76DojWqbM,19415
10135
10135
  simo/fleet/utils.py,sha256=D0EGFbDmW8zyhyxf5ozGtRpo4Sy5Ov6ZixukBK_e2Do,3462
10136
10136
  simo/fleet/views.py,sha256=PbdZpsM_7-oyKzuDX1A5WULNABA1_B7ISF70UJX97FE,1662
10137
10137
  simo/fleet/__pycache__/__init__.cpython-38.pyc,sha256=pIZE7EL6-cuJ3pQtaSwjKLrKLsTYelp1k9sRhXKLh6s,159
@@ -10175,7 +10175,7 @@ simo/fleet/migrations/0023_colonel_is_authorized.py,sha256=IoyPUR45axv9V6QsntPTj
10175
10175
  simo/fleet/migrations/0024_colonel_pwm_frequency.py,sha256=nfTDs3GeIEkkiuQMB3_tc8TuyZSNOdQ2KhnJKTtQ9XE,498
10176
10176
  simo/fleet/migrations/0025_auto_20240130_1334.py,sha256=dHcQnlF7LBp6ikx_s5AB_I19sQ-iMB8XJ1X1yoWVvBs,825
10177
10177
  simo/fleet/migrations/0026_rename_i2cinterface_scl_pin_and_more.py,sha256=SEQxhgFsxWaUmfXaMOHHj0q8EqF9bt9PTOVuVa6wrQs,2752
10178
- simo/fleet/migrations/0027_auto_20240306_0802.py,sha256=ZWttZ54aoSrLBliHOiF9RL380gdNSlCEzlGJLX7fUnk,5951
10178
+ simo/fleet/migrations/0027_auto_20240306_0802.py,sha256=kOtLlCbbkqZu1zwzNZyA5rE_0TifF7tURdLCu_hzTLE,5972
10179
10179
  simo/fleet/migrations/0028_remove_i2cinterface_scl_pin_no_and_more.py,sha256=CeoFQPIu3jb4Gtyu5FhRjhl_mKhowMoEdtFU1oWD4UE,450
10180
10180
  simo/fleet/migrations/0029_alter_i2cinterface_scl_pin_and_more.py,sha256=aquymc1ZcUVgdrqZxl3viN6kiCHtw4HjgL6n4d0cSpQ,886
10181
10181
  simo/fleet/migrations/0030_colonelpin_label_alter_colonel_type.py,sha256=5T5bmQxPZrG0UseCLd7ssV-AeFF3O4T_DFxLu3whSqg,706
@@ -10217,9 +10217,9 @@ simo/generic/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10217
10217
  simo/generic/app_widgets.py,sha256=E_pnpA1hxMIhenRCrHoQ5cik06jm2BAHCkl_eo-OudU,1264
10218
10218
  simo/generic/base_types.py,sha256=djymox_boXTHX1BTTCLXrCH7ED-uAsV_idhaDOc3OLI,409
10219
10219
  simo/generic/controllers.py,sha256=Fs95mRBJgT5ZNMnxxwQJ_94TJ7Y_O0IygWSgfjLZzU0,51912
10220
- simo/generic/forms.py,sha256=KVGxTOqlqN9qSMrVBF2Mz2U39xZ_4s5ULHFprvAttb4,19959
10220
+ simo/generic/forms.py,sha256=HYctQMudlMpQRxkI67C6NShaM1dAzo0J-qbu-AFpozU,20194
10221
10221
  simo/generic/gateways.py,sha256=aHU0mA0ADNVzw3EXb8paXI5DI-gNd4CrGtaV5qDhILY,15499
10222
- simo/generic/models.py,sha256=-y3DQmD1k5uGB4cwchrE2PL61217BL6quCjkUWV3x3k,3340
10222
+ simo/generic/models.py,sha256=d00Q-UXtt7mG9MdPpZ3mXnxKjwlTI2FgCEklZpGBk7s,3629
10223
10223
  simo/generic/routing.py,sha256=elQVZmgnPiieEuti4sJ7zITk1hlRxpgbotcutJJgC60,228
10224
10224
  simo/generic/socket_consumers.py,sha256=NfTQGYtVAc864IoogZRxf_0xpDPM0eMCWn0SlKA5P7Y,1751
10225
10225
  simo/generic/static/weather_icons/01d@2x.png,sha256=TZfWi6Rfddb2P-oldWWcjUiuCHiU9Yrc5hyrQAhF26I,948
@@ -10266,7 +10266,7 @@ simo/notifications/admin.py,sha256=y_gmHYXbDh98LUUa-lp9DilTIgM6-pIujWPQPLQsJo8,8
10266
10266
  simo/notifications/api.py,sha256=GXQpq68ULBaJpU8w3SJKaCKuxYGWYehKnGeocGB1RVc,1783
10267
10267
  simo/notifications/models.py,sha256=VZcvweii59j89nPKlWeUSJ44Qz3ZLjJ6mXN6uB9F1Sw,2506
10268
10268
  simo/notifications/serializers.py,sha256=altDEAPWwOhxRcEzE9-34jL8EFpyf3vPoEdAPoVLfGc,523
10269
- simo/notifications/utils.py,sha256=M2UFufqAWD64kHw0rUDIr4nJ1ZkHd47sG_TVa4j918s,907
10269
+ simo/notifications/utils.py,sha256=5CtKOvX0vbWLXFvJD_8WfWulMEp1FuMwrfCGDLHySdA,907
10270
10270
  simo/notifications/migrations/0001_initial.py,sha256=Zh69AQ-EKlQKfqfnMDVRcxvo1MxRY-TFLCdnNcgqi6g,2003
10271
10271
  simo/notifications/migrations/0002_notification_instance.py,sha256=B3msbMeKvsuq-V7gvRADRjj5PFLayhi3pQvHZjqzO5g,563
10272
10272
  simo/notifications/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -10343,8 +10343,8 @@ simo/users/templates/invitations/expired_msg.html,sha256=47DEQpj8HBSa-_TImW-5JCe
10343
10343
  simo/users/templates/invitations/expired_suggestion.html,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10344
10344
  simo/users/templates/invitations/taken_msg.html,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10345
10345
  simo/users/templates/invitations/taken_suggestion.html,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10346
- simo-2.0.1.dist-info/LICENSE.md,sha256=M7wm1EmMGDtwPRdg7kW4d00h1uAXjKOT3HFScYQMeiE,34916
10347
- simo-2.0.1.dist-info/METADATA,sha256=UNG_zOqdwai9Z5jFbVFDrXcVozRzzHrASc4hdMZgKQM,1669
10348
- simo-2.0.1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
10349
- simo-2.0.1.dist-info/top_level.txt,sha256=GmS1hrAbpVqn9OWZh6UX82eIOdRLgYA82RG9fe8v4Rs,5
10350
- simo-2.0.1.dist-info/RECORD,,
10346
+ simo-2.0.3.dist-info/LICENSE.md,sha256=M7wm1EmMGDtwPRdg7kW4d00h1uAXjKOT3HFScYQMeiE,34916
10347
+ simo-2.0.3.dist-info/METADATA,sha256=kIgoVnqVIeZ9Paw4f7G6PA41Nwe3dafpPmpEbRV5JU4,1669
10348
+ simo-2.0.3.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
10349
+ simo-2.0.3.dist-info/top_level.txt,sha256=GmS1hrAbpVqn9OWZh6UX82eIOdRLgYA82RG9fe8v4Rs,5
10350
+ simo-2.0.3.dist-info/RECORD,,
File without changes