simo 2.4.2__py3-none-any.whl → 2.5.2__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/backups/tasks.py +11 -1
- simo/core/__pycache__/admin.cpython-38.pyc +0 -0
- simo/core/__pycache__/api.cpython-38.pyc +0 -0
- simo/core/__pycache__/app_widgets.cpython-38.pyc +0 -0
- simo/core/__pycache__/controllers.cpython-38.pyc +0 -0
- simo/core/__pycache__/events.cpython-38.pyc +0 -0
- simo/core/__pycache__/forms.cpython-38.pyc +0 -0
- simo/core/__pycache__/middleware.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__/socket_consumers.cpython-38.pyc +0 -0
- simo/core/__pycache__/tasks.cpython-38.pyc +0 -0
- simo/core/__pycache__/views.cpython-38.pyc +0 -0
- simo/core/admin.py +4 -4
- simo/core/api.py +20 -4
- simo/core/app_widgets.py +5 -0
- simo/core/controllers.py +4 -3
- simo/core/events.py +13 -4
- simo/core/forms.py +2 -0
- simo/core/management/commands/gateways_manager.py +0 -3
- simo/core/middleware.py +12 -6
- simo/core/models.py +26 -6
- simo/core/serializers.py +17 -17
- simo/core/socket_consumers.py +6 -2
- simo/core/static/admin/js/codemirror-init.js +1 -0
- simo/core/tasks.py +10 -7
- 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__/socket_consumers.cpython-38.pyc +0 -0
- simo/fleet/controllers.py +86 -22
- simo/fleet/forms.py +84 -9
- simo/fleet/migrations/0039_auto_20241016_1047.py +28 -0
- simo/fleet/migrations/0040_alter_colonel_pwm_frequency.py +18 -0
- simo/fleet/migrations/__pycache__/0039_auto_20241016_1047.cpython-38.pyc +0 -0
- simo/fleet/migrations/__pycache__/0040_alter_colonel_pwm_frequency.cpython-38.pyc +0 -0
- simo/fleet/models.py +6 -2
- simo/fleet/socket_consumers.py +13 -5
- simo/generic/__pycache__/controllers.cpython-38.pyc +0 -0
- simo/generic/__pycache__/forms.cpython-38.pyc +0 -0
- simo/generic/__pycache__/gateways.cpython-38.pyc +0 -0
- simo/generic/__pycache__/models.cpython-38.pyc +0 -0
- simo/generic/controllers.py +45 -2
- simo/generic/forms.py +81 -7
- simo/generic/models.py +0 -1
- simo/generic/scripting/__init__.py +16 -0
- simo/generic/scripting/__pycache__/__init__.cpython-38.pyc +0 -0
- simo/generic/scripting/__pycache__/helpers.cpython-38.pyc +0 -0
- simo/generic/scripting/__pycache__/serializers.cpython-38.pyc +0 -0
- simo/generic/scripting/helpers.py +35 -0
- simo/generic/scripting/serializers.py +77 -0
- simo/generic/templates/admin/controller_widgets/weather_forecast.html +2 -2
- simo/notifications/__pycache__/utils.cpython-38.pyc +0 -0
- simo/notifications/utils.py +30 -12
- simo/scripting.py +2 -2
- simo/users/__pycache__/api.cpython-38.pyc +0 -0
- simo/users/__pycache__/managers.cpython-38.pyc +0 -0
- simo/users/__pycache__/models.cpython-38.pyc +0 -0
- simo/users/__pycache__/serializers.cpython-38.pyc +0 -0
- simo/users/__pycache__/utils.cpython-38.pyc +0 -0
- simo/users/api.py +36 -7
- simo/users/managers.py +5 -1
- simo/users/migrations/0034_instanceuser_last_seen_location_and_more.py +24 -0
- simo/users/migrations/__pycache__/0034_instanceuser_last_seen_location_and_more.cpython-38.pyc +0 -0
- simo/users/models.py +37 -32
- simo/users/serializers.py +11 -8
- simo/users/utils.py +14 -3
- {simo-2.4.2.dist-info → simo-2.5.2.dist-info}/METADATA +1 -1
- {simo-2.4.2.dist-info → simo-2.5.2.dist-info}/RECORD +73 -60
- {simo-2.4.2.dist-info → simo-2.5.2.dist-info}/WHEEL +1 -1
- {simo-2.4.2.dist-info → simo-2.5.2.dist-info}/LICENSE.md +0 -0
- {simo-2.4.2.dist-info → simo-2.5.2.dist-info}/entry_points.txt +0 -0
- {simo-2.4.2.dist-info → simo-2.5.2.dist-info}/top_level.txt +0 -0
simo/fleet/forms.py
CHANGED
|
@@ -741,14 +741,21 @@ class ColonelPWMOutputConfigForm(ColonelComponentForm):
|
|
|
741
741
|
help_text="Maximum component value"
|
|
742
742
|
)
|
|
743
743
|
value_units = forms.CharField(required=False)
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
744
|
+
|
|
745
|
+
device_min = forms.IntegerField(
|
|
746
|
+
label="Device minimum (%).",
|
|
747
|
+
help_text="Device will turn off once it reaches this internal value. "
|
|
748
|
+
"Usually it is a good idea to "
|
|
749
|
+
"set this somewhere in between of 5 - 15 %. ",
|
|
750
|
+
initial=10, min_value=0, max_value=100,
|
|
747
751
|
)
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
help_text="
|
|
752
|
+
device_max = forms.IntegerField(
|
|
753
|
+
label="Device maximum (%).",
|
|
754
|
+
help_text="Can be used to prevent reaching maximum values. "
|
|
755
|
+
"Default is 100%",
|
|
756
|
+
initial=100, min_value=0, max_value=100,
|
|
751
757
|
)
|
|
758
|
+
|
|
752
759
|
turn_on_time = forms.IntegerField(
|
|
753
760
|
min_value=0, max_value=60000, initial=1000,
|
|
754
761
|
help_text="Turn on speed in ms. 1500 is a great quick default. "
|
|
@@ -763,9 +770,6 @@ class ColonelPWMOutputConfigForm(ColonelComponentForm):
|
|
|
763
770
|
initial='easeOutSine', choices=EASING_CHOICES,
|
|
764
771
|
help_text="easeOutSine - offers most naturally looking effect."
|
|
765
772
|
)
|
|
766
|
-
inverse = forms.BooleanField(
|
|
767
|
-
label=_("Inverse dimmer signal"), required=False, initial=True
|
|
768
|
-
)
|
|
769
773
|
on_value = forms.FloatField(
|
|
770
774
|
required=True, initial=100,
|
|
771
775
|
help_text="ON value when used with toggle switch"
|
|
@@ -854,6 +858,71 @@ class ColonelPWMOutputConfigForm(ColonelComponentForm):
|
|
|
854
858
|
return obj
|
|
855
859
|
|
|
856
860
|
|
|
861
|
+
class DCDriverConfigForm(ColonelComponentForm):
|
|
862
|
+
output_pin = Select2ModelChoiceField(
|
|
863
|
+
label="Port",
|
|
864
|
+
queryset=ColonelPin.objects.filter(output=True),
|
|
865
|
+
url='autocomplete-colonel-pins',
|
|
866
|
+
forward=[
|
|
867
|
+
forward.Self(),
|
|
868
|
+
forward.Field('colonel'),
|
|
869
|
+
forward.Const({'output': True}, 'filters')
|
|
870
|
+
]
|
|
871
|
+
)
|
|
872
|
+
min = forms.FloatField(
|
|
873
|
+
required=True, initial=0,
|
|
874
|
+
help_text="Minimum component value displayed to the user."
|
|
875
|
+
)
|
|
876
|
+
max = forms.FloatField(
|
|
877
|
+
required=True, initial=24,
|
|
878
|
+
help_text="Maximum component value displayed to the user."
|
|
879
|
+
)
|
|
880
|
+
value_units = forms.CharField(required=False)
|
|
881
|
+
|
|
882
|
+
device_min = forms.FloatField(
|
|
883
|
+
label="Device minimum Voltage.",
|
|
884
|
+
help_text="This will be the lowest possible voltage value of a device.\n"
|
|
885
|
+
"Don't forget to adjust your component min value accordingly "
|
|
886
|
+
"if you change this.",
|
|
887
|
+
initial=0, min_value=0, max_value=24,
|
|
888
|
+
)
|
|
889
|
+
device_max = forms.IntegerField(
|
|
890
|
+
label="Device maximum Voltage.",
|
|
891
|
+
help_text="Can be set lower than it's natural maximum of 24V. \n"
|
|
892
|
+
"Don't forget to adjust your component max value accordingly "
|
|
893
|
+
"if you change this.",
|
|
894
|
+
initial=24, min_value=0, max_value=24,
|
|
895
|
+
)
|
|
896
|
+
|
|
897
|
+
def clean(self):
|
|
898
|
+
super().clean()
|
|
899
|
+
if 'output_pin' in self.cleaned_data:
|
|
900
|
+
self._clean_pin('output_pin')
|
|
901
|
+
return self.cleaned_data
|
|
902
|
+
|
|
903
|
+
|
|
904
|
+
def save(self, commit=True):
|
|
905
|
+
if 'output_pin' in self.cleaned_data:
|
|
906
|
+
self.instance.config['output_pin_no'] = self.cleaned_data['output_pin'].no
|
|
907
|
+
|
|
908
|
+
update_colonel = False
|
|
909
|
+
if not self.instance.pk:
|
|
910
|
+
update_colonel = True
|
|
911
|
+
elif 'output_pin' in self.changed_data:
|
|
912
|
+
update_colonel = True
|
|
913
|
+
|
|
914
|
+
obj = super().save(commit=commit)
|
|
915
|
+
|
|
916
|
+
if not update_colonel:
|
|
917
|
+
GatewayObjectCommand(
|
|
918
|
+
obj.gateway, self.cleaned_data['colonel'], id=obj.id,
|
|
919
|
+
command='call', method='update_config', args=[
|
|
920
|
+
obj.controller._get_colonel_config()
|
|
921
|
+
]
|
|
922
|
+
).publish()
|
|
923
|
+
return obj
|
|
924
|
+
|
|
925
|
+
|
|
857
926
|
class ColonelRGBLightConfigForm(ColonelComponentForm):
|
|
858
927
|
output_pin = Select2ModelChoiceField(
|
|
859
928
|
label="Port",
|
|
@@ -1010,6 +1079,12 @@ class DualMotorValveForm(ColonelComponentForm):
|
|
|
1010
1079
|
required=True, min_value=0.01, max_value=1000000000,
|
|
1011
1080
|
initial=10, help_text="Time in seconds to close."
|
|
1012
1081
|
)
|
|
1082
|
+
min = forms.FloatField(
|
|
1083
|
+
label="Minimum displayed value", required=True, initial=0
|
|
1084
|
+
)
|
|
1085
|
+
max = forms.FloatField(
|
|
1086
|
+
label="Maximum displayed value", required=True, initial=100
|
|
1087
|
+
)
|
|
1013
1088
|
|
|
1014
1089
|
|
|
1015
1090
|
def clean(self):
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Generated by Django 4.2.10 on 2024-10-16 10:47
|
|
2
|
+
from django.db import migrations
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def forwards_func(apps, schema_editor):
|
|
6
|
+
Component = apps.get_model("core", "Component")
|
|
7
|
+
|
|
8
|
+
for comp in Component.objects.filter(controller_uid='simo.fleet.controllers.PWMOutput'):
|
|
9
|
+
duty_min = comp.config.get('duty_min', 0)
|
|
10
|
+
duty_max = comp.config.get('duty_max', 1023)
|
|
11
|
+
comp.config['device_min'] = int((1023 - duty_max) / 1023 * 100)
|
|
12
|
+
comp.config['device_max'] = int(100 - (duty_min / 1023 * 100))
|
|
13
|
+
comp.save()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def reverse_func(apps, schema_editor):
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Migration(migrations.Migration):
|
|
21
|
+
|
|
22
|
+
dependencies = [
|
|
23
|
+
('fleet', '0038_alter_colonel_type'),
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
operations = [
|
|
27
|
+
migrations.RunPython(forwards_func, reverse_func, elidable=True),
|
|
28
|
+
]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Generated by Django 4.2.10 on 2024-10-18 08:00
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
('fleet', '0039_auto_20241016_1047'),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.AlterField(
|
|
14
|
+
model_name='colonel',
|
|
15
|
+
name='pwm_frequency',
|
|
16
|
+
field=models.IntegerField(choices=[(0, '3kHz'), (1, '22kHz')], default=0, help_text='Affects Ample Wall dimmer PWM output (dimmer) frequency'),
|
|
17
|
+
),
|
|
18
|
+
]
|
|
Binary file
|
simo/fleet/models.py
CHANGED
|
@@ -91,7 +91,7 @@ class Colonel(DirtyFieldsMixin, models.Model):
|
|
|
91
91
|
"and reset if a lot of data is being transmitted. "
|
|
92
92
|
"Leave this off, unleess you know what you are doing!"
|
|
93
93
|
)
|
|
94
|
-
pwm_frequency = models.IntegerField(default=
|
|
94
|
+
pwm_frequency = models.IntegerField(default=0, choices=(
|
|
95
95
|
(0, "3kHz"), (1, "22kHz")
|
|
96
96
|
), help_text="Affects Ample Wall dimmer PWM output (dimmer) frequency")
|
|
97
97
|
|
|
@@ -270,6 +270,10 @@ def after_colonel_save(sender, instance, created, *args, **kwargs):
|
|
|
270
270
|
)
|
|
271
271
|
if fleet_gateway.status != 'running':
|
|
272
272
|
fleet_gateway.start()
|
|
273
|
+
# create i2c and dali interfaces automatically for game-changer boards
|
|
274
|
+
if instance.type == 'game-changer':
|
|
275
|
+
Interface.objects.create(colonel=instance, no=1, type='i2c')
|
|
276
|
+
Interface.objects.create(colonel=instance, no=2, type='dali')
|
|
273
277
|
|
|
274
278
|
|
|
275
279
|
@receiver(post_save, sender=Component)
|
|
@@ -311,7 +315,7 @@ def post_component_delete(sender, instance, *args, **kwargs):
|
|
|
311
315
|
comp, instance.config.get('da', 0), remove=True
|
|
312
316
|
)
|
|
313
317
|
|
|
314
|
-
elif instance.controller.family == 'dali':
|
|
318
|
+
elif instance.controller and instance.controller.family == 'dali':
|
|
315
319
|
colonel = Colonel.objects.filter(id=instance.config['colonel']).first()
|
|
316
320
|
if colonel:
|
|
317
321
|
GatewayObjectCommand(
|
simo/fleet/socket_consumers.py
CHANGED
|
@@ -218,18 +218,26 @@ class FleetConsumer(AsyncWebsocketConsumer):
|
|
|
218
218
|
}
|
|
219
219
|
}
|
|
220
220
|
config_data['settings'].update(instance_options)
|
|
221
|
-
|
|
222
|
-
|
|
221
|
+
|
|
222
|
+
def get_interfaces(colonel):
|
|
223
|
+
return list(colonel.interfaces.all().select_related(
|
|
223
224
|
'pin_a', 'pin_b'
|
|
224
|
-
)
|
|
225
|
+
))
|
|
226
|
+
interfaces = await sync_to_async(get_interfaces, thread_sensitive=True)(
|
|
227
|
+
self.colonel
|
|
225
228
|
)
|
|
226
229
|
for interface in interfaces:
|
|
227
230
|
config_data['interfaces'][f'{interface.type}-{interface.no}'] = {
|
|
228
231
|
'pin_a': interface.pin_a.no, 'pin_b': interface.pin_b.no,
|
|
229
232
|
}
|
|
233
|
+
|
|
234
|
+
def get_components(colonel):
|
|
235
|
+
return list(
|
|
236
|
+
colonel.components.all().prefetch_related('slaves')
|
|
237
|
+
)
|
|
230
238
|
components = await sync_to_async(
|
|
231
|
-
|
|
232
|
-
)(self.colonel
|
|
239
|
+
get_components, thread_sensitive=True
|
|
240
|
+
)(self.colonel)
|
|
233
241
|
|
|
234
242
|
def get_comp_config(comp):
|
|
235
243
|
try:
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
simo/generic/controllers.py
CHANGED
|
@@ -3,6 +3,10 @@ import threading
|
|
|
3
3
|
import pytz
|
|
4
4
|
import datetime
|
|
5
5
|
import json
|
|
6
|
+
import requests
|
|
7
|
+
import traceback
|
|
8
|
+
import sys
|
|
9
|
+
from bs4 import BeautifulSoup
|
|
6
10
|
from django.core.exceptions import ValidationError
|
|
7
11
|
from django.utils import timezone
|
|
8
12
|
from django.utils.functional import cached_property
|
|
@@ -16,6 +20,7 @@ from simo.core.events import GatewayObjectCommand
|
|
|
16
20
|
from simo.core.models import RUN_STATUS_CHOICES_MAP, Component
|
|
17
21
|
from simo.core.utils.helpers import get_random_string
|
|
18
22
|
from simo.core.utils.operations import OPERATIONS
|
|
23
|
+
from simo.core.middleware import get_current_instance
|
|
19
24
|
from simo.core.controllers import (
|
|
20
25
|
BEFORE_SEND, BEFORE_SET, ControllerBase,
|
|
21
26
|
BinarySensor, NumericSensor, MultiSensor, Switch, Dimmer, DimmerPlus,
|
|
@@ -42,12 +47,14 @@ from .forms import (
|
|
|
42
47
|
BlindsConfigForm, WateringConfigForm, StateSelectForm,
|
|
43
48
|
AlarmClockConfigForm
|
|
44
49
|
)
|
|
50
|
+
from .scripting import get_current_state
|
|
51
|
+
from .scripting.serializers import UserSerializer
|
|
45
52
|
|
|
46
53
|
# ----------- Generic controllers -----------------------------
|
|
47
54
|
|
|
55
|
+
|
|
48
56
|
class Script(ControllerBase, TimerMixin):
|
|
49
|
-
|
|
50
|
-
name = _("Script")
|
|
57
|
+
name = _("AI Script")
|
|
51
58
|
base_type = 'script'
|
|
52
59
|
gateway_class = GenericGatewayHandler
|
|
53
60
|
app_widget = ScriptWidget
|
|
@@ -101,6 +108,42 @@ class Script(ControllerBase, TimerMixin):
|
|
|
101
108
|
else:
|
|
102
109
|
self.send('start')
|
|
103
110
|
|
|
111
|
+
def ai_assistant(self, wish):
|
|
112
|
+
try:
|
|
113
|
+
request_data = {
|
|
114
|
+
'hub_uid': dynamic_settings['core__hub_uid'],
|
|
115
|
+
'hub_secret': dynamic_settings['core__hub_secret'],
|
|
116
|
+
'instance_uid': get_current_instance().uid,
|
|
117
|
+
'system_data': json.dumps(get_current_state()),
|
|
118
|
+
'wish': wish,
|
|
119
|
+
}
|
|
120
|
+
except Exception as e:
|
|
121
|
+
print(traceback.format_exc(), file=sys.stderr)
|
|
122
|
+
return {'status': 'error', 'result': f"Internal error: {e}"}
|
|
123
|
+
user = get_current_user()
|
|
124
|
+
if user:
|
|
125
|
+
request_data['current_user'] = UserSerializer(user, many=False).data
|
|
126
|
+
try:
|
|
127
|
+
response = requests.post(
|
|
128
|
+
'https://simo.io/hubs/ai-assist/scripts/', json=request_data
|
|
129
|
+
)
|
|
130
|
+
except:
|
|
131
|
+
return {'status': 'error', 'result': "Connection error"}
|
|
132
|
+
|
|
133
|
+
if response.status_code != 200:
|
|
134
|
+
content = response.content.decode()
|
|
135
|
+
if '<html' in content:
|
|
136
|
+
# Parse the HTML content
|
|
137
|
+
soup = BeautifulSoup(response.content, 'html.parser')
|
|
138
|
+
content = F"Server error {response.status_code}: {soup.title.string}"
|
|
139
|
+
return {'status': 'error', 'result': content}
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
'status': 'success',
|
|
143
|
+
'result': response.json()['script'],
|
|
144
|
+
'description': response.json()['description']
|
|
145
|
+
}
|
|
146
|
+
|
|
104
147
|
|
|
105
148
|
class PresenceLighting(Script):
|
|
106
149
|
masters_only = False
|
simo/generic/forms.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import time
|
|
1
2
|
from django import forms
|
|
2
3
|
from django.forms import formset_factory
|
|
3
4
|
from django.db.models import Q
|
|
@@ -35,11 +36,32 @@ class ScriptConfigForm(BaseComponentForm):
|
|
|
35
36
|
initial=True, required=False,
|
|
36
37
|
help_text="Restart the script if it fails. "
|
|
37
38
|
)
|
|
38
|
-
|
|
39
|
+
assistant_request = forms.CharField(
|
|
40
|
+
label="Request for AI assistant", required=False, max_length=1000,
|
|
41
|
+
widget=forms.Textarea(
|
|
42
|
+
attrs={'placeholder':
|
|
43
|
+
"Close the blind and turn on the main light "
|
|
44
|
+
"in my living room when it get's dark."
|
|
45
|
+
}
|
|
46
|
+
),
|
|
47
|
+
help_text="Clearly describe in your own words what kind of automation "
|
|
48
|
+
"you want to happen with this scenario script. <br>"
|
|
49
|
+
"The more defined, exact and clear is your description the more "
|
|
50
|
+
"accurate automation script SIMO.io AI assistanw will generate.<br>"
|
|
51
|
+
"Use component, zone and category id's for best accuracy. <br>"
|
|
52
|
+
"SIMO.io AI will re-generate your automation code and update it's description in Notes field "
|
|
53
|
+
"every time this field is changed and it might take up to 60s to do it. <br>"
|
|
54
|
+
"Actual script code can only be edited via SIMO.io Admin.",
|
|
55
|
+
)
|
|
56
|
+
code = forms.CharField(widget=PythonCode, required=False)
|
|
39
57
|
log = forms.CharField(
|
|
40
58
|
widget=forms.HiddenInput, required=False
|
|
41
59
|
)
|
|
42
60
|
|
|
61
|
+
app_exclude_fields = ('alarm_category', 'code', 'log')
|
|
62
|
+
|
|
63
|
+
_ai_resp = None
|
|
64
|
+
|
|
43
65
|
def __init__(self, *args, **kwargs):
|
|
44
66
|
super().__init__(*args, **kwargs)
|
|
45
67
|
self.basic_fields.extend(['autostart', 'keep_alive'])
|
|
@@ -47,19 +69,20 @@ class ScriptConfigForm(BaseComponentForm):
|
|
|
47
69
|
prefix = get_script_prefix()
|
|
48
70
|
if prefix == '/':
|
|
49
71
|
prefix = ''
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
72
|
+
if 'log' in self.fields:
|
|
73
|
+
self.fields['log'].widget = LogOutputWidget(
|
|
74
|
+
prefix + '/ws/log/%d/%d/' % (
|
|
75
|
+
ContentType.objects.get_for_model(Component).id,
|
|
76
|
+
self.instance.id
|
|
77
|
+
)
|
|
54
78
|
)
|
|
55
|
-
)
|
|
56
79
|
|
|
57
80
|
@classmethod
|
|
58
81
|
def get_admin_fieldsets(cls, request, obj=None):
|
|
59
82
|
base_fields = (
|
|
60
83
|
'id', 'gateway', 'base_type', 'name', 'icon', 'zone', 'category',
|
|
61
84
|
'show_in_app', 'autostart', 'keep_alive',
|
|
62
|
-
'code', 'control', 'log'
|
|
85
|
+
'assistant_request', 'notes', 'code', 'control', 'log'
|
|
63
86
|
)
|
|
64
87
|
|
|
65
88
|
fieldsets = [
|
|
@@ -72,6 +95,44 @@ class ScriptConfigForm(BaseComponentForm):
|
|
|
72
95
|
return fieldsets
|
|
73
96
|
|
|
74
97
|
|
|
98
|
+
def clean(self):
|
|
99
|
+
if self.cleaned_data.get('assistant_request'):
|
|
100
|
+
if self.instance.pk:
|
|
101
|
+
org = Component.objects.get(pk=self.instance.pk)
|
|
102
|
+
call_assistant = org.config.get('assistant_request') \
|
|
103
|
+
!= self.cleaned_data['assistant_request']
|
|
104
|
+
else:
|
|
105
|
+
call_assistant = True
|
|
106
|
+
call_assistant = False
|
|
107
|
+
if call_assistant:
|
|
108
|
+
resp = self.instance.ai_assistant(
|
|
109
|
+
self.cleaned_data['assistant_request'],
|
|
110
|
+
)
|
|
111
|
+
if resp['status'] == 'success':
|
|
112
|
+
self._ai_resp = resp
|
|
113
|
+
elif resp['status'] == 'error':
|
|
114
|
+
self.add_error('assistant_request', resp['result'])
|
|
115
|
+
|
|
116
|
+
return self.cleaned_data
|
|
117
|
+
|
|
118
|
+
def save(self, commit=True):
|
|
119
|
+
if commit and self._ai_resp:
|
|
120
|
+
self.instance.config['code'] = self._ai_resp['result']
|
|
121
|
+
self.instance.notes = self._ai_resp['description']
|
|
122
|
+
if 'code' in self.cleaned_data:
|
|
123
|
+
self.cleaned_data['code'] = self._ai_resp['result']
|
|
124
|
+
if 'notes' in self.cleaned_data:
|
|
125
|
+
self.cleaned_data['notes'] = self._ai_resp['description']
|
|
126
|
+
obj = super().save(commit)
|
|
127
|
+
if commit:
|
|
128
|
+
obj.controller.stop()
|
|
129
|
+
if self.cleaned_data.get('keep_alive') \
|
|
130
|
+
or self.cleaned_data.get('autostart'):
|
|
131
|
+
time.sleep(2)
|
|
132
|
+
obj.controller.start()
|
|
133
|
+
return obj
|
|
134
|
+
|
|
135
|
+
|
|
75
136
|
class ConditionForm(forms.Form):
|
|
76
137
|
component = forms.ModelChoiceField(
|
|
77
138
|
Component.objects.all(),
|
|
@@ -717,6 +778,19 @@ class StateSelectForm(BaseComponentForm):
|
|
|
717
778
|
states = FormsetField(
|
|
718
779
|
formset_factory(StateForm, can_delete=True, can_order=True, extra=0)
|
|
719
780
|
)
|
|
781
|
+
is_main = forms.BooleanField(
|
|
782
|
+
initial=False, required=False,
|
|
783
|
+
help_text="Will be displayed in the app "
|
|
784
|
+
"right top corner for quick access."
|
|
785
|
+
)
|
|
786
|
+
|
|
787
|
+
def save(self, commit=True):
|
|
788
|
+
if commit and self.cleaned_data['is_main']:
|
|
789
|
+
from .controllers import StateSelect
|
|
790
|
+
for c in Component.objects.filter(controller_uid=StateSelect.uid):
|
|
791
|
+
c.config['is_main'] = False
|
|
792
|
+
c.save()
|
|
793
|
+
return super().save(commit)
|
|
720
794
|
|
|
721
795
|
|
|
722
796
|
class AlarmClockEventForm(forms.Form):
|
simo/generic/models.py
CHANGED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from .serializers import *
|
|
2
|
+
from simo.core.models import Zone, Category, Component
|
|
3
|
+
from simo.users.models import InstanceUser
|
|
4
|
+
from simo.core.middleware import get_current_instance
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_current_state():
|
|
8
|
+
get_current_instance()
|
|
9
|
+
return {
|
|
10
|
+
'zones': ZoneSerializer(Zone.objects.all(), many=True).data,
|
|
11
|
+
'categories': CategorySerializer(Category.objects.all(), many=True).data,
|
|
12
|
+
'component': ComponentSerializer(Component.objects.all(), many=True).data,
|
|
13
|
+
'instanceusers': InstanceUserSerializer(
|
|
14
|
+
InstanceUser.objects.all(), many=True
|
|
15
|
+
).data,
|
|
16
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from django.utils import timezone
|
|
2
|
+
from suntime import Sun
|
|
3
|
+
from simo.core.models import Instance
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class LocalSun(Sun):
|
|
7
|
+
|
|
8
|
+
def __init__(self, instance=None):
|
|
9
|
+
if not instance:
|
|
10
|
+
instance = Instance.objects.all().first()
|
|
11
|
+
coordinates = instance.location.split(',')
|
|
12
|
+
try:
|
|
13
|
+
lat = float(coordinates[0])
|
|
14
|
+
except:
|
|
15
|
+
lat = 0
|
|
16
|
+
try:
|
|
17
|
+
lon = float(coordinates[1])
|
|
18
|
+
except:
|
|
19
|
+
lon = 0
|
|
20
|
+
super().__init__(lat, lon)
|
|
21
|
+
|
|
22
|
+
def is_night(self):
|
|
23
|
+
if timezone.now() > self.get_sunset_time():
|
|
24
|
+
return True
|
|
25
|
+
if timezone.now() < self.get_sunrise_time():
|
|
26
|
+
return True
|
|
27
|
+
return False
|
|
28
|
+
|
|
29
|
+
def seconds_to_sunset(self):
|
|
30
|
+
return (self.get_sunset_time() - timezone.now()).total_seconds()
|
|
31
|
+
|
|
32
|
+
def seconds_to_sunrise(self):
|
|
33
|
+
return (self.get_sunrise_time() - timezone.now()).total_seconds()
|
|
34
|
+
|
|
35
|
+
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from rest_framework import serializers
|
|
2
|
+
from simo.core.models import Zone, Category, Component
|
|
3
|
+
from simo.users.models import User, InstanceUser, PermissionsRole
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ZoneSerializer(serializers.ModelSerializer):
|
|
7
|
+
'''Zone serializer for AI scripts helper'''
|
|
8
|
+
|
|
9
|
+
class Meta:
|
|
10
|
+
model = Zone
|
|
11
|
+
fields = 'id', 'name'
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CategorySerializer(serializers.ModelSerializer):
|
|
15
|
+
'''Category serializer for AI scripts helper'''
|
|
16
|
+
|
|
17
|
+
class Meta:
|
|
18
|
+
model = Category
|
|
19
|
+
fields = 'id', 'name'
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ComponentSerializer(serializers.ModelSerializer):
|
|
23
|
+
'''Component serializer for AI scripts helper'''
|
|
24
|
+
|
|
25
|
+
MAX_LENGTH = 500
|
|
26
|
+
|
|
27
|
+
value = serializers.SerializerMethodField()
|
|
28
|
+
meta = serializers.SerializerMethodField()
|
|
29
|
+
config = serializers.SerializerMethodField()
|
|
30
|
+
|
|
31
|
+
class Meta:
|
|
32
|
+
model = Component
|
|
33
|
+
fields = (
|
|
34
|
+
'id', 'name', 'icon', 'zone', 'category', 'base_type',
|
|
35
|
+
'value', 'value_units', 'meta', 'config'
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def get_value(self, obj):
|
|
39
|
+
if obj.base_type in ('ip-camera', ):
|
|
40
|
+
return 'SKIP'
|
|
41
|
+
if len(str(obj.value)) > self.MAX_LENGTH:
|
|
42
|
+
return 'SKIP'
|
|
43
|
+
return obj.value
|
|
44
|
+
|
|
45
|
+
def get_meta(self, obj):
|
|
46
|
+
if len(str(obj.value)) > self.MAX_LENGTH:
|
|
47
|
+
return 'SKIP'
|
|
48
|
+
return obj.value
|
|
49
|
+
|
|
50
|
+
def get_config(self, obj):
|
|
51
|
+
if len(str(obj.value)) > self.MAX_LENGTH:
|
|
52
|
+
return 'SKIP'
|
|
53
|
+
return obj.value
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class UserSerializer(serializers.ModelSerializer):
|
|
57
|
+
|
|
58
|
+
class Meta:
|
|
59
|
+
model = User
|
|
60
|
+
fields = 'email', 'name'
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class PermissionsRoleSerializer(serializers.ModelSerializer):
|
|
64
|
+
|
|
65
|
+
class Meta:
|
|
66
|
+
model = PermissionsRole
|
|
67
|
+
fields = 'id', 'name', 'is_owner', 'is_superuser'
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class InstanceUserSerializer(serializers.ModelSerializer):
|
|
71
|
+
'''Role serializer for AI scripts helper'''
|
|
72
|
+
user = UserSerializer()
|
|
73
|
+
role = PermissionsRoleSerializer()
|
|
74
|
+
|
|
75
|
+
class Meta:
|
|
76
|
+
model = InstanceUser
|
|
77
|
+
fields = 'user', 'role',
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
data-ws_url="{{ obj.get_socket_url|default_if_none:"" }}">
|
|
4
4
|
<span style="
|
|
5
5
|
width: 20px; height:20px;
|
|
6
|
-
background: url({% static 'weather_icons' %}/{{ obj.value.
|
|
6
|
+
background: url({% static 'weather_icons' %}/{{ obj.value.weather.0.icon }}@2x.png) center no-repeat;
|
|
7
7
|
background-size: 150%;
|
|
8
8
|
display: inline-block;
|
|
9
9
|
position: relative;
|
|
10
10
|
bottom: -4px;
|
|
11
11
|
"></span>
|
|
12
|
-
{{ obj.value.
|
|
12
|
+
{{ obj.value.temp }}ᴼ {% if obj.zone.instance.units_of_measure == 'metric' %}C{% else %}F{% endif %}
|
|
13
13
|
</div>
|
|
Binary file
|