simo 2.11.4__py3-none-any.whl → 3.0.4__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/__pycache__/settings.cpython-312.pyc +0 -0
- simo/asgi.py +25 -6
- simo/automation/__pycache__/controllers.cpython-312.pyc +0 -0
- simo/automation/controllers.py +18 -2
- simo/automation/forms.py +15 -24
- simo/automation/gateways.py +32 -16
- simo/core/__pycache__/admin.cpython-312.pyc +0 -0
- simo/core/__pycache__/base_types.cpython-312.pyc +0 -0
- simo/core/__pycache__/controllers.cpython-312.pyc +0 -0
- simo/core/__pycache__/forms.cpython-312.pyc +0 -0
- simo/core/__pycache__/models.cpython-312.pyc +0 -0
- simo/core/__pycache__/serializers.cpython-312.pyc +0 -0
- simo/core/__pycache__/signal_receivers.cpython-312.pyc +0 -0
- simo/core/__pycache__/tasks.cpython-312.pyc +0 -0
- simo/core/admin.py +5 -4
- simo/core/base_types.py +191 -18
- simo/core/controllers.py +259 -26
- simo/core/forms.py +10 -2
- simo/core/management/_hub_template/hub/nginx.conf +23 -50
- simo/core/management/_hub_template/hub/supervisor.conf +15 -0
- simo/core/mcp.py +154 -0
- simo/core/migrations/0051_instance_ai_memory.py +18 -0
- simo/core/migrations/__pycache__/0051_instance_ai_memory.cpython-312.pyc +0 -0
- simo/core/models.py +3 -0
- simo/core/serializers.py +120 -0
- simo/core/signal_receivers.py +1 -1
- simo/core/tasks.py +1 -3
- simo/core/utils/__pycache__/type_constants.cpython-312.pyc +0 -0
- simo/core/utils/type_constants.py +78 -17
- simo/fleet/__pycache__/admin.cpython-312.pyc +0 -0
- simo/fleet/__pycache__/api.cpython-312.pyc +0 -0
- simo/fleet/__pycache__/base_types.cpython-312.pyc +0 -0
- simo/fleet/__pycache__/controllers.cpython-312.pyc +0 -0
- simo/fleet/__pycache__/forms.cpython-312.pyc +0 -0
- simo/fleet/__pycache__/gateways.cpython-312.pyc +0 -0
- simo/fleet/__pycache__/models.cpython-312.pyc +0 -0
- simo/fleet/__pycache__/serializers.cpython-312.pyc +0 -0
- simo/fleet/admin.py +5 -1
- simo/fleet/api.py +2 -27
- simo/fleet/base_types.py +35 -4
- simo/fleet/controllers.py +162 -156
- simo/fleet/forms.py +58 -88
- simo/fleet/gateways.py +8 -15
- simo/fleet/migrations/0055_colonel_is_vo_active_colonel_last_wake_and_more.py +28 -0
- simo/fleet/migrations/0056_delete_customdalidevice.py +16 -0
- simo/fleet/migrations/__pycache__/0055_colonel_is_vo_active_colonel_last_wake_and_more.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0056_delete_customdalidevice.cpython-312.pyc +0 -0
- simo/fleet/models.py +13 -72
- simo/fleet/serializers.py +1 -48
- simo/fleet/socket_consumers.py +100 -39
- simo/fleet/tasks.py +2 -22
- simo/fleet/voice_assistant.py +903 -0
- simo/generic/__pycache__/base_types.cpython-312.pyc +0 -0
- simo/generic/__pycache__/controllers.cpython-312.pyc +0 -0
- simo/generic/__pycache__/gateways.cpython-312.pyc +0 -0
- simo/generic/base_types.py +70 -10
- simo/generic/controllers.py +104 -17
- simo/generic/gateways.py +10 -10
- simo/mcp_server/__init__.py +0 -0
- simo/mcp_server/__pycache__/__init__.cpython-312.pyc +0 -0
- simo/mcp_server/__pycache__/admin.cpython-312.pyc +0 -0
- simo/mcp_server/__pycache__/models.cpython-312.pyc +0 -0
- simo/mcp_server/admin.py +18 -0
- simo/mcp_server/app.py +4 -0
- simo/mcp_server/auth.py +34 -0
- simo/mcp_server/dummy.py +22 -0
- simo/mcp_server/migrations/0001_initial.py +30 -0
- simo/mcp_server/migrations/0002_alter_instanceaccesstoken_date_expired.py +18 -0
- simo/mcp_server/migrations/0003_instanceaccesstoken_issuer.py +18 -0
- simo/mcp_server/migrations/__init__.py +0 -0
- simo/mcp_server/migrations/__pycache__/0001_initial.cpython-312.pyc +0 -0
- simo/mcp_server/migrations/__pycache__/0002_alter_instanceaccesstoken_date_expired.cpython-312.pyc +0 -0
- simo/mcp_server/migrations/__pycache__/0003_instanceaccesstoken_issuer.cpython-312.pyc +0 -0
- simo/mcp_server/migrations/__pycache__/__init__.cpython-312.pyc +0 -0
- simo/mcp_server/models.py +27 -0
- simo/mcp_server/server.py +60 -0
- simo/mcp_server/tasks.py +19 -0
- simo/multimedia/__pycache__/base_types.cpython-312.pyc +0 -0
- simo/multimedia/__pycache__/controllers.cpython-312.pyc +0 -0
- simo/multimedia/base_types.py +29 -4
- simo/multimedia/controllers.py +66 -19
- simo/settings.py +1 -0
- simo/users/__pycache__/utils.cpython-312.pyc +0 -0
- simo/users/utils.py +10 -0
- {simo-2.11.4.dist-info → simo-3.0.4.dist-info}/METADATA +11 -4
- {simo-2.11.4.dist-info → simo-3.0.4.dist-info}/RECORD +90 -64
- simo/fleet/custom_dali_operations.py +0 -287
- {simo-2.11.4.dist-info → simo-3.0.4.dist-info}/WHEEL +0 -0
- {simo-2.11.4.dist-info → simo-3.0.4.dist-info}/entry_points.txt +0 -0
- {simo-2.11.4.dist-info → simo-3.0.4.dist-info}/licenses/LICENSE.md +0 -0
- {simo-2.11.4.dist-info → simo-3.0.4.dist-info}/top_level.txt +0 -0
simo/fleet/forms.py
CHANGED
|
@@ -7,7 +7,7 @@ from django.urls.base import get_script_prefix
|
|
|
7
7
|
from django.contrib.contenttypes.models import ContentType
|
|
8
8
|
from django.utils import timezone
|
|
9
9
|
from dal import forward
|
|
10
|
-
from simo.core.models import Component
|
|
10
|
+
from simo.core.models import Component, Category
|
|
11
11
|
from simo.core.forms import (
|
|
12
12
|
BaseComponentForm, ValueLimitForm, NumericSensorForm
|
|
13
13
|
)
|
|
@@ -25,7 +25,7 @@ from simo.core.form_fields import (
|
|
|
25
25
|
)
|
|
26
26
|
from simo.core.form_fields import PlainLocationField
|
|
27
27
|
from simo.users.models import PermissionsRole
|
|
28
|
-
from .models import Colonel, ColonelPin, Interface
|
|
28
|
+
from .models import Colonel, ColonelPin, Interface
|
|
29
29
|
from .utils import INTERFACES_PINS_MAP, get_all_control_input_choices
|
|
30
30
|
|
|
31
31
|
|
|
@@ -1792,81 +1792,55 @@ class DALIButtonConfigForm(DALIDeviceConfigForm, BaseComponentForm):
|
|
|
1792
1792
|
pass
|
|
1793
1793
|
|
|
1794
1794
|
|
|
1795
|
-
class
|
|
1796
|
-
|
|
1795
|
+
class SentinelDeviceConfigForm(BaseComponentForm):
|
|
1796
|
+
colonel = Select2ModelChoiceField(
|
|
1797
|
+
label="Sentinel", queryset=Colonel.objects.filter(type='sentinel'),
|
|
1798
|
+
url='autocomplete-colonels',
|
|
1799
|
+
)
|
|
1797
1800
|
|
|
1798
1801
|
def __init__(self, *args, **kwargs):
|
|
1799
1802
|
super().__init__(*args, **kwargs)
|
|
1800
|
-
|
|
1803
|
+
# Limit colonels to current instance for convenience
|
|
1801
1804
|
instance = get_current_instance()
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
choices.append((f"wifi-{colonel.id}", colonel.name))
|
|
1806
|
-
for device in CustomDaliDevice.objects.filter(
|
|
1807
|
-
instance=instance,
|
|
1808
|
-
last_seen__gt=timezone.now() - datetime.timedelta(minutes=10)
|
|
1809
|
-
):
|
|
1810
|
-
choices.append((f"dali-{device.id}", device.name))
|
|
1811
|
-
self.fields['device'].choices = choices
|
|
1812
|
-
|
|
1813
|
-
def get_device(self, field_val):
|
|
1814
|
-
if field_val.startswith('wifi'):
|
|
1815
|
-
return Colonel.objects.filter(
|
|
1816
|
-
id=self.cleaned_data['device'][5:]
|
|
1817
|
-
).first()
|
|
1818
|
-
else:
|
|
1819
|
-
return CustomDaliDevice.objects.filter(
|
|
1820
|
-
id=self.cleaned_data['device'][5:]
|
|
1805
|
+
if instance:
|
|
1806
|
+
self.fields['colonel'].queryset = self.fields['colonel'].queryset.filter(
|
|
1807
|
+
instance=instance
|
|
1821
1808
|
)
|
|
1822
1809
|
|
|
1823
|
-
|
|
1824
|
-
class RoomSensorDeviceConfigForm(CustomDaliDeviceForm):
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
1810
|
def save(self, commit=True):
|
|
1828
1811
|
from simo.core.models import Icon
|
|
1829
|
-
colonel =
|
|
1830
|
-
|
|
1831
|
-
device = self.get_device(self.cleaned_data['device'])
|
|
1832
|
-
if not device:
|
|
1812
|
+
colonel = self.cleaned_data.get('colonel')
|
|
1813
|
+
if not colonel:
|
|
1833
1814
|
return
|
|
1834
|
-
if isinstance(device, CustomDaliDevice):
|
|
1835
|
-
dali_device = device
|
|
1836
|
-
else:
|
|
1837
|
-
colonel = device
|
|
1838
1815
|
|
|
1839
1816
|
from .controllers import (
|
|
1840
1817
|
RoomSiren, AirQualitySensor, TempHumSensor, AmbientLightSensor,
|
|
1841
|
-
RoomPresenceSensor
|
|
1818
|
+
RoomPresenceSensor, VoiceAssistant, SmokeDetector
|
|
1842
1819
|
)
|
|
1843
1820
|
|
|
1844
1821
|
org_name = self.cleaned_data['name']
|
|
1845
1822
|
org_icon = self.cleaned_data['icon']
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
(
|
|
1849
|
-
(
|
|
1850
|
-
(
|
|
1851
|
-
(
|
|
1823
|
+
last_comp = None
|
|
1824
|
+
for CtrlClass, icon, suffix, cat_slug in (
|
|
1825
|
+
(RoomSiren, 'siren', 'siren', 'security'),
|
|
1826
|
+
(AirQualitySensor, 'leaf', 'air quality', 'climate'),
|
|
1827
|
+
(TempHumSensor, 'temperature-half', 'temperature', 'climate'),
|
|
1828
|
+
(AmbientLightSensor, 'brightness-low', 'brightness', 'light'),
|
|
1829
|
+
(RoomPresenceSensor, 'person', 'presence', 'security'),
|
|
1830
|
+
(VoiceAssistant, 'microphone-lines', 'voice assistant', 'other'),
|
|
1831
|
+
(SmokeDetector, 'fire-smoke', 'dust/pollution', 'security'),
|
|
1852
1832
|
):
|
|
1853
1833
|
default_icon = Icon.objects.filter(slug=icon).first()
|
|
1854
|
-
if default_icon
|
|
1855
|
-
self.cleaned_data['icon'] = default_icon.slug
|
|
1856
|
-
else:
|
|
1857
|
-
self.cleaned_data['icon'] = org_icon
|
|
1834
|
+
self.cleaned_data['icon'] = default_icon.slug if default_icon else org_icon
|
|
1858
1835
|
self.cleaned_data['name'] = f"{org_name} {suffix}"
|
|
1836
|
+
self.cleaned_data['category'] = Category.objects.filter(
|
|
1837
|
+
name__icontains=cat_slug
|
|
1838
|
+
).first()
|
|
1859
1839
|
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
).first()
|
|
1865
|
-
else:
|
|
1866
|
-
comp = Component.objects.filter(
|
|
1867
|
-
config__dali_device=dali_device.id,
|
|
1868
|
-
controller_uid=CtrlClass.uid
|
|
1869
|
-
).first()
|
|
1840
|
+
comp = Component.objects.filter(
|
|
1841
|
+
config__colonel=colonel.id,
|
|
1842
|
+
controller_uid=CtrlClass.uid
|
|
1843
|
+
).first()
|
|
1870
1844
|
|
|
1871
1845
|
form = CtrlClass.config_form(
|
|
1872
1846
|
controller_uid=CtrlClass.uid, instance=comp,
|
|
@@ -1874,47 +1848,43 @@ class RoomSensorDeviceConfigForm(CustomDaliDeviceForm):
|
|
|
1874
1848
|
)
|
|
1875
1849
|
if form.is_valid():
|
|
1876
1850
|
comp = form.save()
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
else:
|
|
1880
|
-
comp.config['dali_device'] = dali_device.id
|
|
1851
|
+
comp.value_units = CtrlClass.default_value_units
|
|
1852
|
+
comp.config['colonel'] = colonel.id
|
|
1881
1853
|
comp.save()
|
|
1854
|
+
last_comp = comp
|
|
1882
1855
|
else:
|
|
1883
1856
|
raise Exception(form.errors)
|
|
1884
1857
|
|
|
1885
|
-
if colonel:
|
|
1858
|
+
if colonel and last_comp:
|
|
1886
1859
|
GatewayObjectCommand(
|
|
1887
|
-
|
|
1860
|
+
last_comp.gateway, colonel, id=last_comp.id,
|
|
1888
1861
|
command='call', method='update_config', args=[
|
|
1889
|
-
|
|
1862
|
+
last_comp.controller._get_colonel_config()
|
|
1890
1863
|
]
|
|
1891
1864
|
).publish()
|
|
1892
1865
|
|
|
1893
|
-
return
|
|
1866
|
+
return last_comp
|
|
1894
1867
|
|
|
1895
1868
|
|
|
1896
|
-
class RoomZonePresenceConfigForm(
|
|
1869
|
+
class RoomZonePresenceConfigForm(BaseComponentForm):
|
|
1870
|
+
colonel = Select2ModelChoiceField(
|
|
1871
|
+
label="Sentinel", queryset=Colonel.objects.filter(type='sentinel'),
|
|
1872
|
+
url='autocomplete-colonels',
|
|
1873
|
+
)
|
|
1897
1874
|
|
|
1898
|
-
def
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
id=value[5:]
|
|
1905
|
-
).first()
|
|
1906
|
-
free_slots = {0, 1, 2, 3, 4, 5, 6, 7}
|
|
1907
|
-
for comp in Component.objects.filter(
|
|
1908
|
-
controller_uid=RoomZonePresenceSensor.uid,
|
|
1909
|
-
config__dali_device=dali_device.id
|
|
1910
|
-
):
|
|
1911
|
-
try:
|
|
1912
|
-
free_slots.remove(int(comp.config['slot']))
|
|
1913
|
-
except:
|
|
1914
|
-
continue
|
|
1915
|
-
if not free_slots:
|
|
1916
|
-
raise forms.ValidationError(
|
|
1917
|
-
"This device already has 8 zones defined. "
|
|
1918
|
-
"Please first delete some to add a new one."
|
|
1875
|
+
def __init__(self, *args, **kwargs):
|
|
1876
|
+
super().__init__(*args, **kwargs)
|
|
1877
|
+
instance = get_current_instance()
|
|
1878
|
+
if instance:
|
|
1879
|
+
self.fields['colonel'].queryset = self.fields['colonel'].queryset.filter(
|
|
1880
|
+
instance=instance
|
|
1919
1881
|
)
|
|
1920
|
-
|
|
1882
|
+
|
|
1883
|
+
|
|
1884
|
+
class VoiceAssistantConfigForm(BaseComponentForm):
|
|
1885
|
+
voice = forms.ChoiceField(
|
|
1886
|
+
label="Voice",
|
|
1887
|
+
required=True, choices=(
|
|
1888
|
+
('male', "Male"), ('female', "Female"),
|
|
1889
|
+
),
|
|
1890
|
+
)
|
simo/fleet/gateways.py
CHANGED
|
@@ -127,24 +127,17 @@ class FleetGatewayHandler(BaseObjectCommandsGatewayHandler):
|
|
|
127
127
|
form_cleaned_data = deserialize_form_data(
|
|
128
128
|
gw.discovery['init_data']
|
|
129
129
|
)
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
130
|
+
# Room-zone presence discovery now only supports network sentinels
|
|
131
|
+
colonel = Colonel.objects.filter(
|
|
132
|
+
id=form_cleaned_data['colonel'].id
|
|
133
|
+
if hasattr(form_cleaned_data.get('colonel'), 'id')
|
|
134
|
+
else form_cleaned_data.get('colonel')
|
|
135
|
+
).first()
|
|
136
|
+
if colonel:
|
|
134
137
|
GatewayObjectCommand(
|
|
135
138
|
gw, colonel,
|
|
136
139
|
command='discover', type=self.uid.split('.')[-1],
|
|
137
140
|
).publish()
|
|
138
|
-
else:
|
|
139
|
-
from .models import CustomDaliDevice
|
|
140
|
-
from .custom_dali_operations import Frame
|
|
141
|
-
dali_device = CustomDaliDevice.objects.filter(
|
|
142
|
-
id=form_cleaned_data['device'][5:]
|
|
143
|
-
).first()
|
|
144
|
-
frame = Frame(40, bytes(bytearray(5)))
|
|
145
|
-
frame[8:11] = 15 # command to custom dali device
|
|
146
|
-
frame[12:15] = 0 # action to perform: start room zone discovery
|
|
147
|
-
dali_device.transmit(frame)
|
|
148
141
|
|
|
149
142
|
|
|
150
143
|
|
|
@@ -178,4 +171,4 @@ class FleetGatewayHandler(BaseObjectCommandsGatewayHandler):
|
|
|
178
171
|
f"| Btn type: {method}"
|
|
179
172
|
)
|
|
180
173
|
comp.controller._ctrl(j, btn.value, method)
|
|
181
|
-
break
|
|
174
|
+
break
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Generated by Django 4.2.10 on 2025-09-11 06:02
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
('fleet', '0054_auto_20250507_1256'),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.AddField(
|
|
14
|
+
model_name='colonel',
|
|
15
|
+
name='is_vo_active',
|
|
16
|
+
field=models.BooleanField(db_index=True, default=False),
|
|
17
|
+
),
|
|
18
|
+
migrations.AddField(
|
|
19
|
+
model_name='colonel',
|
|
20
|
+
name='last_wake',
|
|
21
|
+
field=models.DateTimeField(db_index=True, editable=False, null=True),
|
|
22
|
+
),
|
|
23
|
+
migrations.AddField(
|
|
24
|
+
model_name='colonel',
|
|
25
|
+
name='wake_stats',
|
|
26
|
+
field=models.JSONField(db_index=True, default=dict, editable=False),
|
|
27
|
+
),
|
|
28
|
+
]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Generated by Django 4.2.10 on 2025-09-26 06:54
|
|
2
|
+
|
|
3
|
+
from django.db import migrations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
('fleet', '0055_colonel_is_vo_active_colonel_last_wake_and_more'),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.DeleteModel(
|
|
14
|
+
name='CustomDaliDevice',
|
|
15
|
+
),
|
|
16
|
+
]
|
|
Binary file
|
simo/fleet/models.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import requests
|
|
2
2
|
import time
|
|
3
|
-
import random
|
|
4
3
|
import datetime
|
|
5
4
|
from actstream import action
|
|
6
5
|
from django.core.exceptions import ValidationError
|
|
@@ -68,7 +67,7 @@ class Colonel(DirtyFieldsMixin, models.Model):
|
|
|
68
67
|
('ample-wall', "Ample Wall"),
|
|
69
68
|
('game-changer', "Game Changer"),
|
|
70
69
|
('game-changer-mini', "Game Changer Mini"),
|
|
71
|
-
('
|
|
70
|
+
('sentinel', "Sentinel")
|
|
72
71
|
)
|
|
73
72
|
)
|
|
74
73
|
firmware_version = models.CharField(
|
|
@@ -105,6 +104,13 @@ class Colonel(DirtyFieldsMixin, models.Model):
|
|
|
105
104
|
(0, "3kHz"), (1, "22kHz")
|
|
106
105
|
), help_text="Affects Ample Wall dimmer PWM output (dimmer) frequency")
|
|
107
106
|
|
|
107
|
+
# Sentinel voice assistant specific fields
|
|
108
|
+
wake_stats = models.JSONField(
|
|
109
|
+
default=dict, editable=False, db_index=True
|
|
110
|
+
)
|
|
111
|
+
last_wake = models.DateTimeField(null=True, editable=False, db_index=True)
|
|
112
|
+
is_vo_active = models.BooleanField(default=False, db_index=True)
|
|
113
|
+
|
|
108
114
|
objects = ColonelsManager()
|
|
109
115
|
|
|
110
116
|
def __str__(self):
|
|
@@ -121,6 +127,11 @@ class Colonel(DirtyFieldsMixin, models.Model):
|
|
|
121
127
|
if self.major_upgrade_available and self.firmware_version == self.major_upgrade_available:
|
|
122
128
|
self.major_upgrade_available = None
|
|
123
129
|
|
|
130
|
+
if self.is_vo_active:
|
|
131
|
+
Colonel.objects.filter(
|
|
132
|
+
instance=self.instance
|
|
133
|
+
).exclude(id=self.id).update(is_vo_active=False)
|
|
134
|
+
|
|
124
135
|
return super().save(*args, **kwargs)
|
|
125
136
|
|
|
126
137
|
@property
|
|
@@ -558,73 +569,3 @@ def post_interface_delete(sender, instance, *args, **kwargs):
|
|
|
558
569
|
ColonelPin.objects.bulk_update(
|
|
559
570
|
pins, ["occupied_by_content_type", "occupied_by_id"]
|
|
560
571
|
)
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
class CustomDaliDevice(models.Model):
|
|
564
|
-
'''
|
|
565
|
-
Our own custom dali line device,
|
|
566
|
-
not compatible with anything else of DALI!
|
|
567
|
-
'''
|
|
568
|
-
instance = models.ForeignKey(Instance, on_delete=models.CASCADE)
|
|
569
|
-
uid = models.CharField(max_length=100, db_index=True)
|
|
570
|
-
random_address = models.PositiveIntegerField(db_index=True, editable=False)
|
|
571
|
-
name = models.CharField(
|
|
572
|
-
max_length=200, help_text="User given name on initial pairing"
|
|
573
|
-
)
|
|
574
|
-
interface = models.ForeignKey(
|
|
575
|
-
Interface, null=True, blank=True, editable=False,
|
|
576
|
-
on_delete=models.SET_NULL, related_name='custom_devices',
|
|
577
|
-
help_text="Colonel interface on which it operates."
|
|
578
|
-
)
|
|
579
|
-
last_seen = models.DateTimeField(null=True, editable=False)
|
|
580
|
-
components = models.ManyToManyField(Component)
|
|
581
|
-
|
|
582
|
-
class Meta:
|
|
583
|
-
unique_together = 'instance', 'random_address'
|
|
584
|
-
|
|
585
|
-
def save(self, *args, **kwargs):
|
|
586
|
-
if not self.random_address:
|
|
587
|
-
while True:
|
|
588
|
-
self.random_address = random.randint(0, 255)
|
|
589
|
-
if CustomDaliDevice.objects.filter(
|
|
590
|
-
random_address=self.random_address,
|
|
591
|
-
instance=self.instance
|
|
592
|
-
).first():
|
|
593
|
-
continue
|
|
594
|
-
break
|
|
595
|
-
return super().save(*args, **kwargs)
|
|
596
|
-
|
|
597
|
-
def transmit(self, frame):
|
|
598
|
-
from .gateways import FleetGatewayHandler
|
|
599
|
-
frame[0:7] = self.random_address
|
|
600
|
-
gateway = Gateway.objects.filter(type=FleetGatewayHandler.uid).first()
|
|
601
|
-
GatewayObjectCommand(
|
|
602
|
-
gateway, self.interface.colonel,
|
|
603
|
-
command='da-tx', interface=self.interface.no,
|
|
604
|
-
msg=frame.pack.hex()
|
|
605
|
-
).publish()
|
|
606
|
-
|
|
607
|
-
@property
|
|
608
|
-
def is_alive(self):
|
|
609
|
-
if not self.last_seen:
|
|
610
|
-
return False
|
|
611
|
-
return self.last_seen + datetime.timedelta(seconds=60) > timezone.now()
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
@receiver(post_save, sender=Component)
|
|
615
|
-
def attatch_components_to_dali_device(sender, instance, created, *args, **kwargs):
|
|
616
|
-
if not instance.controller_uid.startswith('simo.fleet'):
|
|
617
|
-
return
|
|
618
|
-
if 'config' not in instance.get_dirty_fields():
|
|
619
|
-
return
|
|
620
|
-
dali_device = CustomDaliDevice.objects.filter(
|
|
621
|
-
id=instance.config.get('dali_device', 0)
|
|
622
|
-
).first()
|
|
623
|
-
if not dali_device:
|
|
624
|
-
return
|
|
625
|
-
dali_device.components.add(instance)
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
@receiver(pre_delete, sender=CustomDaliDevice)
|
|
629
|
-
def delete_dali_device_components(sender, instance, *args, **kwargs):
|
|
630
|
-
instance.components.all().delete()
|
simo/fleet/serializers.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from rest_framework import serializers
|
|
2
2
|
from simo.core.serializers import TimestampField
|
|
3
3
|
from .models import (
|
|
4
|
-
InstanceOptions, Colonel, ColonelPin, Interface
|
|
4
|
+
InstanceOptions, Colonel, ColonelPin, Interface
|
|
5
5
|
)
|
|
6
6
|
|
|
7
7
|
|
|
@@ -87,50 +87,3 @@ class ColonelSerializer(serializers.ModelSerializer):
|
|
|
87
87
|
instance.update_config()
|
|
88
88
|
return instance
|
|
89
89
|
|
|
90
|
-
|
|
91
|
-
class CustomDaliDeviceSerializer(serializers.ModelSerializer):
|
|
92
|
-
is_empty = serializers.SerializerMethodField()
|
|
93
|
-
is_alive = serializers.SerializerMethodField()
|
|
94
|
-
last_seen = TimestampField(read_only=True)
|
|
95
|
-
|
|
96
|
-
class Meta:
|
|
97
|
-
model = CustomDaliDevice
|
|
98
|
-
fields = (
|
|
99
|
-
'id', 'uid', 'random_address', 'name', 'is_empty',
|
|
100
|
-
'is_alive', 'last_seen'
|
|
101
|
-
)
|
|
102
|
-
read_only_fields = (
|
|
103
|
-
'random_address', 'is_empty', 'is_alive', 'last_seen'
|
|
104
|
-
)
|
|
105
|
-
|
|
106
|
-
def validate(self, data):
|
|
107
|
-
instance = self.context.get('instance')
|
|
108
|
-
uid = data.get('uid')
|
|
109
|
-
if instance and uid:
|
|
110
|
-
if CustomDaliDevice.objects.filter(
|
|
111
|
-
uid=uid, instance=instance
|
|
112
|
-
).exists():
|
|
113
|
-
raise serializers.ValidationError(
|
|
114
|
-
f"A device with uid '{uid}' already exists for this instance."
|
|
115
|
-
)
|
|
116
|
-
return data
|
|
117
|
-
|
|
118
|
-
def validate_uid(self, value):
|
|
119
|
-
"""
|
|
120
|
-
Prevent changing the uid on update.
|
|
121
|
-
"""
|
|
122
|
-
# self.instance will be None for creation, but set for updates.
|
|
123
|
-
if self.instance and self.instance.uid != value:
|
|
124
|
-
raise serializers.ValidationError("Changing uid is not allowed.")
|
|
125
|
-
return value
|
|
126
|
-
|
|
127
|
-
def create(self, validated_data):
|
|
128
|
-
validated_data['instance'] = self.context['instance']
|
|
129
|
-
return super().create(validated_data)
|
|
130
|
-
|
|
131
|
-
def get_is_empty(self, obj):
|
|
132
|
-
return not bool(obj.components.all().count())
|
|
133
|
-
|
|
134
|
-
def get_is_alive(self, obj):
|
|
135
|
-
return obj.is_alive
|
|
136
|
-
|