simo 2.3.2__py3-none-any.whl → 2.3.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.

Files changed (47) hide show
  1. simo/__pycache__/asgi.cpython-38.pyc +0 -0
  2. simo/__pycache__/settings.cpython-38.pyc +0 -0
  3. simo/backups/__init__.py +0 -0
  4. simo/backups/__pycache__/__init__.cpython-38.pyc +0 -0
  5. simo/backups/__pycache__/admin.cpython-38.pyc +0 -0
  6. simo/backups/__pycache__/dynamic_settings.cpython-38.pyc +0 -0
  7. simo/backups/__pycache__/models.cpython-38.pyc +0 -0
  8. simo/backups/admin.py +19 -0
  9. simo/backups/dynamic_settings.py +21 -0
  10. simo/backups/migrations/0001_initial.py +27 -0
  11. simo/backups/migrations/__init__.py +0 -0
  12. simo/backups/migrations/__pycache__/0001_initial.cpython-38.pyc +0 -0
  13. simo/backups/migrations/__pycache__/__init__.cpython-38.pyc +0 -0
  14. simo/backups/models.py +18 -0
  15. simo/backups/tasks.py +272 -0
  16. simo/core/__pycache__/context.cpython-38.pyc +0 -0
  17. simo/core/__pycache__/dynamic_settings.cpython-38.pyc +0 -0
  18. simo/core/__pycache__/middleware.cpython-38.pyc +0 -0
  19. simo/core/__pycache__/serializers.cpython-38.pyc +0 -0
  20. simo/core/__pycache__/tasks.cpython-38.pyc +0 -0
  21. simo/core/context.py +2 -2
  22. simo/core/management/commands/on_http_start.py +1 -2
  23. simo/core/management/update.py +0 -19
  24. simo/core/middleware.py +16 -0
  25. simo/core/serializers.py +3 -1
  26. simo/core/tasks.py +0 -2
  27. simo/fleet/__pycache__/controllers.cpython-38.pyc +0 -0
  28. simo/fleet/__pycache__/forms.cpython-38.pyc +0 -0
  29. simo/fleet/controllers.py +1 -2
  30. simo/fleet/forms.py +6 -0
  31. simo/generic/__pycache__/forms.cpython-38.pyc +0 -0
  32. simo/generic/__pycache__/socket_consumers.cpython-38.pyc +0 -0
  33. simo/generic/forms.py +4 -2
  34. simo/settings.py +1 -0
  35. simo/users/__pycache__/admin.cpython-38.pyc +0 -0
  36. simo/users/__pycache__/models.cpython-38.pyc +0 -0
  37. simo/users/admin.py +14 -15
  38. simo/users/models.py +22 -20
  39. {simo-2.3.2.dist-info → simo-2.3.4.dist-info}/METADATA +1 -1
  40. {simo-2.3.2.dist-info → simo-2.3.4.dist-info}/RECORD +44 -34
  41. simo/core/templates/admin/formset_widget_old.html +0 -122
  42. simo/core/templates/setup_wizard/clearable_easy_thumbnails_widget.html +0 -15
  43. simo/core/templates/setup_wizard/form.html +0 -225
  44. {simo-2.3.2.dist-info → simo-2.3.4.dist-info}/LICENSE.md +0 -0
  45. {simo-2.3.2.dist-info → simo-2.3.4.dist-info}/WHEEL +0 -0
  46. {simo-2.3.2.dist-info → simo-2.3.4.dist-info}/entry_points.txt +0 -0
  47. {simo-2.3.2.dist-info → simo-2.3.4.dist-info}/top_level.txt +0 -0
Binary file
Binary file
File without changes
simo/backups/admin.py ADDED
@@ -0,0 +1,19 @@
1
+ from django.contrib import admin
2
+ from .models import Backup
3
+
4
+
5
+ @admin.register(Backup)
6
+ class BackupAdmin(admin.ModelAdmin):
7
+ list_display = 'datetime', 'device', 'filepath'
8
+ fields = 'datetime', 'device', 'filepath'
9
+ readonly_fields = 'datetime', 'device', 'filepath'
10
+ list_filter = 'datetime', 'mac',
11
+
12
+ def has_change_permission(self, request, obj=None):
13
+ return False
14
+
15
+ def has_add_permission(self, request):
16
+ return False
17
+
18
+ def has_delete_permission(self, request, obj=None):
19
+ return False
@@ -0,0 +1,21 @@
1
+ from dynamic_preferences.preferences import Section
2
+ from dynamic_preferences.types import (
3
+ BooleanPreference, StringPreference, ChoicePreference, IntegerPreference,
4
+ )
5
+ from dynamic_preferences.registries import global_preferences_registry
6
+
7
+ backups = Section('backups')
8
+
9
+
10
+ @global_preferences_registry.register
11
+ class LastBackupError(StringPreference):
12
+ section = backups
13
+ name = 'last_error'
14
+ default = ''
15
+
16
+
17
+ @global_preferences_registry.register
18
+ class LastBackupCheck(IntegerPreference):
19
+ section = backups
20
+ name = 'last_check'
21
+ default = 0
@@ -0,0 +1,27 @@
1
+ # Generated by Django 4.2.10 on 2024-10-01 13:01
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ initial = True
9
+
10
+ dependencies = [
11
+ ]
12
+
13
+ operations = [
14
+ migrations.CreateModel(
15
+ name='Backup',
16
+ fields=[
17
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
18
+ ('datetime', models.DateTimeField(db_index=True)),
19
+ ('mac', models.CharField(db_index=True, max_length=100)),
20
+ ('filepath', models.CharField(max_length=200)),
21
+ ],
22
+ options={
23
+ 'ordering': ('datetime',),
24
+ 'unique_together': {('datetime', 'mac')},
25
+ },
26
+ ),
27
+ ]
File without changes
simo/backups/models.py ADDED
@@ -0,0 +1,18 @@
1
+ import uuid
2
+ from django.db import models
3
+
4
+
5
+ class Backup(models.Model):
6
+ datetime = models.DateTimeField(db_index=True)
7
+ mac = models.CharField(max_length=100, db_index=True)
8
+ filepath = models.CharField(max_length=200)
9
+
10
+ class Meta:
11
+ unique_together = 'datetime', 'mac'
12
+ ordering = 'datetime',
13
+
14
+ @property
15
+ def device(self):
16
+ if self.mac == str(hex(uuid.getnode())):
17
+ return "This machine"
18
+ return self.mac
simo/backups/tasks.py ADDED
@@ -0,0 +1,272 @@
1
+ import os, subprocess, json, uuid, datetime, shutil
2
+ from datetime import datetime, timezone
3
+ from celeryc import celery_app
4
+ from simo.conf import dynamic_settings
5
+
6
+
7
+ @celery_app.task
8
+ def check_backups():
9
+ '''
10
+ syncs up backups on external medium to the database
11
+ '''
12
+ from .models import Backup
13
+
14
+ try:
15
+ lv_group, lv_name, mountpoint = get_partitions()
16
+ except:
17
+ return Backup.objects.all().delete()
18
+
19
+
20
+ backups_dir = os.path.join(mountpoint, 'simo_backups')
21
+ if not os.path.exists(backups_dir):
22
+ return Backup.objects.all().delete()
23
+
24
+ backups_mentioned = []
25
+ for item in os.listdir(backups_dir):
26
+ if not item.startswith('hub-'):
27
+ continue
28
+ hub_mac = item.split('-')[1]
29
+ hub_dir = os.path.join(backups_dir, item)
30
+ for month_folder in os.listdir(hub_dir):
31
+ try:
32
+ year, month = month_folder.split('-')
33
+ year, month = int(year), int(month)
34
+ except:
35
+ continue
36
+ for filename in os.listdir(os.path.join(hub_dir, month_folder)):
37
+ try:
38
+ day, time, back = filename.split('.')
39
+ hour, minute, second = time.split('-')
40
+ day, hour, minute, second = \
41
+ int(day), int(hour), int(minute), int(second)
42
+ except:
43
+ continue
44
+
45
+ obj, new = Backup.objects.update_or_create(
46
+ datetime=datetime(
47
+ year, month, day, hour, minute, second,
48
+ tzinfo=timezone.utc
49
+ ), mac=hub_mac, defaults={
50
+ 'filepath': os.path.join(
51
+ hub_dir, month_folder, filename
52
+ )
53
+ }
54
+ )
55
+ backups_mentioned.append(obj.id)
56
+
57
+ Backup.objects.all().exclude(id__in=backups_mentioned).delete()
58
+
59
+ dynamic_settings['backups__last_check'] = datetime.now()
60
+
61
+
62
+ def create_snap(lv_group, lv_name):
63
+ try:
64
+ return subprocess.check_output(
65
+ f'lvcreate -s -n {lv_name}-snap {lv_group}/{lv_name} -L 3G',
66
+ shell=True
67
+ ).decode()
68
+ except:
69
+ return ''
70
+
71
+
72
+ def get_lvm_partition(lsblk_data):
73
+ for device in lsblk_data:
74
+ if device['type'] == 'lvm' and device['mountpoint'] == '/':
75
+ return device
76
+ if 'children' in device:
77
+ return get_lvm_partition(device['children'])
78
+
79
+
80
+ def get_backup_device(lsblk_data):
81
+ for device in lsblk_data:
82
+ if not device['hotplug']:
83
+ continue
84
+ target_device = None
85
+ if device.get('fstype') == 'exfat':
86
+ target_device = device
87
+ elif device.get('children'):
88
+ for child in device.get('children'):
89
+ if child.get('fstype') == 'exfat':
90
+ target_device = child
91
+ if target_device:
92
+ return target_device
93
+
94
+
95
+ def get_partitions():
96
+
97
+ lsblk_data = json.loads(subprocess.check_output(
98
+ 'lsblk --output NAME,HOTPLUG,MOUNTPOINT,FSTYPE,TYPE,LABEL,PARTLABEL --json',
99
+ shell=True
100
+ ).decode())['blockdevices']
101
+
102
+ # Figure out if we are running in an LVM group
103
+
104
+ lvm_partition = get_lvm_partition(lsblk_data)
105
+ if not lvm_partition:
106
+ print("No LVM partition!")
107
+ dynamic_settings['backups__last_error'] = 'No LVM partition!'
108
+ return
109
+
110
+ try:
111
+ name = lvm_partition.get('name')
112
+ split_at = name.find('-')
113
+ lv_group = name[:split_at]
114
+ lv_name = name[split_at + 1:].replace('--', '-')
115
+ except:
116
+ print("Failed to identify LVM partition")
117
+ dynamic_settings['backups__last_error'] = \
118
+ 'Failed to identify LVM partition'
119
+ return
120
+
121
+ if not lv_name:
122
+ print("LVM was not found on this system. Abort!")
123
+ dynamic_settings['backups__last_error'] = \
124
+ 'Failed to identify LVM partition name'
125
+ return
126
+
127
+
128
+ # check if we have any removable devices storage devices plugged in
129
+
130
+ backup_device = get_backup_device(lsblk_data)
131
+
132
+ if not backup_device:
133
+ dynamic_settings['backups__last_error'] = \
134
+ 'No external exFAT backup device on this machine'
135
+ return
136
+
137
+ if lvm_partition.get('partlabel'):
138
+ mountpoint = f"/media/{backup_device['partlabel']}"
139
+ elif lvm_partition.get('label'):
140
+ mountpoint = f"/media/{backup_device['label']}"
141
+ else:
142
+ mountpoint = f"/media/{backup_device['name']}"
143
+
144
+ if not os.path.exists(mountpoint):
145
+ os.makedirs(mountpoint)
146
+
147
+ if backup_device.get('mountpoint') != mountpoint:
148
+
149
+ if backup_device.get('mountpoint'):
150
+ subprocess.call(f"umount {backup_device['mountpoint']}", shell=True)
151
+
152
+ subprocess.call(
153
+ f'mount /dev/{backup_device["name"]} {mountpoint}', shell=True,
154
+ stdout=subprocess.PIPE
155
+ )
156
+
157
+ return lv_group, lv_name, mountpoint
158
+
159
+
160
+ @celery_app.task
161
+ def perform_backup():
162
+
163
+ try:
164
+ lv_group, lv_name, mountpoint = get_partitions()
165
+ except:
166
+ return
167
+
168
+ output = create_snap(lv_group, lv_name)
169
+ if not output:
170
+ subprocess.check_output(
171
+ f'lvremove -f {lv_group}/{lv_name}-snap',
172
+ shell=True
173
+ )
174
+ output = create_snap(lv_group, lv_name)
175
+
176
+ if f'Logical volume "{lv_name}-snap" created' not in output:
177
+ print(output)
178
+ print(f"Unable to create {lv_name}-snap.")
179
+ return
180
+
181
+ mac = str(hex(uuid.getnode()))
182
+ device_backups_path = f'{mountpoint}/simo_backups/hub-{mac}'
183
+ if not os.path.exists(device_backups_path):
184
+ os.makedirs(device_backups_path)
185
+
186
+ now = datetime.datetime.now()
187
+ level = now.day
188
+ month_folder = os.path.join(
189
+ device_backups_path, f'{now.year}-{now.month}'
190
+ )
191
+ if not os.path.exists(month_folder):
192
+ os.makedirs(month_folder)
193
+ level = 0
194
+
195
+ if level != 0:
196
+ # check if level 0 exists
197
+ level_0_exists = False
198
+ for filename in os.listdir(month_folder):
199
+ if '-' not in filename:
200
+ continue
201
+ try:
202
+ level, date = filename.split('-')
203
+ level = int(level)
204
+ if level == 0:
205
+ level_0_exists = True
206
+ break
207
+ except:
208
+ continue
209
+ if not level_0_exists:
210
+ print("Level 0 does not exist! Backups must be started from 0!")
211
+ shutil.rmtree(month_folder)
212
+ os.makedirs(month_folder)
213
+ level = 0
214
+
215
+ time_mark = now.strftime("%H-%M-%S")
216
+ backup_file = f"{month_folder}/{now.day}.{time_mark}.back"
217
+ snap_mapper = f"/dev/mapper/{lv_group}-{lv_name.replace('-', '--')}--snap"
218
+ label = f"simo {now.strftime('%Y-%m-%d')}"
219
+ dumpdates_file = os.path.join(month_folder, 'dumpdates')
220
+
221
+ estimated_size = int(subprocess.check_output(
222
+ f'dump -{level} -Squz9 -b 1024 {snap_mapper}',
223
+ shell=True
224
+ )) * 0.5
225
+
226
+ folders = []
227
+ for item in os.listdir(device_backups_path):
228
+ if not os.path.isdir(os.path.join(device_backups_path, item)):
229
+ continue
230
+ try:
231
+ year, month = item.split('-')
232
+ folders.append([
233
+ os.path.join(device_backups_path, item),
234
+ int(year) * 12 + int(month)
235
+ ])
236
+ except:
237
+ continue
238
+ folders.sort(key=lambda v: v[1])
239
+
240
+ # delete old backups to free up space required for this backup to take place
241
+ while shutil.disk_usage('/media/backup').free < estimated_size:
242
+ remove_folder = folders.pop()[0]
243
+ print(f"REMOVE: {remove_folder}")
244
+ shutil.rmtree(remove_folder)
245
+
246
+ success = False
247
+ try:
248
+ subprocess.check_call(
249
+ f'dump -{level} -quz9 -b 1024 -L "{label}" -D {dumpdates_file} -f {backup_file} {snap_mapper}',
250
+ shell=True,
251
+ )
252
+ success = True
253
+ except:
254
+ try:
255
+ os.remove(backup_file)
256
+ print("Dump failed!")
257
+ except:
258
+ pass
259
+
260
+ subprocess.call(
261
+ f"lvremove -f {lv_group}/{lv_name}-snap", shell=True,
262
+ stdout=subprocess.PIPE
263
+ )
264
+ if success:
265
+ print("DONE!")
266
+ dynamic_settings['backups__last_error'] = ''
267
+
268
+
269
+ @celery_app.on_after_finalize.connect
270
+ def setup_periodic_tasks(sender, **kwargs):
271
+ sender.add_periodic_task(60 * 60, check_backups.s())
272
+ sender.add_periodic_task(60 * 60 * 8, perform_backup.s()) # perform auto backup every 8 hours
Binary file
simo/core/context.py CHANGED
@@ -15,11 +15,11 @@ def additional_templates_context(request):
15
15
  'dynamic_settings': dynamic_settings,
16
16
  'current_version': pkg_resources.get_distribution('simo').version,
17
17
  'update_available': is_update_available(True),
18
- 'instances': Instance.objects.all(),
18
+ 'instances': request.user.instances,
19
19
  'current_instance': get_current_instance()
20
20
  }
21
21
 
22
- if request.path.endswith('/admin/'):
22
+ if request.path == '/admin/':
23
23
  ctx['todos'] = []
24
24
  for app_name, app in apps.app_configs.items():
25
25
  try:
@@ -74,11 +74,10 @@ def update_auto_update():
74
74
  cron.write()
75
75
 
76
76
 
77
-
78
-
79
77
  class Command(BaseCommand):
80
78
 
81
79
  def handle(self, *args, **options):
82
80
  prepare_mosquitto()
83
81
  from simo.core.tasks import maybe_update_to_latest
84
82
  maybe_update_to_latest.delay()
83
+ update_auto_update()
@@ -8,23 +8,6 @@ import pkg_resources
8
8
  HUB_DIR = '/etc/SIMO/hub'
9
9
 
10
10
 
11
- def install_dependencies():
12
-
13
- status = subprocess.call(
14
- 'apt install postgresql libpq-dev postgresql-client '
15
- 'postgresql-client-common python3-pip redis-server supervisor '
16
- 'mosquitto libopenjp2-7 libtiff5-dev pkg-config libcairo2-dev '
17
- 'libgirepository1.0-dev libcairo2 libudev-dev gdal-bin net-tools '
18
- 'nginx postgis openvpn ffmpeg libsm6 libxext6 ssh keychain -y',
19
- shell=True
20
- )
21
- if status != 0:
22
- print("Unable install required packages.")
23
- return
24
-
25
- return True
26
-
27
-
28
11
  def perform_update():
29
12
 
30
13
  proc = subprocess.Popen(
@@ -35,8 +18,6 @@ def perform_update():
35
18
  if proc.returncode:
36
19
  raise Exception(err.decode())
37
20
 
38
- install_dependencies()
39
-
40
21
  proc = subprocess.Popen(
41
22
  [os.path.join(HUB_DIR, 'manage.py'), 'migrate'],
42
23
  cwd=HUB_DIR,
simo/core/middleware.py CHANGED
@@ -2,6 +2,7 @@ import pytz
2
2
  import threading
3
3
  import re
4
4
  from django.utils import timezone
5
+ from django.shortcuts import render
5
6
 
6
7
 
7
8
  _thread_locals = threading.local()
@@ -15,9 +16,13 @@ def get_current_request():
15
16
 
16
17
 
17
18
  def introduce_instance(instance, request=None):
19
+ if request.user.is_authenticated \
20
+ and instance not in request.user.instances:
21
+ return
18
22
  _thread_locals.instance = instance
19
23
  if request:
20
24
  request.session['instance_id'] = instance.id
25
+ request.instance = instance
21
26
 
22
27
 
23
28
  def get_current_instance(request=None):
@@ -51,6 +56,17 @@ def simo_router_middleware(get_response):
51
56
  def instance_middleware(get_response):
52
57
 
53
58
  def middleware(request):
59
+
60
+ if request.path.startswith('/admin'):
61
+ if not (request.user.is_authenticated and request.user.is_master):
62
+ return render(request, 'admin/msg_page.html', {
63
+ 'page_title': "You are not allowed in here",
64
+ 'msg': "Page you are trying to access is only for hub masters.",
65
+ 'suggestion': "Try switching your user to the one who has proper "
66
+ "rights to come here or ask for somebody who already has "
67
+ "master rights enable these rights for you."
68
+ })
69
+
54
70
  from simo.core.models import Instance
55
71
 
56
72
  instance = None
simo/core/serializers.py CHANGED
@@ -416,11 +416,13 @@ class ComponentSerializer(FormSerializer):
416
416
  controller_uid=controller_uid, instance=instance,
417
417
  **kwargs
418
418
  )
419
+ # only masters and superusers can fully manage components via app
420
+ # others can only change basic fields
419
421
  if not self.context['request'].user.is_master:
420
422
  user_role = self.context['request'].user.get_role(
421
423
  self.context['instance']
422
424
  )
423
- if not user_role.is_superuser and user_role.is_owner:
425
+ if not user_role.is_superuser:
424
426
  for field_name in list(form.fields.keys()):
425
427
  if field_name not in form.basic_fields:
426
428
  del form.fields[field_name]
simo/core/tasks.py CHANGED
@@ -7,8 +7,6 @@ import requests
7
7
  import subprocess
8
8
  import threading
9
9
  import pkg_resources
10
- import sys
11
- import traceback
12
10
  import uuid
13
11
  from django.db.models import Q
14
12
  from django.db import connection, transaction
Binary file
simo/fleet/controllers.py CHANGED
@@ -424,12 +424,11 @@ class PWMOutput(FadeMixin, FleeDeviceMixin, BasicOutputMixin, BaseDimmer):
424
424
  return value
425
425
 
426
426
 
427
-
428
427
  class RGBLight(FleeDeviceMixin, BasicOutputMixin, BaseRGBWLight):
429
428
  config_form = ColonelRGBLightConfigForm
430
429
 
431
430
 
432
- class DualMotorValve(FleeDeviceMixin, BasicOutputMixin, BaseSwitch):
431
+ class DualMotorValve(FleeDeviceMixin, BasicOutputMixin, BaseDimmer):
433
432
  gateway_class = FleetGatewayHandler
434
433
  config_form = DualMotorValveForm
435
434
  name = "Dual Motor Valve"
simo/fleet/forms.py CHANGED
@@ -1109,6 +1109,12 @@ class BlindsConfigForm(ColonelComponentForm):
1109
1109
  help_text="Time in seconds it takes for your blinds to go "
1110
1110
  "from fully closed to fully open."
1111
1111
  )
1112
+ close_duration = forms.FloatField(
1113
+ label='Close duration', min_value=1, max_value=360000,
1114
+ initial=30,
1115
+ help_text="Time in seconds it takes for your blinds to go "
1116
+ "from fully open to fully closed."
1117
+ )
1112
1118
  control_type = forms.ChoiceField(
1113
1119
  initial=0, required=True, choices=(
1114
1120
  ('hold', "Hold"), ('click', 'Click')
simo/generic/forms.py CHANGED
@@ -7,7 +7,7 @@ from django.contrib.contenttypes.models import ContentType
7
7
  from simo.core.forms import HiddenField, BaseComponentForm
8
8
  from simo.core.models import Icon, Component
9
9
  from simo.core.controllers import (
10
- BEFORE_SET, BinarySensor, NumericSensor, MultiSensor, Switch
10
+ BEFORE_SET, BinarySensor, NumericSensor, MultiSensor, Switch, Dimmer
11
11
  )
12
12
  from simo.core.widgets import PythonCode, LogOutputWidget
13
13
  from dal import autocomplete, forward
@@ -615,7 +615,9 @@ class ContourForm(forms.Form):
615
615
 
616
616
  name = forms.CharField()
617
617
  switch = forms.ModelChoiceField(
618
- Component.objects.filter(base_type=Switch.base_type),
618
+ Component.objects.filter(
619
+ base_type__in=(Switch.base_type, Dimmer.base_type)
620
+ ),
619
621
  widget=autocomplete.ModelSelect2(
620
622
  url='autocomplete-component', attrs={'data-html': True},
621
623
  forward=(
simo/settings.py CHANGED
@@ -72,6 +72,7 @@ INSTALLED_APPS = [
72
72
  'simo.generic',
73
73
  'simo.multimedia',
74
74
  'simo.fleet',
75
+ 'simo.backups',
75
76
 
76
77
  'django.contrib.admin',
77
78
  'adminsortable2',
Binary file