simo 2.4.2__py3-none-any.whl → 2.5.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/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__/tasks.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 +2 -2
- simo/core/events.py +2 -0
- simo/core/forms.py +2 -0
- simo/core/management/commands/gateways_manager.py +0 -3
- simo/core/middleware.py +7 -1
- simo/core/models.py +26 -6
- simo/core/serializers.py +17 -17
- 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/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 +2 -2
- 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 +41 -2
- simo/generic/forms.py +71 -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__/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.1.dist-info}/METADATA +1 -1
- {simo-2.4.2.dist-info → simo-2.5.1.dist-info}/RECORD +66 -54
- {simo-2.4.2.dist-info → simo-2.5.1.dist-info}/WHEEL +1 -1
- {simo-2.4.2.dist-info → simo-2.5.1.dist-info}/LICENSE.md +0 -0
- {simo-2.4.2.dist-info → simo-2.5.1.dist-info}/entry_points.txt +0 -0
- {simo-2.4.2.dist-info → simo-2.5.1.dist-info}/top_level.txt +0 -0
|
@@ -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
|
|
|
@@ -311,7 +311,7 @@ def post_component_delete(sender, instance, *args, **kwargs):
|
|
|
311
311
|
comp, instance.config.get('da', 0), remove=True
|
|
312
312
|
)
|
|
313
313
|
|
|
314
|
-
elif instance.controller.family == 'dali':
|
|
314
|
+
elif instance.controller and instance.controller.family == 'dali':
|
|
315
315
|
colonel = Colonel.objects.filter(id=instance.config['colonel']).first()
|
|
316
316
|
if colonel:
|
|
317
317
|
GatewayObjectCommand(
|
|
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,38 @@ 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 {'status': 'success', 'result': response.content.decode()}
|
|
142
|
+
|
|
104
143
|
|
|
105
144
|
class PresenceLighting(Script):
|
|
106
145
|
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,30 @@ 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, "
|
|
53
|
+
"every time this field is changed. <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
|
+
|
|
43
63
|
def __init__(self, *args, **kwargs):
|
|
44
64
|
super().__init__(*args, **kwargs)
|
|
45
65
|
self.basic_fields.extend(['autostart', 'keep_alive'])
|
|
@@ -47,19 +67,20 @@ class ScriptConfigForm(BaseComponentForm):
|
|
|
47
67
|
prefix = get_script_prefix()
|
|
48
68
|
if prefix == '/':
|
|
49
69
|
prefix = ''
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
70
|
+
if 'log' in self.fields:
|
|
71
|
+
self.fields['log'].widget = LogOutputWidget(
|
|
72
|
+
prefix + '/ws/log/%d/%d/' % (
|
|
73
|
+
ContentType.objects.get_for_model(Component).id,
|
|
74
|
+
self.instance.id
|
|
75
|
+
)
|
|
54
76
|
)
|
|
55
|
-
)
|
|
56
77
|
|
|
57
78
|
@classmethod
|
|
58
79
|
def get_admin_fieldsets(cls, request, obj=None):
|
|
59
80
|
base_fields = (
|
|
60
81
|
'id', 'gateway', 'base_type', 'name', 'icon', 'zone', 'category',
|
|
61
82
|
'show_in_app', 'autostart', 'keep_alive',
|
|
62
|
-
'code', 'control', 'log'
|
|
83
|
+
'assistant_request', 'code', 'control', 'log'
|
|
63
84
|
)
|
|
64
85
|
|
|
65
86
|
fieldsets = [
|
|
@@ -71,6 +92,36 @@ class ScriptConfigForm(BaseComponentForm):
|
|
|
71
92
|
]
|
|
72
93
|
return fieldsets
|
|
73
94
|
|
|
95
|
+
def clean(self):
|
|
96
|
+
if self.cleaned_data.get('assistant_request'):
|
|
97
|
+
if self.instance.pk:
|
|
98
|
+
org = Component.objects.get(pk=self.instance.pk)
|
|
99
|
+
call_assistant = org.config.get('assistant_request') \
|
|
100
|
+
!= self.cleaned_data['assistant_request']
|
|
101
|
+
else:
|
|
102
|
+
call_assistant = True
|
|
103
|
+
if call_assistant:
|
|
104
|
+
resp = self.instance.ai_assistant(
|
|
105
|
+
self.cleaned_data['assistant_request'],
|
|
106
|
+
)
|
|
107
|
+
if resp['status'] == 'success':
|
|
108
|
+
self.cleaned_data['code'] = resp['result']
|
|
109
|
+
self.instance.config['code'] = resp['result']
|
|
110
|
+
elif resp['status'] == 'error':
|
|
111
|
+
self.add_error('assistant_request', resp['result'])
|
|
112
|
+
|
|
113
|
+
return self.cleaned_data
|
|
114
|
+
|
|
115
|
+
def save(self, commit=True):
|
|
116
|
+
obj = super().save(commit)
|
|
117
|
+
if commit:
|
|
118
|
+
obj.controller.stop()
|
|
119
|
+
if self.cleaned_data.get('keep_alive') \
|
|
120
|
+
or self.cleaned_data.get('autostart'):
|
|
121
|
+
time.sleep(2)
|
|
122
|
+
obj.controller.start()
|
|
123
|
+
return obj
|
|
124
|
+
|
|
74
125
|
|
|
75
126
|
class ConditionForm(forms.Form):
|
|
76
127
|
component = forms.ModelChoiceField(
|
|
@@ -717,6 +768,19 @@ class StateSelectForm(BaseComponentForm):
|
|
|
717
768
|
states = FormsetField(
|
|
718
769
|
formset_factory(StateForm, can_delete=True, can_order=True, extra=0)
|
|
719
770
|
)
|
|
771
|
+
is_main = forms.BooleanField(
|
|
772
|
+
initial=False, required=False,
|
|
773
|
+
help_text="Will be displayed in the app "
|
|
774
|
+
"right top corner for quick access."
|
|
775
|
+
)
|
|
776
|
+
|
|
777
|
+
def save(self, commit=True):
|
|
778
|
+
if commit and self.cleaned_data['is_main']:
|
|
779
|
+
from .controllers import StateSelect
|
|
780
|
+
for c in Component.objects.filter(controller_uid=StateSelect.uid):
|
|
781
|
+
c.config['is_main'] = False
|
|
782
|
+
c.save()
|
|
783
|
+
return super().save(commit)
|
|
720
784
|
|
|
721
785
|
|
|
722
786
|
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
|
|
@@ -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
|
simo/notifications/utils.py
CHANGED
|
@@ -1,8 +1,27 @@
|
|
|
1
|
-
from simo.
|
|
1
|
+
from simo.core.middleware import get_current_instance
|
|
2
2
|
from .models import Notification, UserNotification
|
|
3
3
|
|
|
4
4
|
|
|
5
|
-
def notify_users(
|
|
5
|
+
def notify_users(severity, title, body=None, component=None, instance_users=None, instance=None):
|
|
6
|
+
'''
|
|
7
|
+
Sends a notification to specified users with a given severity level and message details.
|
|
8
|
+
:param severity: One of: 'info', 'warning', 'alarm'
|
|
9
|
+
:param title: A short, descriptive title of the event.
|
|
10
|
+
:param body: (Optional) A more detailed description of the event.
|
|
11
|
+
:param component: (Optional) simo.core.Component linked to this event.
|
|
12
|
+
:param instance_users: List of instance users to receive this notification. All active instance users will receive the message if not specified.
|
|
13
|
+
:return:
|
|
14
|
+
'''
|
|
15
|
+
if not instance:
|
|
16
|
+
if component:
|
|
17
|
+
instance = component.zone.instance
|
|
18
|
+
else:
|
|
19
|
+
instance = get_current_instance()
|
|
20
|
+
if not instance:
|
|
21
|
+
return
|
|
22
|
+
if component and component.zone.instance != instance:
|
|
23
|
+
# something is completely wrong!
|
|
24
|
+
return
|
|
6
25
|
assert severity in ('info', 'warning', 'alarm')
|
|
7
26
|
notification = Notification.objects.create(
|
|
8
27
|
instance=instance,
|
|
@@ -10,20 +29,19 @@ def notify_users(instance, severity, title, body=None, component=None, users=Non
|
|
|
10
29
|
severity=severity, body=body,
|
|
11
30
|
component=component
|
|
12
31
|
)
|
|
13
|
-
if not
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
for user in users:
|
|
32
|
+
if not instance_users:
|
|
33
|
+
instance_users = instance.instance_users.filter(
|
|
34
|
+
is_active=True
|
|
35
|
+
).select_related('user')
|
|
36
|
+
for iuser in instance_users:
|
|
19
37
|
# do not send emails to system users
|
|
20
|
-
if user.email.endswith('simo.io'):
|
|
38
|
+
if iuser.user.email.endswith('simo.io'):
|
|
21
39
|
continue
|
|
22
|
-
if instance
|
|
40
|
+
if iuser.instance.id != instance.id:
|
|
23
41
|
continue
|
|
24
|
-
if component and not component.
|
|
42
|
+
if component and not component.can_read(iuser.user):
|
|
25
43
|
continue
|
|
26
44
|
UserNotification.objects.create(
|
|
27
|
-
user=user, notification=notification,
|
|
45
|
+
user=iuser.user, notification=notification,
|
|
28
46
|
)
|
|
29
47
|
notification.dispatch()
|
simo/scripting.py
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
from django.utils import timezone
|
|
2
2
|
from suntime import Sun
|
|
3
|
-
from simo.core.
|
|
3
|
+
from simo.core.middleware import get_current_instance
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class LocalSun(Sun):
|
|
7
7
|
|
|
8
8
|
def __init__(self, instance=None):
|
|
9
9
|
if not instance:
|
|
10
|
-
instance =
|
|
10
|
+
instance = get_current_instance()
|
|
11
11
|
coordinates = instance.location.split(',')
|
|
12
12
|
try:
|
|
13
13
|
lat = float(coordinates[0])
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
simo/users/api.py
CHANGED
|
@@ -4,17 +4,18 @@ from rest_framework import viewsets, mixins, status
|
|
|
4
4
|
from rest_framework.serializers import Serializer
|
|
5
5
|
from rest_framework.decorators import action
|
|
6
6
|
from rest_framework.response import Response as RESTResponse
|
|
7
|
-
from rest_framework.exceptions import ValidationError
|
|
7
|
+
from rest_framework.exceptions import ValidationError, PermissionDenied
|
|
8
8
|
from django.contrib.gis.geos import Point
|
|
9
9
|
from django.utils import timezone
|
|
10
|
+
from django_filters.rest_framework import DjangoFilterBackend
|
|
10
11
|
from simo.core.api import InstanceMixin
|
|
11
12
|
from .models import (
|
|
12
13
|
User, UserDevice, UserDeviceReportLog, PermissionsRole, InstanceInvitation,
|
|
13
|
-
Fingerprint
|
|
14
|
+
Fingerprint, ComponentPermission, InstanceUser
|
|
14
15
|
)
|
|
15
16
|
from .serializers import (
|
|
16
17
|
UserSerializer, PermissionsRoleSerializer, InstanceInvitationSerializer,
|
|
17
|
-
FingerprintSerializer
|
|
18
|
+
FingerprintSerializer, ComponentPermissionSerializer
|
|
18
19
|
)
|
|
19
20
|
|
|
20
21
|
|
|
@@ -63,9 +64,6 @@ class UsersViewSet(mixins.RetrieveModelMixin,
|
|
|
63
64
|
request.data.pop(key)
|
|
64
65
|
|
|
65
66
|
|
|
66
|
-
target_user.set_instance(self.instance)
|
|
67
|
-
|
|
68
|
-
|
|
69
67
|
serializer = self.get_serializer(
|
|
70
68
|
target_user, data=request.data, partial=partial
|
|
71
69
|
)
|
|
@@ -135,12 +133,38 @@ class RolesViewsets(InstanceMixin, viewsets.ReadOnlyModelViewSet):
|
|
|
135
133
|
url = 'users/roles'
|
|
136
134
|
basename = 'roles'
|
|
137
135
|
serializer_class = PermissionsRoleSerializer
|
|
138
|
-
queryset = PermissionsRole.objects.all()
|
|
139
136
|
|
|
140
137
|
def get_queryset(self):
|
|
141
138
|
return PermissionsRole.objects.filter(instance=self.instance)
|
|
142
139
|
|
|
143
140
|
|
|
141
|
+
class ComponentPermissionViewsets(
|
|
142
|
+
InstanceMixin,
|
|
143
|
+
mixins.RetrieveModelMixin, mixins.UpdateModelMixin,
|
|
144
|
+
mixins.ListModelMixin, viewsets.GenericViewSet
|
|
145
|
+
):
|
|
146
|
+
url = 'users/componentpermissions'
|
|
147
|
+
basename = 'componentpermissions'
|
|
148
|
+
serializer_class = ComponentPermissionSerializer
|
|
149
|
+
filter_backends = [DjangoFilterBackend]
|
|
150
|
+
filterset_fields = ['component', 'role']
|
|
151
|
+
|
|
152
|
+
def get_queryset(self):
|
|
153
|
+
return ComponentPermission.objects.filter(role__instance=self.instance)
|
|
154
|
+
|
|
155
|
+
def update(self, request, *args, **kwargs):
|
|
156
|
+
if request.user.is_master:
|
|
157
|
+
return super().update(request, *args, **kwargs)
|
|
158
|
+
iuser = InstanceUser.objects.get(
|
|
159
|
+
instance=self.instance, user=request.user
|
|
160
|
+
).select_related('role')
|
|
161
|
+
if not iuser.is_active:
|
|
162
|
+
raise PermissionDenied()
|
|
163
|
+
if iuser.role.is_owner or iuser.role.is_superuser:
|
|
164
|
+
return super().update(request, *args, **kwargs)
|
|
165
|
+
raise PermissionDenied()
|
|
166
|
+
|
|
167
|
+
|
|
144
168
|
class UserDeviceReport(InstanceMixin, viewsets.GenericViewSet):
|
|
145
169
|
url = 'users'
|
|
146
170
|
basename = 'device_report'
|
|
@@ -188,6 +212,11 @@ class UserDeviceReport(InstanceMixin, viewsets.GenericViewSet):
|
|
|
188
212
|
).exclude(id=user_device.id).update(is_primary=False)
|
|
189
213
|
user_device.save()
|
|
190
214
|
|
|
215
|
+
for iu in request.user.instance_roles.filter(is_active=True):
|
|
216
|
+
iu.last_seen_location = user_device.last_seen_location
|
|
217
|
+
iu.last_seen_location_datetime = user_device.last_seen
|
|
218
|
+
iu.save()
|
|
219
|
+
|
|
191
220
|
request.user.last_seen_location = user_device.last_seen_location
|
|
192
221
|
request.user.last_seen_location_datetime = user_device.last_seen
|
|
193
222
|
request.user.save()
|
simo/users/managers.py
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
from django.db import models
|
|
2
|
+
from simo.core.middleware import get_current_instance
|
|
2
3
|
|
|
3
4
|
|
|
4
5
|
class ActiveInstanceManager(models.Manager):
|
|
5
6
|
|
|
6
7
|
def get_queryset(self):
|
|
7
|
-
|
|
8
|
+
instance = get_current_instance()
|
|
9
|
+
return super().get_queryset().filter(
|
|
10
|
+
instance__is_active=True, instance=instance
|
|
11
|
+
)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Generated by Django 4.2.10 on 2024-10-18 08:00
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
import location_field.models.plain
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Migration(migrations.Migration):
|
|
8
|
+
|
|
9
|
+
dependencies = [
|
|
10
|
+
('users', '0033_alter_user_ssh_key'),
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
operations = [
|
|
14
|
+
migrations.AddField(
|
|
15
|
+
model_name='instanceuser',
|
|
16
|
+
name='last_seen_location',
|
|
17
|
+
field=location_field.models.plain.PlainLocationField(blank=True, help_text='Sent by user mobile app', max_length=63, null=True),
|
|
18
|
+
),
|
|
19
|
+
migrations.AddField(
|
|
20
|
+
model_name='instanceuser',
|
|
21
|
+
name='last_seen_location_datetime',
|
|
22
|
+
field=models.DateTimeField(blank=True, null=True),
|
|
23
|
+
),
|
|
24
|
+
]
|