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.
- 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/automation/__pycache__/helpers.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 +290 -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/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__/api.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/__pycache__/tasks.cpython-38.pyc +0 -0
- simo/core/admin.py +7 -4
- simo/core/api.py +8 -1
- simo/core/filters.py +61 -0
- simo/core/management/_hub_template/hub/supervisor.conf +0 -1
- 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__/controllers.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/forms.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/serializers.cpython-38.pyc +0 -0
- simo/fleet/api.py +6 -0
- simo/fleet/controllers.py +1 -0
- simo/fleet/forms.py +22 -3
- 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 -260
- simo/generic/forms.py +2 -269
- simo/generic/gateways.py +4 -193
- simo/generic/migrations/0002_auto_20241126_0726.py +34 -0
- simo/generic/migrations/__pycache__/0002_auto_20241126_0726.cpython-38.pyc +0 -0
- simo/notifications/__pycache__/api.cpython-38.pyc +0 -0
- simo/notifications/api.py +1 -1
- simo/settings.py +1 -0
- simo/users/__pycache__/api.cpython-38.pyc +0 -0
- simo/users/api.py +1 -2
- {simo-2.5.41.dist-info → simo-2.6.2.dist-info}/METADATA +1 -1
- {simo-2.5.41.dist-info → simo-2.6.2.dist-info}/RECORD +69 -51
- simo/core/templates/core/auto_state_script.py +0 -78
- simo/generic/scripting/__pycache__/helpers.cpython-38.pyc +0 -0
- /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.41.dist-info → simo-2.6.2.dist-info}/LICENSE.md +0 -0
- {simo-2.5.41.dist-info → simo-2.6.2.dist-info}/WHEEL +0 -0
- {simo-2.5.41.dist-info → simo-2.6.2.dist-info}/entry_points.txt +0 -0
- {simo-2.5.41.dist-info → simo-2.6.2.dist-info}/top_level.txt +0 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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.
|
simo/automation/forms.py
ADDED
|
@@ -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
|