simo 2.5.42__py3-none-any.whl → 2.6.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/__pycache__/settings.cpython-38.pyc +0 -0
- simo/automation/__pycache__/__init__.cpython-38.pyc +0 -0
- simo/automation/__pycache__/app_widgets.cpython-38.pyc +0 -0
- simo/automation/__pycache__/controllers.cpython-38.pyc +0 -0
- simo/automation/__pycache__/forms.cpython-38.pyc +0 -0
- simo/automation/__pycache__/gateways.cpython-38.pyc +0 -0
- simo/{generic/scripting → automation}/__pycache__/helpers.cpython-38.pyc +0 -0
- simo/automation/__pycache__/models.cpython-38.pyc +0 -0
- simo/{generic/scripting → automation}/__pycache__/serializers.cpython-38.pyc +0 -0
- simo/{generic/scripting/__pycache__/__init__.cpython-38.pyc → automation/__pycache__/state.cpython-38.pyc} +0 -0
- simo/automation/app_widgets.py +8 -0
- simo/automation/controllers.py +273 -0
- simo/automation/forms.py +273 -0
- simo/automation/gateways.py +257 -0
- simo/automation/migrations/0001_initial.py +39 -0
- simo/automation/migrations/0002_update_helpers_in_scripts.py +29 -0
- simo/automation/migrations/__init__.py +0 -0
- simo/automation/migrations/__pycache__/0001_initial.cpython-38.pyc +0 -0
- simo/automation/migrations/__pycache__/0002_update_helpers_in_scripts.cpython-38.pyc +0 -0
- simo/automation/migrations/__pycache__/__init__.cpython-38.pyc +0 -0
- simo/automation/models.py +31 -0
- simo/automation/templates/automations/auto_away.py +55 -0
- simo/automation/templates/automations/auto_state_script.py +31 -0
- simo/{core/templates/core/auto_night_day_script.py → automation/templates/automations/phones_sleep_script.py} +25 -13
- simo/core/__pycache__/admin.cpython-38.pyc +0 -0
- simo/core/__pycache__/filters.cpython-38.pyc +0 -0
- simo/core/__pycache__/models.cpython-38.pyc +0 -0
- simo/core/__pycache__/signal_receivers.cpython-38.pyc +0 -0
- simo/core/admin.py +7 -4
- simo/core/filters.py +61 -0
- simo/core/signal_receivers.py +50 -17
- simo/core/utils/__pycache__/type_constants.cpython-38.pyc +0 -0
- simo/core/utils/type_constants.py +1 -1
- simo/fleet/__pycache__/api.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/serializers.cpython-38.pyc +0 -0
- simo/fleet/api.py +6 -0
- simo/fleet/serializers.py +9 -1
- simo/generic/__pycache__/app_widgets.cpython-38.pyc +0 -0
- simo/generic/__pycache__/controllers.cpython-38.pyc +0 -0
- simo/generic/__pycache__/forms.cpython-38.pyc +0 -0
- simo/generic/__pycache__/gateways.cpython-38.pyc +0 -0
- simo/generic/app_widgets.py +0 -6
- simo/generic/controllers.py +4 -262
- simo/generic/forms.py +2 -280
- simo/generic/gateways.py +4 -193
- simo/settings.py +1 -0
- simo/users/__pycache__/api.cpython-38.pyc +0 -0
- simo/users/api.py +1 -2
- {simo-2.5.42.dist-info → simo-2.6.3.dist-info}/METADATA +1 -1
- {simo-2.5.42.dist-info → simo-2.6.3.dist-info}/RECORD +59 -41
- simo/core/templates/core/auto_state_script.py +0 -78
- /simo/{generic/scripting/example.py → automation/__init__.py} +0 -0
- /simo/{generic/scripting → automation}/helpers.py +0 -0
- /simo/{generic/scripting → automation}/serializers.py +0 -0
- /simo/{generic/scripting/__init__.py → automation/state.py} +0 -0
- /simo/{generic → automation}/templates/admin/controller_widgets/script.html +0 -0
- {simo-2.5.42.dist-info → simo-2.6.3.dist-info}/LICENSE.md +0 -0
- {simo-2.5.42.dist-info → simo-2.6.3.dist-info}/WHEEL +0 -0
- {simo-2.5.42.dist-info → simo-2.6.3.dist-info}/entry_points.txt +0 -0
- {simo-2.5.42.dist-info → simo-2.6.3.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
|
-
|
|
8
|
+
NumericSensor, MultiSensor, Switch, Dimmer
|
|
12
9
|
)
|
|
13
|
-
from
|
|
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,280 +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 LightTurnOnForm(forms.Form):
|
|
204
|
-
light = Select2ModelChoiceField(
|
|
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
|
-
on_value = forms.IntegerField(
|
|
216
|
-
min_value=0, initial=100,
|
|
217
|
-
help_text="Value applicable for dimmers. "
|
|
218
|
-
"Switches will receive turn on command."
|
|
219
|
-
)
|
|
220
|
-
off_value = forms.TypedChoiceField(
|
|
221
|
-
coerce=int, initial=1, choices=(
|
|
222
|
-
(0, "0"), (1, "Original value before turning the light on.")
|
|
223
|
-
)
|
|
224
|
-
)
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
class PresenceLightingConfigForm(BaseComponentForm):
|
|
228
|
-
presence_sensors = Select2ModelMultipleChoiceField(
|
|
229
|
-
queryset=Component.objects.filter(
|
|
230
|
-
base_type__in=('binary-sensor', 'switch')
|
|
231
|
-
),
|
|
232
|
-
required=True,
|
|
233
|
-
url='autocomplete-component',
|
|
234
|
-
forward=(forward.Const(['binary-sensor', 'switch'], 'base_type'),)
|
|
235
|
-
)
|
|
236
|
-
act_on = forms.TypedChoiceField(
|
|
237
|
-
coerce=int, initial=0, choices=(
|
|
238
|
-
(0, "At least one sensor detects presence"),
|
|
239
|
-
(1, "All sensors detect presence"),
|
|
240
|
-
)
|
|
241
|
-
)
|
|
242
|
-
hold_time = forms.TypedChoiceField(
|
|
243
|
-
initial=3, coerce=int, required=False, choices=(
|
|
244
|
-
(0, '----'),
|
|
245
|
-
(1, "10 s"), (2, "20 s"), (3, "30 s"), (4, "40 s"), (5, "50 s"),
|
|
246
|
-
(6, "1 min"), (9, "1.5 min"), (12, "2 min"), (18, "3 min"),
|
|
247
|
-
(30, "5 min"), (60, "10 min"), (120, "20 min"), (180, "30 min"),
|
|
248
|
-
(3600, "1 h")
|
|
249
|
-
),
|
|
250
|
-
help_text="Hold off time after last presence detector is deactivated."
|
|
251
|
-
)
|
|
252
|
-
conditions = FormsetField(
|
|
253
|
-
formset_factory(
|
|
254
|
-
ConditionForm, can_delete=True, can_order=True, extra=0
|
|
255
|
-
), label='Additional conditions'
|
|
256
|
-
)
|
|
257
|
-
|
|
258
|
-
lights = FormsetField(
|
|
259
|
-
formset_factory(
|
|
260
|
-
LightTurnOnForm, can_delete=True, can_order=True, extra=0
|
|
261
|
-
), label='Lights'
|
|
262
|
-
)
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
autostart = forms.BooleanField(
|
|
266
|
-
initial=True, required=False,
|
|
267
|
-
help_text="Start automatically on system boot."
|
|
268
|
-
)
|
|
269
|
-
keep_alive = forms.BooleanField(
|
|
270
|
-
initial=True, required=False,
|
|
271
|
-
help_text="Restart the script if it fails. "
|
|
272
|
-
)
|
|
273
|
-
log = forms.CharField(
|
|
274
|
-
widget=forms.HiddenInput, required=False
|
|
275
|
-
)
|
|
276
|
-
|
|
277
|
-
app_exclude_fields = ('alarm_category', 'code', 'log')
|
|
278
|
-
|
|
279
|
-
def __init__(self, *args, **kwargs):
|
|
280
|
-
super().__init__(*args, **kwargs)
|
|
281
|
-
self.basic_fields.extend(
|
|
282
|
-
['lights', 'on_value', 'off_value', 'presence_sensors',
|
|
283
|
-
'act_on', 'hold_time', 'conditions', 'autostart', 'keep_alive']
|
|
284
|
-
)
|
|
285
|
-
if self.instance.pk and 'log' in self.fields:
|
|
286
|
-
prefix = get_script_prefix()
|
|
287
|
-
if prefix == '/':
|
|
288
|
-
prefix = ''
|
|
289
|
-
self.fields['log'].widget = LogOutputWidget(
|
|
290
|
-
prefix + '/ws/log/%d/%d/' % (
|
|
291
|
-
ContentType.objects.get_for_model(Component).id,
|
|
292
|
-
self.instance.id
|
|
293
|
-
)
|
|
294
|
-
)
|
|
295
|
-
|
|
296
|
-
def save(self, commit=True):
|
|
297
|
-
obj = super().save(commit)
|
|
298
|
-
if commit:
|
|
299
|
-
obj.controller.stop()
|
|
300
|
-
if self.cleaned_data.get('keep_alive') \
|
|
301
|
-
or self.cleaned_data.get('autostart'):
|
|
302
|
-
time.sleep(2)
|
|
303
|
-
obj.controller.start()
|
|
304
|
-
return obj
|
|
305
|
-
|
|
306
|
-
|
|
307
29
|
class ThermostatConfigForm(BaseComponentForm):
|
|
308
30
|
temperature_sensor = Select2ModelChoiceField(
|
|
309
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
|
|
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
|
-
|
|
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
|
|
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
|
|
simo/settings.py
CHANGED
|
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.
|
|
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)
|