simo 2.7.10__py3-none-any.whl → 2.7.12__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/automation/__pycache__/gateways.cpython-38.pyc +0 -0
- simo/automation/__pycache__/helpers.cpython-38.pyc +0 -0
- simo/automation/__pycache__/models.cpython-38.pyc +0 -0
- simo/automation/__pycache__/serializers.cpython-38.pyc +0 -0
- simo/automation/gateways.py +45 -25
- simo/core/__pycache__/admin.cpython-38.pyc +0 -0
- simo/core/__pycache__/api_meta.cpython-38.pyc +0 -0
- simo/core/__pycache__/auto_urls.cpython-38.pyc +0 -0
- simo/core/__pycache__/form_fields.cpython-38.pyc +0 -0
- simo/core/__pycache__/forms.cpython-38.pyc +0 -0
- simo/core/__pycache__/models.cpython-38.pyc +0 -0
- simo/core/__pycache__/serializers.cpython-38.pyc +0 -0
- simo/core/__pycache__/signal_receivers.cpython-38.pyc +0 -0
- simo/core/__pycache__/socket_consumers.cpython-38.pyc +0 -0
- simo/core/__pycache__/views.cpython-38.pyc +0 -0
- simo/core/api_meta.py +3 -1
- simo/core/auto_urls.py +3 -2
- simo/core/form_fields.py +7 -1
- simo/core/forms.py +13 -5
- simo/core/migrations/0047_alter_component_value_translation.py +18 -0
- simo/core/migrations/0048_publicfile_privatefile.py +35 -0
- simo/core/migrations/__pycache__/0047_alter_component_value_translation.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0048_publicfile_privatefile.cpython-38.pyc +0 -0
- simo/core/models.py +32 -0
- simo/core/serializers.py +7 -1
- simo/core/signal_receivers.py +13 -2
- simo/core/templates/admin/wizard/wizard_add.html +1 -1
- simo/core/views.py +16 -2
- simo/fleet/__pycache__/forms.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/socket_consumers.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/views.cpython-38.pyc +0 -0
- simo/fleet/migrations/__pycache__/0044_auto_20241210_0707.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/__pycache__/socket_consumers.cpython-38.pyc +0 -0
- simo/generic/controllers.py +10 -1
- simo/generic/forms.py +75 -1
- simo/generic/gateways.py +50 -9
- simo/multimedia/__pycache__/admin.cpython-38.pyc +0 -0
- simo/multimedia/__pycache__/auto_urls.cpython-38.pyc +0 -0
- simo/multimedia/__pycache__/controllers.cpython-38.pyc +0 -0
- simo/multimedia/__pycache__/models.cpython-38.pyc +0 -0
- simo/multimedia/__pycache__/views.cpython-38.pyc +0 -0
- simo/multimedia/admin.py +4 -4
- simo/multimedia/auto_urls.py +10 -0
- simo/multimedia/controllers.py +12 -4
- simo/multimedia/migrations/0005_remove_sound_slug_sound_date_uploaded.py +24 -0
- simo/multimedia/migrations/__pycache__/0005_remove_sound_slug_sound_date_uploaded.cpython-38.pyc +0 -0
- simo/multimedia/models.py +16 -2
- simo/multimedia/views.py +19 -0
- simo/notifications/__pycache__/utils.cpython-38.pyc +0 -0
- simo/users/__pycache__/models.cpython-38.pyc +0 -0
- {simo-2.7.10.dist-info → simo-2.7.12.dist-info}/METADATA +1 -1
- {simo-2.7.10.dist-info → simo-2.7.12.dist-info}/RECORD +59 -49
- simo/multimedia/requirements.txt +0 -2
- {simo-2.7.10.dist-info → simo-2.7.12.dist-info}/LICENSE.md +0 -0
- {simo-2.7.10.dist-info → simo-2.7.12.dist-info}/WHEEL +0 -0
- {simo-2.7.10.dist-info → simo-2.7.12.dist-info}/entry_points.txt +0 -0
- {simo-2.7.10.dist-info → simo-2.7.12.dist-info}/top_level.txt +0 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
simo/automation/gateways.py
CHANGED
|
@@ -33,9 +33,12 @@ class ScriptRunHandler(multiprocessing.Process):
|
|
|
33
33
|
component = None
|
|
34
34
|
logger = None
|
|
35
35
|
|
|
36
|
-
def __init__(self, component_id, *args, **kwargs):
|
|
36
|
+
def __init__(self, component_id, exit_event, *args, **kwargs):
|
|
37
37
|
super().__init__(*args, **kwargs)
|
|
38
38
|
self.component_id = component_id
|
|
39
|
+
self.exit_event = exit_event
|
|
40
|
+
self.exit_in_use = multiprocessing.Event()
|
|
41
|
+
self.exin_in_use_fail = multiprocessing.Event()
|
|
39
42
|
|
|
40
43
|
def run(self):
|
|
41
44
|
db_connection.connect()
|
|
@@ -51,34 +54,40 @@ class ScriptRunHandler(multiprocessing.Process):
|
|
|
51
54
|
sys.stderr = StreamToLogger(self.logger, logging.ERROR)
|
|
52
55
|
self.component.meta['pid'] = os.getpid()
|
|
53
56
|
self.component.set('running')
|
|
54
|
-
|
|
55
|
-
if hasattr(self.component.controller, '_run'):
|
|
56
|
-
def run_code():
|
|
57
|
-
self.component.controller._run()
|
|
58
|
-
else:
|
|
59
|
-
code = self.component.config.get('code')
|
|
60
|
-
def run_code():
|
|
61
|
-
start = time.time()
|
|
62
|
-
exec(code, globals())
|
|
63
|
-
if 'class Automation:' in code and time.time() - start < 1:
|
|
64
|
-
Automation().run()
|
|
65
|
-
|
|
66
|
-
if not code:
|
|
67
|
-
self.component.value = 'finished'
|
|
68
|
-
self.component.save(update_fields=['value'])
|
|
69
|
-
return
|
|
70
57
|
print("------START-------")
|
|
71
58
|
try:
|
|
72
|
-
run_code()
|
|
59
|
+
self.run_code()
|
|
73
60
|
except:
|
|
74
61
|
print("------ERROR------")
|
|
75
62
|
self.component.set('error')
|
|
76
63
|
raise
|
|
77
64
|
else:
|
|
78
|
-
|
|
79
|
-
|
|
65
|
+
if not self.exit_event.is_set():
|
|
66
|
+
print("------FINISH-----")
|
|
67
|
+
self.component.set('finished')
|
|
80
68
|
return
|
|
81
69
|
|
|
70
|
+
def run_code(self):
|
|
71
|
+
if hasattr(self.component.controller, '_run'):
|
|
72
|
+
self.component.controller._run()
|
|
73
|
+
else:
|
|
74
|
+
code = self.component.config.get('code')
|
|
75
|
+
if not code:
|
|
76
|
+
self.component.value = 'finished'
|
|
77
|
+
self.component.save(update_fields=['value'])
|
|
78
|
+
return
|
|
79
|
+
start = time.time()
|
|
80
|
+
namespace = {}
|
|
81
|
+
exec(code, namespace)
|
|
82
|
+
if 'Automation' in namespace and time.time() - start < 1:
|
|
83
|
+
self.exit_in_use.set()
|
|
84
|
+
try:
|
|
85
|
+
namespace['Automation']().run(self.exit_event)
|
|
86
|
+
except:
|
|
87
|
+
self.exin_in_use_fail.set()
|
|
88
|
+
namespace['Automation']().run()
|
|
89
|
+
|
|
90
|
+
|
|
82
91
|
|
|
83
92
|
class GatesHandler:
|
|
84
93
|
'''
|
|
@@ -225,6 +234,9 @@ class AutomationsGatewayHandler(GatesHandler, BaseObjectCommandsGatewayHandler):
|
|
|
225
234
|
dead_scripts = False
|
|
226
235
|
for id, process in list(self.running_scripts.items()):
|
|
227
236
|
comp = Component.objects.filter(id=id).first()
|
|
237
|
+
if comp.value == 'finished':
|
|
238
|
+
self.running_scripts.pop(id)
|
|
239
|
+
continue
|
|
228
240
|
if process.is_alive():
|
|
229
241
|
if not comp and id not in self.terminating_scripts:
|
|
230
242
|
# script is deleted, or instance deactivated
|
|
@@ -236,7 +248,7 @@ class AutomationsGatewayHandler(GatesHandler, BaseObjectCommandsGatewayHandler):
|
|
|
236
248
|
if comp:
|
|
237
249
|
logger = get_component_logger(comp)
|
|
238
250
|
logger.log(logging.INFO, "-------DEAD!-------")
|
|
239
|
-
self.stop_script(comp, '
|
|
251
|
+
self.stop_script(comp, 'dead')
|
|
240
252
|
|
|
241
253
|
if dead_scripts:
|
|
242
254
|
# give 10s air before we wake these dead scripts up!
|
|
@@ -326,7 +338,9 @@ class AutomationsGatewayHandler(GatesHandler, BaseObjectCommandsGatewayHandler):
|
|
|
326
338
|
def start_script(self, component):
|
|
327
339
|
print("START SCRIPT %s" % str(component))
|
|
328
340
|
if component.id in self.running_scripts:
|
|
329
|
-
if component.
|
|
341
|
+
if component.value == 'finished':
|
|
342
|
+
self.running_scripts.pop(component.id)
|
|
343
|
+
elif component.id not in self.terminating_scripts:
|
|
330
344
|
if component.value != 'running':
|
|
331
345
|
component.value = 'running'
|
|
332
346
|
component.save()
|
|
@@ -343,7 +357,8 @@ class AutomationsGatewayHandler(GatesHandler, BaseObjectCommandsGatewayHandler):
|
|
|
343
357
|
return self.stop_script(component, 'error')
|
|
344
358
|
|
|
345
359
|
self.running_scripts[component.id] = ScriptRunHandler(
|
|
346
|
-
component.id,
|
|
360
|
+
component.id, multiprocessing.Event(),
|
|
361
|
+
daemon=True
|
|
347
362
|
)
|
|
348
363
|
self.running_scripts[component.id].start()
|
|
349
364
|
|
|
@@ -360,9 +375,14 @@ class AutomationsGatewayHandler(GatesHandler, BaseObjectCommandsGatewayHandler):
|
|
|
360
375
|
logger = get_component_logger(component)
|
|
361
376
|
if stop_status == 'error':
|
|
362
377
|
logger.log(logging.INFO, "-------GATEWAY STOP-------")
|
|
363
|
-
|
|
378
|
+
elif stop_status == 'stopped':
|
|
364
379
|
logger.log(logging.INFO, "-------STOP-------")
|
|
365
|
-
|
|
380
|
+
|
|
381
|
+
if self.running_scripts[component.id].exit_in_use.is_set()\
|
|
382
|
+
and not self.running_scripts[component.id].exin_in_use_fail.is_set():
|
|
383
|
+
self.running_scripts[component.id].exit_event.set()
|
|
384
|
+
else:
|
|
385
|
+
self.running_scripts[component.id].terminate()
|
|
366
386
|
|
|
367
387
|
def kill():
|
|
368
388
|
start = time.time()
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
simo/core/api_meta.py
CHANGED
|
@@ -8,7 +8,8 @@ from simo.core.models import Icon, Instance, Category, Zone
|
|
|
8
8
|
from simo.core.middleware import introduce_instance
|
|
9
9
|
from .serializers import (
|
|
10
10
|
HiddenSerializerField, ComponentManyToManyRelatedField,
|
|
11
|
-
TextAreaSerializerField, Component, LocationSerializer
|
|
11
|
+
TextAreaSerializerField, Component, LocationSerializer,
|
|
12
|
+
SoundSerializer
|
|
12
13
|
)
|
|
13
14
|
|
|
14
15
|
|
|
@@ -33,6 +34,7 @@ class SIMOAPIMetadata(SimpleMetadata):
|
|
|
33
34
|
serializers.MultipleChoiceField: 'multiple choice',
|
|
34
35
|
serializers.FileField: 'file upload',
|
|
35
36
|
serializers.ImageField: 'image upload',
|
|
37
|
+
SoundSerializer: 'sound upload',
|
|
36
38
|
serializers.ListField: 'list',
|
|
37
39
|
serializers.DictField: 'nested object',
|
|
38
40
|
serializers.Serializer: 'nested object',
|
simo/core/auto_urls.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from django.urls import path
|
|
2
2
|
from .views import (
|
|
3
|
-
get_timestamp, upgrade, restart, reboot, set_instance
|
|
3
|
+
get_timestamp, upgrade, restart, reboot, set_instance, delete_instance
|
|
4
4
|
)
|
|
5
5
|
from .autocomplete_views import (
|
|
6
6
|
IconModelAutocomplete,
|
|
@@ -33,5 +33,6 @@ urlpatterns = [
|
|
|
33
33
|
path('set-instance/<slug:instance_slug>/', set_instance, name='set-instance'),
|
|
34
34
|
path('upgrade/', upgrade, name='upgrade'),
|
|
35
35
|
path('restart/', restart, name='restart'),
|
|
36
|
-
path('reboot/', reboot, name='reboot')
|
|
36
|
+
path('reboot/', reboot, name='reboot'),
|
|
37
|
+
path('delete-instance/', delete_instance, name='delete-instance')
|
|
37
38
|
]
|
simo/core/form_fields.py
CHANGED
|
@@ -182,4 +182,10 @@ class PlainLocationField(forms.fields.CharField):
|
|
|
182
182
|
if attr in kwargs:
|
|
183
183
|
dwargs[attr] = kwargs[attr]
|
|
184
184
|
|
|
185
|
-
super(PlainLocationField, self).__init__(*args, **dwargs)
|
|
185
|
+
super(PlainLocationField, self).__init__(*args, **dwargs)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class SoundField(forms.fields.FileField):
|
|
190
|
+
pass
|
|
191
|
+
|
simo/core/forms.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import traceback
|
|
1
|
+
import traceback, json
|
|
2
2
|
from dal import forward
|
|
3
3
|
from django.contrib.admin.forms import AdminAuthenticationForm as OrgAdminAuthenticationForm
|
|
4
4
|
from django.db import models
|
|
@@ -117,8 +117,11 @@ class ConfigFieldsMixin:
|
|
|
117
117
|
obj.pk for obj in self.cleaned_data[field_name]
|
|
118
118
|
]
|
|
119
119
|
else:
|
|
120
|
-
|
|
121
|
-
self.
|
|
120
|
+
try:
|
|
121
|
+
self.instance.config[field_name] = \
|
|
122
|
+
json.loads(json.dumps(self.cleaned_data[field_name]))
|
|
123
|
+
except:
|
|
124
|
+
continue
|
|
122
125
|
|
|
123
126
|
if commit:
|
|
124
127
|
from simo.users.middleware import get_current_user
|
|
@@ -269,9 +272,9 @@ class ComponentAdminForm(forms.ModelForm):
|
|
|
269
272
|
self.instance.config = self.controller.default_config
|
|
270
273
|
self.instance.meta = self.controller.default_meta
|
|
271
274
|
|
|
272
|
-
self.cleanup_missing_keys(kwargs.get("data"))
|
|
275
|
+
self.cleanup_missing_keys(kwargs.get("data"), kwargs.get("files"))
|
|
273
276
|
|
|
274
|
-
def cleanup_missing_keys(self, data):
|
|
277
|
+
def cleanup_missing_keys(self, data, files=None):
|
|
275
278
|
"""
|
|
276
279
|
Removes missing keys from fields on form submission.
|
|
277
280
|
This avoids resetting fields that are not present in
|
|
@@ -286,6 +289,11 @@ class ComponentAdminForm(forms.ModelForm):
|
|
|
286
289
|
return
|
|
287
290
|
|
|
288
291
|
got_keys = list(data.keys())
|
|
292
|
+
if files:
|
|
293
|
+
try:
|
|
294
|
+
got_keys += list(files.keys())
|
|
295
|
+
except:
|
|
296
|
+
pass
|
|
289
297
|
formset_fields = set()
|
|
290
298
|
for key in got_keys:
|
|
291
299
|
if key.endswith('-TOTAL_FORMS'):
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Generated by Django 4.2.10 on 2024-12-11 14:14
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
('core', '0046_component_value_translation_alter_gateway_type'),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.AlterField(
|
|
14
|
+
model_name='component',
|
|
15
|
+
name='value_translation',
|
|
16
|
+
field=models.TextField(blank=True, default="def translate(value, occasion):\n if occasion == 'before-set':\n return value\n else: # 'before-send'\n return value", help_text='Adjust this to make value translations before value isset on to a component and before it is sent to a device from your SIMO.io smart home instance.'),
|
|
17
|
+
),
|
|
18
|
+
]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Generated by Django 4.2.10 on 2024-12-13 09:44
|
|
2
|
+
|
|
3
|
+
import django.core.files.storage
|
|
4
|
+
from django.db import migrations, models
|
|
5
|
+
import django.db.models.deletion
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Migration(migrations.Migration):
|
|
9
|
+
|
|
10
|
+
dependencies = [
|
|
11
|
+
('core', '0047_alter_component_value_translation'),
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
operations = [
|
|
15
|
+
migrations.CreateModel(
|
|
16
|
+
name='PublicFile',
|
|
17
|
+
fields=[
|
|
18
|
+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
19
|
+
('file', models.FileField(storage=django.core.files.storage.FileSystemStorage(base_url='/public_media/', location='/home/simanas/Projects/SIMO/_var/public_media'), upload_to='public_files')),
|
|
20
|
+
('date_uploaded', models.DateTimeField(auto_now_add=True)),
|
|
21
|
+
('meta', models.JSONField(default=dict)),
|
|
22
|
+
('component', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='public_files', to='core.component')),
|
|
23
|
+
],
|
|
24
|
+
),
|
|
25
|
+
migrations.CreateModel(
|
|
26
|
+
name='PrivateFile',
|
|
27
|
+
fields=[
|
|
28
|
+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
29
|
+
('file', models.FileField(upload_to='private_files')),
|
|
30
|
+
('date_uploaded', models.DateTimeField(auto_now_add=True)),
|
|
31
|
+
('meta', models.JSONField(default=dict)),
|
|
32
|
+
('component', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='private_files', to='core.component')),
|
|
33
|
+
],
|
|
34
|
+
),
|
|
35
|
+
]
|
|
Binary file
|
simo/core/models.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import inspect
|
|
2
2
|
import time
|
|
3
|
+
import os
|
|
3
4
|
from collections.abc import Iterable
|
|
4
5
|
from django.utils.text import slugify
|
|
5
6
|
from django.utils.functional import cached_property
|
|
@@ -9,6 +10,8 @@ from django.db import models
|
|
|
9
10
|
from django.db.models.signals import post_delete
|
|
10
11
|
from django.dispatch import receiver
|
|
11
12
|
from django.template.loader import render_to_string
|
|
13
|
+
from django.conf import settings
|
|
14
|
+
from django.core.files.storage import FileSystemStorage
|
|
12
15
|
from timezone_utils.choices import ALL_TIMEZONES_CHOICES
|
|
13
16
|
from location_field.models.plain import PlainLocationField
|
|
14
17
|
from model_utils import FieldTracker
|
|
@@ -602,6 +605,35 @@ def is_in_alarm(self):
|
|
|
602
605
|
return c_methods
|
|
603
606
|
|
|
604
607
|
|
|
608
|
+
class PublicFile(models.Model):
|
|
609
|
+
component = models.ForeignKey(
|
|
610
|
+
Component, on_delete=models.CASCADE, related_name='public_files'
|
|
611
|
+
)
|
|
612
|
+
file = models.FileField(
|
|
613
|
+
upload_to='public_files', storage=FileSystemStorage(
|
|
614
|
+
location=os.path.join(settings.VAR_DIR, 'public_media'),
|
|
615
|
+
base_url='/public_media/'
|
|
616
|
+
)
|
|
617
|
+
)
|
|
618
|
+
date_uploaded = models.DateTimeField(auto_now_add=True)
|
|
619
|
+
meta = models.JSONField(default=dict)
|
|
620
|
+
|
|
621
|
+
def get_absolute_url(self):
|
|
622
|
+
return self.file.url
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
class PrivateFile(models.Model):
|
|
626
|
+
component = models.ForeignKey(
|
|
627
|
+
Component, on_delete=models.CASCADE, related_name='private_files'
|
|
628
|
+
)
|
|
629
|
+
file = models.FileField(upload_to='private_files')
|
|
630
|
+
date_uploaded = models.DateTimeField(auto_now_add=True)
|
|
631
|
+
meta = models.JSONField(default=dict)
|
|
632
|
+
|
|
633
|
+
def get_absolute_url(self):
|
|
634
|
+
return self.file.url
|
|
635
|
+
|
|
636
|
+
|
|
605
637
|
class ComponentHistory(models.Model):
|
|
606
638
|
component = models.ForeignKey(
|
|
607
639
|
Component, on_delete=models.CASCADE, related_name='history'
|
simo/core/serializers.py
CHANGED
|
@@ -16,7 +16,7 @@ from simo.core.forms import HiddenField, FormsetField
|
|
|
16
16
|
from simo.core.form_fields import (
|
|
17
17
|
Select2ListChoiceField, Select2ModelChoiceField,
|
|
18
18
|
Select2ListMultipleChoiceField, Select2ModelMultipleChoiceField,
|
|
19
|
-
PlainLocationField
|
|
19
|
+
PlainLocationField, SoundField
|
|
20
20
|
)
|
|
21
21
|
from simo.core.models import Component
|
|
22
22
|
from rest_framework.relations import PrimaryKeyRelatedField, ManyRelatedField
|
|
@@ -33,6 +33,10 @@ class LocationSerializer(serializers.CharField):
|
|
|
33
33
|
pass
|
|
34
34
|
|
|
35
35
|
|
|
36
|
+
class SoundSerializer(serializers.FileField):
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
|
|
36
40
|
|
|
37
41
|
class TimestampField(serializers.Field):
|
|
38
42
|
|
|
@@ -145,6 +149,7 @@ class ComponentFormsetField(FormSerializer):
|
|
|
145
149
|
field_mapping = {
|
|
146
150
|
HiddenField: HiddenSerializerField,
|
|
147
151
|
PlainLocationField: LocationSerializer,
|
|
152
|
+
SoundField: SoundSerializer,
|
|
148
153
|
Select2ListChoiceField: serializers.ChoiceField,
|
|
149
154
|
forms.ModelChoiceField: FormsetPrimaryKeyRelatedField,
|
|
150
155
|
Select2ModelChoiceField: FormsetPrimaryKeyRelatedField,
|
|
@@ -306,6 +311,7 @@ class ComponentSerializer(FormSerializer):
|
|
|
306
311
|
Select2ListMultipleChoiceField: ComponentManyToManyRelatedField,
|
|
307
312
|
Select2ModelMultipleChoiceField: ComponentManyToManyRelatedField,
|
|
308
313
|
FormsetField: ComponentFormsetField,
|
|
314
|
+
SoundField: SoundSerializer
|
|
309
315
|
}
|
|
310
316
|
|
|
311
317
|
|
simo/core/signal_receivers.py
CHANGED
|
@@ -8,7 +8,9 @@ from django.conf import settings
|
|
|
8
8
|
from django.template.loader import render_to_string
|
|
9
9
|
from actstream import action
|
|
10
10
|
from simo.users.models import PermissionsRole
|
|
11
|
-
from .models import
|
|
11
|
+
from .models import (
|
|
12
|
+
Instance, Gateway, Component, Icon, Zone, Category, PublicFile, PrivateFile
|
|
13
|
+
)
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
@receiver(post_save, sender=Instance)
|
|
@@ -192,4 +194,13 @@ def gateway_post_save(sender, instance, created, *args, **kwargs):
|
|
|
192
194
|
|
|
193
195
|
@receiver(post_delete, sender=Gateway)
|
|
194
196
|
def gateway_post_delete(sender, instance, *args, **kwargs):
|
|
195
|
-
instance.stop()
|
|
197
|
+
instance.stop()
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@receiver(post_delete, sender=PublicFile)
|
|
201
|
+
@receiver(post_delete, sender=PrivateFile)
|
|
202
|
+
def delete_file_itself(sender, instance, *args, **kwargs):
|
|
203
|
+
try:
|
|
204
|
+
os.remove(instance.file.path)
|
|
205
|
+
except:
|
|
206
|
+
pass
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
</ul>
|
|
34
34
|
{% endif %}{% endif %}
|
|
35
35
|
{% endblock %}
|
|
36
|
-
<form
|
|
36
|
+
<form enctype="multipart/form-data" action="{{ form_url }}" method="post" id="{{ opts.model_name }}_form" novalidate>{% csrf_token %}{% block form_top %}{% endblock %}
|
|
37
37
|
<div>
|
|
38
38
|
{% if is_popup %}<input type="hidden" name="{{ is_popup_var }}" value="1">{% endif %}
|
|
39
39
|
{% if to_field %}<input type="hidden" name="{{ to_field_var }}" value="{{ to_field }}">{% endif %}
|
simo/core/views.py
CHANGED
|
@@ -2,9 +2,11 @@ import time
|
|
|
2
2
|
import re
|
|
3
3
|
from django.contrib.auth.decorators import login_required
|
|
4
4
|
from django.urls import reverse
|
|
5
|
-
from django.shortcuts import redirect
|
|
5
|
+
from django.shortcuts import redirect, get_object_or_404
|
|
6
6
|
from django.views.decorators.csrf import csrf_exempt
|
|
7
|
-
from django.http import
|
|
7
|
+
from django.http import (
|
|
8
|
+
HttpResponse, Http404, JsonResponse, HttpResponseForbidden
|
|
9
|
+
)
|
|
8
10
|
from django.contrib import messages
|
|
9
11
|
from simo.conf import dynamic_settings
|
|
10
12
|
from .models import Instance
|
|
@@ -69,6 +71,18 @@ def set_instance(request, instance_slug):
|
|
|
69
71
|
return redirect(reverse('admin:index'))
|
|
70
72
|
|
|
71
73
|
|
|
74
|
+
@login_required
|
|
75
|
+
@csrf_exempt
|
|
76
|
+
def delete_instance(request):
|
|
77
|
+
if request.method != 'POST':
|
|
78
|
+
raise Http404()
|
|
79
|
+
if not request.user.is_master:
|
|
80
|
+
return HttpResponseForbidden()
|
|
81
|
+
instance = get_object_or_404(Instance, uid=request.GET['uid'])
|
|
82
|
+
instance.delete()
|
|
83
|
+
return HttpResponse('success')
|
|
84
|
+
|
|
85
|
+
|
|
72
86
|
def hub_info(request):
|
|
73
87
|
data = {"hub_uid": dynamic_settings['core__hub_uid']}
|
|
74
88
|
if not Instance.objects.filter(is_active=True).count():
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
simo/generic/controllers.py
CHANGED
|
@@ -37,7 +37,7 @@ from .forms import (
|
|
|
37
37
|
ThermostatConfigForm, AlarmGroupConfigForm,
|
|
38
38
|
IPCameraConfigForm, WeatherForm,
|
|
39
39
|
WateringConfigForm, StateSelectForm, MainStateSelectForm,
|
|
40
|
-
AlarmClockConfigForm
|
|
40
|
+
AlarmClockConfigForm, AudioAlertConfigForm
|
|
41
41
|
)
|
|
42
42
|
|
|
43
43
|
# ----------- Generic controllers -----------------------------
|
|
@@ -1054,6 +1054,12 @@ class AlarmClock(ControllerBase):
|
|
|
1054
1054
|
return current_value
|
|
1055
1055
|
|
|
1056
1056
|
|
|
1057
|
+
class AudioAlert(Switch):
|
|
1058
|
+
gateway_class = GenericGatewayHandler
|
|
1059
|
+
name = _("Audio Alert")
|
|
1060
|
+
config_form = AudioAlertConfigForm
|
|
1061
|
+
|
|
1062
|
+
|
|
1057
1063
|
class StateSelect(ControllerBase):
|
|
1058
1064
|
gateway_class = GenericGatewayHandler
|
|
1059
1065
|
name = _("State select")
|
|
@@ -1188,6 +1194,9 @@ class MainState(StateSelect):
|
|
|
1188
1194
|
return any(phones_on_charge)
|
|
1189
1195
|
|
|
1190
1196
|
|
|
1197
|
+
|
|
1198
|
+
|
|
1199
|
+
|
|
1191
1200
|
# ----------- Dummy controllers -----------------------------
|
|
1192
1201
|
|
|
1193
1202
|
class DummyBinarySensor(BinarySensor):
|
simo/generic/forms.py
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import librosa
|
|
3
|
+
import tempfile
|
|
1
4
|
from django import forms
|
|
2
5
|
from django.forms import formset_factory
|
|
3
6
|
from django.db.models import Q
|
|
4
7
|
from django.utils.translation import gettext_lazy as _
|
|
5
8
|
from simo.core.forms import HiddenField, BaseComponentForm
|
|
6
|
-
from simo.core.models import Icon, Component
|
|
9
|
+
from simo.core.models import Icon, Component, PublicFile
|
|
7
10
|
from simo.core.controllers import (
|
|
8
11
|
NumericSensor, MultiSensor, Switch, Dimmer
|
|
9
12
|
)
|
|
@@ -17,6 +20,7 @@ from simo.core.form_fields import (
|
|
|
17
20
|
Select2ModelMultipleChoiceField
|
|
18
21
|
)
|
|
19
22
|
from simo.core.forms import DimmerConfigForm, SwitchForm
|
|
23
|
+
from simo.core.form_fields import SoundField
|
|
20
24
|
|
|
21
25
|
ACTION_METHODS = (
|
|
22
26
|
('turn_on', "Turn ON"), ('turn_off', "Turn OFF"),
|
|
@@ -613,3 +617,73 @@ class AlarmClockConfigForm(BaseComponentForm):
|
|
|
613
617
|
if c:
|
|
614
618
|
obj.slaves.add(c)
|
|
615
619
|
return obj
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
class AudioAlertConfigForm(BaseComponentForm):
|
|
623
|
+
sound = SoundField(required=False)
|
|
624
|
+
loop = forms.BooleanField(initial=False, required=False)
|
|
625
|
+
volume = forms.IntegerField(initial=30, min_value=2, max_value=100)
|
|
626
|
+
players = Select2ModelMultipleChoiceField(
|
|
627
|
+
queryset=Component.objects.all(),#filter(base_type='audio-player'),
|
|
628
|
+
url='autocomplete-component',
|
|
629
|
+
)
|
|
630
|
+
|
|
631
|
+
def __init__(self, *args, **kwargs):
|
|
632
|
+
super().__init__(*args, **kwargs)
|
|
633
|
+
self.basic_fields.extend(['sound', 'loop', 'volume', 'players'])
|
|
634
|
+
if self.instance.id:
|
|
635
|
+
public_file = PublicFile.objects.filter(
|
|
636
|
+
component=self.instance
|
|
637
|
+
).first()
|
|
638
|
+
if public_file:
|
|
639
|
+
self.fields['sound'].help_text = f"Currently: {public_file.file}"
|
|
640
|
+
|
|
641
|
+
def clean_sound(self):
|
|
642
|
+
if not self.cleaned_data.get('sound'):
|
|
643
|
+
if not self.instance.pk:
|
|
644
|
+
raise forms.ValidationError("Please pick a sound!")
|
|
645
|
+
return
|
|
646
|
+
if self.cleaned_data['sound'].size > 1024 * 1024 * 50:
|
|
647
|
+
raise forms.ValidationError("No more than 50Mb please.")
|
|
648
|
+
temp_path = os.path.join(
|
|
649
|
+
tempfile.gettempdir(), self.cleaned_data['sound'].name
|
|
650
|
+
)
|
|
651
|
+
with open(temp_path, 'wb') as temp_file:
|
|
652
|
+
for chunk in self.cleaned_data['sound'].chunks():
|
|
653
|
+
temp_file.write(chunk)
|
|
654
|
+
|
|
655
|
+
try:
|
|
656
|
+
self.cleaned_data['sound'].duration = int(
|
|
657
|
+
librosa.core.get_duration(sr=22050, filename=temp_path)
|
|
658
|
+
)
|
|
659
|
+
except:
|
|
660
|
+
try:
|
|
661
|
+
os.remove(temp_path)
|
|
662
|
+
except:
|
|
663
|
+
pass
|
|
664
|
+
raise forms.ValidationError("This doesn't look like audio file!")
|
|
665
|
+
try:
|
|
666
|
+
os.remove(temp_path)
|
|
667
|
+
except:
|
|
668
|
+
pass
|
|
669
|
+
|
|
670
|
+
return self.cleaned_data['sound']
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
def save(self, commit=True):
|
|
674
|
+
if commit and self.cleaned_data.get('sound') \
|
|
675
|
+
and self.cleaned_data['sound'] != self.fields['sound'].initial:
|
|
676
|
+
public_file = PublicFile(component=self.instance)
|
|
677
|
+
public_file.file.save(
|
|
678
|
+
self.cleaned_data['sound'].name, self.cleaned_data['sound'],
|
|
679
|
+
save=True
|
|
680
|
+
)
|
|
681
|
+
org = PublicFile.objects.filter(
|
|
682
|
+
id=self.instance.config.get('public_file_id', 0)
|
|
683
|
+
)
|
|
684
|
+
if org:
|
|
685
|
+
org.delete()
|
|
686
|
+
self.instance.config['public_file_id'] = public_file.id
|
|
687
|
+
self.instance.config['duration'] = self.cleaned_data['sound'].duration
|
|
688
|
+
#self.cleaned_data.pop('sound')
|
|
689
|
+
return super().save(commit=commit)
|