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
Binary file
@@ -0,0 +1,8 @@
1
+ from django.utils.translation import gettext_lazy as _
2
+ from simo.core.app_widgets import BaseAppWidget
3
+
4
+
5
+ class ScriptWidget(BaseAppWidget):
6
+ uid = 'script'
7
+ name = _("Script")
8
+ size = [2, 1]
@@ -0,0 +1,273 @@
1
+ import time
2
+ import json
3
+ import requests
4
+ import traceback
5
+ import sys
6
+ import random
7
+ from bs4 import BeautifulSoup
8
+ from django.core.exceptions import ValidationError
9
+ from django.utils.translation import gettext_lazy as _
10
+ from simo.conf import dynamic_settings
11
+ from simo.users.middleware import get_current_user
12
+ from simo.core.models import RUN_STATUS_CHOICES_MAP, Component
13
+ from simo.core.utils.operations import OPERATIONS
14
+ from simo.core.middleware import get_current_instance
15
+ from simo.core.controllers import (
16
+ BEFORE_SEND, BEFORE_SET, ControllerBase, TimerMixin,
17
+ )
18
+ from .gateways import AutomationsGatewayHandler
19
+ from .app_widgets import ScriptWidget
20
+ from .forms import (
21
+ ScriptConfigForm, PresenceLightingConfigForm
22
+ )
23
+ from .state import get_current_state
24
+ from .serializers import UserSerializer
25
+
26
+
27
+ class Script(ControllerBase, TimerMixin):
28
+ name = _("AI Script")
29
+ base_type = 'script'
30
+ gateway_class = AutomationsGatewayHandler
31
+ app_widget = ScriptWidget
32
+ config_form = ScriptConfigForm
33
+ admin_widget_template = 'admin/controller_widgets/script.html'
34
+ default_config = {'autostart': True, 'autorestart': True}
35
+ default_value = 'stopped'
36
+
37
+ def _validate_val(self, value, occasion=None):
38
+ if occasion == BEFORE_SEND:
39
+ if value not in ('start', 'stop'):
40
+ raise ValidationError("Must be 'start' or 'stop'")
41
+ elif occasion == BEFORE_SET:
42
+ if value not in RUN_STATUS_CHOICES_MAP.keys():
43
+ raise ValidationError(
44
+ "Invalid script controller status!"
45
+ )
46
+ return value
47
+
48
+ def _prepare_for_send(self, value):
49
+ if value == 'start':
50
+ new_code = getattr(self.component, 'new_code', None)
51
+ if new_code:
52
+ self.component.new_code = None
53
+ self.component.refresh_from_db()
54
+ self.component.config['code'] = new_code
55
+ self.component.save(update_fields=['config'])
56
+ return value
57
+
58
+ def _val_to_success(self, value):
59
+ if value == 'start':
60
+ return 'running'
61
+ else:
62
+ return 'stopped'
63
+
64
+ def start(self, new_code=None):
65
+ if new_code:
66
+ self.component.new_code = new_code
67
+ self.send('start')
68
+
69
+ def play(self):
70
+ return self.start()
71
+
72
+ def stop(self):
73
+ self.send('stop')
74
+
75
+ def toggle(self):
76
+ self.component.refresh_from_db()
77
+ if self.component.value == 'running':
78
+ self.send('stop')
79
+ else:
80
+ self.send('start')
81
+
82
+ def ai_assistant(self, wish):
83
+ try:
84
+ request_data = {
85
+ 'hub_uid': dynamic_settings['core__hub_uid'],
86
+ 'hub_secret': dynamic_settings['core__hub_secret'],
87
+ 'instance_uid': get_current_instance().uid,
88
+ 'system_data': json.dumps(get_current_state()),
89
+ 'wish': wish,
90
+ }
91
+ except Exception as e:
92
+ print(traceback.format_exc(), file=sys.stderr)
93
+ return {'status': 'error', 'result': f"Internal error: {e}"}
94
+ user = get_current_user()
95
+ if user:
96
+ request_data['current_user'] = UserSerializer(user, many=False).data
97
+ try:
98
+ response = requests.post(
99
+ 'https://simo.io/hubs/ai-assist/scripts/', json=request_data
100
+ )
101
+ except:
102
+ return {'status': 'error', 'result': "Connection error"}
103
+
104
+ if response.status_code != 200:
105
+ content = response.content.decode()
106
+ if '<html' in content:
107
+ # Parse the HTML content
108
+ soup = BeautifulSoup(response.content, 'html.parser')
109
+ content = F"Server error {response.status_code}: {soup.title.string}"
110
+ return {'status': 'error', 'result': content}
111
+
112
+ return {
113
+ 'status': 'success',
114
+ 'result': response.json()['script'],
115
+ 'description': response.json()['description']
116
+ }
117
+
118
+
119
+ class PresenceLighting(Script):
120
+ masters_only = False
121
+ name = _("Presence lighting")
122
+ config_form = PresenceLightingConfigForm
123
+
124
+ # script specific variables
125
+ sensors = {}
126
+ condition_comps = {}
127
+ light_org_values = {}
128
+ is_on = False
129
+ turn_off_task = None
130
+ last_presence = 0
131
+ hold_time = 60
132
+ conditions = []
133
+
134
+ def _run(self):
135
+ self.hold_time = self.component.config.get('hold_time', 0) * 10
136
+ for id in self.component.config['presence_sensors']:
137
+ sensor = Component.objects.filter(id=id).first()
138
+ if sensor:
139
+ sensor.on_change(self._on_sensor)
140
+ self.sensors[id] = sensor
141
+
142
+ for light_params in self.component.config['lights']:
143
+ light = Component.objects.filter(
144
+ id=light_params.get('light')
145
+ ).first()
146
+ if not light or not light.controller:
147
+ continue
148
+ light.on_change(self._on_light_change)
149
+
150
+ for condition in self.component.config.get('conditions', []):
151
+ comp = Component.objects.filter(
152
+ id=condition.get('component', 0)
153
+ ).first()
154
+ if comp:
155
+ condition['component'] = comp
156
+ self.conditions.append(condition)
157
+ comp.on_change(self._on_condition)
158
+ self.condition_comps[comp.id] = comp
159
+
160
+ while True:
161
+ self._regulate(on_val_change=False)
162
+ time.sleep(random.randint(5, 15))
163
+
164
+ def _on_sensor(self, sensor=None):
165
+ if sensor:
166
+ self.sensors[sensor.id] = sensor
167
+ self._regulate()
168
+
169
+ def _on_condition(self, condition_comp=None):
170
+ if condition_comp:
171
+ for condition in self.conditions:
172
+ if condition['component'].id == condition_comp.id:
173
+ condition['component'] = condition_comp
174
+ self._regulate()
175
+
176
+ def _on_light_change(self, light):
177
+ if self.is_on:
178
+ self.light_org_values[light.id] = light.value
179
+
180
+ def _regulate(self, on_val_change=True):
181
+ presence_values = [s.value for id, s in self.sensors.items()]
182
+ if self.component.config.get('act_on', 0) == 0:
183
+ must_on = any(presence_values)
184
+ else:
185
+ must_on = all(presence_values)
186
+
187
+ if must_on and on_val_change:
188
+ print("Presence detected!")
189
+
190
+ additional_conditions_met = True
191
+ for condition in self.conditions:
192
+
193
+ comp = condition['component']
194
+
195
+ op = OPERATIONS.get(condition.get('op'))
196
+ if not op:
197
+ continue
198
+
199
+ if condition['op'] == 'in':
200
+ if comp.value not in self._string_to_vals(condition['value']):
201
+ if must_on and on_val_change:
202
+ print(
203
+ f"Condition not met: [{comp} value:{comp.value} "
204
+ f"{condition['op']} {condition['value']}]"
205
+ )
206
+ additional_conditions_met = False
207
+ break
208
+
209
+ if not op(comp.value, condition['value']):
210
+ if must_on and on_val_change:
211
+ print(
212
+ f"Condition not met: [{comp} value:{comp.value} "
213
+ f"{condition['op']} {condition['value']}]"
214
+ )
215
+ additional_conditions_met = False
216
+ break
217
+
218
+ if must_on and additional_conditions_met and not self.is_on:
219
+ print("Turn the lights ON!")
220
+ self.is_on = True
221
+ self.light_org_values = {}
222
+ for light_params in self.component.config['lights']:
223
+ comp = Component.objects.filter(
224
+ id=light_params.get('light')
225
+ ).first()
226
+ if not comp or not comp.controller:
227
+ continue
228
+ self.light_org_values[comp.id] = comp.value
229
+ print(f"Send {light_params['on_value']} to {comp}!")
230
+ comp.controller.send(light_params['on_value'])
231
+ return
232
+
233
+ if self.is_on:
234
+ if not additional_conditions_met:
235
+ return self._turn_it_off()
236
+ if not any(presence_values):
237
+ if not self.component.config.get('hold_time', 0):
238
+ return self._turn_it_off()
239
+
240
+ if not self.last_presence:
241
+ self.last_presence = time.time()
242
+
243
+ if self.hold_time and (
244
+ time.time() - self.hold_time > self.last_presence
245
+ ):
246
+ self._turn_it_off()
247
+
248
+
249
+ def _turn_it_off(self):
250
+ print("Turn the lights OFF!")
251
+ self.is_on = False
252
+ self.last_presence = 0
253
+ for light_params in self.component.config['lights']:
254
+ comp = Component.objects.filter(
255
+ id=light_params.get('light')
256
+ ).first()
257
+ if not comp or not comp.controller:
258
+ continue
259
+
260
+ if not light_params.get('off_value', 0):
261
+ off_val = 0
262
+ else:
263
+ off_val = self.light_org_values.get(comp.id, 0)
264
+ print(f"Send {off_val} to {comp}!")
265
+ comp.send(off_val)
266
+
267
+
268
+ # TODO: Night lighting
269
+ #
270
+ # Lights: components (switches, dimmers)
271
+ # On value: 40
272
+ # Sunset offset (mins): negative = earlier, positive = later
273
+ # Save energy at night: 1 - 6 turn the lights completely off at night.
@@ -0,0 +1,290 @@
1
+ import time
2
+ from django import forms
3
+ from django.forms import formset_factory
4
+ from django.utils.translation import gettext_lazy as _
5
+ from django.urls.base import get_script_prefix
6
+ from django.contrib.contenttypes.models import ContentType
7
+ from simo.core.forms import BaseComponentForm
8
+ from simo.core.models import Component
9
+ from simo.core.controllers import BEFORE_SET
10
+ from simo.core.widgets import PythonCode, LogOutputWidget
11
+ from dal import forward
12
+ from simo.core.utils.formsets import FormsetField
13
+ from simo.core.form_fields import (
14
+ Select2ModelChoiceField,
15
+ Select2ModelMultipleChoiceField
16
+ )
17
+
18
+
19
+ class ScriptConfigForm(BaseComponentForm):
20
+ autostart = forms.BooleanField(
21
+ initial=True, required=False,
22
+ help_text="Start automatically on system boot."
23
+ )
24
+ keep_alive = forms.BooleanField(
25
+ initial=True, required=False,
26
+ help_text="Restart the script if it fails. "
27
+ )
28
+ assistant_request = forms.CharField(
29
+ label="Request for AI assistant", required=False, max_length=1000,
30
+ widget=forms.Textarea(
31
+ attrs={'placeholder':
32
+ "Close the blind and turn on the main light "
33
+ "in my living room when it get's dark."
34
+ }
35
+ ),
36
+ help_text="Clearly describe in your own words what kind of automation "
37
+ "you want to happen with this scenario script. <br>"
38
+ "The more defined, exact and clear is your description the more "
39
+ "accurate automation script SIMO.io AI assistanw will generate.<br>"
40
+ "Use component, zone and category id's for best accuracy. <br>"
41
+ "SIMO.io AI will re-generate your automation code and update it's description in Notes field "
42
+ "every time this field is changed and it might take up to 60s to do it. <br>"
43
+ "Actual script code can only be edited via SIMO.io Admin.",
44
+ )
45
+ code = forms.CharField(widget=PythonCode, required=False)
46
+ log = forms.CharField(
47
+ widget=forms.HiddenInput, required=False
48
+ )
49
+
50
+ app_exclude_fields = ('alarm_category', 'code', 'log')
51
+
52
+ _ai_resp = None
53
+
54
+ def __init__(self, *args, **kwargs):
55
+ super().__init__(*args, **kwargs)
56
+ self.basic_fields.extend(['autostart', 'keep_alive'])
57
+ if self.instance.pk:
58
+ prefix = get_script_prefix()
59
+ if prefix == '/':
60
+ prefix = ''
61
+ if 'log' in self.fields:
62
+ self.fields['log'].widget = LogOutputWidget(
63
+ prefix + '/ws/log/%d/%d/' % (
64
+ ContentType.objects.get_for_model(Component).id,
65
+ self.instance.id
66
+ )
67
+ )
68
+
69
+ @classmethod
70
+ def get_admin_fieldsets(cls, request, obj=None):
71
+ base_fields = (
72
+ 'id', 'gateway', 'base_type', 'name', 'icon', 'zone', 'category',
73
+ 'show_in_app', 'autostart', 'keep_alive',
74
+ 'assistant_request', 'notes', 'code', 'control', 'log'
75
+ )
76
+
77
+ fieldsets = [
78
+ (_("Base settings"), {'fields': base_fields}),
79
+ (_("History"), {
80
+ 'fields': ('history',),
81
+ 'classes': ('collapse',),
82
+ }),
83
+ ]
84
+ return fieldsets
85
+
86
+
87
+ def clean(self):
88
+ if self.cleaned_data.get('assistant_request'):
89
+ if self.instance.pk:
90
+ org = Component.objects.get(pk=self.instance.pk)
91
+ call_assistant = org.config.get('assistant_request') \
92
+ != self.cleaned_data['assistant_request']
93
+ else:
94
+ call_assistant = True
95
+ call_assistant = False
96
+ if call_assistant:
97
+ resp = self.instance.ai_assistant(
98
+ self.cleaned_data['assistant_request'],
99
+ )
100
+ if resp['status'] == 'success':
101
+ self._ai_resp = resp
102
+ elif resp['status'] == 'error':
103
+ self.add_error('assistant_request', resp['result'])
104
+
105
+ return self.cleaned_data
106
+
107
+ def save(self, commit=True):
108
+ if commit and self._ai_resp:
109
+ self.instance.config['code'] = self._ai_resp['result']
110
+ self.instance.notes = self._ai_resp['description']
111
+ if 'code' in self.cleaned_data:
112
+ self.cleaned_data['code'] = self._ai_resp['result']
113
+ if 'notes' in self.cleaned_data:
114
+ self.cleaned_data['notes'] = self._ai_resp['description']
115
+ obj = super().save(commit)
116
+ if commit:
117
+ obj.controller.stop()
118
+ if self.cleaned_data.get('keep_alive') \
119
+ or self.cleaned_data.get('autostart'):
120
+ time.sleep(2)
121
+ obj.controller.start()
122
+ return obj
123
+
124
+
125
+ class ConditionForm(forms.Form):
126
+ component = Select2ModelChoiceField(
127
+ queryset=Component.objects.all(),
128
+ url='autocomplete-component',
129
+ )
130
+ op = forms.ChoiceField(
131
+ initial="==", choices=(
132
+ ('==', "is equal to"),
133
+ ('>', "is greather than"), ('>=', "Is greather or equal to"),
134
+ ('<', "is lower than"), ('<=', "is lower or equal to"),
135
+ ('in', "is one of")
136
+ )
137
+ )
138
+ value = forms.CharField()
139
+ prefix = 'breach_events'
140
+
141
+ def clean(self):
142
+ if not self.cleaned_data.get('component'):
143
+ return self.cleaned_data
144
+ if not self.cleaned_data.get('op'):
145
+ return self.cleaned_data
146
+ component = self.cleaned_data.get('component')
147
+
148
+ if self.cleaned_data['op'] == 'in':
149
+ self.cleaned_data['value'] = self.cleaned_data['value']\
150
+ .strip('(').strip('[').rstrip(')').rstrip(']').strip()
151
+ values = self.cleaned_data['value'].split(',')
152
+ else:
153
+ values = [self.cleaned_data['value']]
154
+
155
+ final_values = []
156
+ controller_val_type = type(component.controller.default_value)
157
+ for val in values:
158
+ val = val.strip()
159
+ if controller_val_type == 'bool':
160
+ if val.lower() in ('0', 'false', 'none', 'null'):
161
+ final_val = False
162
+ else:
163
+ final_val = True
164
+ else:
165
+ try:
166
+ final_val = controller_val_type(val)
167
+ except:
168
+ self.add_error(
169
+ 'value', f"{val} bad value type for selected component."
170
+ )
171
+ continue
172
+ try:
173
+ component.controller._validate_val(final_val, BEFORE_SET)
174
+ except Exception as e:
175
+ self.add_error(
176
+ 'value', f"{val} is not compatible with selected component."
177
+ )
178
+ continue
179
+ final_values.append(final_val)
180
+
181
+ if self.cleaned_data['op'] == 'in':
182
+ self.cleaned_data['value'] = ', '.join(str(v) for v in final_values)
183
+ elif final_values:
184
+ self.cleaned_data['value'] = final_values[0]
185
+
186
+ return self.cleaned_data
187
+
188
+
189
+ class LightTurnOnForm(forms.Form):
190
+ light = Select2ModelChoiceField(
191
+ queryset=Component.objects.filter(
192
+ base_type__in=('switch', 'dimmer', 'rgbw-light', 'rgb-light')
193
+ ),
194
+ required=True,
195
+ url='autocomplete-component',
196
+ forward=(
197
+ forward.Const(['switch', 'dimmer', 'rgbw-light', 'rgb-light'],
198
+ 'base_type'),
199
+ )
200
+ )
201
+ on_value = forms.IntegerField(
202
+ min_value=0, initial=100,
203
+ help_text="Value applicable for dimmers. "
204
+ "Switches will receive turn on command."
205
+ )
206
+ off_value = forms.TypedChoiceField(
207
+ coerce=int, initial=1, choices=(
208
+ (0, "0"), (1, "Original value before turning the light on.")
209
+ )
210
+ )
211
+
212
+
213
+ class PresenceLightingConfigForm(BaseComponentForm):
214
+ presence_sensors = Select2ModelMultipleChoiceField(
215
+ queryset=Component.objects.filter(
216
+ base_type__in=('binary-sensor', 'switch')
217
+ ),
218
+ required=True,
219
+ url='autocomplete-component',
220
+ forward=(forward.Const(['binary-sensor', 'switch'], 'base_type'),)
221
+ )
222
+ act_on = forms.TypedChoiceField(
223
+ coerce=int, initial=0, choices=(
224
+ (0, "At least one sensor detects presence"),
225
+ (1, "All sensors detect presence"),
226
+ )
227
+ )
228
+ hold_time = forms.TypedChoiceField(
229
+ initial=3, coerce=int, required=False, choices=(
230
+ (0, '----'),
231
+ (1, "10 s"), (2, "20 s"), (3, "30 s"), (4, "40 s"), (5, "50 s"),
232
+ (6, "1 min"), (9, "1.5 min"), (12, "2 min"), (18, "3 min"),
233
+ (30, "5 min"), (60, "10 min"), (120, "20 min"), (180, "30 min"),
234
+ (3600, "1 h")
235
+ ),
236
+ help_text="Hold off time after last presence detector is deactivated."
237
+ )
238
+ conditions = FormsetField(
239
+ formset_factory(
240
+ ConditionForm, can_delete=True, can_order=True, extra=0
241
+ ), label='Additional conditions'
242
+ )
243
+
244
+ lights = FormsetField(
245
+ formset_factory(
246
+ LightTurnOnForm, can_delete=True, can_order=True, extra=0
247
+ ), label='Lights'
248
+ )
249
+
250
+
251
+ autostart = forms.BooleanField(
252
+ initial=True, required=False,
253
+ help_text="Start automatically on system boot."
254
+ )
255
+ keep_alive = forms.BooleanField(
256
+ initial=True, required=False,
257
+ help_text="Restart the script if it fails. "
258
+ )
259
+ log = forms.CharField(
260
+ widget=forms.HiddenInput, required=False
261
+ )
262
+
263
+ app_exclude_fields = ('alarm_category', 'code', 'log')
264
+
265
+ def __init__(self, *args, **kwargs):
266
+ super().__init__(*args, **kwargs)
267
+ self.basic_fields.extend(
268
+ ['lights', 'on_value', 'off_value', 'presence_sensors',
269
+ 'act_on', 'hold_time', 'conditions', 'autostart', 'keep_alive']
270
+ )
271
+ if self.instance.pk and 'log' in self.fields:
272
+ prefix = get_script_prefix()
273
+ if prefix == '/':
274
+ prefix = ''
275
+ self.fields['log'].widget = LogOutputWidget(
276
+ prefix + '/ws/log/%d/%d/' % (
277
+ ContentType.objects.get_for_model(Component).id,
278
+ self.instance.id
279
+ )
280
+ )
281
+
282
+ def save(self, commit=True):
283
+ obj = super().save(commit)
284
+ if commit:
285
+ obj.controller.stop()
286
+ if self.cleaned_data.get('keep_alive') \
287
+ or self.cleaned_data.get('autostart'):
288
+ time.sleep(2)
289
+ obj.controller.start()
290
+ return obj