simo 2.8.15__py3-none-any.whl → 2.10.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of simo might be problematic. Click here for more details.
- simo/automation/__pycache__/gateways.cpython-312.pyc +0 -0
- simo/automation/gateways.py +12 -10
- simo/core/__pycache__/admin.cpython-312.pyc +0 -0
- simo/core/__pycache__/auto_urls.cpython-312.pyc +0 -0
- simo/core/__pycache__/controllers.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__/tasks.cpython-312.pyc +0 -0
- simo/core/__pycache__/views.cpython-312.pyc +0 -0
- simo/core/admin.py +5 -2
- simo/core/auto_urls.py +4 -1
- simo/core/controllers.py +42 -5
- simo/core/models.py +32 -16
- simo/core/serializers.py +2 -2
- simo/core/tasks.py +8 -1
- simo/core/templates/admin/core/component_change_form.html +1 -1
- simo/core/templates/admin/wizard/discovery.html +3 -4
- simo/core/templates/admin/wizard/wizard_add.html +1 -1
- simo/core/views.py +26 -2
- 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__/managers.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/__pycache__/socket_consumers.cpython-312.pyc +0 -0
- simo/fleet/api.py +26 -3
- simo/fleet/base_types.py +1 -0
- simo/fleet/controllers.py +240 -7
- simo/fleet/custom_dali_operations.py +275 -0
- simo/fleet/forms.py +132 -3
- simo/fleet/managers.py +3 -1
- simo/fleet/migrations/0045_alter_colonel_type_customdalidevice.py +29 -0
- simo/fleet/migrations/0046_delete_customdalidevice.py +16 -0
- simo/fleet/migrations/0047_customdalidevice.py +28 -0
- simo/fleet/migrations/0048_remove_customdalidevice_colonel_and_more.py +28 -0
- simo/fleet/migrations/__pycache__/0045_alter_colonel_type_customdalidevice.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0046_delete_customdalidevice.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0047_customdalidevice.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0048_remove_customdalidevice_colonel_and_more.cpython-312.pyc +0 -0
- simo/fleet/models.py +54 -9
- simo/fleet/serializers.py +15 -1
- simo/fleet/socket_consumers.py +6 -0
- simo/fleet/tasks.py +22 -2
- simo/fleet/templates/fleet/controllers_info/RoomZonePresenceSensor.md +8 -0
- simo/generic/__pycache__/controllers.cpython-312.pyc +0 -0
- simo/generic/__pycache__/forms.cpython-312.pyc +0 -0
- simo/generic/__pycache__/gateways.cpython-312.pyc +0 -0
- simo/generic/controllers.py +99 -43
- simo/generic/forms.py +13 -10
- simo/generic/gateways.py +91 -2
- simo/generic/migrations/0003_auto_20250409_1404.py +33 -0
- simo/generic/migrations/__pycache__/0003_auto_20250409_1404.cpython-312.pyc +0 -0
- simo/users/__pycache__/api.cpython-312.pyc +0 -0
- simo/users/__pycache__/dynamic_settings.cpython-312.pyc +0 -0
- simo/users/api.py +71 -18
- simo/users/dynamic_settings.py +1 -1
- {simo-2.8.15.dist-info → simo-2.10.1.dist-info}/METADATA +1 -1
- {simo-2.8.15.dist-info → simo-2.10.1.dist-info}/RECORD +64 -52
- {simo-2.8.15.dist-info → simo-2.10.1.dist-info}/WHEEL +0 -0
- {simo-2.8.15.dist-info → simo-2.10.1.dist-info}/entry_points.txt +0 -0
- {simo-2.8.15.dist-info → simo-2.10.1.dist-info}/licenses/LICENSE.md +0 -0
- {simo-2.8.15.dist-info → simo-2.10.1.dist-info}/top_level.txt +0 -0
simo/fleet/forms.py
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import time
|
|
2
|
+
import datetime
|
|
2
3
|
from django import forms
|
|
3
4
|
from django.utils.translation import gettext_lazy as _
|
|
4
5
|
from django.forms import formset_factory
|
|
5
6
|
from django.urls.base import get_script_prefix
|
|
6
7
|
from django.contrib.contenttypes.models import ContentType
|
|
7
|
-
from
|
|
8
|
+
from django.utils import timezone
|
|
8
9
|
from dal import forward
|
|
9
10
|
from simo.core.models import Component
|
|
10
11
|
from simo.core.forms import (
|
|
@@ -24,7 +25,7 @@ from simo.core.form_fields import (
|
|
|
24
25
|
)
|
|
25
26
|
from simo.core.form_fields import PlainLocationField
|
|
26
27
|
from simo.users.models import PermissionsRole
|
|
27
|
-
from .models import Colonel, ColonelPin, Interface
|
|
28
|
+
from .models import Colonel, ColonelPin, Interface, CustomDaliDevice
|
|
28
29
|
from .utils import INTERFACES_PINS_MAP, get_all_control_input_choices
|
|
29
30
|
|
|
30
31
|
|
|
@@ -1817,4 +1818,132 @@ class DALILightSensorConfigForm(DALIDeviceConfigForm, BaseComponentForm):
|
|
|
1817
1818
|
|
|
1818
1819
|
|
|
1819
1820
|
class DALIButtonConfigForm(DALIDeviceConfigForm, BaseComponentForm):
|
|
1820
|
-
pass
|
|
1821
|
+
pass
|
|
1822
|
+
|
|
1823
|
+
|
|
1824
|
+
class CustomDaliDeviceForm(BaseComponentForm):
|
|
1825
|
+
device = forms.ChoiceField()
|
|
1826
|
+
|
|
1827
|
+
def __init__(self, *args, **kwargs):
|
|
1828
|
+
super().__init__(*args, **kwargs)
|
|
1829
|
+
choices = []
|
|
1830
|
+
instance = get_current_instance()
|
|
1831
|
+
for colonel in Colonel.objects.filter(
|
|
1832
|
+
type='room-sensor', instance=instance
|
|
1833
|
+
):
|
|
1834
|
+
if not colonel.is_connected:
|
|
1835
|
+
continue
|
|
1836
|
+
choices.append((f"wifi-{colonel.id}", colonel.name))
|
|
1837
|
+
for device in CustomDaliDevice.objects.filter(
|
|
1838
|
+
instance=instance,
|
|
1839
|
+
last_seen__gt=timezone.now() - datetime.timedelta(minutes=10)
|
|
1840
|
+
):
|
|
1841
|
+
choices.append((f"dali-{device.id}", device.name))
|
|
1842
|
+
self.fields['device'].choices = choices
|
|
1843
|
+
|
|
1844
|
+
def get_device(self, field_val):
|
|
1845
|
+
if field_val.startswith('wifi'):
|
|
1846
|
+
return Colonel.objects.filter(
|
|
1847
|
+
id=self.cleaned_data['device'][5:]
|
|
1848
|
+
).first()
|
|
1849
|
+
else:
|
|
1850
|
+
return CustomDaliDevice.objects.filter(
|
|
1851
|
+
id=self.cleaned_data['device'][5:]
|
|
1852
|
+
)
|
|
1853
|
+
|
|
1854
|
+
|
|
1855
|
+
class RoomSensorDeviceConfigForm(CustomDaliDeviceForm):
|
|
1856
|
+
|
|
1857
|
+
def save(self, commit=True):
|
|
1858
|
+
from simo.core.models import Icon
|
|
1859
|
+
colonel = None
|
|
1860
|
+
dali_device = None
|
|
1861
|
+
device = self.get_device(self.cleaned_data['device'])
|
|
1862
|
+
if not device:
|
|
1863
|
+
return
|
|
1864
|
+
if isinstance(device, CustomDaliDevice):
|
|
1865
|
+
dali_device = device
|
|
1866
|
+
else:
|
|
1867
|
+
colonel = device
|
|
1868
|
+
|
|
1869
|
+
from .controllers import (
|
|
1870
|
+
AirQualitySensor, TempHumSensor, AmbientLightSensor,
|
|
1871
|
+
RoomPresenceSensor
|
|
1872
|
+
)
|
|
1873
|
+
|
|
1874
|
+
org_name = self.cleaned_data['name']
|
|
1875
|
+
org_icon = self.cleaned_data['icon']
|
|
1876
|
+
for CtrlClass, icon, suffix in (
|
|
1877
|
+
(AirQualitySensor, 'leaf', 'air quality'),
|
|
1878
|
+
(TempHumSensor, 'temperature-half', 'temperature'),
|
|
1879
|
+
(AmbientLightSensor, 'brightness-low', 'brightness'),
|
|
1880
|
+
(RoomPresenceSensor, 'person', 'presence')
|
|
1881
|
+
):
|
|
1882
|
+
default_icon = Icon.objects.filter(slug=icon).first()
|
|
1883
|
+
if default_icon:
|
|
1884
|
+
self.cleaned_data['icon'] = default_icon.slug
|
|
1885
|
+
else:
|
|
1886
|
+
self.cleaned_data['icon'] = org_icon
|
|
1887
|
+
self.cleaned_data['name'] = f"{org_name} {suffix}"
|
|
1888
|
+
|
|
1889
|
+
if colonel:
|
|
1890
|
+
comp = Component.objects.filter(
|
|
1891
|
+
config__colonel=colonel.id,
|
|
1892
|
+
controller_uid=CtrlClass.uid
|
|
1893
|
+
).first()
|
|
1894
|
+
else:
|
|
1895
|
+
comp = Component.objects.filter(
|
|
1896
|
+
config__dali_device=dali_device.id,
|
|
1897
|
+
controller_uid=CtrlClass.uid
|
|
1898
|
+
).first()
|
|
1899
|
+
|
|
1900
|
+
form = CtrlClass.config_form(
|
|
1901
|
+
controller_uid=CtrlClass.uid, instance=comp,
|
|
1902
|
+
data=self.cleaned_data
|
|
1903
|
+
)
|
|
1904
|
+
if form.is_valid():
|
|
1905
|
+
comp = form.save()
|
|
1906
|
+
if colonel:
|
|
1907
|
+
comp.config['colonel'] = colonel.id
|
|
1908
|
+
else:
|
|
1909
|
+
comp.config['dali_device'] = dali_device.id
|
|
1910
|
+
comp.save()
|
|
1911
|
+
else:
|
|
1912
|
+
raise Exception(form.errors)
|
|
1913
|
+
|
|
1914
|
+
if colonel:
|
|
1915
|
+
GatewayObjectCommand(
|
|
1916
|
+
comp.gateway, colonel, id=comp.id,
|
|
1917
|
+
command='call', method='update_config', args=[
|
|
1918
|
+
comp.controller._get_colonel_config()
|
|
1919
|
+
]
|
|
1920
|
+
).publish()
|
|
1921
|
+
|
|
1922
|
+
return comp
|
|
1923
|
+
|
|
1924
|
+
|
|
1925
|
+
class RoomZonePresenceConfigForm(CustomDaliDeviceForm):
|
|
1926
|
+
|
|
1927
|
+
def clean_device(self):
|
|
1928
|
+
value = self.cleaned_data['device']
|
|
1929
|
+
if value.startswith('wifi'):
|
|
1930
|
+
return value
|
|
1931
|
+
from .controllers import RoomZonePresenceSensor
|
|
1932
|
+
dali_device = CustomDaliDevice.objects.filter(
|
|
1933
|
+
id=value[5:]
|
|
1934
|
+
).first()
|
|
1935
|
+
free_slots = {0, 1, 2, 3, 4, 5, 6, 7}
|
|
1936
|
+
for comp in Component.objects.filter(
|
|
1937
|
+
controller_uid=RoomZonePresenceSensor.uid,
|
|
1938
|
+
config__dali_device=dali_device.id
|
|
1939
|
+
):
|
|
1940
|
+
try:
|
|
1941
|
+
free_slots.remove(int(comp.config['slot']))
|
|
1942
|
+
except:
|
|
1943
|
+
continue
|
|
1944
|
+
if not free_slots:
|
|
1945
|
+
raise forms.ValidationError(
|
|
1946
|
+
"This device already has 8 zones defined. "
|
|
1947
|
+
"Please first delete some to add a new one."
|
|
1948
|
+
)
|
|
1949
|
+
return value
|
simo/fleet/managers.py
CHANGED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Generated by Django 4.2.10 on 2025-04-03 14:35
|
|
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
|
+
('core', '0049_alter_gateway_type'),
|
|
11
|
+
('fleet', '0044_auto_20241210_0707'),
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
operations = [
|
|
15
|
+
migrations.AlterField(
|
|
16
|
+
model_name='colonel',
|
|
17
|
+
name='type',
|
|
18
|
+
field=models.CharField(choices=[('4-relays', '4 Relay'), ('ample-wall', 'Ample Wall'), ('game-changer', 'Game Changer'), ('game-changer-mini', 'Game Changer Mini'), ('room-sensor', 'Room Sensor')], default='ample-wall', max_length=20),
|
|
19
|
+
),
|
|
20
|
+
migrations.CreateModel(
|
|
21
|
+
name='CustomDaliDevice',
|
|
22
|
+
fields=[
|
|
23
|
+
('random_address', models.PositiveIntegerField(primary_key=True, serialize=False)),
|
|
24
|
+
('name', models.CharField(help_text='User given name on initial pairing', max_length=200)),
|
|
25
|
+
('colonel', models.ForeignKey(blank=True, editable=False, help_text='Colonel on which it has already appeared.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='fleet.colonel')),
|
|
26
|
+
('instance', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.instance')),
|
|
27
|
+
],
|
|
28
|
+
),
|
|
29
|
+
]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Generated by Django 4.2.10 on 2025-04-04 05:28
|
|
2
|
+
|
|
3
|
+
from django.db import migrations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
('fleet', '0045_alter_colonel_type_customdalidevice'),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.DeleteModel(
|
|
14
|
+
name='CustomDaliDevice',
|
|
15
|
+
),
|
|
16
|
+
]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Generated by Django 4.2.10 on 2025-04-04 05:32
|
|
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
|
+
('core', '0049_alter_gateway_type'),
|
|
11
|
+
('fleet', '0046_delete_customdalidevice'),
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
operations = [
|
|
15
|
+
migrations.CreateModel(
|
|
16
|
+
name='CustomDaliDevice',
|
|
17
|
+
fields=[
|
|
18
|
+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
19
|
+
('random_address', models.PositiveIntegerField(db_index=True, editable=False)),
|
|
20
|
+
('name', models.CharField(help_text='User given name on initial pairing', max_length=200)),
|
|
21
|
+
('colonel', models.ForeignKey(blank=True, editable=False, help_text='Colonel on which it has already appeared.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='fleet.colonel')),
|
|
22
|
+
('instance', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.instance')),
|
|
23
|
+
],
|
|
24
|
+
options={
|
|
25
|
+
'unique_together': {('instance', 'random_address')},
|
|
26
|
+
},
|
|
27
|
+
),
|
|
28
|
+
]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Generated by Django 4.2.10 on 2025-04-07 07:27
|
|
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
|
+
('fleet', '0047_customdalidevice'),
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
operations = [
|
|
14
|
+
migrations.RemoveField(
|
|
15
|
+
model_name='customdalidevice',
|
|
16
|
+
name='colonel',
|
|
17
|
+
),
|
|
18
|
+
migrations.AddField(
|
|
19
|
+
model_name='customdalidevice',
|
|
20
|
+
name='interface',
|
|
21
|
+
field=models.ForeignKey(blank=True, editable=False, help_text='Colonel interface on which it operates.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='fleet.interface'),
|
|
22
|
+
),
|
|
23
|
+
migrations.AddField(
|
|
24
|
+
model_name='customdalidevice',
|
|
25
|
+
name='last_seen',
|
|
26
|
+
field=models.DateTimeField(editable=False, null=True),
|
|
27
|
+
),
|
|
28
|
+
]
|
|
Binary file
|
|
Binary file
|
simo/fleet/migrations/__pycache__/0048_remove_customdalidevice_colonel_and_more.cpython-312.pyc
ADDED
|
Binary file
|
simo/fleet/models.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import requests
|
|
2
2
|
import time
|
|
3
|
+
import random
|
|
3
4
|
from actstream import action
|
|
4
5
|
from django.core.exceptions import ValidationError
|
|
5
6
|
from django.db import transaction
|
|
@@ -61,6 +62,7 @@ class Colonel(DirtyFieldsMixin, models.Model):
|
|
|
61
62
|
('ample-wall', "Ample Wall"),
|
|
62
63
|
('game-changer', "Game Changer"),
|
|
63
64
|
('game-changer-mini', "Game Changer Mini"),
|
|
65
|
+
('room-sensor', "Room Sensor")
|
|
64
66
|
)
|
|
65
67
|
)
|
|
66
68
|
firmware_version = models.CharField(
|
|
@@ -258,7 +260,7 @@ class ColonelPin(models.Model):
|
|
|
258
260
|
@receiver(post_save, sender=Colonel)
|
|
259
261
|
def after_colonel_save(sender, instance, created, *args, **kwargs):
|
|
260
262
|
if created:
|
|
261
|
-
for no, data in GPIO_PINS.get(instance.type).items():
|
|
263
|
+
for no, data in GPIO_PINS.get(instance.type, {}).items():
|
|
262
264
|
ColonelPin.objects.get_or_create(
|
|
263
265
|
colonel=instance, no=no,
|
|
264
266
|
defaults={
|
|
@@ -306,11 +308,13 @@ def post_component_save(sender, instance, created, *args, **kwargs):
|
|
|
306
308
|
colonel.components.add(instance)
|
|
307
309
|
from .controllers import (
|
|
308
310
|
TTLock, DALILamp, DALIGearGroup, DALIRelay, DALIOccupancySensor,
|
|
309
|
-
DALILightSensor, DALIButton
|
|
311
|
+
DALILightSensor, DALIButton,
|
|
312
|
+
AirQualitySensor, TempHumSensor, AmbientLightSensor, RoomPresenceSensor
|
|
310
313
|
)
|
|
311
314
|
if instance.controller and instance.controller_cls in (
|
|
312
315
|
TTLock, DALILamp, DALIGearGroup, DALIRelay, DALIOccupancySensor,
|
|
313
|
-
DALILightSensor, DALIButton
|
|
316
|
+
DALILightSensor, DALIButton,
|
|
317
|
+
AirQualitySensor, TempHumSensor, AmbientLightSensor, RoomPresenceSensor
|
|
314
318
|
):
|
|
315
319
|
return
|
|
316
320
|
colonel.rebuild_occupied_pins()
|
|
@@ -466,16 +470,13 @@ def post_interface_save(sender, instance, created, *args, **kwargs):
|
|
|
466
470
|
address=addr,
|
|
467
471
|
)
|
|
468
472
|
elif instance.type == 'dali':
|
|
469
|
-
InterfaceAddress.objects.filter(
|
|
470
|
-
interface=instance
|
|
471
|
-
).exclude(address_type__startswith='dali').delete()
|
|
472
473
|
for addr in range(64):
|
|
473
|
-
InterfaceAddress.objects.
|
|
474
|
+
InterfaceAddress.objects.get_or_create(
|
|
474
475
|
interface=instance, address_type='dali-gear',
|
|
475
476
|
address=addr,
|
|
476
477
|
)
|
|
477
478
|
for addr in range(16):
|
|
478
|
-
InterfaceAddress.objects.
|
|
479
|
+
InterfaceAddress.objects.get_or_create(
|
|
479
480
|
interface=instance, address_type='dali-group',
|
|
480
481
|
address=addr,
|
|
481
482
|
)
|
|
@@ -492,4 +493,48 @@ def post_interface_delete(sender, instance, *args, **kwargs):
|
|
|
492
493
|
occupied_by_content_type=ct,
|
|
493
494
|
occupied_by_id=instance.id
|
|
494
495
|
):
|
|
495
|
-
pin.occupied_by_content_type = None
|
|
496
|
+
pin.occupied_by_content_type = None
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
class CustomDaliDevice(models.Model):
|
|
500
|
+
'''
|
|
501
|
+
Our own custom dali line device,
|
|
502
|
+
not compatible with anything else of DALI!
|
|
503
|
+
'''
|
|
504
|
+
instance = models.ForeignKey(Instance, on_delete=models.CASCADE)
|
|
505
|
+
random_address = models.PositiveIntegerField(db_index=True, editable=False)
|
|
506
|
+
name = models.CharField(
|
|
507
|
+
max_length=200, help_text="User given name on initial pairing"
|
|
508
|
+
)
|
|
509
|
+
interface = models.ForeignKey(
|
|
510
|
+
Interface, null=True, blank=True, editable=False,
|
|
511
|
+
on_delete=models.SET_NULL, related_name='custom_devices',
|
|
512
|
+
help_text="Colonel interface on which it operates."
|
|
513
|
+
)
|
|
514
|
+
last_seen = models.DateTimeField(null=True, editable=False)
|
|
515
|
+
|
|
516
|
+
class Meta:
|
|
517
|
+
unique_together = 'instance', 'random_address'
|
|
518
|
+
|
|
519
|
+
def save(self, *args, **kwargs):
|
|
520
|
+
if not self.random_address:
|
|
521
|
+
while True:
|
|
522
|
+
self.random_address = random.randint(0, 255)
|
|
523
|
+
if CustomDaliDevice.objects.filter(
|
|
524
|
+
random_address=self.random_address,
|
|
525
|
+
instance=self.instance
|
|
526
|
+
).first():
|
|
527
|
+
continue
|
|
528
|
+
break
|
|
529
|
+
return super().save(*args, **kwargs)
|
|
530
|
+
|
|
531
|
+
def transmit(self, frame):
|
|
532
|
+
from .gateways import FleetGatewayHandler
|
|
533
|
+
frame[0:7] = self.random_address
|
|
534
|
+
gateway = Gateway.objects.filter(type=FleetGatewayHandler.uid).first()
|
|
535
|
+
GatewayObjectCommand(
|
|
536
|
+
gateway, self.interface.colonel,
|
|
537
|
+
command='da-tx', interface=self.interface.no,
|
|
538
|
+
msg=frame.pack.hex()
|
|
539
|
+
).publish()
|
|
540
|
+
|
simo/fleet/serializers.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
from rest_framework import serializers
|
|
2
2
|
from simo.core.serializers import TimestampField
|
|
3
|
-
from .models import
|
|
3
|
+
from .models import (
|
|
4
|
+
InstanceOptions, Colonel, ColonelPin, Interface, CustomDaliDevice
|
|
5
|
+
)
|
|
4
6
|
|
|
5
7
|
|
|
6
8
|
class InstanceOptionsSerializer(serializers.ModelSerializer):
|
|
@@ -84,3 +86,15 @@ class ColonelSerializer(serializers.ModelSerializer):
|
|
|
84
86
|
instance = super().update(instance, validated_data)
|
|
85
87
|
instance.update_config()
|
|
86
88
|
return instance
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class CustomDaliDeviceSerializer(serializers.ModelSerializer):
|
|
92
|
+
|
|
93
|
+
class Meta:
|
|
94
|
+
model = CustomDaliDevice
|
|
95
|
+
fields = 'id', 'random_address', 'name'
|
|
96
|
+
read_only_fields = 'random_address',
|
|
97
|
+
|
|
98
|
+
def create(self, validated_data):
|
|
99
|
+
validated_data['instance'] = self.context['instance']
|
|
100
|
+
return super().create(validated_data)
|
simo/fleet/socket_consumers.py
CHANGED
|
@@ -450,6 +450,12 @@ class FleetConsumer(AsyncWebsocketConsumer):
|
|
|
450
450
|
process_discovery_result, thread_sensitive=True
|
|
451
451
|
)()
|
|
452
452
|
|
|
453
|
+
elif 'dali-raw' in data:
|
|
454
|
+
from .custom_dali_operations import process_frame
|
|
455
|
+
await sync_to_async(process_frame, thread_sensitive=True)(
|
|
456
|
+
self.colonel.id, data['dali-raw'], data['data']
|
|
457
|
+
)
|
|
458
|
+
|
|
453
459
|
|
|
454
460
|
elif bytes_data:
|
|
455
461
|
if not self.colonel_logger:
|
simo/fleet/tasks.py
CHANGED
|
@@ -20,18 +20,38 @@ def check_colonels_connected():
|
|
|
20
20
|
@celery_app.task
|
|
21
21
|
def check_colonel_components_alive():
|
|
22
22
|
from simo.core.models import Component
|
|
23
|
-
from .
|
|
23
|
+
from .gateways import FleetGatewayHandler
|
|
24
|
+
from .models import Colonel, CustomDaliDevice
|
|
24
25
|
drop_current_instance()
|
|
25
26
|
for lost_colonel in Colonel.objects.filter(
|
|
26
27
|
last_seen__lt=timezone.now() - datetime.timedelta(seconds=60)
|
|
27
28
|
).prefetch_related(Prefetch(
|
|
28
29
|
'components', queryset=Component.objects.filter(alive=True),
|
|
29
30
|
to_attr='alive_components'
|
|
30
|
-
)):
|
|
31
|
+
), 'interfaces'):
|
|
31
32
|
for comp in lost_colonel.alive_components:
|
|
32
33
|
print(f"{comp} is no longer alive!")
|
|
33
34
|
comp.alive = False
|
|
34
35
|
comp.save()
|
|
36
|
+
for interface in lost_colonel.interfaces.all():
|
|
37
|
+
if interface.type == 'dali':
|
|
38
|
+
for device in interface.custom_devices.all():
|
|
39
|
+
for comp in Component.objects.filter(
|
|
40
|
+
gateway__type=FleetGatewayHandler.uid,
|
|
41
|
+
config__dali_device=device.id, alive=True
|
|
42
|
+
):
|
|
43
|
+
comp.alive = False
|
|
44
|
+
comp.save()
|
|
45
|
+
|
|
46
|
+
for device in CustomDaliDevice.objects.filter(
|
|
47
|
+
last_seen__gt=timezone.now() - datetime.timedelta(seconds=60)
|
|
48
|
+
):
|
|
49
|
+
for comp in Component.objects.filter(
|
|
50
|
+
gateway__type=FleetGatewayHandler.uid,
|
|
51
|
+
config__dali_device=device.id, alive=True
|
|
52
|
+
):
|
|
53
|
+
comp.alive = False
|
|
54
|
+
comp.save()
|
|
35
55
|
|
|
36
56
|
|
|
37
57
|
@celery_app.on_after_finalize.connect
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
simo/generic/controllers.py
CHANGED
|
@@ -69,11 +69,16 @@ class Thermostat(ControllerBase):
|
|
|
69
69
|
|
|
70
70
|
@property
|
|
71
71
|
def default_config(self):
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
instance = get_current_instance()
|
|
73
|
+
min = 4
|
|
74
|
+
max = 36
|
|
75
|
+
if instance and instance.units_of_measure == 'imperial':
|
|
76
|
+
min = 40
|
|
77
|
+
max = 95
|
|
74
78
|
return {
|
|
75
79
|
'temperature_sensor': 0, 'heater': 0, 'cooler': 0,
|
|
76
|
-
'
|
|
80
|
+
'engagement': 'dynamic','reaction_difference': 2,
|
|
81
|
+
'min': min, 'max': max,
|
|
77
82
|
'has_real_feel': False,
|
|
78
83
|
'user_config': config_to_dict(self._get_default_user_config())
|
|
79
84
|
}
|
|
@@ -161,12 +166,12 @@ class Thermostat(ControllerBase):
|
|
|
161
166
|
temperature_sensor = Component.objects.filter(
|
|
162
167
|
pk=self.component.config.get('temperature_sensor')
|
|
163
168
|
).first()
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
)
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
)
|
|
169
|
+
heaters = Component.objects.filter(
|
|
170
|
+
pk__in=self.component.config.get('heater')
|
|
171
|
+
)
|
|
172
|
+
coolers = Component.objects.filter(
|
|
173
|
+
pk__in=self.component.config.get('cooler')
|
|
174
|
+
)
|
|
170
175
|
|
|
171
176
|
if not temperature_sensor or not temperature_sensor.alive:
|
|
172
177
|
self.component.error_msg = "No temperature sensor"
|
|
@@ -187,50 +192,100 @@ class Thermostat(ControllerBase):
|
|
|
187
192
|
target_temp = self.get_current_target_temperature()
|
|
188
193
|
mode = self.component.config['user_config'].get('mode', 'auto')
|
|
189
194
|
|
|
195
|
+
low = target_temp - self.component.config['reaction_difference']
|
|
196
|
+
high = target_temp + self.component.config['reaction_difference']
|
|
197
|
+
|
|
198
|
+
heating = False
|
|
199
|
+
cooling = False
|
|
200
|
+
|
|
201
|
+
if self.component.config.get('engagement', 'static'):
|
|
202
|
+
if heaters:
|
|
203
|
+
for heater in heaters:
|
|
204
|
+
if current_temp < low:
|
|
205
|
+
if heater.base_type == 'dimmer':
|
|
206
|
+
heater.max_out()
|
|
207
|
+
else:
|
|
208
|
+
heater.turn_on()
|
|
209
|
+
heating = True
|
|
210
|
+
elif current_temp > high:
|
|
211
|
+
heater.turn_off()
|
|
212
|
+
heating = False
|
|
213
|
+
else:
|
|
214
|
+
if heater.value:
|
|
215
|
+
heating = True
|
|
216
|
+
break
|
|
217
|
+
|
|
218
|
+
if coolers:
|
|
219
|
+
for cooler in coolers:
|
|
220
|
+
if heating: # Do not cool if heating!
|
|
221
|
+
cooler.turn_off()
|
|
222
|
+
else:
|
|
223
|
+
if current_temp > high:
|
|
224
|
+
if heater.base_type == 'dimmer':
|
|
225
|
+
cooler.max_out()
|
|
226
|
+
else:
|
|
227
|
+
cooler.turn_on()
|
|
228
|
+
cooling = True
|
|
229
|
+
elif current_temp < low:
|
|
230
|
+
if cooler.value:
|
|
231
|
+
cooler.turn_off()
|
|
232
|
+
cooling = False
|
|
233
|
+
else:
|
|
234
|
+
if cooler.value:
|
|
235
|
+
cooling = True
|
|
236
|
+
break
|
|
237
|
+
|
|
238
|
+
else:
|
|
239
|
+
window = high - low
|
|
240
|
+
if heaters:
|
|
241
|
+
reach = high - current_temp
|
|
242
|
+
reaction_force = self._get_reaction_force(window, reach)
|
|
243
|
+
if reaction_force:
|
|
244
|
+
heating = True
|
|
245
|
+
self._engage_devices(heaters, reaction_force)
|
|
246
|
+
if coolers:
|
|
247
|
+
if heating: # Do not cool if heating!
|
|
248
|
+
reaction_force = 0
|
|
249
|
+
else:
|
|
250
|
+
reach = current_temp - low
|
|
251
|
+
reaction_force = self._get_reaction_force(window, reach)
|
|
252
|
+
self._engage_devices(coolers, reaction_force)
|
|
253
|
+
|
|
190
254
|
self.component.set({
|
|
191
255
|
'mode': mode,
|
|
192
256
|
'current_temp': current_temp,
|
|
193
257
|
'target_temp': target_temp,
|
|
194
|
-
'heating':
|
|
258
|
+
'heating': heating, 'cooling': cooling
|
|
195
259
|
}, actor=get_system_user())
|
|
196
260
|
|
|
197
|
-
low = target_temp - self.component.config['reaction_difference'] / 2
|
|
198
|
-
high = target_temp + self.component.config['reaction_difference'] / 2
|
|
199
|
-
|
|
200
|
-
if mode in ('auto', 'heater'):
|
|
201
|
-
if (not heater or not heater.alive) and mode == 'heater':
|
|
202
|
-
self.component.error_msg = "No heater"
|
|
203
|
-
self.component.alive = False
|
|
204
|
-
self.component.save()
|
|
205
|
-
return
|
|
206
|
-
if current_temp < low:
|
|
207
|
-
if not heater.value:
|
|
208
|
-
heater.turn_on()
|
|
209
|
-
self.component.value['heating'] = True
|
|
210
|
-
elif current_temp > high:
|
|
211
|
-
if heater.value:
|
|
212
|
-
heater.turn_off()
|
|
213
|
-
self.component.value['heating'] = False
|
|
214
|
-
if mode in ('auto', 'cooler') and cooler:
|
|
215
|
-
if not cooler or not cooler.alive:
|
|
216
|
-
if mode == 'cooler' or (not heater or not heater.alive):
|
|
217
|
-
print(f"No cooler or heater on {self.component}!")
|
|
218
|
-
self.component.alive = False
|
|
219
|
-
self.component.save()
|
|
220
|
-
return
|
|
221
|
-
if current_temp > high:
|
|
222
|
-
if not cooler.value:
|
|
223
|
-
cooler.turn_on()
|
|
224
|
-
self.component.value['cooling'] = True
|
|
225
|
-
elif current_temp < low:
|
|
226
|
-
if cooler.value:
|
|
227
|
-
cooler.turn_off()
|
|
228
|
-
self.component.value['cooling'] = False
|
|
229
|
-
|
|
230
261
|
self.component.error_msg = None
|
|
231
262
|
self.component.alive = True
|
|
232
263
|
self.component.save()
|
|
233
264
|
|
|
265
|
+
|
|
266
|
+
def _get_reaction_force(self, window, reach):
|
|
267
|
+
if reach > window:
|
|
268
|
+
reaction_force = 100
|
|
269
|
+
elif reach <= 0:
|
|
270
|
+
reaction_force = 0
|
|
271
|
+
else:
|
|
272
|
+
reaction_force = reach / window * 100
|
|
273
|
+
return reaction_force
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _engage_devices(self, devices, reaction_force):
|
|
277
|
+
for device in devices:
|
|
278
|
+
if device.base_type == 'dimmer':
|
|
279
|
+
device.output_percent(reaction_force)
|
|
280
|
+
elif device.base_type == 'switch':
|
|
281
|
+
if reaction_force == 100:
|
|
282
|
+
device.turn_on()
|
|
283
|
+
elif reaction_force == 0:
|
|
284
|
+
device.turn_off()
|
|
285
|
+
else:
|
|
286
|
+
device.pulse(30, reaction_force)
|
|
287
|
+
|
|
288
|
+
|
|
234
289
|
def update_user_conf(self, new_conf):
|
|
235
290
|
self.component.refresh_from_db()
|
|
236
291
|
self.component.config['user_config'] = validate_new_conf(
|
|
@@ -241,6 +296,7 @@ class Thermostat(ControllerBase):
|
|
|
241
296
|
self.component.save()
|
|
242
297
|
self.evaluate()
|
|
243
298
|
|
|
299
|
+
|
|
244
300
|
def hold(self, temperature=None):
|
|
245
301
|
if temperature != None:
|
|
246
302
|
self.component.config['user_config']['hard'] = {
|