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.

Files changed (60) hide show
  1. simo/automation/__pycache__/gateways.cpython-38.pyc +0 -0
  2. simo/automation/__pycache__/helpers.cpython-38.pyc +0 -0
  3. simo/automation/__pycache__/models.cpython-38.pyc +0 -0
  4. simo/automation/__pycache__/serializers.cpython-38.pyc +0 -0
  5. simo/automation/gateways.py +45 -25
  6. simo/core/__pycache__/admin.cpython-38.pyc +0 -0
  7. simo/core/__pycache__/api_meta.cpython-38.pyc +0 -0
  8. simo/core/__pycache__/auto_urls.cpython-38.pyc +0 -0
  9. simo/core/__pycache__/form_fields.cpython-38.pyc +0 -0
  10. simo/core/__pycache__/forms.cpython-38.pyc +0 -0
  11. simo/core/__pycache__/models.cpython-38.pyc +0 -0
  12. simo/core/__pycache__/serializers.cpython-38.pyc +0 -0
  13. simo/core/__pycache__/signal_receivers.cpython-38.pyc +0 -0
  14. simo/core/__pycache__/socket_consumers.cpython-38.pyc +0 -0
  15. simo/core/__pycache__/views.cpython-38.pyc +0 -0
  16. simo/core/api_meta.py +3 -1
  17. simo/core/auto_urls.py +3 -2
  18. simo/core/form_fields.py +7 -1
  19. simo/core/forms.py +13 -5
  20. simo/core/migrations/0047_alter_component_value_translation.py +18 -0
  21. simo/core/migrations/0048_publicfile_privatefile.py +35 -0
  22. simo/core/migrations/__pycache__/0047_alter_component_value_translation.cpython-38.pyc +0 -0
  23. simo/core/migrations/__pycache__/0048_publicfile_privatefile.cpython-38.pyc +0 -0
  24. simo/core/models.py +32 -0
  25. simo/core/serializers.py +7 -1
  26. simo/core/signal_receivers.py +13 -2
  27. simo/core/templates/admin/wizard/wizard_add.html +1 -1
  28. simo/core/views.py +16 -2
  29. simo/fleet/__pycache__/forms.cpython-38.pyc +0 -0
  30. simo/fleet/__pycache__/socket_consumers.cpython-38.pyc +0 -0
  31. simo/fleet/__pycache__/views.cpython-38.pyc +0 -0
  32. simo/fleet/migrations/__pycache__/0044_auto_20241210_0707.cpython-38.pyc +0 -0
  33. simo/generic/__pycache__/controllers.cpython-38.pyc +0 -0
  34. simo/generic/__pycache__/forms.cpython-38.pyc +0 -0
  35. simo/generic/__pycache__/gateways.cpython-38.pyc +0 -0
  36. simo/generic/__pycache__/socket_consumers.cpython-38.pyc +0 -0
  37. simo/generic/controllers.py +10 -1
  38. simo/generic/forms.py +75 -1
  39. simo/generic/gateways.py +50 -9
  40. simo/multimedia/__pycache__/admin.cpython-38.pyc +0 -0
  41. simo/multimedia/__pycache__/auto_urls.cpython-38.pyc +0 -0
  42. simo/multimedia/__pycache__/controllers.cpython-38.pyc +0 -0
  43. simo/multimedia/__pycache__/models.cpython-38.pyc +0 -0
  44. simo/multimedia/__pycache__/views.cpython-38.pyc +0 -0
  45. simo/multimedia/admin.py +4 -4
  46. simo/multimedia/auto_urls.py +10 -0
  47. simo/multimedia/controllers.py +12 -4
  48. simo/multimedia/migrations/0005_remove_sound_slug_sound_date_uploaded.py +24 -0
  49. simo/multimedia/migrations/__pycache__/0005_remove_sound_slug_sound_date_uploaded.cpython-38.pyc +0 -0
  50. simo/multimedia/models.py +16 -2
  51. simo/multimedia/views.py +19 -0
  52. simo/notifications/__pycache__/utils.cpython-38.pyc +0 -0
  53. simo/users/__pycache__/models.cpython-38.pyc +0 -0
  54. {simo-2.7.10.dist-info → simo-2.7.12.dist-info}/METADATA +1 -1
  55. {simo-2.7.10.dist-info → simo-2.7.12.dist-info}/RECORD +59 -49
  56. simo/multimedia/requirements.txt +0 -2
  57. {simo-2.7.10.dist-info → simo-2.7.12.dist-info}/LICENSE.md +0 -0
  58. {simo-2.7.10.dist-info → simo-2.7.12.dist-info}/WHEEL +0 -0
  59. {simo-2.7.10.dist-info → simo-2.7.12.dist-info}/entry_points.txt +0 -0
  60. {simo-2.7.10.dist-info → simo-2.7.12.dist-info}/top_level.txt +0 -0
@@ -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
- print("------FINISH-----")
79
- self.component.set('finished')
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, 'error')
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.id not in self.terminating_scripts:
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, daemon=True
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
- else:
378
+ elif stop_status == 'stopped':
364
379
  logger.log(logging.INFO, "-------STOP-------")
365
- self.running_scripts[component.id].terminate()
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
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
- self.instance.config[field_name] = \
121
- self.cleaned_data[field_name]
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
+ ]
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
 
@@ -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 Instance, Gateway, Component, Icon, Zone, Category
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 {% if has_file_field %}enctype="multipart/form-data" {% endif %}action="{{ form_url }}" method="post" id="{{ opts.model_name }}_form" novalidate>{% csrf_token %}{% block form_top %}{% endblock %}
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 HttpResponse, Http404, JsonResponse
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
@@ -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)