simo 2.11.4__py3-none-any.whl → 3.0.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 (90) hide show
  1. simo/__pycache__/settings.cpython-312.pyc +0 -0
  2. simo/asgi.py +25 -6
  3. simo/automation/__pycache__/controllers.cpython-312.pyc +0 -0
  4. simo/automation/controllers.py +18 -2
  5. simo/automation/forms.py +15 -24
  6. simo/core/__pycache__/admin.cpython-312.pyc +0 -0
  7. simo/core/__pycache__/base_types.cpython-312.pyc +0 -0
  8. simo/core/__pycache__/controllers.cpython-312.pyc +0 -0
  9. simo/core/__pycache__/forms.cpython-312.pyc +0 -0
  10. simo/core/__pycache__/models.cpython-312.pyc +0 -0
  11. simo/core/__pycache__/serializers.cpython-312.pyc +0 -0
  12. simo/core/__pycache__/signal_receivers.cpython-312.pyc +0 -0
  13. simo/core/__pycache__/tasks.cpython-312.pyc +0 -0
  14. simo/core/admin.py +5 -4
  15. simo/core/base_types.py +191 -18
  16. simo/core/controllers.py +259 -26
  17. simo/core/forms.py +10 -2
  18. simo/core/management/_hub_template/hub/nginx.conf +23 -50
  19. simo/core/management/_hub_template/hub/supervisor.conf +15 -0
  20. simo/core/mcp.py +154 -0
  21. simo/core/migrations/0051_instance_ai_memory.py +18 -0
  22. simo/core/migrations/__pycache__/0051_instance_ai_memory.cpython-312.pyc +0 -0
  23. simo/core/models.py +3 -0
  24. simo/core/serializers.py +120 -0
  25. simo/core/signal_receivers.py +1 -1
  26. simo/core/tasks.py +1 -3
  27. simo/core/utils/__pycache__/type_constants.cpython-312.pyc +0 -0
  28. simo/core/utils/type_constants.py +78 -17
  29. simo/fleet/__pycache__/admin.cpython-312.pyc +0 -0
  30. simo/fleet/__pycache__/api.cpython-312.pyc +0 -0
  31. simo/fleet/__pycache__/base_types.cpython-312.pyc +0 -0
  32. simo/fleet/__pycache__/controllers.cpython-312.pyc +0 -0
  33. simo/fleet/__pycache__/forms.cpython-312.pyc +0 -0
  34. simo/fleet/__pycache__/gateways.cpython-312.pyc +0 -0
  35. simo/fleet/__pycache__/models.cpython-312.pyc +0 -0
  36. simo/fleet/__pycache__/serializers.cpython-312.pyc +0 -0
  37. simo/fleet/admin.py +5 -1
  38. simo/fleet/api.py +2 -27
  39. simo/fleet/base_types.py +35 -4
  40. simo/fleet/controllers.py +150 -156
  41. simo/fleet/forms.py +56 -88
  42. simo/fleet/gateways.py +8 -15
  43. simo/fleet/migrations/0055_colonel_is_vo_active_colonel_last_wake_and_more.py +28 -0
  44. simo/fleet/migrations/0056_delete_customdalidevice.py +16 -0
  45. simo/fleet/migrations/__pycache__/0055_colonel_is_vo_active_colonel_last_wake_and_more.cpython-312.pyc +0 -0
  46. simo/fleet/migrations/__pycache__/0056_delete_customdalidevice.cpython-312.pyc +0 -0
  47. simo/fleet/models.py +13 -72
  48. simo/fleet/serializers.py +1 -48
  49. simo/fleet/socket_consumers.py +100 -39
  50. simo/fleet/tasks.py +2 -22
  51. simo/fleet/voice_assistant.py +893 -0
  52. simo/generic/__pycache__/base_types.cpython-312.pyc +0 -0
  53. simo/generic/__pycache__/controllers.cpython-312.pyc +0 -0
  54. simo/generic/__pycache__/gateways.cpython-312.pyc +0 -0
  55. simo/generic/base_types.py +70 -10
  56. simo/generic/controllers.py +102 -15
  57. simo/generic/gateways.py +10 -10
  58. simo/mcp_server/__init__.py +0 -0
  59. simo/mcp_server/__pycache__/__init__.cpython-312.pyc +0 -0
  60. simo/mcp_server/__pycache__/admin.cpython-312.pyc +0 -0
  61. simo/mcp_server/__pycache__/models.cpython-312.pyc +0 -0
  62. simo/mcp_server/admin.py +18 -0
  63. simo/mcp_server/app.py +4 -0
  64. simo/mcp_server/auth.py +34 -0
  65. simo/mcp_server/dummy.py +22 -0
  66. simo/mcp_server/migrations/0001_initial.py +30 -0
  67. simo/mcp_server/migrations/0002_alter_instanceaccesstoken_date_expired.py +18 -0
  68. simo/mcp_server/migrations/0003_instanceaccesstoken_issuer.py +18 -0
  69. simo/mcp_server/migrations/__init__.py +0 -0
  70. simo/mcp_server/migrations/__pycache__/0001_initial.cpython-312.pyc +0 -0
  71. simo/mcp_server/migrations/__pycache__/0002_alter_instanceaccesstoken_date_expired.cpython-312.pyc +0 -0
  72. simo/mcp_server/migrations/__pycache__/0003_instanceaccesstoken_issuer.cpython-312.pyc +0 -0
  73. simo/mcp_server/migrations/__pycache__/__init__.cpython-312.pyc +0 -0
  74. simo/mcp_server/models.py +27 -0
  75. simo/mcp_server/server.py +60 -0
  76. simo/mcp_server/tasks.py +19 -0
  77. simo/multimedia/__pycache__/base_types.cpython-312.pyc +0 -0
  78. simo/multimedia/__pycache__/controllers.cpython-312.pyc +0 -0
  79. simo/multimedia/base_types.py +29 -4
  80. simo/multimedia/controllers.py +66 -19
  81. simo/settings.py +1 -0
  82. simo/users/__pycache__/utils.cpython-312.pyc +0 -0
  83. simo/users/utils.py +10 -0
  84. {simo-2.11.4.dist-info → simo-3.0.1.dist-info}/METADATA +12 -4
  85. {simo-2.11.4.dist-info → simo-3.0.1.dist-info}/RECORD +89 -63
  86. simo/fleet/custom_dali_operations.py +0 -287
  87. {simo-2.11.4.dist-info → simo-3.0.1.dist-info}/WHEEL +0 -0
  88. {simo-2.11.4.dist-info → simo-3.0.1.dist-info}/entry_points.txt +0 -0
  89. {simo-2.11.4.dist-info → simo-3.0.1.dist-info}/licenses/LICENSE.md +0 -0
  90. {simo-2.11.4.dist-info → simo-3.0.1.dist-info}/top_level.txt +0 -0
simo/core/controllers.py CHANGED
@@ -14,6 +14,13 @@ from simo.users.utils import introduce_user, get_current_user, get_device_user
14
14
  from .utils.helpers import is_hex_color, classproperty
15
15
  # from django.utils.functional import classproperty
16
16
  from .gateways import BaseGatewayHandler
17
+ from .base_types import (
18
+ BaseComponentType,
19
+ NumericSensorType, MultiSensorType, BinarySensorType, ButtonType,
20
+ DimmerType, DimmerPlusType, RGBWLightType, SwitchType,
21
+ DoubleSwitchType, TripleSwitchType, QuadrupleSwitchType, QuintupleSwitchType,
22
+ LockType, BlindsType, GateType
23
+ )
17
24
  from .app_widgets import *
18
25
  from .forms import (
19
26
  BaseComponentForm, NumericSensorForm,
@@ -29,7 +36,24 @@ BEFORE_SEND = 'before-send'
29
36
  BEFORE_SET = 'before-set'
30
37
 
31
38
 
32
- class ControllerBase(ABC):
39
+ class ControllerMeta(ABCMeta):
40
+ """Metaclass that normalizes class-level access to `base_type`.
41
+
42
+ If a controller sets `base_type` to a BaseComponentType subclass,
43
+ accessing `Controller.base_type` returns the slug string to preserve
44
+ legacy comparisons. Assignment remains the class.
45
+ """
46
+ def __getattribute__(cls, name):
47
+ val = super().__getattribute__(name)
48
+ if name == 'base_type':
49
+ if isinstance(val, str):
50
+ return val
51
+ if isinstance(val, type) and issubclass(val, BaseComponentType):
52
+ return val.slug
53
+ return val
54
+
55
+
56
+ class ControllerBase(ABC, metaclass=ControllerMeta):
33
57
  config_form = BaseComponentForm
34
58
  admin_widget_template = 'admin/controller_widgets/generic.html'
35
59
  default_config = {}
@@ -40,6 +64,7 @@ class ControllerBase(ABC):
40
64
  family = None
41
65
  masters_only = False # component can be created/modified by hub masters only
42
66
  info_template_path = None
67
+ accepts_value = True
43
68
 
44
69
  @property
45
70
  @abstractmethod
@@ -57,9 +82,9 @@ class ControllerBase(ABC):
57
82
 
58
83
  @property
59
84
  @abstractmethod
60
- def base_type(self) -> str:
61
- """
62
- :return" base type name
85
+ def base_type(self):
86
+ """Base type identifier. Accepts either a slug string (legacy)
87
+ or a BaseComponentType subclass (preferred).
63
88
  """
64
89
 
65
90
  @property
@@ -91,8 +116,18 @@ class ControllerBase(ABC):
91
116
  assert issubclass(self.gateway_class, BaseGatewayHandler)
92
117
  assert issubclass(self.config_form, BaseComponentForm)
93
118
  assert issubclass(self.app_widget, BaseAppWidget)
94
- assert self.base_type in ALL_BASE_TYPES, \
95
- f"{self.base_type} must be defined in BASE_TYPES!"
119
+ # Normalize to slug whether base_type is a string or a BaseComponentType subclass
120
+ bt = getattr(self.__class__, 'base_type', None)
121
+ # ControllerMeta makes class attribute access return slug when
122
+ # a BaseComponentType subclass is assigned to base_type.
123
+ # So `bt` should already be a slug string in most cases.
124
+ if isinstance(bt, str):
125
+ slug = bt
126
+ elif isinstance(bt, type) and issubclass(bt, BaseComponentType):
127
+ slug = bt.slug
128
+ else:
129
+ slug = getattr(bt, 'slug', None)
130
+ assert slug in ALL_BASE_TYPES, f"{slug} must be defined in BASE TYPES!"
96
131
 
97
132
  @classproperty
98
133
  @classmethod
@@ -230,6 +265,12 @@ class ControllerBase(ABC):
230
265
  return vals
231
266
 
232
267
  def send(self, value):
268
+ """Send a value/command to the device via the gateway.
269
+
270
+ Prefer controller-specific helpers (e.g., `turn_on()`, `open()`) when
271
+ available. This performs validation, publishes to the gateway, and
272
+ updates the component value when appropriate.
273
+ """
233
274
  from .models import Component
234
275
  try:
235
276
  self.component.refresh_from_db()
@@ -270,7 +311,17 @@ class ControllerBase(ABC):
270
311
  self.component.value_previous = self.component.value
271
312
  self.component.value = value
272
313
 
273
- def set(self, value, actor=None):
314
+ def set(self, value, actor=None, alive=True, error_msg=None):
315
+ """Set the component value locally and record history.
316
+
317
+ This is called by `send()` after the device confirms or when device
318
+ reports a new value. Do not call from clients directly; prefer
319
+ controller action methods or `send()`.
320
+
321
+ Parameters:
322
+ - value: JSON-serializable value after translation/validation for this type.
323
+ - actor: Optional user that initiated the change.
324
+ """
274
325
  from simo.users.models import InstanceUser
275
326
  if self.component.value_translation:
276
327
  try:
@@ -308,6 +359,8 @@ class ControllerBase(ABC):
308
359
  self.component.change_init_date = None
309
360
  self.component.change_init_to = None
310
361
  self.component.change_init_fingerprint = None
362
+ self.component.alive = alive
363
+ self.component.error_msg = error_msg
311
364
  self.component.change_actor = InstanceUser.objects.filter(
312
365
  instance=self.component.zone.instance,
313
366
  user=actor
@@ -372,7 +425,11 @@ class ControllerBase(ABC):
372
425
  ]
373
426
 
374
427
  def poke(self):
375
- '''Use this when component is dead to try and wake it up'''
428
+ """Best-effort wake-up: quickly toggle output to nudge the device.
429
+
430
+ Some gateways/devices can recover from a brief toggle. This flips
431
+ the state on subsequent calls to stimulate a response.
432
+ """
376
433
  pass
377
434
 
378
435
  def _prepare_for_send(self, value):
@@ -392,6 +449,20 @@ class TimerMixin:
392
449
  "Controller must have toggle method defined to support timer mixin."
393
450
 
394
451
  def set_timer(self, to_timestamp, event=None):
452
+ """Schedule a controller action at a specific UNIX timestamp.
453
+
454
+ Parameters:
455
+ - to_timestamp (float|int): Absolute UNIX epoch seconds in the future.
456
+ - event (str|None): Optional controller method name to invoke when the
457
+ timer elapses. Defaults to 'toggle' if not provided or invalid.
458
+
459
+ Behavior:
460
+ - Stores timer metadata in component.meta and persists it. The gateway or
461
+ background tasks are expected to call the method when due.
462
+
463
+ Raises:
464
+ - ValidationError: if `to_timestamp` is not in the future.
465
+ """
395
466
  if to_timestamp > time.time():
396
467
  self.component.refresh_from_db()
397
468
  self.component.meta['timer_to'] = to_timestamp
@@ -409,6 +480,13 @@ class TimerMixin:
409
480
  )
410
481
 
411
482
  def pause_timer(self):
483
+ """Pause a running timer.
484
+
485
+ Stores the remaining time and clears the scheduled timestamp.
486
+
487
+ Raises:
488
+ - ValidationError: if no timer is currently scheduled.
489
+ """
412
490
  if self.component.meta.get('timer_to', 0) > time.time():
413
491
  time_left = self.component.meta['timer_to'] - time.time()
414
492
  self.component.meta['timer_left'] = time_left
@@ -420,6 +498,13 @@ class TimerMixin:
420
498
  )
421
499
 
422
500
  def resume_timer(self):
501
+ """Resume a previously paused timer.
502
+
503
+ Recomputes the target timestamp from the saved remaining time.
504
+
505
+ Raises:
506
+ - ValidationError: if the timer is not in a paused state.
507
+ """
423
508
  if self.component.meta.get('timer_left', 0):
424
509
  self.component.meta['timer_to'] = \
425
510
  time.time() + self.component.meta['timer_left']
@@ -431,12 +516,14 @@ class TimerMixin:
431
516
  )
432
517
 
433
518
  def stop_timer(self):
519
+ """Stop and clear any active or paused timer for this component."""
434
520
  self.component.meta['timer_to'] = 0
435
521
  self.component.meta['timer_left'] = 0
436
522
  self.component.meta['timer_start'] = 0
437
523
  self.component.save(update_fields=['meta'])
438
524
 
439
525
  def timer_engaged(self):
526
+ """Return True if there is an active or paused timer configured."""
440
527
  return any([
441
528
  self.component.meta.get('timer_to'),
442
529
  self.component.meta.get('timer_left'),
@@ -449,9 +536,10 @@ class TimerMixin:
449
536
 
450
537
  class NumericSensor(ControllerBase):
451
538
  name = _("Numeric sensor")
452
- base_type = 'numeric-sensor'
539
+ base_type = NumericSensorType
453
540
  config_form = NumericSensorForm
454
541
  default_value = 0
542
+ accepts_value = False
455
543
 
456
544
  def _validate_val(self, value, occasion=None):
457
545
  if type(value) not in (int, float, D):
@@ -473,7 +561,7 @@ class NumericSensor(ControllerBase):
473
561
 
474
562
  class MultiSensor(ControllerBase):
475
563
  name = _("Multi sensor")
476
- base_type = 'multi-sensor'
564
+ base_type = MultiSensorType
477
565
  app_widget = MultiSensorWidget
478
566
  config_form = MultiSensorConfigForm
479
567
  default_value = [
@@ -481,6 +569,7 @@ class MultiSensor(ControllerBase):
481
569
  ["Value 2", 50, "ᴼ C"],
482
570
  ["Value 3", False, ""]
483
571
  ]
572
+ accepts_value = False
484
573
 
485
574
  def _validate_val(self, value, occasion=None):
486
575
  if len(value) != len(self.default_value):
@@ -520,6 +609,12 @@ class MultiSensor(ControllerBase):
520
609
 
521
610
 
522
611
  def get_val(self, param):
612
+ """Return value for a given label in a multi-sensor array.
613
+
614
+ Parameters:
615
+ - param (str): The label/name of the vector to read.
616
+ Returns: The value part from [label, value, unit] or None.
617
+ """
523
618
  for item in self.component.value:
524
619
  if item[0] == param:
525
620
  return item[1]
@@ -527,10 +622,11 @@ class MultiSensor(ControllerBase):
527
622
 
528
623
  class BinarySensor(ControllerBase):
529
624
  name = _("Binary sensor")
530
- base_type = 'binary-sensor'
625
+ base_type = BinarySensorType
531
626
  app_widget = BinarySensorWidget
532
627
  admin_widget_template = 'admin/controller_widgets/binary_sensor.html'
533
628
  default_value = False
629
+ accepts_value = False
534
630
 
535
631
  def _validate_val(self, value, occasion=None):
536
632
  if not isinstance(value, bool):
@@ -545,7 +641,7 @@ class BinarySensor(ControllerBase):
545
641
 
546
642
  class Button(ControllerBase):
547
643
  name = _("Button")
548
- base_type = 'button'
644
+ base_type = ButtonType
549
645
  app_widget = ButtonWidget
550
646
  admin_widget_template = 'admin/controller_widgets/button.html'
551
647
  default_value = 'up'
@@ -555,13 +651,28 @@ class Button(ControllerBase):
555
651
  raise ValidationError("Bad button value!")
556
652
  return value
557
653
 
654
+ def send(self, value):
655
+ """Simulate a button event.
656
+
657
+ Parameters:
658
+ - value (str): One of 'down', 'up', 'hold', 'click', 'double-click'.
659
+ """
660
+ return super().send(value)
661
+
558
662
  def is_down(self):
663
+ """Return True while the button is pressed ('down' or 'hold')."""
559
664
  return self.component.value in ('down', 'hold')
560
665
 
561
666
  def is_held(self):
667
+ """Return True if the button is currently in 'hold' state."""
562
668
  return self.component.value == 'hold'
563
669
 
564
670
  def get_bonded_gear(self):
671
+ """Return components configured to be controlled by this button.
672
+
673
+ Scans other components' config for controls referencing this button's id.
674
+ Returns: list[Component]
675
+ """
565
676
  from simo.core.models import Component
566
677
  gear = []
567
678
  for comp in Component.objects.filter(config__has_key='controls'):
@@ -576,6 +687,7 @@ class OnOffPokerMixin:
576
687
  _poke_toggle = False
577
688
 
578
689
  def poke(self):
690
+ """Best-effort wake-up: briefly toggle output to stimulate response."""
579
691
  if self._poke_toggle:
580
692
  self._poke_toggle = False
581
693
  self.turn_on()
@@ -586,7 +698,7 @@ class OnOffPokerMixin:
586
698
 
587
699
  class Dimmer(ControllerBase, TimerMixin, OnOffPokerMixin):
588
700
  name = _("Dimmer")
589
- base_type = 'dimmer'
701
+ base_type = DimmerType
590
702
  app_widget = KnobWidget
591
703
  config_form = DimmerConfigForm
592
704
  admin_widget_template = 'admin/controller_widgets/knob.html'
@@ -624,9 +736,15 @@ class Dimmer(ControllerBase, TimerMixin, OnOffPokerMixin):
624
736
  return value
625
737
 
626
738
  def turn_off(self):
739
+ """Turn output to the configured minimum level (usually 0%)."""
627
740
  self.send(self.component.config.get('min', 0.0))
628
741
 
629
742
  def turn_on(self):
743
+ """Turn output on, restoring the last level when possible.
744
+
745
+ If there is a previous level, restores it; otherwise uses configured
746
+ maximum level.
747
+ """
630
748
  self.component.refresh_from_db()
631
749
  if not self.component.value:
632
750
  if self.component.value_previous:
@@ -635,15 +753,22 @@ class Dimmer(ControllerBase, TimerMixin, OnOffPokerMixin):
635
753
  self.send(self.component.config.get('max', 90))
636
754
 
637
755
  def max_out(self):
756
+ """Set output to the configured maximum level."""
638
757
  self.send(self.component.config.get('max', 90))
639
758
 
640
759
  def output_percent(self, value):
760
+ """Set output by percentage (0–100).
761
+
762
+ Parameters:
763
+ - value (int|float): Percentage of configured [min, max] range.
764
+ """
641
765
  min = self.component.config.get('min', 0)
642
766
  max = self.component.config.get('max', 100)
643
767
  delta = max - min
644
768
  self.send(min + delta * value / 100)
645
769
 
646
770
  def toggle(self):
771
+ """Toggle output: turn off if non-zero, otherwise turn on."""
647
772
  self.component.refresh_from_db()
648
773
  if self.component.value:
649
774
  self.turn_off()
@@ -651,18 +776,30 @@ class Dimmer(ControllerBase, TimerMixin, OnOffPokerMixin):
651
776
  self.turn_on()
652
777
 
653
778
  def fade_up(self):
779
+ """Start increasing brightness smoothly (if supported by gateway)."""
654
780
  raise NotImplemented()
655
781
 
656
782
  def fade_down(self):
783
+ """Start decreasing brightness smoothly (if supported by gateway)."""
657
784
  raise NotImplemented()
658
785
 
659
786
  def fade_stop(self):
787
+ """Stop any ongoing fade operation (if supported by gateway)."""
660
788
  raise NotImplemented()
661
789
 
790
+ def send(self, value):
791
+ """Set dimmer level.
792
+
793
+ Parameters:
794
+ - value (int|float): absolute level within configured min/max, or
795
+ - value (bool): True to restore previous/non-zero level, False for 0.
796
+ """
797
+ return super().send(value)
798
+
662
799
 
663
800
  class DimmerPlus(ControllerBase, TimerMixin, OnOffPokerMixin):
664
801
  name = _("Dimmer Plus")
665
- base_type = 'dimmer-plus'
802
+ base_type = DimmerPlusType
666
803
  app_widget = KnobPlusWidget
667
804
  config_form = DimmerPlusConfigForm
668
805
  default_config = {
@@ -712,6 +849,7 @@ class DimmerPlus(ControllerBase, TimerMixin, OnOffPokerMixin):
712
849
  return value
713
850
 
714
851
  def turn_off(self):
852
+ """Turn both channels to their configured minimums."""
715
853
  self.send(
716
854
  {
717
855
  'main': self.component.config.get('main_min', 0.0),
@@ -721,6 +859,7 @@ class DimmerPlus(ControllerBase, TimerMixin, OnOffPokerMixin):
721
859
  )
722
860
 
723
861
  def turn_on(self):
862
+ """Turn on: main at maximum, secondary to mid-range."""
724
863
  self.component.refresh_from_db()
725
864
  if not self.component.value:
726
865
  if self.component.value_previous:
@@ -734,6 +873,7 @@ class DimmerPlus(ControllerBase, TimerMixin, OnOffPokerMixin):
734
873
  })
735
874
 
736
875
  def toggle(self):
876
+ """Toggle on/off based on the 'main' channel state."""
737
877
  if self.component.value.get('main'):
738
878
  self.turn_off()
739
879
  else:
@@ -741,18 +881,30 @@ class DimmerPlus(ControllerBase, TimerMixin, OnOffPokerMixin):
741
881
 
742
882
 
743
883
  def fade_up(self):
884
+ """Start increasing brightness smoothly (if supported by gateway)."""
744
885
  raise NotImplemented()
745
886
 
746
887
  def fade_down(self):
888
+ """Start decreasing brightness smoothly (if supported by gateway)."""
747
889
  raise NotImplemented()
748
890
 
749
891
  def fade_stop(self):
892
+ """Stop any ongoing fade operation (if supported by gateway)."""
750
893
  raise NotImplemented()
751
894
 
895
+ def send(self, value):
896
+ """Set Dimmer Plus channels.
897
+
898
+ Parameters:
899
+ - value (dict): {'main': number, 'secondary': number}, or
900
+ - value (bool): True to restore previous, False to minimums.
901
+ """
902
+ return super().send(value)
903
+
752
904
 
753
905
  class RGBWLight(ControllerBase, TimerMixin, OnOffPokerMixin):
754
906
  name = _("RGB(W) Light")
755
- base_type = 'rgbw-light'
907
+ base_type = RGBWLightType
756
908
  app_widget = RGBWidget
757
909
  config_form = RGBWConfigForm
758
910
  admin_widget_template = 'admin/controller_widgets/rgb.html'
@@ -793,16 +945,19 @@ class RGBWLight(ControllerBase, TimerMixin, OnOffPokerMixin):
793
945
  return value
794
946
 
795
947
  def turn_off(self):
948
+ """Turn the light off (sets `is_on` to False and sends current value)."""
796
949
  self.component.refresh_from_db()
797
950
  self.component.value['is_on'] = False
798
951
  self.send(self.component.value)
799
952
 
800
953
  def turn_on(self):
954
+ """Turn the light on (sets `is_on` to True and sends current value)."""
801
955
  self.component.refresh_from_db()
802
956
  self.component.value['is_on'] = True
803
957
  self.send(self.component.value)
804
958
 
805
959
  def toggle(self):
960
+ """Toggle the light between on and off."""
806
961
  self.component.refresh_from_db()
807
962
  self.component.value['is_on'] = not self.component.value['is_on']
808
963
  self.send(self.component.value)
@@ -817,6 +972,15 @@ class RGBWLight(ControllerBase, TimerMixin, OnOffPokerMixin):
817
972
  def fade_stop(self):
818
973
  raise NotImplemented()
819
974
 
975
+ def send(self, value):
976
+ """Set RGB(W) light scenes and on/off.
977
+
978
+ Parameters:
979
+ - value (dict): {'scenes': ["#rrggbb[ww]"...], 'active': int 0-4,
980
+ 'is_on': bool}
981
+ """
982
+ return super().send(value)
983
+
820
984
 
821
985
  class MultiSwitchBase(ControllerBase):
822
986
 
@@ -847,37 +1011,59 @@ class MultiSwitchBase(ControllerBase):
847
1011
  ))
848
1012
  return value
849
1013
 
1014
+ def send(self, value):
1015
+ """Set one or multiple switch channels.
1016
+
1017
+ Parameters:
1018
+ - value (bool) to set all channels, or
1019
+ - value (list[bool]) with a boolean per channel.
1020
+ """
1021
+ return super().send(value)
1022
+
850
1023
 
851
1024
  class Switch(MultiSwitchBase, TimerMixin, OnOffPokerMixin):
852
1025
  name = _("Switch")
853
- base_type = 'switch'
1026
+ base_type = SwitchType
854
1027
  app_widget = SingleSwitchWidget
855
1028
  config_form = SwitchForm
856
1029
  admin_widget_template = 'admin/controller_widgets/switch.html'
857
1030
  default_value = False
858
1031
 
1032
+ def send(self, value):
1033
+ """Send value to device.
1034
+ If non boolean value is provided it is translated to a boolean one.
1035
+
1036
+ Parameters:
1037
+ - value (bool): True = ON, False = OFF.
1038
+ """
1039
+ return super().send(value)
1040
+
859
1041
  def turn_on(self):
1042
+ """Turn the switch on (send True)."""
860
1043
  if self.component.meta.get('pulse'):
861
1044
  self.component.meta.pop('pulse')
862
1045
  self.component.save()
863
1046
  self.send(True)
864
1047
 
865
1048
  def turn_off(self):
1049
+ """Turn the switch off (send False)."""
866
1050
  if self.component.meta.get('pulse'):
867
1051
  self.component.meta.pop('pulse')
868
1052
  self.component.save()
869
1053
  self.send(False)
870
1054
 
871
1055
  def toggle(self):
1056
+ """Toggle the switch state based on current value."""
872
1057
  if self.component.meta.get('pulse'):
873
1058
  self.component.meta.pop('pulse')
874
1059
  self.component.save()
875
1060
  self.send(not self.component.value)
876
1061
 
877
1062
  def click(self):
878
- '''
879
- Gateway specific implementation is very welcome of this!
880
- '''
1063
+ """Simulate a short press: turn on momentarily then off.
1064
+
1065
+ This sends an on command and schedules an automatic off after ~1s.
1066
+ """
881
1067
  if self.component.meta.get('pulse'):
882
1068
  self.component.meta.pop('pulse')
883
1069
  self.component.save()
@@ -888,6 +1074,12 @@ class Switch(MultiSwitchBase, TimerMixin, OnOffPokerMixin):
888
1074
  ).apply_async(countdown=1)
889
1075
 
890
1076
  def pulse(self, frame_length_s, on_percentage):
1077
+ """Generate a PWM-like pulse train (only if gateway supports it).
1078
+
1079
+ Parameters:
1080
+ - frame_length_s (float): Duration of a full on+off frame in seconds.
1081
+ - on_percentage (float|int): Duty cycle percentage (0–100) for on time.
1082
+ """
891
1083
  self.component.meta['pulse'] = {
892
1084
  'frame': frame_length_s, 'duty': on_percentage / 100
893
1085
  }
@@ -906,7 +1098,7 @@ class Switch(MultiSwitchBase, TimerMixin, OnOffPokerMixin):
906
1098
 
907
1099
  class DoubleSwitch(MultiSwitchBase):
908
1100
  name = _("Double Switch")
909
- base_type = 'switch-double'
1101
+ base_type = DoubleSwitchType
910
1102
  app_widget = DoubleSwitchWidget
911
1103
  config_form = DoubleSwitchConfigForm
912
1104
  default_value = [False, False]
@@ -919,7 +1111,7 @@ class DoubleSwitch(MultiSwitchBase):
919
1111
 
920
1112
  class TripleSwitch(MultiSwitchBase):
921
1113
  name = _("Triple Switch")
922
- base_type = 'switch-triple'
1114
+ base_type = TripleSwitchType
923
1115
  app_widget = TripleSwitchWidget
924
1116
  config_form = TrippleSwitchConfigForm
925
1117
  default_value = [False, False, False]
@@ -932,7 +1124,7 @@ class TripleSwitch(MultiSwitchBase):
932
1124
 
933
1125
  class QuadrupleSwitch(MultiSwitchBase):
934
1126
  name = _("Quadruple Switch")
935
- base_type = 'switch-quadruple'
1127
+ base_type = QuadrupleSwitchType
936
1128
  app_widget = QuadrupleSwitchWidget
937
1129
  config_form = QuadrupleSwitchConfigForm
938
1130
  default_value = [False, False, False, False]
@@ -945,7 +1137,7 @@ class QuadrupleSwitch(MultiSwitchBase):
945
1137
 
946
1138
  class QuintupleSwitch(MultiSwitchBase):
947
1139
  name = _("Quintuple Switch")
948
- base_type = 'switch-quintuple'
1140
+ base_type = QuintupleSwitchType
949
1141
  app_widget = QuintupleSwitchWidget
950
1142
  config_form = QuintupleSwitchConfigForm
951
1143
  default_value = [False, False, False, False, False]
@@ -958,7 +1150,7 @@ class QuintupleSwitch(MultiSwitchBase):
958
1150
 
959
1151
  class Lock(Switch):
960
1152
  name = _("Lock")
961
- base_type = 'lock'
1153
+ base_type = LockType
962
1154
  app_widget = LockWidget
963
1155
  admin_widget_template = 'admin/controller_widgets/lock.html'
964
1156
  default_value = 'unlocked'
@@ -970,11 +1162,21 @@ class Lock(Switch):
970
1162
  FAULT = 4
971
1163
 
972
1164
  def lock(self):
1165
+ """Lock the device (equivalent to `turn_on()`)."""
973
1166
  self.turn_on()
974
1167
 
975
1168
  def unlock(self):
1169
+ """Unlock the device (equivalent to `turn_off()`)."""
976
1170
  self.turn_off()
977
1171
 
1172
+ def send(self, value):
1173
+ """Lock/unlock.
1174
+
1175
+ Parameters:
1176
+ - value (bool): True to lock; False to unlock.
1177
+ """
1178
+ return super().send(value)
1179
+
978
1180
  def _receive_from_device(
979
1181
  self, value, *args, **kwargs
980
1182
  ):
@@ -1024,7 +1226,7 @@ class Lock(Switch):
1024
1226
 
1025
1227
  class Blinds(ControllerBase, TimerMixin):
1026
1228
  name = _("Blind")
1027
- base_type = 'blinds'
1229
+ base_type = BlindsType
1028
1230
  admin_widget_template = 'admin/controller_widgets/blinds.html'
1029
1231
  default_config = {}
1030
1232
 
@@ -1107,6 +1309,10 @@ class Blinds(ControllerBase, TimerMixin):
1107
1309
  return value
1108
1310
 
1109
1311
  def open(self):
1312
+ """Open blinds fully.
1313
+
1314
+ Sends {'target': 0} and preserves current angle if present.
1315
+ """
1110
1316
  send_val = {'target': 0}
1111
1317
  angle = self.component.value.get('angle')
1112
1318
  if angle is not None and 0 <= angle <= 180:
@@ -1114,6 +1320,10 @@ class Blinds(ControllerBase, TimerMixin):
1114
1320
  self.send(send_val)
1115
1321
 
1116
1322
  def close(self):
1323
+ """Close blinds fully.
1324
+
1325
+ Sends {'target': open_duration_ms} and preserves current angle.
1326
+ """
1117
1327
  send_val = {'target': self.component.config['open_duration'] * 1000}
1118
1328
  angle = self.component.value.get('angle')
1119
1329
  if angle is not None and 0 <= angle <= 180:
@@ -1121,16 +1331,29 @@ class Blinds(ControllerBase, TimerMixin):
1121
1331
  self.send(send_val)
1122
1332
 
1123
1333
  def stop(self):
1334
+ """Stop blinds movement immediately.
1335
+
1336
+ Sends {'target': -1} and preserves current angle.
1337
+ """
1124
1338
  send_val = {'target': -1}
1125
1339
  angle = self.component.value.get('angle')
1126
1340
  if angle is not None and 0 <= angle <= 180:
1127
1341
  send_val['angle'] = angle
1128
1342
  self.send(send_val)
1129
1343
 
1344
+ def send(self, value):
1345
+ """Control blinds position/angle.
1346
+
1347
+ Parameters:
1348
+ - value (dict): {'target': milliseconds or -1 for stop,
1349
+ 'angle': optional 0-180}
1350
+ """
1351
+ return super().send(value)
1352
+
1130
1353
 
1131
1354
  class Gate(ControllerBase, TimerMixin):
1132
1355
  name = _("Gate")
1133
- base_type = 'gate'
1356
+ base_type = GateType
1134
1357
  app_widget = GateWidget
1135
1358
  admin_widget_template = 'admin/controller_widgets/gate.html'
1136
1359
  default_config = {
@@ -1168,11 +1391,21 @@ class Gate(ControllerBase, TimerMixin):
1168
1391
  return value
1169
1392
 
1170
1393
  def open(self):
1394
+ """Command the gate to open."""
1171
1395
  self.send('open')
1172
1396
 
1173
1397
  def close(self):
1398
+ """Command the gate to close."""
1174
1399
  self.send('close')
1175
1400
 
1176
1401
  def call(self):
1402
+ """Trigger the 'call' action (impulse) for gates in call-mode."""
1177
1403
  self.send('call')
1178
1404
 
1405
+ def send(self, value):
1406
+ """Control gate.
1407
+
1408
+ Parameters:
1409
+ - value (str): 'open', 'close', or 'call' (depending on config).
1410
+ """
1411
+ return super().send(value)