simo 2.11.4__py3-none-any.whl → 3.0.4__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 (91) hide show
  1. simo/__pycache__/settings.cpython-312.pyc +0 -0
  2. simo/asgi.py +25 -6
  3. simo/automation/__pycache__/controllers.cpython-312.pyc +0 -0
  4. simo/automation/controllers.py +18 -2
  5. simo/automation/forms.py +15 -24
  6. simo/automation/gateways.py +32 -16
  7. simo/core/__pycache__/admin.cpython-312.pyc +0 -0
  8. simo/core/__pycache__/base_types.cpython-312.pyc +0 -0
  9. simo/core/__pycache__/controllers.cpython-312.pyc +0 -0
  10. simo/core/__pycache__/forms.cpython-312.pyc +0 -0
  11. simo/core/__pycache__/models.cpython-312.pyc +0 -0
  12. simo/core/__pycache__/serializers.cpython-312.pyc +0 -0
  13. simo/core/__pycache__/signal_receivers.cpython-312.pyc +0 -0
  14. simo/core/__pycache__/tasks.cpython-312.pyc +0 -0
  15. simo/core/admin.py +5 -4
  16. simo/core/base_types.py +191 -18
  17. simo/core/controllers.py +259 -26
  18. simo/core/forms.py +10 -2
  19. simo/core/management/_hub_template/hub/nginx.conf +23 -50
  20. simo/core/management/_hub_template/hub/supervisor.conf +15 -0
  21. simo/core/mcp.py +154 -0
  22. simo/core/migrations/0051_instance_ai_memory.py +18 -0
  23. simo/core/migrations/__pycache__/0051_instance_ai_memory.cpython-312.pyc +0 -0
  24. simo/core/models.py +3 -0
  25. simo/core/serializers.py +120 -0
  26. simo/core/signal_receivers.py +1 -1
  27. simo/core/tasks.py +1 -3
  28. simo/core/utils/__pycache__/type_constants.cpython-312.pyc +0 -0
  29. simo/core/utils/type_constants.py +78 -17
  30. simo/fleet/__pycache__/admin.cpython-312.pyc +0 -0
  31. simo/fleet/__pycache__/api.cpython-312.pyc +0 -0
  32. simo/fleet/__pycache__/base_types.cpython-312.pyc +0 -0
  33. simo/fleet/__pycache__/controllers.cpython-312.pyc +0 -0
  34. simo/fleet/__pycache__/forms.cpython-312.pyc +0 -0
  35. simo/fleet/__pycache__/gateways.cpython-312.pyc +0 -0
  36. simo/fleet/__pycache__/models.cpython-312.pyc +0 -0
  37. simo/fleet/__pycache__/serializers.cpython-312.pyc +0 -0
  38. simo/fleet/admin.py +5 -1
  39. simo/fleet/api.py +2 -27
  40. simo/fleet/base_types.py +35 -4
  41. simo/fleet/controllers.py +162 -156
  42. simo/fleet/forms.py +58 -88
  43. simo/fleet/gateways.py +8 -15
  44. simo/fleet/migrations/0055_colonel_is_vo_active_colonel_last_wake_and_more.py +28 -0
  45. simo/fleet/migrations/0056_delete_customdalidevice.py +16 -0
  46. simo/fleet/migrations/__pycache__/0055_colonel_is_vo_active_colonel_last_wake_and_more.cpython-312.pyc +0 -0
  47. simo/fleet/migrations/__pycache__/0056_delete_customdalidevice.cpython-312.pyc +0 -0
  48. simo/fleet/models.py +13 -72
  49. simo/fleet/serializers.py +1 -48
  50. simo/fleet/socket_consumers.py +100 -39
  51. simo/fleet/tasks.py +2 -22
  52. simo/fleet/voice_assistant.py +903 -0
  53. simo/generic/__pycache__/base_types.cpython-312.pyc +0 -0
  54. simo/generic/__pycache__/controllers.cpython-312.pyc +0 -0
  55. simo/generic/__pycache__/gateways.cpython-312.pyc +0 -0
  56. simo/generic/base_types.py +70 -10
  57. simo/generic/controllers.py +104 -17
  58. simo/generic/gateways.py +10 -10
  59. simo/mcp_server/__init__.py +0 -0
  60. simo/mcp_server/__pycache__/__init__.cpython-312.pyc +0 -0
  61. simo/mcp_server/__pycache__/admin.cpython-312.pyc +0 -0
  62. simo/mcp_server/__pycache__/models.cpython-312.pyc +0 -0
  63. simo/mcp_server/admin.py +18 -0
  64. simo/mcp_server/app.py +4 -0
  65. simo/mcp_server/auth.py +34 -0
  66. simo/mcp_server/dummy.py +22 -0
  67. simo/mcp_server/migrations/0001_initial.py +30 -0
  68. simo/mcp_server/migrations/0002_alter_instanceaccesstoken_date_expired.py +18 -0
  69. simo/mcp_server/migrations/0003_instanceaccesstoken_issuer.py +18 -0
  70. simo/mcp_server/migrations/__init__.py +0 -0
  71. simo/mcp_server/migrations/__pycache__/0001_initial.cpython-312.pyc +0 -0
  72. simo/mcp_server/migrations/__pycache__/0002_alter_instanceaccesstoken_date_expired.cpython-312.pyc +0 -0
  73. simo/mcp_server/migrations/__pycache__/0003_instanceaccesstoken_issuer.cpython-312.pyc +0 -0
  74. simo/mcp_server/migrations/__pycache__/__init__.cpython-312.pyc +0 -0
  75. simo/mcp_server/models.py +27 -0
  76. simo/mcp_server/server.py +60 -0
  77. simo/mcp_server/tasks.py +19 -0
  78. simo/multimedia/__pycache__/base_types.cpython-312.pyc +0 -0
  79. simo/multimedia/__pycache__/controllers.cpython-312.pyc +0 -0
  80. simo/multimedia/base_types.py +29 -4
  81. simo/multimedia/controllers.py +66 -19
  82. simo/settings.py +1 -0
  83. simo/users/__pycache__/utils.cpython-312.pyc +0 -0
  84. simo/users/utils.py +10 -0
  85. {simo-2.11.4.dist-info → simo-3.0.4.dist-info}/METADATA +11 -4
  86. {simo-2.11.4.dist-info → simo-3.0.4.dist-info}/RECORD +90 -64
  87. simo/fleet/custom_dali_operations.py +0 -287
  88. {simo-2.11.4.dist-info → simo-3.0.4.dist-info}/WHEEL +0 -0
  89. {simo-2.11.4.dist-info → simo-3.0.4.dist-info}/entry_points.txt +0 -0
  90. {simo-2.11.4.dist-info → simo-3.0.4.dist-info}/licenses/LICENSE.md +0 -0
  91. {simo-2.11.4.dist-info → simo-3.0.4.dist-info}/top_level.txt +0 -0
@@ -6,7 +6,6 @@ import traceback
6
6
  import sys
7
7
  import zlib
8
8
  import time
9
- from django.db import transaction
10
9
  from logging.handlers import RotatingFileHandler
11
10
  from django.utils import timezone
12
11
  from django.conf import settings
@@ -24,12 +23,11 @@ from simo.users.models import Fingerprint
24
23
  from .gateways import FleetGatewayHandler
25
24
  from .models import Colonel
26
25
  from .controllers import TTLock
26
+ from .voice_assistant import VoiceAssistantSession, VoiceAssistantArbitrator
27
27
 
28
28
 
29
29
  @capture_socket_errors
30
30
  class FleetConsumer(AsyncWebsocketConsumer):
31
-
32
-
33
31
  def __init__(self, *args, **kwargs):
34
32
  super().__init__(*args, **kwargs)
35
33
  self.colonel = None
@@ -37,18 +35,50 @@ class FleetConsumer(AsyncWebsocketConsumer):
37
35
  self.connected = False
38
36
  self.mqtt_client = None
39
37
  self.last_seen = 0
38
+ self._va = None
39
+ self._arb = None
40
40
 
41
41
 
42
42
  async def disconnect(self, code):
43
- print("Colonel %s socket disconnected!" % str(self.colonel))
43
+ try:
44
+ print(f"Colonel {self.colonel} socket disconnected! code={code}")
45
+ except Exception:
46
+ print("Colonel socket disconnected!")
44
47
  self.connected = False
45
48
  if self.mqtt_client:
46
49
  self.mqtt_client.loop_stop()
50
+ try:
51
+ if self._va and (self._va.active or self.colonel.is_vo_active):
52
+ await self._va._end_session(cloud_also=True)
53
+ elif getattr(self.colonel, 'is_vo_active', False):
54
+ def _save():
55
+ self.colonel.is_vo_active = False
56
+ self.colonel.save(update_fields=['is_vo_active'])
57
+ await sync_to_async(_save, thread_sensitive=True)()
58
+ try:
59
+ base = await sync_to_async(lambda: dynamic_settings.get('core__remote_http'), thread_sensitive=True)()
60
+ hub_uid = await sync_to_async(lambda: dynamic_settings['core__hub_uid'], thread_sensitive=True)()
61
+ hub_secret = await sync_to_async(lambda: dynamic_settings['core__hub_secret'], thread_sensitive=True)()
62
+ base = base or 'https://simo.io'
63
+ url = base.rstrip('/') + '/ai/finish-session/'
64
+ payload = {
65
+ 'hub_uid': hub_uid,
66
+ 'hub_secret': hub_secret,
67
+ 'instance_uid': self.instance.uid,
68
+ }
69
+ import requests
70
+ loop = asyncio.get_running_loop()
71
+ await loop.run_in_executor(None, lambda: requests.post(url, json=payload, timeout=5))
72
+ except Exception:
73
+ pass
74
+ except Exception:
75
+ pass
47
76
 
48
77
  def save_disconect():
49
78
  if self.colonel:
50
79
  self.colonel.socket_connected = False
51
- self.colonel.save(update_fields=['socket_connected'])
80
+ self.colonel.is_vo_active = False
81
+ self.colonel.save(update_fields=['socket_connected', 'is_vo_active'])
52
82
  await sync_to_async(save_disconect, thread_sensitive=True)()
53
83
 
54
84
 
@@ -102,9 +132,6 @@ class FleetConsumer(AsyncWebsocketConsumer):
102
132
  'last_seen': timezone.now(),
103
133
  'enabled': True
104
134
  }
105
- # !!!!! ATETION! !!!!!!!
106
- # update_or_create and get_or_create doesn't
107
- # provide reliable operation in socket/async environment.
108
135
  new = False
109
136
  colonel = Colonel.objects.filter(uid=headers['colonel-uid']).first()
110
137
  if not colonel:
@@ -138,6 +165,14 @@ class FleetConsumer(AsyncWebsocketConsumer):
138
165
  self.connected = True
139
166
 
140
167
  await self.log_colonel_connected()
168
+ def _reset_vo():
169
+ if self.colonel.is_vo_active:
170
+ self.colonel.is_vo_active = False
171
+ self.colonel.save(update_fields=['is_vo_active'])
172
+ try:
173
+ await sync_to_async(_reset_vo, thread_sensitive=True)()
174
+ except Exception:
175
+ pass
141
176
 
142
177
 
143
178
  def get_gateway():
@@ -167,18 +202,6 @@ class FleetConsumer(AsyncWebsocketConsumer):
167
202
  port=settings.MQTT_PORT)
168
203
  self.mqtt_client.loop_start()
169
204
 
170
- # DO NOT FORCE CONFIG DATA!!!!
171
- # as colonels might already have config and want to
172
- # send updated values of components, like for example
173
- # somebody turned some lights on/off while colonel was
174
- # not connected to the main hub.
175
- # If we force this, vales get overridden by what is last
176
- # known by the hub
177
- # config = await self.get_config_data()
178
- # await self.send_data(
179
- # 'command': 'set_config', 'data': config
180
- # })
181
-
182
205
  await self.send_data({'command': 'hello'})
183
206
 
184
207
  asyncio.create_task(self.watch_connection())
@@ -195,13 +218,15 @@ class FleetConsumer(AsyncWebsocketConsumer):
195
218
  self.colonel.minor_upgrade_available
196
219
  )
197
220
 
198
- # Default pinging system sometimes get's lost somewhere,
199
- # therefore we use our own to ensure connection and understand if
200
- # colonel is connected or not
201
-
202
221
  if time.time() - self.last_seen > 2:
203
222
  await self.send_data({'command': 'ping'})
204
223
 
224
+ try:
225
+ if self._va and self._va.active and (time.time() - self.last_seen) > 60:
226
+ await self._va._end_session(cloud_also=True)
227
+ except Exception:
228
+ pass
229
+
205
230
  await asyncio.sleep(2)
206
231
 
207
232
 
@@ -339,7 +364,7 @@ class FleetConsumer(AsyncWebsocketConsumer):
339
364
  config = await self.get_config_data()
340
365
  await self.send_data({
341
366
  'command': 'set_config', 'data': config
342
- }, compress=True)
367
+ }, compress=self.colonel.type != 'sentinel')
343
368
  asyncio.run(send_config())
344
369
  elif payload.get('command') == 'discover':
345
370
  print(f"SEND discover command for {payload['type']}")
@@ -385,7 +410,7 @@ class FleetConsumer(AsyncWebsocketConsumer):
385
410
  print("Send config: ", config)
386
411
  await self.send_data({
387
412
  'command': 'set_config', 'data': config
388
- }, compress=True)
413
+ }, compress=self.colonel.type != 'sentinel')
389
414
  elif 'comp' in data:
390
415
  try:
391
416
  try:
@@ -450,25 +475,64 @@ class FleetConsumer(AsyncWebsocketConsumer):
450
475
  process_discovery_result, thread_sensitive=True
451
476
  )()
452
477
 
453
- elif 'dali-raw' in data:
454
- from .custom_dali_operations import process_frame
455
- await sync_to_async(process_frame, thread_sensitive=True)(
456
- self.colonel.id, data['dali-raw'], data['data']
457
- )
458
478
 
479
+ elif 'va' in data and isinstance(data['va'], dict):
480
+ va = data['va']
481
+ if va.get('session') == 'finish':
482
+ if not self._va:
483
+ self._va = VoiceAssistantSession(self)
484
+ await self._va._end_session(cloud_also=True)
485
+
486
+ elif 'wake-stats' in data and self.colonel.type == 'sentinel':
487
+ def update_wake_stats():
488
+ va_component = Component.objects.filter(
489
+ config__colonel=self.colonel.id,
490
+ pk=data.get('id', 0)
491
+ ).select_related('zone').first()
492
+ self.colonel.wake_stats = data['wake-stats']
493
+ self.colonel.last_wake = timezone.now()
494
+ self.colonel.save()
495
+ return va_component
496
+ va_component = await sync_to_async(
497
+ update_wake_stats, thread_sensitive=True
498
+ )()
499
+ if not self._va:
500
+ self._va = VoiceAssistantSession(self)
501
+ self._va.voice = data.get('voice', 'male')
502
+ self._va.zone = va_component.zone.id
459
503
 
460
504
  elif bytes_data:
461
- if not self.colonel_logger:
462
- await self.start_logger()
463
-
464
- for logline in bytes_data.decode(errors='replace').split('\n'):
465
- self.colonel_logger.log(logging.INFO, logline)
505
+ if self.colonel.type == 'sentinel':
506
+ if bytes_data[0] == 32:
507
+ await self.capture_logs(bytes_data[1:])
508
+ else:
509
+ if not self._va:
510
+ self._va = VoiceAssistantSession(self)
511
+ if not self._arb:
512
+ self._arb = VoiceAssistantArbitrator(self, self._va)
513
+ await self._va.prewarm_on_first_audio()
514
+ if await self._arb.maybe_reject_busy():
515
+ return
516
+ self._arb.start_window_if_needed()
517
+ await self._va.on_audio_chunk(bytes_data[1:])
518
+ else:
519
+ if bytes_data[0] == 32:
520
+ await self.capture_logs(bytes_data[1:])
521
+ else:
522
+ await self.capture_logs(bytes_data)
466
523
 
467
524
  await self.log_colonel_connected()
468
525
  except Exception as e:
469
526
  print(traceback.format_exc(), file=sys.stderr)
470
527
 
471
528
 
529
+ async def capture_logs(self, bytes_data):
530
+ if not self.colonel_logger:
531
+ await self.start_logger()
532
+
533
+ for logline in bytes_data.decode(errors='replace').split('\n'):
534
+ self.colonel_logger.log(logging.INFO, logline)
535
+
472
536
 
473
537
  async def log_colonel_connected(self):
474
538
  self.last_seen = time.time()
@@ -513,6 +577,3 @@ class FleetConsumer(AsyncWebsocketConsumer):
513
577
  )
514
578
  file_handler.setFormatter(formatter)
515
579
  self.colonel_logger.addHandler(file_handler)
516
-
517
-
518
-
simo/fleet/tasks.py CHANGED
@@ -20,8 +20,7 @@ def check_colonels_connected():
20
20
  @celery_app.task
21
21
  def check_colonel_components_alive():
22
22
  from simo.core.models import Component
23
- from .gateways import FleetGatewayHandler
24
- from .models import Colonel, CustomDaliDevice
23
+ from .models import Colonel
25
24
  drop_current_instance()
26
25
  for lost_colonel in Colonel.objects.filter(
27
26
  last_seen__lt=timezone.now() - datetime.timedelta(seconds=60)
@@ -33,28 +32,9 @@ def check_colonel_components_alive():
33
32
  print(f"{comp} is no longer alive!")
34
33
  comp.alive = False
35
34
  comp.save()
36
- for interface in lost_colonel.interfaces.all():
37
- if interface.type == 'dali':
38
- for device in interface.custom_devices.all():
39
- for comp in Component.objects.filter(
40
- gateway__type=FleetGatewayHandler.uid,
41
- config__dali_device=device.id, alive=True
42
- ):
43
- comp.alive = False
44
- comp.save()
45
-
46
- for device in CustomDaliDevice.objects.filter(
47
- last_seen__gt=timezone.now() - datetime.timedelta(seconds=60)
48
- ):
49
- for comp in Component.objects.filter(
50
- gateway__type=FleetGatewayHandler.uid,
51
- config__dali_device=device.id, alive=True
52
- ):
53
- comp.alive = False
54
- comp.save()
55
35
 
56
36
 
57
37
  @celery_app.on_after_finalize.connect
58
38
  def setup_periodic_tasks(sender, **kwargs):
59
39
  sender.add_periodic_task(10, check_colonels_connected.s())
60
- sender.add_periodic_task(20, check_colonel_components_alive.s())
40
+ sender.add_periodic_task(20, check_colonel_components_alive.s())