simo 2.5.41__py3-none-any.whl → 2.6.2__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 (71) hide show
  1. simo/__pycache__/settings.cpython-38.pyc +0 -0
  2. simo/automation/__pycache__/__init__.cpython-38.pyc +0 -0
  3. simo/automation/__pycache__/app_widgets.cpython-38.pyc +0 -0
  4. simo/automation/__pycache__/controllers.cpython-38.pyc +0 -0
  5. simo/automation/__pycache__/forms.cpython-38.pyc +0 -0
  6. simo/automation/__pycache__/gateways.cpython-38.pyc +0 -0
  7. simo/automation/__pycache__/helpers.cpython-38.pyc +0 -0
  8. simo/{generic/scripting → automation}/__pycache__/serializers.cpython-38.pyc +0 -0
  9. simo/{generic/scripting/__pycache__/__init__.cpython-38.pyc → automation/__pycache__/state.cpython-38.pyc} +0 -0
  10. simo/automation/app_widgets.py +8 -0
  11. simo/automation/controllers.py +273 -0
  12. simo/automation/forms.py +290 -0
  13. simo/automation/gateways.py +257 -0
  14. simo/automation/migrations/0001_initial.py +39 -0
  15. simo/automation/migrations/0002_update_helpers_in_scripts.py +29 -0
  16. simo/automation/migrations/__init__.py +0 -0
  17. simo/automation/migrations/__pycache__/0001_initial.cpython-38.pyc +0 -0
  18. simo/automation/migrations/__pycache__/0002_update_helpers_in_scripts.cpython-38.pyc +0 -0
  19. simo/automation/migrations/__pycache__/__init__.cpython-38.pyc +0 -0
  20. simo/automation/templates/automations/auto_away.py +55 -0
  21. simo/automation/templates/automations/auto_state_script.py +31 -0
  22. simo/{core/templates/core/auto_night_day_script.py → automation/templates/automations/phones_sleep_script.py} +25 -13
  23. simo/core/__pycache__/admin.cpython-38.pyc +0 -0
  24. simo/core/__pycache__/api.cpython-38.pyc +0 -0
  25. simo/core/__pycache__/filters.cpython-38.pyc +0 -0
  26. simo/core/__pycache__/models.cpython-38.pyc +0 -0
  27. simo/core/__pycache__/signal_receivers.cpython-38.pyc +0 -0
  28. simo/core/__pycache__/tasks.cpython-38.pyc +0 -0
  29. simo/core/admin.py +7 -4
  30. simo/core/api.py +8 -1
  31. simo/core/filters.py +61 -0
  32. simo/core/management/_hub_template/hub/supervisor.conf +0 -1
  33. simo/core/signal_receivers.py +50 -17
  34. simo/core/utils/__pycache__/type_constants.cpython-38.pyc +0 -0
  35. simo/core/utils/type_constants.py +1 -1
  36. simo/fleet/__pycache__/api.cpython-38.pyc +0 -0
  37. simo/fleet/__pycache__/controllers.cpython-38.pyc +0 -0
  38. simo/fleet/__pycache__/forms.cpython-38.pyc +0 -0
  39. simo/fleet/__pycache__/serializers.cpython-38.pyc +0 -0
  40. simo/fleet/api.py +6 -0
  41. simo/fleet/controllers.py +1 -0
  42. simo/fleet/forms.py +22 -3
  43. simo/fleet/serializers.py +9 -1
  44. simo/generic/__pycache__/app_widgets.cpython-38.pyc +0 -0
  45. simo/generic/__pycache__/controllers.cpython-38.pyc +0 -0
  46. simo/generic/__pycache__/forms.cpython-38.pyc +0 -0
  47. simo/generic/__pycache__/gateways.cpython-38.pyc +0 -0
  48. simo/generic/app_widgets.py +0 -6
  49. simo/generic/controllers.py +4 -260
  50. simo/generic/forms.py +2 -269
  51. simo/generic/gateways.py +4 -193
  52. simo/generic/migrations/0002_auto_20241126_0726.py +34 -0
  53. simo/generic/migrations/__pycache__/0002_auto_20241126_0726.cpython-38.pyc +0 -0
  54. simo/notifications/__pycache__/api.cpython-38.pyc +0 -0
  55. simo/notifications/api.py +1 -1
  56. simo/settings.py +1 -0
  57. simo/users/__pycache__/api.cpython-38.pyc +0 -0
  58. simo/users/api.py +1 -2
  59. {simo-2.5.41.dist-info → simo-2.6.2.dist-info}/METADATA +1 -1
  60. {simo-2.5.41.dist-info → simo-2.6.2.dist-info}/RECORD +69 -51
  61. simo/core/templates/core/auto_state_script.py +0 -78
  62. simo/generic/scripting/__pycache__/helpers.cpython-38.pyc +0 -0
  63. /simo/{generic/scripting/example.py → automation/__init__.py} +0 -0
  64. /simo/{generic/scripting → automation}/helpers.py +0 -0
  65. /simo/{generic/scripting → automation}/serializers.py +0 -0
  66. /simo/{generic/scripting/__init__.py → automation/state.py} +0 -0
  67. /simo/{generic → automation}/templates/admin/controller_widgets/script.html +0 -0
  68. {simo-2.5.41.dist-info → simo-2.6.2.dist-info}/LICENSE.md +0 -0
  69. {simo-2.5.41.dist-info → simo-2.6.2.dist-info}/WHEEL +0 -0
  70. {simo-2.5.41.dist-info → simo-2.6.2.dist-info}/entry_points.txt +0 -0
  71. {simo-2.5.41.dist-info → simo-2.6.2.dist-info}/top_level.txt +0 -0
simo/generic/forms.py CHANGED
@@ -1,17 +1,13 @@
1
- import time
2
1
  from django import forms
3
2
  from django.forms import formset_factory
4
3
  from django.db.models import Q
5
4
  from django.utils.translation import gettext_lazy as _
6
- from django.urls.base import get_script_prefix
7
- from django.contrib.contenttypes.models import ContentType
8
5
  from simo.core.forms import HiddenField, BaseComponentForm
9
6
  from simo.core.models import Icon, Component
10
7
  from simo.core.controllers import (
11
- BEFORE_SET, BinarySensor, NumericSensor, MultiSensor, Switch, Dimmer
8
+ NumericSensor, MultiSensor, Switch, Dimmer
12
9
  )
13
- from simo.core.widgets import PythonCode, LogOutputWidget
14
- from dal import autocomplete, forward
10
+ from dal import forward
15
11
  from simo.core.utils.config_values import config_to_dict
16
12
  from simo.core.utils.formsets import FormsetField
17
13
  from simo.core.utils.helpers import get_random_string
@@ -30,269 +26,6 @@ ACTION_METHODS = (
30
26
  )
31
27
 
32
28
 
33
- class ScriptConfigForm(BaseComponentForm):
34
- autostart = forms.BooleanField(
35
- initial=True, required=False,
36
- help_text="Start automatically on system boot."
37
- )
38
- keep_alive = forms.BooleanField(
39
- initial=True, required=False,
40
- help_text="Restart the script if it fails. "
41
- )
42
- assistant_request = forms.CharField(
43
- label="Request for AI assistant", required=False, max_length=1000,
44
- widget=forms.Textarea(
45
- attrs={'placeholder':
46
- "Close the blind and turn on the main light "
47
- "in my living room when it get's dark."
48
- }
49
- ),
50
- help_text="Clearly describe in your own words what kind of automation "
51
- "you want to happen with this scenario script. <br>"
52
- "The more defined, exact and clear is your description the more "
53
- "accurate automation script SIMO.io AI assistanw will generate.<br>"
54
- "Use component, zone and category id's for best accuracy. <br>"
55
- "SIMO.io AI will re-generate your automation code and update it's description in Notes field "
56
- "every time this field is changed and it might take up to 60s to do it. <br>"
57
- "Actual script code can only be edited via SIMO.io Admin.",
58
- )
59
- code = forms.CharField(widget=PythonCode, required=False)
60
- log = forms.CharField(
61
- widget=forms.HiddenInput, required=False
62
- )
63
-
64
- app_exclude_fields = ('alarm_category', 'code', 'log')
65
-
66
- _ai_resp = None
67
-
68
- def __init__(self, *args, **kwargs):
69
- super().__init__(*args, **kwargs)
70
- self.basic_fields.extend(['autostart', 'keep_alive'])
71
- if self.instance.pk:
72
- prefix = get_script_prefix()
73
- if prefix == '/':
74
- prefix = ''
75
- if 'log' in self.fields:
76
- self.fields['log'].widget = LogOutputWidget(
77
- prefix + '/ws/log/%d/%d/' % (
78
- ContentType.objects.get_for_model(Component).id,
79
- self.instance.id
80
- )
81
- )
82
-
83
- @classmethod
84
- def get_admin_fieldsets(cls, request, obj=None):
85
- base_fields = (
86
- 'id', 'gateway', 'base_type', 'name', 'icon', 'zone', 'category',
87
- 'show_in_app', 'autostart', 'keep_alive',
88
- 'assistant_request', 'notes', 'code', 'control', 'log'
89
- )
90
-
91
- fieldsets = [
92
- (_("Base settings"), {'fields': base_fields}),
93
- (_("History"), {
94
- 'fields': ('history',),
95
- 'classes': ('collapse',),
96
- }),
97
- ]
98
- return fieldsets
99
-
100
-
101
- def clean(self):
102
- if self.cleaned_data.get('assistant_request'):
103
- if self.instance.pk:
104
- org = Component.objects.get(pk=self.instance.pk)
105
- call_assistant = org.config.get('assistant_request') \
106
- != self.cleaned_data['assistant_request']
107
- else:
108
- call_assistant = True
109
- call_assistant = False
110
- if call_assistant:
111
- resp = self.instance.ai_assistant(
112
- self.cleaned_data['assistant_request'],
113
- )
114
- if resp['status'] == 'success':
115
- self._ai_resp = resp
116
- elif resp['status'] == 'error':
117
- self.add_error('assistant_request', resp['result'])
118
-
119
- return self.cleaned_data
120
-
121
- def save(self, commit=True):
122
- if commit and self._ai_resp:
123
- self.instance.config['code'] = self._ai_resp['result']
124
- self.instance.notes = self._ai_resp['description']
125
- if 'code' in self.cleaned_data:
126
- self.cleaned_data['code'] = self._ai_resp['result']
127
- if 'notes' in self.cleaned_data:
128
- self.cleaned_data['notes'] = self._ai_resp['description']
129
- obj = super().save(commit)
130
- if commit:
131
- obj.controller.stop()
132
- if self.cleaned_data.get('keep_alive') \
133
- or self.cleaned_data.get('autostart'):
134
- time.sleep(2)
135
- obj.controller.start()
136
- return obj
137
-
138
-
139
- class ConditionForm(forms.Form):
140
- component = Select2ModelChoiceField(
141
- queryset=Component.objects.all(),
142
- url='autocomplete-component',
143
- )
144
- op = forms.ChoiceField(
145
- initial="==", choices=(
146
- ('==', "is equal to"),
147
- ('>', "is greather than"), ('>=', "Is greather or equal to"),
148
- ('<', "is lower than"), ('<=', "is lower or equal to"),
149
- ('in', "is one of")
150
- )
151
- )
152
- value = forms.CharField()
153
- prefix = 'breach_events'
154
-
155
- def clean(self):
156
- if not self.cleaned_data.get('component'):
157
- return self.cleaned_data
158
- if not self.cleaned_data.get('op'):
159
- return self.cleaned_data
160
- component = self.cleaned_data.get('component')
161
-
162
- if self.cleaned_data['op'] == 'in':
163
- self.cleaned_data['value'] = self.cleaned_data['value']\
164
- .strip('(').strip('[').rstrip(')').rstrip(']').strip()
165
- values = self.cleaned_data['value'].split(',')
166
- else:
167
- values = [self.cleaned_data['value']]
168
-
169
- final_values = []
170
- controller_val_type = type(component.controller.default_value)
171
- for val in values:
172
- val = val.strip()
173
- if controller_val_type == 'bool':
174
- if val.lower() in ('0', 'false', 'none', 'null'):
175
- final_val = False
176
- else:
177
- final_val = True
178
- else:
179
- try:
180
- final_val = controller_val_type(val)
181
- except:
182
- self.add_error(
183
- 'value', f"{val} bad value type for selected component."
184
- )
185
- continue
186
- try:
187
- component.controller._validate_val(final_val, BEFORE_SET)
188
- except Exception as e:
189
- self.add_error(
190
- 'value', f"{val} is not compatible with selected component."
191
- )
192
- continue
193
- final_values.append(final_val)
194
-
195
- if self.cleaned_data['op'] == 'in':
196
- self.cleaned_data['value'] = ', '.join(str(v) for v in final_values)
197
- elif final_values:
198
- self.cleaned_data['value'] = final_values[0]
199
-
200
- return self.cleaned_data
201
-
202
-
203
- class PresenceLightingConfigForm(BaseComponentForm):
204
- lights = Select2ModelMultipleChoiceField(
205
- queryset=Component.objects.filter(
206
- base_type__in=('switch', 'dimmer', 'rgbw-light', 'rgb-light')
207
- ),
208
- required=True,
209
- url='autocomplete-component',
210
- forward=(
211
- forward.Const(['switch', 'dimmer', 'rgbw-light', 'rgb-light'],
212
- 'base_type'),
213
- )
214
- )
215
-
216
- on_value = forms.IntegerField(
217
- min_value=0, initial=100,
218
- help_text="Value applicable for dimmers. "
219
- "Switches will receive tunrn on command."
220
- )
221
- off_value = forms.TypedChoiceField(
222
- coerce=int, initial=1, choices=(
223
- (0, "0"), (1, "Original value before turning the lights on.")
224
- )
225
- )
226
- presence_sensors = Select2ModelMultipleChoiceField(
227
- queryset=Component.objects.filter(
228
- base_type__in=('binary-sensor', 'switch')
229
- ),
230
- required=True,
231
- url='autocomplete-component',
232
- forward=(forward.Const(['binary-sensor', 'switch'], 'base_type'),)
233
- )
234
- act_on = forms.TypedChoiceField(
235
- coerce=int, initial=0, choices=(
236
- (0, "At least one sensor detects presence"),
237
- (1, "All sensors detect presence"),
238
- )
239
- )
240
- hold_time = forms.TypedChoiceField(
241
- initial=3, coerce=int, required=False, choices=(
242
- (0, '----'),
243
- (1, "10 s"), (2, "20 s"), (3, "30 s"), (4, "40 s"), (5, "50 s"),
244
- (6, "1 min"), (9, "1.5 min"), (12, "2 min"), (18, "3 min"),
245
- (30, "5 min"), (60, "10 min"), (120, "20 min"),
246
- ),
247
- help_text="Hold off time after last presence detector is deactivated."
248
- )
249
- conditions = FormsetField(
250
- formset_factory(
251
- ConditionForm, can_delete=True, can_order=True, extra=0
252
- ), label='Additional conditions'
253
- )
254
- autostart = forms.BooleanField(
255
- initial=True, required=False,
256
- help_text="Start automatically on system boot."
257
- )
258
- keep_alive = forms.BooleanField(
259
- initial=True, required=False,
260
- help_text="Restart the script if it fails. "
261
- )
262
- log = forms.CharField(
263
- widget=forms.HiddenInput, required=False
264
- )
265
-
266
- app_exclude_fields = ('alarm_category', 'code', 'log')
267
-
268
- def __init__(self, *args, **kwargs):
269
- super().__init__(*args, **kwargs)
270
- self.basic_fields.extend(
271
- ['lights', 'on_value', 'off_value', 'presence_sensors',
272
- 'act_on', 'hold_time', 'conditions', 'autostart', 'keep_alive']
273
- )
274
- if self.instance.pk and 'log' in self.fields:
275
- prefix = get_script_prefix()
276
- if prefix == '/':
277
- prefix = ''
278
- self.fields['log'].widget = LogOutputWidget(
279
- prefix + '/ws/log/%d/%d/' % (
280
- ContentType.objects.get_for_model(Component).id,
281
- self.instance.id
282
- )
283
- )
284
-
285
- def save(self, commit=True):
286
- obj = super().save(commit)
287
- if commit:
288
- obj.controller.stop()
289
- if self.cleaned_data.get('keep_alive') \
290
- or self.cleaned_data.get('autostart'):
291
- time.sleep(2)
292
- obj.controller.start()
293
- return obj
294
-
295
-
296
29
  class ThermostatConfigForm(BaseComponentForm):
297
30
  temperature_sensor = Select2ModelChoiceField(
298
31
  queryset=Component.objects.filter(
simo/generic/gateways.py CHANGED
@@ -58,63 +58,6 @@ class CameraWatcher(threading.Thread):
58
58
  # self.run()
59
59
 
60
60
 
61
- class ScriptRunHandler(multiprocessing.Process):
62
- '''
63
- Threading offers better overall stability, but we use
64
- multiprocessing for Scripts so that they are better isolated and
65
- we are able to kill them whenever we need.
66
- '''
67
- component = None
68
- logger = None
69
-
70
- def __init__(self, component_id, *args, **kwargs):
71
- super().__init__(*args, **kwargs)
72
- self.component_id = component_id
73
-
74
- def run(self):
75
- db_connection.connect()
76
- self.component = Component.objects.get(id=self.component_id)
77
- try:
78
- tz = pytz.timezone(self.component.zone.instance.timezone)
79
- except:
80
- tz = pytz.timezone('UTC')
81
- timezone.activate(tz)
82
- introduce_instance(self.component.zone.instance)
83
- self.logger = get_component_logger(self.component)
84
- sys.stdout = StreamToLogger(self.logger, logging.INFO)
85
- sys.stderr = StreamToLogger(self.logger, logging.ERROR)
86
- self.component.value = 'running'
87
- self.component.save(update_fields=['value'])
88
-
89
- if hasattr(self.component.controller, '_run'):
90
- def run_code():
91
- self.component.controller._run()
92
- else:
93
- code = self.component.config.get('code')
94
- def run_code():
95
- start = time.time()
96
- exec(code, globals())
97
- if 'class Automation:' in code and time.time() - start < 1:
98
- Automation().run()
99
-
100
- if not code:
101
- self.component.value = 'finished'
102
- self.component.save(update_fields=['value'])
103
- return
104
- print("------START-------")
105
- try:
106
- run_code()
107
- except:
108
- print("------ERROR------")
109
- self.component.value = 'error'
110
- self.component.save(update_fields=['value'])
111
- raise
112
- else:
113
- print("------FINISH-----")
114
- self.component.value = 'finished'
115
- self.component.save(update_fields=['value'])
116
- return
117
-
118
61
 
119
62
  class GenericGatewayHandler(BaseObjectCommandsGatewayHandler):
120
63
  name = "Generic"
@@ -124,7 +67,6 @@ class GenericGatewayHandler(BaseObjectCommandsGatewayHandler):
124
67
  periodic_tasks = (
125
68
  ('watch_thermostats', 60),
126
69
  ('watch_alarm_clocks', 30),
127
- ('watch_scripts', 10),
128
70
  ('watch_watering', 60),
129
71
  ('watch_alarm_events', 1),
130
72
  ('watch_timers', 1)
@@ -153,35 +95,6 @@ class GenericGatewayHandler(BaseObjectCommandsGatewayHandler):
153
95
  timezone.activate(tz)
154
96
  alarm_clock.tick()
155
97
 
156
- def watch_scripts(self):
157
- drop_current_instance()
158
- # observe running scripts and drop the ones that are no longer alive
159
- dead_scripts = False
160
- for id, process in list(self.running_scripts.items()):
161
- comp = Component.objects.filter(id=id).first()
162
- if process.is_alive():
163
- if not comp and id not in self.terminating_scripts:
164
- # script is deleted, or instance deactivated
165
- process.kill()
166
- continue
167
- else:
168
- if id not in self.terminating_scripts:
169
- dead_scripts = True
170
- logger = get_component_logger(comp)
171
- logger.log(logging.INFO, "-------DEAD!-------")
172
- self.stop_script(comp, 'error')
173
-
174
- if dead_scripts:
175
- # give 10s air before we wake these dead scripts up!
176
- return
177
-
178
- from simo.generic.controllers import Script
179
- for script in Component.objects.filter(
180
- controller_uid=Script.uid,
181
- config__keep_alive=True
182
- ).exclude(value__in=('running', 'stopped', 'finished')):
183
- self.start_script(script)
184
-
185
98
  def watch_watering(self):
186
99
  drop_current_instance()
187
100
  from .controllers import Watering
@@ -205,7 +118,7 @@ class GenericGatewayHandler(BaseObjectCommandsGatewayHandler):
205
118
  target=self._run_periodic_task, args=(exit, task, period), daemon=True
206
119
  ).start()
207
120
 
208
- from simo.generic.controllers import Script, IPCamera
121
+ from simo.generic.controllers import IPCamera
209
122
 
210
123
  mqtt_client = mqtt.Client()
211
124
  mqtt_client.username_pw_set('root', settings.SECRET_KEY)
@@ -213,25 +126,6 @@ class GenericGatewayHandler(BaseObjectCommandsGatewayHandler):
213
126
  mqtt_client.on_message = self.on_mqtt_message
214
127
  mqtt_client.connect(host=settings.MQTT_HOST, port=settings.MQTT_PORT)
215
128
 
216
- # We presume that this is the only running gateway, therefore
217
- # if there are any running scripts, that is not true.
218
- for component in Component.objects.filter(
219
- controller_uid=Script.uid, value='running'
220
- ):
221
- component.value = 'error'
222
- component.save()
223
-
224
- # Start scripts that are designed to be autostarted
225
- # as well as those that are designed to be kept alive, but
226
- # got terminated unexpectedly
227
- for script in Component.objects.filter(
228
- base_type='script',
229
- ).filter(
230
- Q(config__autostart=True) |
231
- Q(value='error', config__keep_alive=True)
232
- ).distinct():
233
- self.start_script(script)
234
-
235
129
  for cam in Component.objects.filter(
236
130
  controller_uid=IPCamera.uid
237
131
  ):
@@ -243,16 +137,6 @@ class GenericGatewayHandler(BaseObjectCommandsGatewayHandler):
243
137
  mqtt_client.loop()
244
138
  mqtt_client.disconnect()
245
139
 
246
- script_ids = [id for id in self.running_scripts.keys()]
247
- for id in script_ids:
248
- self.stop_script(
249
- Component.objects.get(id=id), 'error'
250
- )
251
-
252
- time.sleep(0.5)
253
- while len(self.running_scripts.keys()):
254
- print("Still running scripts: ", self.running_scripts.keys())
255
- time.sleep(0.5)
256
140
 
257
141
  def on_mqtt_connect(self, mqtt_client, userdata, flags, rc):
258
142
  command = GatewayObjectCommand(self.gateway_instance)
@@ -260,9 +144,8 @@ class GenericGatewayHandler(BaseObjectCommandsGatewayHandler):
260
144
 
261
145
  def on_mqtt_message(self, client, userdata, msg):
262
146
  print("Mqtt message: ", msg.payload)
263
- from simo.generic.controllers import (
264
- Script, AlarmGroup
265
- )
147
+ from simo.generic.controllers import AlarmGroup
148
+
266
149
  payload = json.loads(msg.payload)
267
150
  drop_current_instance()
268
151
  component = get_event_obj(payload, Component)
@@ -270,13 +153,7 @@ class GenericGatewayHandler(BaseObjectCommandsGatewayHandler):
270
153
  return
271
154
  introduce_instance(component.zone.instance)
272
155
  try:
273
- if isinstance(component.controller, Script):
274
- if payload.get('set_val') == 'start':
275
- self.start_script(component)
276
- elif payload.get('set_val') == 'stop':
277
- self.stop_script(component)
278
- return
279
- elif component.controller_uid == AlarmGroup.uid:
156
+ if component.controller_uid == AlarmGroup.uid:
280
157
  self.control_alarm_group(component, payload.get('set_val'))
281
158
  else:
282
159
  component.controller.set(payload.get('set_val'))
@@ -284,72 +161,6 @@ class GenericGatewayHandler(BaseObjectCommandsGatewayHandler):
284
161
  print(traceback.format_exc(), file=sys.stderr)
285
162
 
286
163
 
287
- def start_script(self, component):
288
- print("START SCRIPT %s" % str(component))
289
- if component.id in self.running_scripts:
290
- if component.id not in self.terminating_scripts:
291
- if component.value != 'running':
292
- component.value = 'running'
293
- component.save()
294
- return
295
- else:
296
- good_to_go = False
297
- for i in range(12): # wait for 3s
298
- time.sleep(0.2)
299
- component.refresh_from_db()
300
- if component.id not in self.running_scripts:
301
- good_to_go = True
302
- break
303
- if not good_to_go:
304
- return self.stop_script(component, 'error')
305
-
306
- self.running_scripts[component.id] = ScriptRunHandler(
307
- component.id, daemon=True
308
- )
309
- self.running_scripts[component.id].start()
310
-
311
- def stop_script(self, component, stop_status='stopped'):
312
- self.terminating_scripts.add(component.id)
313
- if component.id not in self.running_scripts:
314
- if component.value == 'running':
315
- component.value = stop_status
316
- component.save(update_fields=['value'])
317
- return
318
-
319
- tz = pytz.timezone(component.zone.instance.timezone)
320
- timezone.activate(tz)
321
- logger = get_component_logger(component)
322
- if stop_status == 'error':
323
- logger.log(logging.INFO, "-------GATEWAY STOP-------")
324
- else:
325
- logger.log(logging.INFO, "-------STOP-------")
326
- self.running_scripts[component.id].terminate()
327
-
328
- def kill():
329
- start = time.time()
330
- terminated = False
331
- while start > time.time() - 2:
332
- if not self.running_scripts[component.id].is_alive():
333
- terminated = True
334
- break
335
- time.sleep(0.1)
336
- if not terminated:
337
- if stop_status == 'error':
338
- logger.log(logging.INFO, "-------GATEWAY KILL-------")
339
- else:
340
- logger.log(logging.INFO, "-------KILL!-------")
341
- self.running_scripts[component.id].kill()
342
-
343
- component.value = stop_status
344
- component.save(update_fields=['value'])
345
- self.terminating_scripts.remove(component.id)
346
- # making sure it's fully killed along with it's child processes
347
- self.running_scripts[component.id].kill()
348
- self.running_scripts.pop(component.id, None)
349
- logger.handlers = []
350
-
351
- threading.Thread(target=kill, daemon=True).start()
352
-
353
164
  def control_alarm_group(self, alarm_group, value):
354
165
  from simo.generic.controllers import AlarmGroup
355
166
 
@@ -0,0 +1,34 @@
1
+ # Generated by Django 4.2.10 on 2024-11-26 07:26
2
+ from django.db import migrations
3
+
4
+
5
+ def forwards_func(apps, schema_editor):
6
+ Component = apps.get_model("core", "Component")
7
+
8
+ for script in Component.objects.filter(
9
+ controller_uid='simo.generic.controllers.PresenceLighting'
10
+ ):
11
+ lights = []
12
+ on_value = script.config.pop('on_value', 100)
13
+ off_value = script.config.get('off_value', 0)
14
+ for light_id in script.config.get('lights', []):
15
+ lights.append({
16
+ 'light': light_id, 'on_value': on_value, 'off_value': off_value
17
+ })
18
+ script.config['lights'] = lights
19
+ script.save()
20
+
21
+
22
+ def reverse_func(apps, schema_editor):
23
+ pass
24
+
25
+
26
+ class Migration(migrations.Migration):
27
+
28
+ dependencies = [
29
+ ('generic', '0001_initial'),
30
+ ]
31
+
32
+ operations = [
33
+ migrations.RunPython(forwards_func, reverse_func, elidable=True),
34
+ ]
simo/notifications/api.py CHANGED
@@ -34,7 +34,7 @@ class NotificationsViewSet(
34
34
  archived = bool(int(self.request.query_params['archived']))
35
35
  except:
36
36
  archived = False
37
- qs = Notification.objects.filter(
37
+ qs = qs.objects.filter(
38
38
  user_notifications__archived__isnull=not archived,
39
39
  user_notifications__user=self.request.user
40
40
  )
simo/settings.py CHANGED
@@ -71,6 +71,7 @@ INSTALLED_APPS = [
71
71
  'simo.users',
72
72
  'simo.notifications',
73
73
  'simo.generic',
74
+ 'simo.automation',
74
75
  'simo.multimedia',
75
76
  'simo.fleet',
76
77
  'simo.backups',
Binary file
simo/users/api.py CHANGED
@@ -173,7 +173,7 @@ class UserDeviceReport(InstanceMixin, viewsets.GenericViewSet):
173
173
 
174
174
  @action(url_path='device-report', detail=False, methods=['post'])
175
175
  def report(self, request, *args, **kwargs):
176
- from simo.generic.scripting.helpers import haversine_distance
176
+ from simo.automation.helpers import haversine_distance
177
177
  if not request.data.get('device_token'):
178
178
  return RESTResponse(
179
179
  {'status': 'error', 'msg': 'device_token - not provided'},
@@ -220,7 +220,6 @@ class UserDeviceReport(InstanceMixin, viewsets.GenericViewSet):
220
220
  at_home=False, location__isnull=False
221
221
  ).last()
222
222
  if prev_log:
223
- from simo.generic.scripting.helpers import haversine_distance
224
223
  meters_traveled = haversine_distance(location, prev_log.location)
225
224
  seconds_passed = (log_datetime - prev_log.datetime).total_seconds()
226
225
  avg_speed_kmh = round(meters_traveled / seconds_passed * 3.6, 0)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: simo
3
- Version: 2.5.41
3
+ Version: 2.6.2
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