simo 3.1.4__py3-none-any.whl → 3.1.6__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.

@@ -311,11 +311,19 @@ class AutomationsGatewayHandler(GatesHandler, BaseObjectCommandsGatewayHandler):
311
311
 
312
312
  from .controllers import Script
313
313
 
314
- mqtt_client = mqtt.Client()
315
- mqtt_client.username_pw_set('root', settings.SECRET_KEY)
316
- mqtt_client.on_connect = self.on_mqtt_connect
317
- mqtt_client.on_message = self.on_mqtt_message
318
- mqtt_client.connect(host=settings.MQTT_HOST, port=settings.MQTT_PORT)
314
+ self.mqtt_client = mqtt.Client()
315
+ self.mqtt_client.username_pw_set('root', settings.SECRET_KEY)
316
+ self.mqtt_client.on_connect = self.on_mqtt_connect
317
+ self.mqtt_client.on_message = self.on_mqtt_message
318
+ try:
319
+ self.mqtt_client.reconnect_delay_set(min_delay=1, max_delay=30)
320
+ except Exception:
321
+ pass
322
+ try:
323
+ # Avoid raising if broker restarts or is down at boot
324
+ self.mqtt_client.connect_async(host=settings.MQTT_HOST, port=settings.MQTT_PORT)
325
+ except Exception:
326
+ pass
319
327
 
320
328
  # We presume that this is the only running gateway, therefore
321
329
  # if there are any running scripts, that is not true.
@@ -337,9 +345,11 @@ class AutomationsGatewayHandler(GatesHandler, BaseObjectCommandsGatewayHandler):
337
345
  self.start_script(script)
338
346
 
339
347
  print("GATEWAY STARTED!")
348
+ self.mqtt_client.loop_start()
340
349
  while not exit.is_set():
341
- mqtt_client.loop()
342
- mqtt_client.disconnect()
350
+ time.sleep(1)
351
+ self.mqtt_client.loop_stop()
352
+ self.mqtt_client.disconnect()
343
353
 
344
354
  script_ids = [id for id in self.running_scripts.keys()]
345
355
  for id in script_ids:
@@ -1,19 +1,73 @@
1
1
  from django.contrib.gis.db.backends.postgis.base import (
2
2
  DatabaseWrapper as PostGisPsycopg2DatabaseWrapper
3
3
  )
4
- from django.db.utils import OperationalError, InterfaceError
4
+ """Custom PostGIS database wrapper with light self-healing.
5
+
6
+ We retry cursor creation on transient driver-level errors. Previously we
7
+ were catching Django's wrapper exceptions (django.db.utils.InterfaceError/
8
+ OperationalError). However, those are only raised by the outer
9
+ `wrap_database_errors` context. Inside `create_cursor()` the driver
10
+ (`psycopg2`) raises its own exceptions, so our except block never ran and
11
+ connections weren't healed.
12
+
13
+ By catching psycopg2 errors here (in addition to Django's), we ensure
14
+ we can close() and reconnect() this connection when `self.connection`
15
+ is already closed or becomes unusable, avoiding busy error loops in
16
+ MQTT threads and periodic tasks.
17
+ """
18
+
19
+ from django.db.utils import OperationalError as DjangoOperationalError, InterfaceError as DjangoInterfaceError
20
+ try:
21
+ # Catch driver-level errors where they originate.
22
+ from psycopg2 import OperationalError as PsycopgOperationalError, InterfaceError as PsycopgInterfaceError
23
+ except Exception: # pragma: no cover - very defensive
24
+ PsycopgOperationalError = PsycopgInterfaceError = Exception
5
25
  from django.utils.asyncio import async_unsafe
26
+ import time
27
+ import random
6
28
 
7
29
 
8
30
  class DatabaseWrapper(PostGisPsycopg2DatabaseWrapper):
9
31
  @async_unsafe
10
32
  def create_cursor(self, name=None):
33
+ """Create a DB cursor with a single, simple heal-once path.
34
+
35
+ - Fast path: return cursor immediately.
36
+ - On error: if it's a connectivity issue (known exceptions or
37
+ connection.closed set), close + backoff + reconnect, then retry once.
38
+ - Otherwise: re-raise the original exception.
39
+ """
11
40
  try:
12
41
  return super().create_cursor(name=name)
13
- except (InterfaceError, OperationalError):
14
- # Heal this very connection
15
- self.close()
16
- self.connect()
17
- return super().create_cursor(name=name)
42
+ except Exception as e:
43
+ # Determine if this is a connectivity problem
44
+ is_connectivity_err = isinstance(
45
+ e,
46
+ (
47
+ DjangoInterfaceError,
48
+ DjangoOperationalError,
49
+ PsycopgInterfaceError,
50
+ PsycopgOperationalError,
51
+ ),
52
+ )
53
+ if not is_connectivity_err:
54
+ try:
55
+ conn = getattr(self, 'connection', None)
56
+ is_connectivity_err = bool(getattr(conn, 'closed', 0))
57
+ except Exception:
58
+ is_connectivity_err = False
18
59
 
60
+ if not is_connectivity_err:
61
+ # Not a connection issue; bubble up unchanged
62
+ raise
19
63
 
64
+ # Heal this very connection and retry once
65
+ try:
66
+ self.close()
67
+ finally:
68
+ try:
69
+ time.sleep(0.05 + random.random() * 0.15) # 50–200 ms
70
+ except Exception:
71
+ pass
72
+ self.connect()
73
+ return super().create_cursor(name=name)
simo/core/events.py CHANGED
@@ -181,9 +181,21 @@ class OnChangeMixin:
181
181
  self._mqtt_client.username_pw_set('root', settings.SECRET_KEY)
182
182
  self._mqtt_client.on_connect = self.on_mqtt_connect
183
183
  self._mqtt_client.on_message = self.on_mqtt_message
184
- self._mqtt_client.connect(
185
- host=settings.MQTT_HOST, port=settings.MQTT_PORT
186
- )
184
+ # Be gentle when broker is down or connection flaps
185
+ try:
186
+ # paho 1.x supports reconnect backoff configuration
187
+ self._mqtt_client.reconnect_delay_set(min_delay=1, max_delay=30)
188
+ except Exception:
189
+ pass
190
+ try:
191
+ self._mqtt_client.connect_async(
192
+ host=settings.MQTT_HOST, port=settings.MQTT_PORT
193
+ )
194
+ except Exception:
195
+ # connect_async should not normally raise; in case of programming
196
+ # errors keep the API surface consistent by cleaning up
197
+ self._mqtt_client = None
198
+ raise
187
199
  self._mqtt_client.loop_start()
188
200
  self._on_change_function = function
189
201
  self._obj_ct_id = ContentType.objects.get_for_model(self).pk
simo/core/gateways.py CHANGED
@@ -63,6 +63,10 @@ class BaseObjectCommandsGatewayHandler(BaseGatewayHandler):
63
63
  self.mqtt_client.username_pw_set('root', settings.SECRET_KEY)
64
64
  self.mqtt_client.on_connect = self._on_mqtt_connect
65
65
  self.mqtt_client.on_message = self._on_mqtt_message
66
+ try:
67
+ self.mqtt_client.reconnect_delay_set(min_delay=1, max_delay=30)
68
+ except Exception:
69
+ pass
66
70
 
67
71
 
68
72
  def run(self, exit):
@@ -74,7 +78,12 @@ class BaseObjectCommandsGatewayHandler(BaseGatewayHandler):
74
78
  target=self._run_periodic_task, args=(self.exit, task, period), daemon=True
75
79
  ).start()
76
80
 
77
- self.mqtt_client.connect(host=settings.MQTT_HOST, port=settings.MQTT_PORT)
81
+ # Use async connect so we don't crash if broker is temporarily down
82
+ try:
83
+ self.mqtt_client.connect_async(host=settings.MQTT_HOST, port=settings.MQTT_PORT)
84
+ except Exception:
85
+ # connect_async shouldn't raise for normal scenarios; ignore just in case
86
+ pass
78
87
  self.mqtt_client.loop_start()
79
88
 
80
89
  while not self.exit.is_set():
@@ -137,4 +146,3 @@ class BaseObjectCommandsGatewayHandler(BaseGatewayHandler):
137
146
  self.perform_value_send(component, val)
138
147
  except Exception as e:
139
148
  self.logger.error(e, exc_info=True)
140
-
@@ -117,18 +117,28 @@ class GatewaysManager:
117
117
  self.mqtt_client.username_pw_set('root', settings.SECRET_KEY)
118
118
  self.mqtt_client.on_connect = self.on_mqtt_connect
119
119
  self.mqtt_client.on_message = self.on_mqtt_message
120
- self.mqtt_client.connect(
121
- host=settings.MQTT_HOST, port=settings.MQTT_PORT
122
- )
120
+ try:
121
+ self.mqtt_client.reconnect_delay_set(min_delay=1, max_delay=30)
122
+ except Exception:
123
+ pass
124
+ try:
125
+ self.mqtt_client.connect_async(
126
+ host=settings.MQTT_HOST, port=settings.MQTT_PORT
127
+ )
128
+ except Exception:
129
+ pass
123
130
 
131
+ self.mqtt_client.loop_start()
124
132
  while not self.exit_event.is_set():
125
- self.mqtt_client.loop()
133
+ time.sleep(1)
126
134
 
127
135
  ids_to_stop = [id for id in self.running_gateways.keys()]
128
136
  for id in ids_to_stop:
129
137
  self.stop_gateway(Gateway(id=id))
130
138
  while self.running_gateways.keys():
131
139
  time.sleep(0.3)
140
+ self.mqtt_client.loop_stop()
141
+ self.mqtt_client.disconnect()
132
142
  close_old_connections()
133
143
  print("-------------Gateways Manager STOPPED.------------------")
134
144
  return sys.exit()
simo/generic/gateways.py CHANGED
@@ -297,11 +297,19 @@ class GenericGatewayHandler(
297
297
 
298
298
  from simo.generic.controllers import IPCamera
299
299
 
300
- mqtt_client = mqtt.Client()
301
- mqtt_client.username_pw_set('root', settings.SECRET_KEY)
302
- mqtt_client.on_connect = self.on_mqtt_connect
303
- mqtt_client.on_message = self.on_mqtt_message
304
- mqtt_client.connect(host=settings.MQTT_HOST, port=settings.MQTT_PORT)
300
+ # Use non-blocking MQTT loop to avoid busy-spin when broker is down
301
+ self.mqtt_client = mqtt.Client()
302
+ self.mqtt_client.username_pw_set('root', settings.SECRET_KEY)
303
+ self.mqtt_client.on_connect = self.on_mqtt_connect
304
+ self.mqtt_client.on_message = self.on_mqtt_message
305
+ try:
306
+ self.mqtt_client.reconnect_delay_set(min_delay=1, max_delay=30)
307
+ except Exception:
308
+ pass
309
+ try:
310
+ self.mqtt_client.connect_async(host=settings.MQTT_HOST, port=settings.MQTT_PORT)
311
+ except Exception:
312
+ pass
305
313
 
306
314
  for cam in Component.objects.filter(
307
315
  controller_uid=IPCamera.uid
@@ -315,9 +323,11 @@ class GenericGatewayHandler(
315
323
  ).start()
316
324
 
317
325
  print("GATEWAY STARTED!")
326
+ self.mqtt_client.loop_start()
318
327
  while not exit.is_set():
319
- mqtt_client.loop()
320
- mqtt_client.disconnect()
328
+ time.sleep(1)
329
+ self.mqtt_client.loop_stop()
330
+ self.mqtt_client.disconnect()
321
331
 
322
332
 
323
333
  def on_mqtt_connect(self, mqtt_client, userdata, flags, rc):
@@ -425,8 +435,14 @@ class GenericGatewayHandler(
425
435
  base_type='binary-sensor', alarm_category='security'
426
436
  ):
427
437
  if sensor.id not in self.sensors_on_watch[state.id]:
428
- self.sensors_on_watch[state.id][sensor.id] = i_id
429
- sensor.on_change(self.security_sensor_change)
438
+ # Register callback only when MQTT subscription succeeds
439
+ try:
440
+ sensor.on_change(self.security_sensor_change)
441
+ except Exception:
442
+ # Leave it untracked so we retry on next tick
443
+ raise
444
+ else:
445
+ self.sensors_on_watch[state.id][sensor.id] = i_id
430
446
 
431
447
  if state.controller._check_is_away(self.last_sensor_actions.get(i_id, 0)):
432
448
  if state.value != 'away':
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simo
3
- Version: 3.1.4
3
+ Version: 3.1.6
4
4
  Summary: Smart Home Supremacy
5
5
  Author-email: "Simon V." <simon@simo.io>
6
6
  Project-URL: Homepage, https://simo.io
@@ -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=jtoG91nYlUPW-Pkl025bBoGELWFm8SQD3duTPsjRhfk,12230
23
23
  simo/automation/forms.py,sha256=NAQUS9qrXp2d4GuvdpVyWj0Yh7vqMyX6pzs5oX4ze5Y,9800
24
- simo/automation/gateways.py,sha256=SGq91nr2K1i8aSFh0XTZDHBb7TzFU_X24-wkt8IQkeM,17406
24
+ simo/automation/gateways.py,sha256=WhZNliJhpuf8FnHGqIaWXzW3qcac5AtqpU9THPYYgw4,17770
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=Pg-hMaASQPB5_BTAMkfqM6z4jdHWH8xMYWOvDxIvmx8,2126
@@ -104,11 +104,11 @@ simo/core/base_types.py,sha256=FNIS9Y7wmdbVl-dISLdSBYvMEiV4zSLpBOBDYOVyam0,6580
104
104
  simo/core/context.py,sha256=LKw1I4iIRnlnzoTCuSLLqDX7crHdBnMo3hjqYvVmzFc,1557
105
105
  simo/core/controllers.py,sha256=jGn9bDvUuAbFL_uCAV8aqXgDJz9akYDroJuyU0ONsC8,48563
106
106
  simo/core/dynamic_settings.py,sha256=bUs58XEZOCIEhg1TigR3LmYggli13KMryBZ9pC7ugAQ,1872
107
- simo/core/events.py,sha256=GQwolnqO4ZoxgyVi9C4eKXpMULD3gMstGJsxWhc1TWU,6172
107
+ simo/core/events.py,sha256=mjrYsByw0hGct4rXoLmnbBodViN8S1ReBIZG3g660Yo,6735
108
108
  simo/core/filters.py,sha256=6wbn8C2WvKTTjtfMwwLBp2Fib1V0-DMpS4iqJd6jJQo,2540
109
109
  simo/core/form_fields.py,sha256=b4wZ4n7OO0m0_BPPS9ILVrwBvhhjUB079YrroveFUWA,5222
110
110
  simo/core/forms.py,sha256=IwuljCxwqPcSn3eXJRF2pWjktvlQ4DH2LCsncHzCHw0,22720
111
- simo/core/gateways.py,sha256=ik9C9L0Z_pTuWXMDXCW09MuTgb6IGdMPn8uVQAJVw5I,4377
111
+ simo/core/gateways.py,sha256=cM_du3VsHbSYUSWL5JHxXMV8yn6s-QrSzB3WreglGjw,4736
112
112
  simo/core/loggers.py,sha256=EBdq23gTQScVfQVH-xeP90-wII2DQFDjoROAW6ggUP4,1645
113
113
  simo/core/managers.py,sha256=Ampwe5K7gfE6IJULNCV35V8ysmMOdS_wz7mRzfaLZUw,3014
114
114
  simo/core/mcp.py,sha256=MDx_m6BmkYDxCrfFegchz6NMCDCB0Mrbjx4gb2iJxHU,5188
@@ -191,7 +191,7 @@ simo/core/__pycache__/views.cpython-38.pyc,sha256=kYKvEcyKicdkTcN0iEJ4pT101-KHiZ
191
191
  simo/core/__pycache__/widgets.cpython-312.pyc,sha256=f_AiYFfA7Wl4Wm9FqUOlluSGLAN_nkLyDUsKpkScsXg,4447
192
192
  simo/core/__pycache__/widgets.cpython-38.pyc,sha256=sR0ZeHCHrhnNDBJuRrxp3zUsfBp0xrtF0xrK2TkQv1o,3520
193
193
  simo/core/db_backend/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
194
- simo/core/db_backend/base.py,sha256=4vl0Nx8PcBG5C8QQpAQPFu-TKH8Hs6R5OmzJqIiLibQ,598
194
+ simo/core/db_backend/base.py,sha256=fGagw119bvP3Ns3NFvIDlrlNSa9-LLt2HMuIjnB9h9g,2933
195
195
  simo/core/db_backend/__pycache__/__init__.cpython-312.pyc,sha256=Brq4UBCggJrt590OFV3VmgARJYRK902FnnFFFK_Azng,133
196
196
  simo/core/db_backend/__pycache__/__init__.cpython-38.pyc,sha256=sxC6PmFqnwe6RRKzSR7QtUFzRc_aRRR1fffsR3TPLRc,169
197
197
  simo/core/db_backend/__pycache__/base.cpython-312.pyc,sha256=ykKrTOC28v2cl0C--bqeLEaydHS5CG1RFGL0lon53HU,1135
@@ -296,7 +296,7 @@ simo/core/management/_hub_template/hub/__pycache__/settings.cpython-312.pyc,sha2
296
296
  simo/core/management/_hub_template/hub/__pycache__/urls.cpython-312.pyc,sha256=dNFa6cLiHRIB5Efyaxd9SmC8hPbMkpvp8zvtkiwt1hc,178
297
297
  simo/core/management/_hub_template/hub/__pycache__/wsgi.cpython-312.pyc,sha256=oA9duba-bsc_OOQnUaz_qiaB-0OcsS5dfjSVHTscrUM,529
298
298
  simo/core/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
299
- simo/core/management/commands/gateways_manager.py,sha256=oHzgC-eV4w_KHkiz6eCAlt3XMIwtS8_7mHDQ4UsU5y0,6967
299
+ simo/core/management/commands/gateways_manager.py,sha256=_PlFQ40rtqML38RC0U_KFMbqHma-ACNgEQ_iUwvwUBs,7276
300
300
  simo/core/management/commands/on_http_start.py,sha256=kxtWB3lOSyQA8tVmZRmLmaoVsliy5mhZp9_wwCvkj_A,6279
301
301
  simo/core/management/commands/republish_mqtt_state.py,sha256=mc8e7qnhDyC9_fiYr6d0g2s_3wGUXCrLo95-HBIrkOA,2248
302
302
  simo/core/management/commands/run_app_mqtt_control.py,sha256=-eDDAXbGtjqN1OK7Ym2FIdP1wVNJYR0DdvIyRzmXPDc,5592
@@ -10675,7 +10675,7 @@ simo/generic/app_widgets.py,sha256=y8W3jR76Hh26O9pPQyg2SophMbYIOtAWD33MPKbB8Mg,8
10675
10675
  simo/generic/base_types.py,sha256=gJUJYpd_gE-f1ogzagAPA1u2TYljhyU0_SMlgGUvCVk,2318
10676
10676
  simo/generic/controllers.py,sha256=oQ7mLYU-MxO4lpCQEGJA0ClIw253Ayzx3pOAFbkqC_4,53273
10677
10677
  simo/generic/forms.py,sha256=0RIDtLLzCkiSb9OxlioicOQW9yp1OjLKekpjbxzGVfM,26272
10678
- simo/generic/gateways.py,sha256=mnr4nAwLLESI50cwz1-LeeaLt0ABOt8s0D2_U-NgRsc,21586
10678
+ simo/generic/gateways.py,sha256=HWLwr9hmZPNbMRFiKtsjYhKxLPHlTqoIUBCmFD0JB08,22234
10679
10679
  simo/generic/models.py,sha256=59fkYowOX0imviIhA6uwupvuharrpBykmBm674rJNoI,7279
10680
10680
  simo/generic/routing.py,sha256=elQVZmgnPiieEuti4sJ7zITk1hlRxpgbotcutJJgC60,228
10681
10681
  simo/generic/socket_consumers.py,sha256=qesKZVhI56Kh7vdIUDD3hzDUi0FcXwIfcmE_a3YS6JQ,1772
@@ -11034,9 +11034,9 @@ simo/users/templates/invitations/expired_msg.html,sha256=47DEQpj8HBSa-_TImW-5JCe
11034
11034
  simo/users/templates/invitations/expired_suggestion.html,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11035
11035
  simo/users/templates/invitations/taken_msg.html,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11036
11036
  simo/users/templates/invitations/taken_suggestion.html,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11037
- simo-3.1.4.dist-info/licenses/LICENSE.md,sha256=M7wm1EmMGDtwPRdg7kW4d00h1uAXjKOT3HFScYQMeiE,34916
11038
- simo-3.1.4.dist-info/METADATA,sha256=fdQuK5JIjFNaNn7FMY7R2vbKhlMuFf282kuWRWawPhc,2224
11039
- simo-3.1.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
11040
- simo-3.1.4.dist-info/entry_points.txt,sha256=S9PwnUYmTSW7681GKDCxUbL0leRJIaRk6fDQIKgbZBA,135
11041
- simo-3.1.4.dist-info/top_level.txt,sha256=GmS1hrAbpVqn9OWZh6UX82eIOdRLgYA82RG9fe8v4Rs,5
11042
- simo-3.1.4.dist-info/RECORD,,
11037
+ simo-3.1.6.dist-info/licenses/LICENSE.md,sha256=M7wm1EmMGDtwPRdg7kW4d00h1uAXjKOT3HFScYQMeiE,34916
11038
+ simo-3.1.6.dist-info/METADATA,sha256=3ZPTUNXT0K8Vdj8pNXmdll7TIhFDbYpKHtByuNk1Kio,2224
11039
+ simo-3.1.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
11040
+ simo-3.1.6.dist-info/entry_points.txt,sha256=S9PwnUYmTSW7681GKDCxUbL0leRJIaRk6fDQIKgbZBA,135
11041
+ simo-3.1.6.dist-info/top_level.txt,sha256=GmS1hrAbpVqn9OWZh6UX82eIOdRLgYA82RG9fe8v4Rs,5
11042
+ simo-3.1.6.dist-info/RECORD,,
File without changes