simo 2.0.31__py3-none-any.whl → 2.0.33__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/cli.py +2 -17
- simo/core/__pycache__/admin.cpython-38.pyc +0 -0
- simo/core/__pycache__/api.cpython-38.pyc +0 -0
- simo/core/__pycache__/controllers.cpython-38.pyc +0 -0
- simo/core/__pycache__/forms.cpython-38.pyc +0 -0
- simo/core/__pycache__/managers.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__/tasks.cpython-38.pyc +0 -0
- simo/core/admin.py +2 -2
- simo/core/api.py +25 -2
- simo/core/controllers.py +11 -7
- simo/core/forms.py +1 -2
- simo/core/managers.py +1 -5
- simo/core/migrations/0003_create_default_zones_and_categories.py +2 -39
- simo/core/migrations/0004_create_generic.py +22 -21
- simo/core/migrations/0013_auto_20231003_0754.py +45 -43
- simo/core/migrations/0018_auto_20231005_0622.py +18 -16
- simo/core/migrations/0033_auto_20240509_0821.py +25 -0
- simo/core/migrations/0034_component_error_msg.py +18 -0
- simo/core/migrations/0035_remove_instance_share_location.py +17 -0
- simo/core/migrations/__pycache__/0003_create_default_zones_and_categories.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0004_create_generic.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0013_auto_20231003_0754.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0018_auto_20231005_0622.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0033_auto_20240509_0821.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0034_component_error_msg.cpython-38.pyc +0 -0
- simo/core/migrations/__pycache__/0035_remove_instance_share_location.cpython-38.pyc +0 -0
- simo/core/models.py +25 -23
- simo/core/serializers.py +5 -3
- simo/core/signal_receivers.py +82 -1
- simo/core/tasks.py +7 -4
- simo/fleet/__pycache__/admin.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/controllers.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/forms.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/models.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/serializers.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/socket_consumers.cpython-38.pyc +0 -0
- simo/fleet/admin.py +25 -6
- simo/fleet/controllers.py +82 -37
- simo/fleet/forms.py +142 -10
- simo/fleet/migrations/0035_auto_20240514_0855.py +32 -0
- simo/fleet/migrations/__pycache__/0035_auto_20240514_0855.cpython-38.pyc +0 -0
- simo/fleet/models.py +96 -82
- simo/fleet/serializers.py +8 -1
- simo/fleet/socket_consumers.py +3 -15
- simo/generic/__pycache__/gateways.cpython-38.pyc +0 -0
- simo/generic/gateways.py +11 -5
- simo/users/__pycache__/models.cpython-38.pyc +0 -0
- simo/users/__pycache__/utils.cpython-38.pyc +0 -0
- simo/users/migrations/0003_create_roles_and_system_user.py +24 -23
- simo/users/migrations/0019_auto_20231221_1155.py +9 -8
- simo/users/migrations/__pycache__/0003_create_roles_and_system_user.cpython-38.pyc +0 -0
- simo/users/migrations/__pycache__/0019_auto_20231221_1155.cpython-38.pyc +0 -0
- simo/users/models.py +6 -7
- simo/users/utils.py +0 -4
- {simo-2.0.31.dist-info → simo-2.0.33.dist-info}/METADATA +1 -1
- {simo-2.0.31.dist-info → simo-2.0.33.dist-info}/RECORD +62 -54
- {simo-2.0.31.dist-info → simo-2.0.33.dist-info}/LICENSE.md +0 -0
- {simo-2.0.31.dist-info → simo-2.0.33.dist-info}/WHEEL +0 -0
- {simo-2.0.31.dist-info → simo-2.0.33.dist-info}/top_level.txt +0 -0
simo/fleet/forms.py
CHANGED
|
@@ -647,9 +647,9 @@ class ColonelPWMOutputConfigForm(ColonelComponentForm):
|
|
|
647
647
|
|
|
648
648
|
def clean(self):
|
|
649
649
|
super().clean()
|
|
650
|
-
if self.cleaned_data
|
|
650
|
+
if 'output_pin' in self.cleaned_data:
|
|
651
651
|
self._clean_pin('output_pin')
|
|
652
|
-
if self.cleaned_data
|
|
652
|
+
if 'controls' in self.cleaned_data:
|
|
653
653
|
self._clean_controls()
|
|
654
654
|
return self.cleaned_data
|
|
655
655
|
|
|
@@ -745,7 +745,9 @@ class ColonelRGBLightConfigForm(ColonelComponentForm):
|
|
|
745
745
|
def clean(self):
|
|
746
746
|
super().clean()
|
|
747
747
|
|
|
748
|
-
|
|
748
|
+
if 'output_pin' in self.cleaned_data:
|
|
749
|
+
self._clean_pin('output_pin')
|
|
750
|
+
|
|
749
751
|
if self.cleaned_data.get('controls'):
|
|
750
752
|
self._clean_controls()
|
|
751
753
|
|
|
@@ -1034,23 +1036,153 @@ class DALIDeviceConfigForm(ColonelComponentForm):
|
|
|
1034
1036
|
)
|
|
1035
1037
|
)
|
|
1036
1038
|
|
|
1037
|
-
def
|
|
1039
|
+
def clean_interface(self):
|
|
1040
|
+
if not self.instance.pk:
|
|
1041
|
+
return self.cleaned_data['interface']
|
|
1042
|
+
if 'interface' in self.changed_data:
|
|
1043
|
+
raise forms.ValidationError(
|
|
1044
|
+
"Changing interface after component is created "
|
|
1045
|
+
"it is not allowed!"
|
|
1046
|
+
)
|
|
1047
|
+
return self.cleaned_data['interface']
|
|
1048
|
+
|
|
1049
|
+
def save(self, commit=True, update_colonel_config=True):
|
|
1038
1050
|
if 'interface' in self.cleaned_data:
|
|
1039
1051
|
self.instance.config['dali_interface'] = \
|
|
1040
1052
|
self.cleaned_data['interface'].no
|
|
1041
|
-
return super().save(commit=commit)
|
|
1042
1053
|
|
|
1054
|
+
# prevent immediate config update on colonel as dali devices are
|
|
1055
|
+
# added via pairing process, which uses finalization procedure.
|
|
1056
|
+
is_new = not self.instance.pk
|
|
1057
|
+
obj = super(BaseComponentForm, self).save(commit=commit)
|
|
1058
|
+
if commit:
|
|
1059
|
+
self.cleaned_data['colonel'].components.add(obj)
|
|
1060
|
+
if not is_new and update_colonel_config:
|
|
1061
|
+
GatewayObjectCommand(
|
|
1062
|
+
obj.gateway, self.cleaned_data['colonel'], id=obj.id,
|
|
1063
|
+
command='call', method='update_config', args=[
|
|
1064
|
+
obj.controller._get_colonel_config()
|
|
1065
|
+
]
|
|
1066
|
+
).publish()
|
|
1067
|
+
return obj
|
|
1043
1068
|
|
|
1044
|
-
class DaliSwitchForm(DALIDeviceConfigForm, SwitchForm):
|
|
1045
1069
|
|
|
1070
|
+
class DaliLampForm(DALIDeviceConfigForm, BaseComponentForm):
|
|
1071
|
+
boot_update = forms.BooleanField(
|
|
1072
|
+
initial=False, required=False,
|
|
1073
|
+
help_text="Update device config on colonel boot."
|
|
1074
|
+
)
|
|
1075
|
+
fade_time = forms.TypedChoiceField(
|
|
1076
|
+
initial=3, choices=(
|
|
1077
|
+
(1, "0.7 s"), (2, "1.0 s"), (3, "1.4 s"), (4, "2.0 s"), (5, "2.8 s"),
|
|
1078
|
+
(6, "4.0 s"), (7, "5.7 s"), (8, "8.0 s")
|
|
1079
|
+
)
|
|
1080
|
+
)
|
|
1081
|
+
gear_min = forms.IntegerField(
|
|
1082
|
+
min_value=1, max_value=254, initial=90,
|
|
1083
|
+
help_text="Minimum level at which device starts operating up (1 - 254), "
|
|
1084
|
+
"SIMO.io DALI interface detects this value automatically when "
|
|
1085
|
+
"pairing a new device. <br>"
|
|
1086
|
+
"Most LED drivers we tested starts at 86. <br>"
|
|
1087
|
+
"If you set this value to low, you might start seeing device "
|
|
1088
|
+
"beings dying out when you hit it with lower value than it "
|
|
1089
|
+
"is capable of supplying."
|
|
1090
|
+
)
|
|
1046
1091
|
auto_off = forms.FloatField(
|
|
1047
1092
|
required=False, min_value=0.01, max_value=1000000000,
|
|
1048
|
-
help_text="If provided,
|
|
1093
|
+
help_text="If provided, lamp will be turned off after "
|
|
1049
1094
|
"given amount of seconds after last turn on event."
|
|
1050
1095
|
)
|
|
1051
|
-
inverse = forms.BooleanField(
|
|
1052
|
-
label=_("Inverse switch value"), required=False
|
|
1053
|
-
)
|
|
1054
1096
|
|
|
1055
1097
|
|
|
1098
|
+
class DaliGearGroupForm(DALIDeviceConfigForm, BaseComponentForm):
|
|
1099
|
+
auto_off = forms.FloatField(
|
|
1100
|
+
required=False, min_value=0.01, max_value=1000000000,
|
|
1101
|
+
help_text="If provided, group will be turned off after "
|
|
1102
|
+
"given amount of seconds after last turn on event."
|
|
1103
|
+
)
|
|
1104
|
+
members = forms.ModelMultipleChoiceField(
|
|
1105
|
+
Component.objects.filter(
|
|
1106
|
+
controller_uid='simo.fleet.controllers.DALILamp',
|
|
1107
|
+
),
|
|
1108
|
+
label="Members", required=True,
|
|
1109
|
+
widget=autocomplete.ModelSelect2Multiple(
|
|
1110
|
+
url='autocomplete-component', attrs={'data-html': True},
|
|
1111
|
+
forward=(
|
|
1112
|
+
forward.Const(
|
|
1113
|
+
['simo.fleet.controllers.DALILamp', ], 'controller_uid'
|
|
1114
|
+
),
|
|
1115
|
+
)
|
|
1116
|
+
)
|
|
1117
|
+
)
|
|
1056
1118
|
|
|
1119
|
+
def clean(self):
|
|
1120
|
+
if 'members' in self.cleaned_data:
|
|
1121
|
+
if len(self.cleaned_data['members']) < 2:
|
|
1122
|
+
raise forms.ValidationError("At least two members are required.")
|
|
1123
|
+
for member in self.cleaned_data['members']:
|
|
1124
|
+
if member.config['interface'] != self.cleaned_data['interface'].id:
|
|
1125
|
+
self.add_error(
|
|
1126
|
+
'members', f"{member} belongs to other DALI interface."
|
|
1127
|
+
)
|
|
1128
|
+
self.group_addr = None
|
|
1129
|
+
if not self.instance.pk:
|
|
1130
|
+
from .controllers import DALIGearGroup
|
|
1131
|
+
occupied_addresses = set([
|
|
1132
|
+
int(c['config'].get('da', 0)) for c in Component.objects.filter(
|
|
1133
|
+
controller_uid=DALIGearGroup.uid,
|
|
1134
|
+
config__colonel=self.cleaned_data['colonel'].id,
|
|
1135
|
+
config__interface=self.cleaned_data['interface'].id,
|
|
1136
|
+
).values('config')])
|
|
1137
|
+
for addr in range(16):
|
|
1138
|
+
if addr in occupied_addresses:
|
|
1139
|
+
continue
|
|
1140
|
+
self.group_addr = addr
|
|
1141
|
+
break
|
|
1142
|
+
if self.group_addr is None:
|
|
1143
|
+
self.add_error(
|
|
1144
|
+
'interface',
|
|
1145
|
+
"Already has 16 groups. No more groups are allowed on DALI line."
|
|
1146
|
+
)
|
|
1147
|
+
else:
|
|
1148
|
+
self.group_addr = self.instance.config['da']
|
|
1149
|
+
return self.cleaned_data
|
|
1150
|
+
|
|
1151
|
+
def save(self, commit=True, update_colonel_config=True):
|
|
1152
|
+
old_members = self.instance.config.get('members', [])
|
|
1153
|
+
self.instance.config['da'] = self.group_addr
|
|
1154
|
+
is_new = not self.instance.pk
|
|
1155
|
+
obj = super().save(commit, update_colonel_config=False)
|
|
1156
|
+
if commit:
|
|
1157
|
+
self.cleaned_data['colonel'].components.add(obj)
|
|
1158
|
+
new_members = obj.config.get('members', [])
|
|
1159
|
+
for removed_member in Component.objects.filter(
|
|
1160
|
+
id__in=set(old_members) - set(new_members)
|
|
1161
|
+
):
|
|
1162
|
+
self.controller._modify_member_group(
|
|
1163
|
+
removed_member, self.group_addr, remove=True
|
|
1164
|
+
)
|
|
1165
|
+
for member in Component.objects.filter(id__in=new_members):
|
|
1166
|
+
self.controller._modify_member_group(member, self.group_addr)
|
|
1167
|
+
if is_new:
|
|
1168
|
+
GatewayObjectCommand(
|
|
1169
|
+
obj.gateway, self.cleaned_data['colonel'],
|
|
1170
|
+
command='finalize',
|
|
1171
|
+
data={
|
|
1172
|
+
'temp_id': 'none',
|
|
1173
|
+
'permanent_id': obj.id,
|
|
1174
|
+
'comp_config': {
|
|
1175
|
+
'type': obj.controller_uid.split('.')[-1],
|
|
1176
|
+
'family': self.controller.family,
|
|
1177
|
+
'config': obj.config
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
).publish()
|
|
1181
|
+
else:
|
|
1182
|
+
GatewayObjectCommand(
|
|
1183
|
+
obj.gateway, self.cleaned_data['colonel'], id=obj.id,
|
|
1184
|
+
command='call', method='update_config', args=[
|
|
1185
|
+
obj.controller._get_colonel_config()
|
|
1186
|
+
]
|
|
1187
|
+
).publish()
|
|
1188
|
+
return obj
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Generated by Django 3.2.9 on 2024-05-14 08:55
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
import django.db.models.deletion
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Migration(migrations.Migration):
|
|
8
|
+
|
|
9
|
+
dependencies = [
|
|
10
|
+
('contenttypes', '0002_remove_content_type_name'),
|
|
11
|
+
('fleet', '0034_auto_20240418_0735'),
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
operations = [
|
|
15
|
+
migrations.CreateModel(
|
|
16
|
+
name='InterfaceAddress',
|
|
17
|
+
fields=[
|
|
18
|
+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
19
|
+
('address_type', models.CharField(choices=[('i2c', 'I2C'), ('dali-gear', 'DALI Gear'), ('dali-group', 'DALI Gear Group')], db_index=True, max_length=100)),
|
|
20
|
+
('address', models.JSONField(db_index=True)),
|
|
21
|
+
('occupied_by_id', models.PositiveIntegerField(null=True)),
|
|
22
|
+
('interface', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='addresses', to='fleet.interface')),
|
|
23
|
+
('occupied_by_content_type', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
|
|
24
|
+
],
|
|
25
|
+
options={
|
|
26
|
+
'unique_together': {('interface', 'address_type', 'address')},
|
|
27
|
+
},
|
|
28
|
+
),
|
|
29
|
+
migrations.DeleteModel(
|
|
30
|
+
name='I2CInterface',
|
|
31
|
+
),
|
|
32
|
+
]
|
|
Binary file
|
simo/fleet/models.py
CHANGED
|
@@ -181,13 +181,13 @@ class Colonel(DirtyFieldsMixin, models.Model):
|
|
|
181
181
|
pin.occupied_by = component
|
|
182
182
|
pin.save()
|
|
183
183
|
|
|
184
|
-
for interface in self.
|
|
185
|
-
if interface.
|
|
186
|
-
interface.
|
|
187
|
-
interface.
|
|
188
|
-
if interface.
|
|
189
|
-
interface.
|
|
190
|
-
interface.
|
|
184
|
+
for interface in self.interfaces.all():
|
|
185
|
+
if interface.pin_a:
|
|
186
|
+
interface.pin_a.occupied_by = interface
|
|
187
|
+
interface.pin_a.save()
|
|
188
|
+
if interface.pin_b:
|
|
189
|
+
interface.pin_b.occupied_by = interface
|
|
190
|
+
interface.pin_b.save()
|
|
191
191
|
|
|
192
192
|
|
|
193
193
|
def move_to(self, other_colonel):
|
|
@@ -259,6 +259,10 @@ def after_colonel_save(sender, instance, created, *args, **kwargs):
|
|
|
259
259
|
capacitive=data.get('capacitive'), adc=data.get('adc'),
|
|
260
260
|
native=data.get('native'), note=data.get('note')
|
|
261
261
|
)
|
|
262
|
+
fleet_gateway, new = Gateway.objects.get_or_create(
|
|
263
|
+
type='simo.fleet.gateways.FleetGatewayHandler'
|
|
264
|
+
)
|
|
265
|
+
fleet_gateway.run()
|
|
262
266
|
|
|
263
267
|
|
|
264
268
|
@receiver(pre_delete, sender=Component)
|
|
@@ -266,81 +270,34 @@ def post_component_delete(sender, instance, *args, **kwargs):
|
|
|
266
270
|
if not instance.controller_uid.startswith('simo.fleet'):
|
|
267
271
|
return
|
|
268
272
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
transaction.on_commit(update_colonel)
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
i2c_interface_no_choices = (
|
|
281
|
-
(0, "0 - Main"), (1, "1 - Secondary"),
|
|
282
|
-
(2, "2 - Software"), (3, "3 - Software")
|
|
283
|
-
)
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
class I2CInterface(models.Model):
|
|
287
|
-
colonel = models.ForeignKey(
|
|
288
|
-
Colonel, on_delete=models.CASCADE, related_name='i2c_interfaces'
|
|
289
|
-
)
|
|
290
|
-
name = models.CharField(max_length=50)
|
|
291
|
-
no = models.IntegerField(
|
|
292
|
-
default=0, choices=i2c_interface_no_choices
|
|
293
|
-
)
|
|
294
|
-
scl_pin = models.ForeignKey(
|
|
295
|
-
ColonelPin, on_delete=models.CASCADE, limit_choices_to={
|
|
296
|
-
'native': True, 'output': True,
|
|
297
|
-
},
|
|
298
|
-
null=True, related_name='i2c_scl'
|
|
299
|
-
)
|
|
300
|
-
sda_pin = models.ForeignKey(
|
|
301
|
-
ColonelPin, on_delete=models.CASCADE, limit_choices_to={
|
|
302
|
-
'native': True, 'output': True,
|
|
303
|
-
},
|
|
304
|
-
null=True, related_name='i2c_sda'
|
|
305
|
-
)
|
|
306
|
-
freq = models.IntegerField(
|
|
307
|
-
default=100000, help_text="100000 - is a good middle point!"
|
|
308
|
-
)
|
|
309
|
-
|
|
310
|
-
objects = InterfacesManager()
|
|
273
|
+
from .controllers import DALIGearGroup
|
|
274
|
+
if instance.controller_uid == DALIGearGroup.uid:
|
|
275
|
+
for comp in Component.objects.filter(
|
|
276
|
+
id__in=instance.config.get('members', [])
|
|
277
|
+
):
|
|
278
|
+
instance.controller._modify_member_group(
|
|
279
|
+
comp, instance.config.get('da', 0), remove=True
|
|
280
|
+
)
|
|
311
281
|
|
|
312
|
-
|
|
313
|
-
|
|
282
|
+
elif instance.controller.family == 'dali':
|
|
283
|
+
colonel = Colonel.objects.filter(id=instance.config['colonel']).first()
|
|
284
|
+
if colonel:
|
|
285
|
+
GatewayObjectCommand(
|
|
286
|
+
instance.gateway, colonel, id=instance.id,
|
|
287
|
+
command='call', method='destroy',
|
|
288
|
+
).publish()
|
|
314
289
|
|
|
315
|
-
|
|
316
|
-
return self.name
|
|
290
|
+
else:
|
|
317
291
|
|
|
292
|
+
affected_colonels = list(Colonel.objects.filter(components=instance))
|
|
318
293
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
occupied_by_content_type=ct,
|
|
325
|
-
occupied_by_id=instance.id
|
|
326
|
-
):
|
|
327
|
-
pin.occupied_by_content_type = None
|
|
328
|
-
pin.occupied_by_content_id = None
|
|
329
|
-
pin.save()
|
|
294
|
+
def update_colonel():
|
|
295
|
+
for colonel in affected_colonels:
|
|
296
|
+
print("Rebuild occupied pins for :", colonel)
|
|
297
|
+
colonel.rebuild_occupied_pins()
|
|
298
|
+
colonel.update_config()
|
|
330
299
|
|
|
331
|
-
|
|
332
|
-
# at this point, therefore this trhows irrelevant exceptions
|
|
333
|
-
# that we want to fail silenty
|
|
334
|
-
try:
|
|
335
|
-
instance.scl_pin.occupied_by = instance
|
|
336
|
-
instance.scl_pin.save()
|
|
337
|
-
except ColonelPin.DoesNotExist:
|
|
338
|
-
pass
|
|
339
|
-
try:
|
|
340
|
-
instance.sda_pin.occupied_by = instance
|
|
341
|
-
instance.sda_pin.save()
|
|
342
|
-
except ColonelPin.DoesNotExist:
|
|
343
|
-
pass
|
|
300
|
+
transaction.on_commit(update_colonel)
|
|
344
301
|
|
|
345
302
|
|
|
346
303
|
class Interface(models.Model):
|
|
@@ -384,6 +341,49 @@ class Interface(models.Model):
|
|
|
384
341
|
return super().save(*args, **kwargs)
|
|
385
342
|
|
|
386
343
|
|
|
344
|
+
def broadcast_reset(self):
|
|
345
|
+
gw = Gateway.objects.filter(type=FleetGatewayHandler.uid).first()
|
|
346
|
+
if not gw:
|
|
347
|
+
return
|
|
348
|
+
GatewayObjectCommand(
|
|
349
|
+
gw, self.colonel, command='broadcast_reset',
|
|
350
|
+
data={'interface': self.no}
|
|
351
|
+
).publish()
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
class InterfaceAddress(models.Model):
|
|
355
|
+
interface = models.ForeignKey(
|
|
356
|
+
Interface, related_name='addresses', on_delete=models.CASCADE
|
|
357
|
+
)
|
|
358
|
+
address_type = models.CharField(
|
|
359
|
+
db_index=True, max_length=100, choices=(
|
|
360
|
+
('i2c', "I2C"),
|
|
361
|
+
('dali-gear', "DALI Gear"),
|
|
362
|
+
('dali-group', "DALI Gear Group")
|
|
363
|
+
)
|
|
364
|
+
)
|
|
365
|
+
address = models.JSONField(db_index=True)
|
|
366
|
+
occupied_by_content_type = models.ForeignKey(
|
|
367
|
+
ContentType, on_delete=models.CASCADE, null=True
|
|
368
|
+
)
|
|
369
|
+
occupied_by_id = models.PositiveIntegerField(null=True)
|
|
370
|
+
occupied_by = GenericForeignKey(
|
|
371
|
+
"occupied_by_content_type", "occupied_by_id"
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
class Meta:
|
|
375
|
+
unique_together = 'interface', 'address_type', 'address'
|
|
376
|
+
|
|
377
|
+
def __str__(self):
|
|
378
|
+
addr = self.address
|
|
379
|
+
if self.address_type == 'i2c':
|
|
380
|
+
try:
|
|
381
|
+
addr = hex(int(self.address))
|
|
382
|
+
except:
|
|
383
|
+
pass
|
|
384
|
+
return f"{self.get_address_type_display()}: {addr}"
|
|
385
|
+
|
|
386
|
+
|
|
387
387
|
@receiver(post_save, sender=Interface)
|
|
388
388
|
def post_interface_save(sender, instance, created, *args, **kwargs):
|
|
389
389
|
if created:
|
|
@@ -399,6 +399,24 @@ def post_interface_save(sender, instance, created, *args, **kwargs):
|
|
|
399
399
|
instance.pin_b.save()
|
|
400
400
|
instance.save()
|
|
401
401
|
|
|
402
|
+
if instance.type == 'i2c':
|
|
403
|
+
for addr in range(128):
|
|
404
|
+
InterfaceAddress.objects.create(
|
|
405
|
+
interface=instance, address_type='i2c',
|
|
406
|
+
address=addr,
|
|
407
|
+
)
|
|
408
|
+
elif instance.type == 'dali':
|
|
409
|
+
for addr in range(64):
|
|
410
|
+
InterfaceAddress.objects.create(
|
|
411
|
+
interface=instance, address_type='dali-gear',
|
|
412
|
+
address=addr,
|
|
413
|
+
)
|
|
414
|
+
for addr in range(16):
|
|
415
|
+
InterfaceAddress.objects.create(
|
|
416
|
+
interface=instance, address_type='dali-group',
|
|
417
|
+
address=addr,
|
|
418
|
+
)
|
|
419
|
+
|
|
402
420
|
|
|
403
421
|
@receiver(post_delete, sender=Interface)
|
|
404
422
|
def post_interface_delete(sender, instance, *args, **kwargs):
|
|
@@ -408,8 +426,4 @@ def post_interface_delete(sender, instance, *args, **kwargs):
|
|
|
408
426
|
occupied_by_content_type=ct,
|
|
409
427
|
occupied_by_id=instance.id
|
|
410
428
|
):
|
|
411
|
-
pin.occupied_by_content_type = None
|
|
412
|
-
pin.occupied_by_content_id = None
|
|
413
|
-
pin.save()
|
|
414
|
-
|
|
415
|
-
|
|
429
|
+
pin.occupied_by_content_type = None
|
simo/fleet/serializers.py
CHANGED
|
@@ -22,7 +22,14 @@ class ColonelPinSerializer(serializers.ModelSerializer):
|
|
|
22
22
|
read_only_fields = fields
|
|
23
23
|
|
|
24
24
|
def get_occupied(self, obj):
|
|
25
|
-
|
|
25
|
+
try:
|
|
26
|
+
return bool(obj.occupied_by)
|
|
27
|
+
except AttributeError:
|
|
28
|
+
# apparently the item type that this pin was occupied by
|
|
29
|
+
# was deleted from the codebase, so we quickly fix it here. :)
|
|
30
|
+
obj.occupied_by = None
|
|
31
|
+
obj.save()
|
|
32
|
+
return False
|
|
26
33
|
|
|
27
34
|
|
|
28
35
|
class ColonelInterfaceSerializer(serializers.ModelSerializer):
|
simo/fleet/socket_consumers.py
CHANGED
|
@@ -378,7 +378,7 @@ class FleetConsumer(AsyncWebsocketConsumer):
|
|
|
378
378
|
component.change_init_fingerprint = fingerprint
|
|
379
379
|
component.controller._receive_from_device(
|
|
380
380
|
data['val'], bool(data.get('alive')),
|
|
381
|
-
data.get('battery_level')
|
|
381
|
+
data.get('battery_level'), data.get('error_msg')
|
|
382
382
|
)
|
|
383
383
|
await sync_to_async(
|
|
384
384
|
receive_val, thread_sensitive=True
|
|
@@ -404,28 +404,16 @@ class FleetConsumer(AsyncWebsocketConsumer):
|
|
|
404
404
|
|
|
405
405
|
elif 'discovery-result' in data:
|
|
406
406
|
def process_discovery_result():
|
|
407
|
-
# check if component is already created
|
|
408
|
-
if data['discovery-result'] == 'success':
|
|
409
|
-
comp = Component.objects.filter(
|
|
410
|
-
meta__finalization_data__temp_id=data['result']['id']
|
|
411
|
-
).first()
|
|
412
|
-
if comp:
|
|
413
|
-
return comp
|
|
414
|
-
|
|
415
407
|
self.gateway.refresh_from_db()
|
|
416
408
|
try:
|
|
417
409
|
self.gateway.process_discovery(data)
|
|
418
410
|
except Exception as e:
|
|
419
411
|
print(traceback.format_exc(), file=sys.stderr)
|
|
420
412
|
|
|
421
|
-
|
|
413
|
+
await sync_to_async(
|
|
422
414
|
process_discovery_result, thread_sensitive=True
|
|
423
415
|
)()
|
|
424
|
-
|
|
425
|
-
await self.send_data({
|
|
426
|
-
'command': 'finalize',
|
|
427
|
-
'data': finished_comp.meta['finalization_data']
|
|
428
|
-
})
|
|
416
|
+
|
|
429
417
|
|
|
430
418
|
elif bytes_data:
|
|
431
419
|
if not self.colonel_logger:
|
|
Binary file
|
simo/generic/gateways.py
CHANGED
|
@@ -263,7 +263,7 @@ class GenericGatewayHandler(BaseObjectCommandsGatewayHandler):
|
|
|
263
263
|
).filter(
|
|
264
264
|
Q(config__autostart=True) |
|
|
265
265
|
Q(value='error', config__keep_alive=True)
|
|
266
|
-
).
|
|
266
|
+
).distinct():
|
|
267
267
|
self.start_script(script)
|
|
268
268
|
|
|
269
269
|
for cam in Component.objects.filter(
|
|
@@ -332,8 +332,13 @@ class GenericGatewayHandler(BaseObjectCommandsGatewayHandler):
|
|
|
332
332
|
component.save(update_fields=['value'])
|
|
333
333
|
return
|
|
334
334
|
if self.running_scripts[component.id].is_alive():
|
|
335
|
+
tz = pytz.timezone(component.zone.instance.timezone)
|
|
336
|
+
timezone.activate(tz)
|
|
335
337
|
logger = get_component_logger(component)
|
|
336
|
-
|
|
338
|
+
if stop_status == 'error':
|
|
339
|
+
logger.log(logging.INFO, "-------GATEWAY STOP-------")
|
|
340
|
+
else:
|
|
341
|
+
logger.log(logging.INFO, "-------STOP-------")
|
|
337
342
|
self.running_scripts[component.id].terminate()
|
|
338
343
|
|
|
339
344
|
def kill():
|
|
@@ -345,9 +350,10 @@ class GenericGatewayHandler(BaseObjectCommandsGatewayHandler):
|
|
|
345
350
|
break
|
|
346
351
|
time.sleep(0.1)
|
|
347
352
|
if not terminated:
|
|
348
|
-
|
|
349
|
-
logging.INFO, "-------KILL
|
|
350
|
-
|
|
353
|
+
if stop_status == 'error':
|
|
354
|
+
logger.log(logging.INFO, "-------GATEWAY KILL-------")
|
|
355
|
+
else:
|
|
356
|
+
logger.log(logging.INFO, "-------KILL!-------")
|
|
351
357
|
self.running_scripts[component.id].kill()
|
|
352
358
|
|
|
353
359
|
component.value = stop_status
|
|
Binary file
|
|
Binary file
|
|
@@ -4,29 +4,30 @@ from django.db import migrations
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
def create_objects(apps, schema_editor):
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
7
|
+
pass
|
|
8
|
+
# legacy
|
|
9
|
+
# PermissionsRole = apps.get_model("users", "PermissionsRole")
|
|
10
|
+
# User = apps.get_model("users", "User")
|
|
11
|
+
#
|
|
12
|
+
# admin_role, new = PermissionsRole.objects.get_or_create(
|
|
13
|
+
# is_superuser=True, name='Admin'
|
|
14
|
+
# )
|
|
15
|
+
# user_role, new = PermissionsRole.objects.get_or_create(
|
|
16
|
+
# name='User', can_manage_users=False, is_default=True
|
|
17
|
+
# )
|
|
18
|
+
# PermissionsRole.objects.get_or_create(
|
|
19
|
+
# name='Guest'
|
|
20
|
+
# )
|
|
21
|
+
# User.objects.get_or_create(
|
|
22
|
+
# email='system@simo.io', defaults={
|
|
23
|
+
# 'role': admin_role, 'name': "System"
|
|
24
|
+
# }
|
|
25
|
+
# )
|
|
26
|
+
# User.objects.get_or_create(
|
|
27
|
+
# email='device@simo.io', defaults={
|
|
28
|
+
# 'role': user_role, 'name': "Device"
|
|
29
|
+
# }
|
|
30
|
+
#)
|
|
30
31
|
|
|
31
32
|
def delete_objects(apps, schema_editor):
|
|
32
33
|
pass
|
|
@@ -4,14 +4,15 @@ from django.db import migrations
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
def forwards_func(apps, schema_editor):
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
7
|
+
pass
|
|
8
|
+
# legacy
|
|
9
|
+
# PermissionsRole = apps.get_model("users", "PermissionsRole")
|
|
10
|
+
#
|
|
11
|
+
# for role in PermissionsRole.objects.filter(instance=None):
|
|
12
|
+
# for iu in role.instanceuser_set.all():
|
|
13
|
+
# iu.user.is_god = True
|
|
14
|
+
# iu.user.save()
|
|
15
|
+
# role.delete()
|
|
15
16
|
|
|
16
17
|
def reverse_func(apps, schema_editor):
|
|
17
18
|
pass
|
|
Binary file
|
|
Binary file
|
simo/users/models.py
CHANGED
|
@@ -1,19 +1,15 @@
|
|
|
1
1
|
import datetime
|
|
2
2
|
import requests
|
|
3
3
|
import subprocess
|
|
4
|
-
import sys
|
|
5
4
|
from django.urls import reverse
|
|
6
5
|
from django.utils.translation import gettext_lazy as _
|
|
7
6
|
from django.db import models
|
|
8
|
-
from django.db.models import Q
|
|
9
7
|
from django.db import transaction
|
|
10
8
|
from django.db.models.signals import post_save, post_delete, m2m_changed
|
|
11
9
|
from django.dispatch import receiver
|
|
12
|
-
from model_utils import FieldTracker
|
|
13
10
|
from dirtyfields import DirtyFieldsMixin
|
|
14
11
|
from django.contrib.gis.geos import Point
|
|
15
12
|
from geopy.distance import distance
|
|
16
|
-
from django.core.exceptions import ValidationError
|
|
17
13
|
from django.contrib.auth.models import (
|
|
18
14
|
AbstractBaseUser, PermissionsMixin, UserManager as DefaultUserManager
|
|
19
15
|
)
|
|
@@ -481,11 +477,12 @@ def rebuild_mqtt_acls_on_create(sender, instance, created, **kwargs):
|
|
|
481
477
|
def create_component_permissions_comp(sender, instance, created, **kwargs):
|
|
482
478
|
if created:
|
|
483
479
|
for role in PermissionsRole.objects.filter(
|
|
484
|
-
|
|
480
|
+
instance=instance.zone.instance
|
|
485
481
|
):
|
|
486
482
|
ComponentPermission.objects.get_or_create(
|
|
487
483
|
component=instance, role=role, defaults={
|
|
488
|
-
'read': role.is_superuser
|
|
484
|
+
'read': role.is_superuser or role.is_owner,
|
|
485
|
+
'write': role.is_superuser or role.is_owner
|
|
489
486
|
}
|
|
490
487
|
)
|
|
491
488
|
rebuild_mqtt_acls.delay()
|
|
@@ -575,4 +572,6 @@ class InstanceInvitation(models.Model):
|
|
|
575
572
|
return response
|
|
576
573
|
|
|
577
574
|
def get_absolute_url(self):
|
|
578
|
-
return reverse('accept_invitation', kwargs={'token': self.token})
|
|
575
|
+
return reverse('accept_invitation', kwargs={'token': self.token})
|
|
576
|
+
|
|
577
|
+
|