simo 2.8.15__py3-none-any.whl → 2.10.1__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 (64) hide show
  1. simo/automation/__pycache__/gateways.cpython-312.pyc +0 -0
  2. simo/automation/gateways.py +12 -10
  3. simo/core/__pycache__/admin.cpython-312.pyc +0 -0
  4. simo/core/__pycache__/auto_urls.cpython-312.pyc +0 -0
  5. simo/core/__pycache__/controllers.cpython-312.pyc +0 -0
  6. simo/core/__pycache__/models.cpython-312.pyc +0 -0
  7. simo/core/__pycache__/serializers.cpython-312.pyc +0 -0
  8. simo/core/__pycache__/tasks.cpython-312.pyc +0 -0
  9. simo/core/__pycache__/views.cpython-312.pyc +0 -0
  10. simo/core/admin.py +5 -2
  11. simo/core/auto_urls.py +4 -1
  12. simo/core/controllers.py +42 -5
  13. simo/core/models.py +32 -16
  14. simo/core/serializers.py +2 -2
  15. simo/core/tasks.py +8 -1
  16. simo/core/templates/admin/core/component_change_form.html +1 -1
  17. simo/core/templates/admin/wizard/discovery.html +3 -4
  18. simo/core/templates/admin/wizard/wizard_add.html +1 -1
  19. simo/core/views.py +26 -2
  20. simo/fleet/__pycache__/api.cpython-312.pyc +0 -0
  21. simo/fleet/__pycache__/base_types.cpython-312.pyc +0 -0
  22. simo/fleet/__pycache__/controllers.cpython-312.pyc +0 -0
  23. simo/fleet/__pycache__/forms.cpython-312.pyc +0 -0
  24. simo/fleet/__pycache__/managers.cpython-312.pyc +0 -0
  25. simo/fleet/__pycache__/models.cpython-312.pyc +0 -0
  26. simo/fleet/__pycache__/serializers.cpython-312.pyc +0 -0
  27. simo/fleet/__pycache__/socket_consumers.cpython-312.pyc +0 -0
  28. simo/fleet/api.py +26 -3
  29. simo/fleet/base_types.py +1 -0
  30. simo/fleet/controllers.py +240 -7
  31. simo/fleet/custom_dali_operations.py +275 -0
  32. simo/fleet/forms.py +132 -3
  33. simo/fleet/managers.py +3 -1
  34. simo/fleet/migrations/0045_alter_colonel_type_customdalidevice.py +29 -0
  35. simo/fleet/migrations/0046_delete_customdalidevice.py +16 -0
  36. simo/fleet/migrations/0047_customdalidevice.py +28 -0
  37. simo/fleet/migrations/0048_remove_customdalidevice_colonel_and_more.py +28 -0
  38. simo/fleet/migrations/__pycache__/0045_alter_colonel_type_customdalidevice.cpython-312.pyc +0 -0
  39. simo/fleet/migrations/__pycache__/0046_delete_customdalidevice.cpython-312.pyc +0 -0
  40. simo/fleet/migrations/__pycache__/0047_customdalidevice.cpython-312.pyc +0 -0
  41. simo/fleet/migrations/__pycache__/0048_remove_customdalidevice_colonel_and_more.cpython-312.pyc +0 -0
  42. simo/fleet/models.py +54 -9
  43. simo/fleet/serializers.py +15 -1
  44. simo/fleet/socket_consumers.py +6 -0
  45. simo/fleet/tasks.py +22 -2
  46. simo/fleet/templates/fleet/controllers_info/RoomZonePresenceSensor.md +8 -0
  47. simo/generic/__pycache__/controllers.cpython-312.pyc +0 -0
  48. simo/generic/__pycache__/forms.cpython-312.pyc +0 -0
  49. simo/generic/__pycache__/gateways.cpython-312.pyc +0 -0
  50. simo/generic/controllers.py +99 -43
  51. simo/generic/forms.py +13 -10
  52. simo/generic/gateways.py +91 -2
  53. simo/generic/migrations/0003_auto_20250409_1404.py +33 -0
  54. simo/generic/migrations/__pycache__/0003_auto_20250409_1404.cpython-312.pyc +0 -0
  55. simo/users/__pycache__/api.cpython-312.pyc +0 -0
  56. simo/users/__pycache__/dynamic_settings.cpython-312.pyc +0 -0
  57. simo/users/api.py +71 -18
  58. simo/users/dynamic_settings.py +1 -1
  59. {simo-2.8.15.dist-info → simo-2.10.1.dist-info}/METADATA +1 -1
  60. {simo-2.8.15.dist-info → simo-2.10.1.dist-info}/RECORD +64 -52
  61. {simo-2.8.15.dist-info → simo-2.10.1.dist-info}/WHEEL +0 -0
  62. {simo-2.8.15.dist-info → simo-2.10.1.dist-info}/entry_points.txt +0 -0
  63. {simo-2.8.15.dist-info → simo-2.10.1.dist-info}/licenses/LICENSE.md +0 -0
  64. {simo-2.8.15.dist-info → simo-2.10.1.dist-info}/top_level.txt +0 -0
@@ -165,7 +165,7 @@ class GatesHandler:
165
165
  if iu_id != iuser.id:
166
166
  continue
167
167
  gate = Component.objects.get(id=gate_id)
168
- if is_out:
168
+ if is_out > 4:
169
169
  print(
170
170
  f"{iuser.user.name} is out, "
171
171
  f"let's see if we must open the gates for him"
@@ -174,17 +174,17 @@ class GatesHandler:
174
174
  # he is now coming back and open the gate for him
175
175
  if self._is_in_geofence(gate, iuser.last_seen_location):
176
176
  print("Yes he is back in a geofence! Open THE GATEEE!!")
177
- self.gate_iusers[gate_id][iuser.id] = False
178
- gate.open()
177
+ self.gate_iusers[gate_id][iuser.id] = 0
178
+ if iuser.last_seen_speed_kmh > 10:
179
+ gate.open()
179
180
  else:
180
181
  print("No he is not back yet.")
181
182
  else:
182
183
  print(f"Check if {iuser.user.name} is out.")
183
- self.gate_iusers[gate_id][iuser.id] = self._is_out_of_geofence(
184
- gate, iuser.last_seen_location
185
- )
186
- if self.gate_iusers[gate_id][iuser.id]:
187
- print(f"YES {iuser.user.name} is out!")
184
+ if self._is_out_of_geofence(gate, iuser.last_seen_location):
185
+ self.gate_iusers[gate_id][iuser.id] += 1
186
+ if self.gate_iusers[gate_id][iuser.id] > 4:
187
+ print(f"YES {iuser.user.name} is truly out!")
188
188
 
189
189
  def watch_gates(self):
190
190
  drop_current_instance()
@@ -204,9 +204,11 @@ class GatesHandler:
204
204
  self.gate_iusers[gate.id] = {}
205
205
  if iuser.id not in self.gate_iusers[gate.id]:
206
206
  if iuser.last_seen_location:
207
- self.gate_iusers[gate.id][iuser.id] = self._is_out_of_geofence(
207
+ self.gate_iusers[gate.id][iuser.id] = 0
208
+ if self._is_out_of_geofence(
208
209
  gate, iuser.last_seen_location
209
- )
210
+ ):
211
+ self.gate_iusers[gate.id][iuser.id] += 1
210
212
  iuser.on_change(self.check_gates)
211
213
 
212
214
 
Binary file
Binary file
Binary file
simo/core/admin.py CHANGED
@@ -366,7 +366,7 @@ class ComponentAdmin(EasyObjectsDeleteMixin, admin.ModelAdmin):
366
366
  pop_fields_from_form(ctx['form'])
367
367
  if ctx['form'].is_valid():
368
368
  if ctx['form'].controller.is_discoverable:
369
- ctx['form'].controller.init_discovery(
369
+ ctx['form'].controller._init_discovery(
370
370
  ctx['form'].cleaned_data
371
371
  )
372
372
  ctx['discovery_msg'] = ctx['form'].controller.discovery_msg
@@ -383,6 +383,9 @@ class ComponentAdmin(EasyObjectsDeleteMixin, admin.ModelAdmin):
383
383
  'components-list',
384
384
  kwargs={'instance_slug': instance.slug}
385
385
  )
386
+ ctx['finish_url'] = reverse(
387
+ 'finish-discovery',
388
+ ) + f"?uid={ctx['form'].controller.uid}"
386
389
  return render(
387
390
  request, 'admin/wizard/discovery.html', ctx
388
391
  )
@@ -486,7 +489,7 @@ class ComponentAdmin(EasyObjectsDeleteMixin, admin.ModelAdmin):
486
489
  def info(self, obj):
487
490
  if not obj.controller:
488
491
  return
489
- info = obj.controller.info()
492
+ info = obj.controller.info(obj)
490
493
  if not info:
491
494
  return
492
495
  return mark_safe(
simo/core/auto_urls.py CHANGED
@@ -1,6 +1,7 @@
1
1
  from django.urls import path
2
2
  from .views import (
3
- get_timestamp, upgrade, restart, reboot, set_instance, delete_instance
3
+ get_timestamp, upgrade, restart, reboot, set_instance, delete_instance,
4
+ finish_discovery
4
5
  )
5
6
  from .autocomplete_views import (
6
7
  IconModelAutocomplete,
@@ -31,6 +32,8 @@ urlpatterns = [
31
32
  ComponentAutocomplete.as_view(), name='autocomplete-component'
32
33
  ),
33
34
  path('set-instance/<slug:instance_slug>/', set_instance, name='set-instance'),
35
+ path('finish-discovery/',
36
+ finish_discovery, name='finish-discovery'),
34
37
  path('upgrade/', upgrade, name='upgrade'),
35
38
  path('restart/', restart, name='restart'),
36
39
  path('reboot/', reboot, name='reboot'),
simo/core/controllers.py CHANGED
@@ -111,9 +111,7 @@ class ControllerBase(ABC):
111
111
  @classmethod
112
112
  def is_discoverable(cls):
113
113
  return hasattr(
114
- cls, 'init_discovery'
115
- ) and hasattr(
116
- cls, '_process_discovery'
114
+ cls, '_init_discovery'
117
115
  )
118
116
 
119
117
  @classmethod
@@ -633,6 +631,15 @@ class Dimmer(ControllerBase, TimerMixin, OnOffPokerMixin):
633
631
  else:
634
632
  self.send(self.component.config.get('max', 90))
635
633
 
634
+ def max_out(self):
635
+ self.send(self.component.config.get('max', 90))
636
+
637
+ def output_percent(self, value):
638
+ min = self.component.config.get('min', 0)
639
+ max = self.component.config.get('max', 100)
640
+ delta = max - min
641
+ self.send(min + delta * value / 100)
642
+
636
643
  def toggle(self):
637
644
  self.component.refresh_from_db()
638
645
  if self.component.value:
@@ -847,21 +854,51 @@ class Switch(MultiSwitchBase, TimerMixin, OnOffPokerMixin):
847
854
  default_value = False
848
855
 
849
856
  def turn_on(self):
857
+ if self.component.meta.get('pulse'):
858
+ self.component.meta.pop('pulse')
859
+ self.component.save()
850
860
  self.send(True)
851
861
 
852
862
  def turn_off(self):
863
+ if self.component.meta.get('pulse'):
864
+ self.component.meta.pop('pulse')
865
+ self.component.save()
853
866
  self.send(False)
854
867
 
855
868
  def toggle(self):
869
+ if self.component.meta.get('pulse'):
870
+ self.component.meta.pop('pulse')
871
+ self.component.save()
856
872
  self.send(not self.component.value)
857
873
 
858
874
  def click(self):
859
875
  '''
860
876
  Gateway specific implementation is very welcome of this!
861
877
  '''
878
+ if self.component.meta.get('pulse'):
879
+ self.component.meta.pop('pulse')
880
+ self.component.save()
862
881
  self.turn_on()
863
- time.sleep(0.5)
864
- self.turn_off()
882
+ from .tasks import component_action
883
+ component_action.s(
884
+ self.component.id, 'turn_off'
885
+ ).apply_async(countdown=1)
886
+
887
+ def pulse(self, frame_length_s, on_percentage):
888
+ self.component.meta['pulse'] = {
889
+ 'frame': frame_length_s, 'duty': on_percentage / 100
890
+ }
891
+ self.component.save()
892
+ from simo.generic.gateways import GenericGatewayHandler
893
+ from .models import Gateway
894
+ generic_gateway = Gateway.objects.filter(
895
+ type=GenericGatewayHandler.uid
896
+ ).first()
897
+ if generic_gateway:
898
+ GatewayObjectCommand(
899
+ generic_gateway, self.component,
900
+ pulse=self.component.meta['pulse']
901
+ ).publish()
865
902
 
866
903
 
867
904
  class DoubleSwitch(MultiSwitchBase):
simo/core/models.py CHANGED
@@ -294,28 +294,44 @@ class Gateway(DirtyFieldsMixin, models.Model, SimoAdminMixin):
294
294
  )
295
295
  if result:
296
296
  self.refresh_from_db()
297
- if not isinstance(result, dict) and isinstance(result, Iterable):
298
- for res in result:
299
- if isinstance(res, models.Model):
300
- if res.pk not in self.discovery['result']:
301
- self.discovery['result'].append(res.pk)
302
- else:
303
- if res not in self.discovery['result']:
304
- self.discovery['result'].append(res)
305
- else:
306
- if isinstance(result, models.Model):
307
- if result.pk not in self.discovery['result']:
308
- self.discovery['result'].append(result.pk)
309
- else:
310
- if result not in self.discovery['result']:
311
- self.discovery['result'].append(result)
312
-
297
+ self.append_discovery_result(result)
313
298
 
314
299
  self.save(update_fields=['discovery'])
315
300
 
301
+
316
302
  def finish_discovery(self):
303
+ from .utils.type_constants import CONTROLLER_TYPES_MAP
317
304
  self.discovery['finished'] = time.time()
318
305
  self.save(update_fields=['discovery'])
306
+ ControllerClass = CONTROLLER_TYPES_MAP.get(
307
+ self.discovery['controller_uid']
308
+ )
309
+ if hasattr(ControllerClass, '_finish_discovery'):
310
+ result = ControllerClass._finish_discovery(
311
+ self.discovery['init_data']
312
+ )
313
+ self.append_discovery_result(result)
314
+ self.save(update_fields=['discovery'])
315
+
316
+
317
+ def append_discovery_result(self, result):
318
+ if not isinstance(result, dict) and isinstance(result, Iterable):
319
+ for res in result:
320
+ if isinstance(res, models.Model):
321
+ if res.pk not in self.discovery['result']:
322
+ self.discovery['result'].append(res.pk)
323
+ else:
324
+ if res not in self.discovery['result']:
325
+ self.discovery['result'].append(res)
326
+ else:
327
+ if isinstance(result, models.Model):
328
+ if result.pk not in self.discovery['result']:
329
+ self.discovery['result'].append(result.pk)
330
+ else:
331
+ if result not in self.discovery['result']:
332
+ self.discovery['result'].append(result)
333
+
334
+
319
335
 
320
336
 
321
337
  class Component(DirtyFieldsMixin, models.Model, SimoAdminMixin, OnChangeMixin):
simo/core/serializers.py CHANGED
@@ -544,7 +544,7 @@ class ComponentSerializer(FormSerializer):
544
544
  )
545
545
  if form.is_valid():
546
546
  if form.controller.is_discoverable:
547
- form.controller.init_discovery(form.cleaned_data)
547
+ form.controller._init_discovery(form.cleaned_data)
548
548
  return form.save(commit=False)
549
549
  return form.save(commit=True)
550
550
  raise serializers.ValidationError(form.errors)
@@ -554,7 +554,7 @@ class ComponentSerializer(FormSerializer):
554
554
 
555
555
  def get_info(self, obj):
556
556
  if obj.controller:
557
- return obj.controller.info()
557
+ return obj.controller.info(obj)
558
558
 
559
559
  def get_read_only(self, obj):
560
560
  user = self.context.get('user')
simo/core/tasks.py CHANGED
@@ -16,11 +16,18 @@ from django.utils import timezone
16
16
  from actstream.models import Action
17
17
  from simo.conf import dynamic_settings
18
18
  from simo.core.utils.helpers import get_self_ip
19
- from simo.core.middleware import introduce_instance
19
+ from simo.core.middleware import introduce_instance, drop_current_instance
20
20
  from simo.users.models import PermissionsRole, InstanceUser
21
21
  from .models import Instance, Component, ComponentHistory, HistoryAggregate
22
22
 
23
23
 
24
+ @celery_app.task
25
+ def component_action(comp_id, method, args=None, kwargs=None):
26
+ drop_current_instance()
27
+ component = Component.objects.get(id=comp_id)
28
+ getattr(component, method)(*args, **kwargs)
29
+
30
+
24
31
  @celery_app.task
25
32
  def supervisor_restart():
26
33
  time.sleep(2)
@@ -56,7 +56,7 @@
56
56
  <h2 style="background: linear-gradient(0.3turn, #5c7ca5, #5a95df);">Info</h2>
57
57
  <div class="form-row">
58
58
  {% if original.controller %}
59
- {{ original.info|markdownify }}
59
+ {{ original.info|markdownify|linebreaksbr }}
60
60
  {% endif %}
61
61
  </div>
62
62
  </div>
@@ -64,8 +64,7 @@
64
64
 
65
65
  <div id="running-discovery">
66
66
  <p style="margin: 30px 15px">
67
- <i class="fas fa-spinner fa-spin fa-lg" style="margin-right: 10px"></i> DISCOVERY MODE ACTIVATED! <br>
68
- Your new components will appear down bellow once discovered.
67
+ <i class="fas fa-spinner fa-spin fa-lg" style="margin-right: 10px"></i> DISCOVERY MODE ACTIVATED!
69
68
  </p>
70
69
  {% if discovery_msg %}
71
70
  <p style="margin: 30px 15px">{{ discovery_msg }}</p>
@@ -108,8 +107,8 @@
108
107
 
109
108
  <div class="submit-row" style="text-align: left;">
110
109
 
111
- <a href="{% url 'admin:core_component_changelist' %}" class="cancel-link" style="display: block; padding:9px 15px">
112
- <i class="fa fa-check"></i> Done
110
+ <a href="{{ finish_url }}" class="cancel-link" style="display: block; padding:9px 15px">
111
+ <i class="fa fa-check"></i> Finish
113
112
  </a>
114
113
 
115
114
  </div>
@@ -63,7 +63,7 @@
63
63
  {% if info %}
64
64
  <div class="form-row">
65
65
  <div class="markdownified-info">
66
- {{ info|markdownify }}
66
+ {{ info|markdownify|linebreaksbr }}
67
67
  </div>
68
68
  </div>
69
69
  {% endif %}
simo/core/views.py CHANGED
@@ -9,7 +9,7 @@ from django.http import (
9
9
  )
10
10
  from django.contrib import messages
11
11
  from simo.conf import dynamic_settings
12
- from .models import Instance
12
+ from .models import Instance, Component, Gateway
13
13
  from .tasks import update as update_task, supervisor_restart, hardware_reboot
14
14
  from .middleware import introduce_instance
15
15
 
@@ -91,4 +91,28 @@ def hub_info(request):
91
91
  request.get_host()
92
92
  ):
93
93
  data['hub_secret'] = dynamic_settings['core__hub_secret']
94
- return JsonResponse(data)
94
+ return JsonResponse(data)
95
+
96
+
97
+ @login_required
98
+ def finish_discovery(request):
99
+ # finish discovery function for admin discovery view
100
+ if not request.user.is_authenticated:
101
+ raise Http404()
102
+ if not request.user.is_master:
103
+ raise Http404()
104
+ result = None
105
+ for gateway in Gateway.objects.filter(
106
+ discovery__start__gt=time.time() - 60 * 60, # no more than an hour
107
+ discovery__controller_uid=request.GET['uid']
108
+ ):
109
+ gateway.finish_discovery()
110
+ for res in gateway.discovery['result']:
111
+ comp = Component.objects.filter(
112
+ controller_uid=request.GET['uid'], pk=res
113
+ ).first()
114
+ if comp:
115
+ result = comp
116
+ if result:
117
+ return redirect(result.get_admin_url())
118
+ return reverse('admin:core_component_changelist')
Binary file
simo/fleet/api.py CHANGED
@@ -4,11 +4,13 @@ from rest_framework import viewsets
4
4
  from rest_framework.response import Response as RESTResponse
5
5
  from rest_framework.decorators import action
6
6
  from rest_framework.exceptions import ValidationError as APIValidationError
7
+ from django_filters.rest_framework import DjangoFilterBackend
7
8
  from simo.core.api import InstanceMixin
8
9
  from simo.core.permissions import IsInstanceSuperuser
9
- from .models import InstanceOptions, Colonel, Interface
10
+ from .models import InstanceOptions, Colonel, Interface, CustomDaliDevice
10
11
  from .serializers import (
11
- InstanceOptionsSerializer, ColonelSerializer, ColonelInterfaceSerializer
12
+ InstanceOptionsSerializer, ColonelSerializer, ColonelInterfaceSerializer,
13
+ CustomDaliDeviceSerializer
12
14
  )
13
15
 
14
16
 
@@ -99,4 +101,25 @@ class InterfaceViewSet(
99
101
  return permissions
100
102
 
101
103
  def get_queryset(self):
102
- return Interface.objects.filter(colonel__instance=self.instance)
104
+ return Interface.objects.filter(colonel__instance=self.instance)
105
+
106
+
107
+ class CustomDaliDeviceViewSet(
108
+ InstanceMixin,
109
+ viewsets.mixins.RetrieveModelMixin, viewsets.mixins.UpdateModelMixin,
110
+ viewsets.mixins.ListModelMixin, viewsets.mixins.CreateModelMixin,
111
+ viewsets.GenericViewSet
112
+ ):
113
+ url = 'fleet/custom-dali-devices'
114
+ basename = 'custom-dali-devices'
115
+ serializer_class = CustomDaliDeviceSerializer
116
+ filter_backends = [DjangoFilterBackend]
117
+ filterset_fields = ['random_address', 'name']
118
+
119
+ def get_permissions(self):
120
+ permissions = super().get_permissions()
121
+ permissions.append(IsInstanceSuperuser())
122
+ return permissions
123
+
124
+ def get_queryset(self):
125
+ return CustomDaliDevice.objects.filter(instance=self.instance)
simo/fleet/base_types.py CHANGED
@@ -2,4 +2,5 @@ from django.utils.translation import gettext_lazy as _
2
2
 
3
3
  BASE_TYPES = {
4
4
  'dali': _("Dali Device"),
5
+ 'room-sensor': _("Room Sensor"),
5
6
  }