simo 3.1.6__py3-none-any.whl → 3.1.7__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/core/controllers.py CHANGED
@@ -647,7 +647,10 @@ class Button(ControllerBase):
647
647
  default_value = 'up'
648
648
 
649
649
  def _validate_val(self, value, occasion=None):
650
- if value not in ('down', 'up', 'hold', 'click', 'double-click'):
650
+ if value not in (
651
+ 'down', 'up', 'hold',
652
+ 'click', 'double-click', 'triple-click', 'quadruple-click', 'quintuple-click'
653
+ ):
651
654
  raise ValidationError("Bad button value!")
652
655
  return value
653
656
 
simo/core/forms.py CHANGED
@@ -71,16 +71,45 @@ class CategoryAdminForm(forms.ModelForm):
71
71
  class ConfigFieldsMixin:
72
72
 
73
73
  def __init__(self, *args, **kwargs):
74
+ """Augment forms with dynamic controller fields and
75
+ persist non-model fields under component.config.
76
+
77
+ Dynamic fields are appended by controllers via
78
+ `controller._get_dynamic_config_fields()` and are excluded from
79
+ automatic component.config persistence. Controllers may handle
80
+ them in `_apply_dynamic_config()` during save.
81
+ """
74
82
  super().__init__(*args, **kwargs)
83
+
84
+ # Inject dynamic fields from controller (if any)
85
+ self._dynamic_fields = []
86
+ controller = getattr(self, 'controller', None)
87
+ if controller and hasattr(controller, '_get_dynamic_config_fields'):
88
+ try:
89
+ dyn_fields = controller._get_dynamic_config_fields() or {}
90
+ if isinstance(dyn_fields, dict):
91
+ for fname, field in dyn_fields.items():
92
+ if fname in self.fields:
93
+ continue
94
+ self.fields[fname] = field
95
+ self._dynamic_fields.append(fname)
96
+ except Exception:
97
+ # Never break the form if controller fails here
98
+ pass
99
+
100
+ # Build config-backed field list (exclude model fields and dynamic fields)
75
101
  self.model_fields = [
76
102
  f.name for f in Component._meta.fields
77
103
  ] + ['slaves', ]
78
104
  self.config_fields = []
79
- for field_name, field in self.fields.items():
105
+ for field_name in list(self.fields.keys()):
80
106
  if field_name in self.model_fields:
81
107
  continue
108
+ if field_name in self._dynamic_fields:
109
+ continue
82
110
  self.config_fields.append(field_name)
83
111
 
112
+ # Initialize config-backed fields from instance.config
84
113
  for field_name in self.config_fields:
85
114
  if field_name not in self.instance.config:
86
115
  continue
@@ -106,38 +135,46 @@ class ConfigFieldsMixin:
106
135
 
107
136
 
108
137
  def save(self, commit=True):
138
+ # Write config-backed fields under component.config
109
139
  for field_name in self.config_fields:
110
140
  # support for partial forms
111
141
  if field_name not in self.cleaned_data:
112
142
  continue
113
143
  if isinstance(self.cleaned_data[field_name], models.Model):
114
- self.instance.config[field_name] = \
115
- self.cleaned_data[field_name].pk
144
+ self.instance.config[field_name] = self.cleaned_data[field_name].pk
116
145
  elif isinstance(self.cleaned_data[field_name], models.QuerySet):
117
- self.instance.config[field_name] = [
118
- obj.pk for obj in self.cleaned_data[field_name]
119
- ]
146
+ self.instance.config[field_name] = [obj.pk for obj in self.cleaned_data[field_name]]
120
147
  else:
121
148
  try:
122
- self.instance.config[field_name] = \
123
- json.loads(json.dumps(self.cleaned_data[field_name]))
124
- except:
149
+ self.instance.config[field_name] = json.loads(json.dumps(self.cleaned_data[field_name]))
150
+ except Exception:
125
151
  continue
126
152
 
153
+ # Save component
127
154
  if commit:
128
155
  from simo.users.utils import get_current_user
129
156
  actor = get_current_user()
130
- if self.instance.pk:
131
- verb = 'modified'
132
- else:
133
- verb = 'created'
134
- action.send(
135
- actor, target=self.instance, verb=verb,
136
- instance_id=self.instance.zone.instance.id,
137
- action_type='management_event'
138
- )
157
+ if actor:
158
+ if self.instance.pk:
159
+ verb = 'modified'
160
+ else:
161
+ verb = 'created'
162
+ action.send(
163
+ actor, target=self.instance, verb=verb,
164
+ instance_id=self.instance.zone.instance.id,
165
+ action_type='management_event'
166
+ )
167
+ result = super().save(commit)
139
168
 
140
- return super().save(commit)
169
+ # Apply dynamic settings via controller hook
170
+ controller = getattr(self, 'controller', None)
171
+ if controller and hasattr(controller, '_apply_dynamic_config'):
172
+ try:
173
+ controller._apply_dynamic_config(self.cleaned_data)
174
+ except Exception:
175
+ pass
176
+
177
+ return result
141
178
 
142
179
 
143
180
  class BaseGatewayForm(ConfigFieldsMixin, forms.ModelForm):
@@ -329,10 +366,24 @@ class ComponentAdminForm(forms.ModelForm):
329
366
  base_fields.append('category')
330
367
  base_fields.append('show_in_app')
331
368
 
369
+ # Include statically declared fields first
332
370
  for field_name in cls.declared_fields:
333
371
  if field_name not in main_fields:
334
372
  base_fields.append(field_name)
335
373
 
374
+ # If editing an existing object, include any dynamic fields
375
+ # that the form instance adds at runtime.
376
+ if obj is not None:
377
+ try:
378
+ tmp_form = cls(request=request, instance=obj, controller_uid=obj.controller_uid)
379
+ for fname in tmp_form.fields.keys():
380
+ if fname in base_fields or fname in main_fields:
381
+ continue
382
+ base_fields.append(fname)
383
+ except Exception:
384
+ # Ignore dynamic field detection issues
385
+ pass
386
+
336
387
  base_fields.append('control')
337
388
  base_fields.append('notes')
338
389
 
@@ -389,6 +440,29 @@ class ComponentAdminForm(forms.ModelForm):
389
440
 
390
441
 
391
442
  class BaseComponentForm(ConfigFieldsMixin, ComponentAdminForm):
443
+ """Base form for all components.
444
+
445
+ Dynamic controller-backed fields
446
+ --------------------------------
447
+ Controllers can expose device-scoped, dynamic options by implementing
448
+ the following optional hooks on the controller instance:
449
+
450
+ - _get_dynamic_config_fields(self) -> dict[str, django.forms.Field]
451
+ Return a mapping of additional form fields to append at runtime.
452
+ These fields are not persisted automatically to component.config.
453
+ Set each field's `initial` to reflect the current device value.
454
+
455
+ - _apply_dynamic_config(self, form_data: dict) -> None
456
+ Called after the form is saved. Receives the full cleaned form
457
+ data (including dynamic fields). Use this to apply device options
458
+ (e.g. send commands to a gateway or write to component.config if
459
+ your integration needs to persist something custom).
460
+
461
+ Notes
462
+ - Dynamic fields are excluded from `basic_fields`, so only higher-level
463
+ users (instance superusers/masters) will see and edit them.
464
+ - All existing static fields and behavior remain unchanged.
465
+ """
392
466
  pass
393
467
 
394
468
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simo
3
- Version: 3.1.6
3
+ Version: 3.1.7
4
4
  Summary: Smart Home Supremacy
5
5
  Author-email: "Simon V." <simon@simo.io>
6
6
  Project-URL: Homepage, https://simo.io
@@ -102,12 +102,12 @@ simo/core/auto_urls.py,sha256=fM9Tqzt0OfJ2FNnePGp7LcbJAWzgEwaNAJy7FNXHY-o,1299
102
102
  simo/core/autocomplete_views.py,sha256=x3MKOZvXYS3xVQ-V1S7Liv_U5bxr-uc0gePa85wv5nA,4561
103
103
  simo/core/base_types.py,sha256=FNIS9Y7wmdbVl-dISLdSBYvMEiV4zSLpBOBDYOVyam0,6580
104
104
  simo/core/context.py,sha256=LKw1I4iIRnlnzoTCuSLLqDX7crHdBnMo3hjqYvVmzFc,1557
105
- simo/core/controllers.py,sha256=jGn9bDvUuAbFL_uCAV8aqXgDJz9akYDroJuyU0ONsC8,48563
105
+ simo/core/controllers.py,sha256=uEC4hSeSpyHLZjojIdMPrzy2-JiK9DkMmCZpE3BcK-I,48651
106
106
  simo/core/dynamic_settings.py,sha256=bUs58XEZOCIEhg1TigR3LmYggli13KMryBZ9pC7ugAQ,1872
107
107
  simo/core/events.py,sha256=mjrYsByw0hGct4rXoLmnbBodViN8S1ReBIZG3g660Yo,6735
108
108
  simo/core/filters.py,sha256=6wbn8C2WvKTTjtfMwwLBp2Fib1V0-DMpS4iqJd6jJQo,2540
109
109
  simo/core/form_fields.py,sha256=b4wZ4n7OO0m0_BPPS9ILVrwBvhhjUB079YrroveFUWA,5222
110
- simo/core/forms.py,sha256=IwuljCxwqPcSn3eXJRF2pWjktvlQ4DH2LCsncHzCHw0,22720
110
+ simo/core/forms.py,sha256=koGOIVt3gE1eejpjHFjAH-QZ1Zm2Woy51F5txh_AJVI,26191
111
111
  simo/core/gateways.py,sha256=cM_du3VsHbSYUSWL5JHxXMV8yn6s-QrSzB3WreglGjw,4736
112
112
  simo/core/loggers.py,sha256=EBdq23gTQScVfQVH-xeP90-wII2DQFDjoROAW6ggUP4,1645
113
113
  simo/core/managers.py,sha256=Ampwe5K7gfE6IJULNCV35V8ysmMOdS_wz7mRzfaLZUw,3014
@@ -11034,9 +11034,9 @@ simo/users/templates/invitations/expired_msg.html,sha256=47DEQpj8HBSa-_TImW-5JCe
11034
11034
  simo/users/templates/invitations/expired_suggestion.html,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11035
11035
  simo/users/templates/invitations/taken_msg.html,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11036
11036
  simo/users/templates/invitations/taken_suggestion.html,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11037
- simo-3.1.6.dist-info/licenses/LICENSE.md,sha256=M7wm1EmMGDtwPRdg7kW4d00h1uAXjKOT3HFScYQMeiE,34916
11038
- simo-3.1.6.dist-info/METADATA,sha256=3ZPTUNXT0K8Vdj8pNXmdll7TIhFDbYpKHtByuNk1Kio,2224
11039
- simo-3.1.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
11040
- simo-3.1.6.dist-info/entry_points.txt,sha256=S9PwnUYmTSW7681GKDCxUbL0leRJIaRk6fDQIKgbZBA,135
11041
- simo-3.1.6.dist-info/top_level.txt,sha256=GmS1hrAbpVqn9OWZh6UX82eIOdRLgYA82RG9fe8v4Rs,5
11042
- simo-3.1.6.dist-info/RECORD,,
11037
+ simo-3.1.7.dist-info/licenses/LICENSE.md,sha256=M7wm1EmMGDtwPRdg7kW4d00h1uAXjKOT3HFScYQMeiE,34916
11038
+ simo-3.1.7.dist-info/METADATA,sha256=478oQ_ahEy-rp4E2VvKgkzhLPFYrTOMG7IJAttlmJXM,2224
11039
+ simo-3.1.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
11040
+ simo-3.1.7.dist-info/entry_points.txt,sha256=S9PwnUYmTSW7681GKDCxUbL0leRJIaRk6fDQIKgbZBA,135
11041
+ simo-3.1.7.dist-info/top_level.txt,sha256=GmS1hrAbpVqn9OWZh6UX82eIOdRLgYA82RG9fe8v4Rs,5
11042
+ simo-3.1.7.dist-info/RECORD,,
File without changes