simo 3.0.5__py3-none-any.whl → 3.1.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/gateways.py +1 -0
- simo/core/api.py +4 -1
- simo/core/db_backend/base.py +8 -10
- simo/core/events.py +30 -3
- simo/core/gateways.py +4 -1
- simo/core/management/_hub_template/hub/nginx.conf +11 -0
- simo/core/management/_hub_template/hub/supervisor.conf +31 -0
- simo/core/management/commands/on_http_start.py +65 -1
- simo/core/management/commands/republish_mqtt_state.py +60 -0
- simo/core/management/commands/run_app_mqtt_control.py +129 -0
- simo/core/management/commands/run_app_mqtt_fanout.py +96 -0
- simo/core/signal_receivers.py +31 -7
- simo/core/tasks.py +0 -33
- simo/generic/gateways.py +65 -14
- simo/generic/tasks.py +1 -22
- simo/settings.py +8 -0
- simo/urls.py +2 -1
- simo/users/api.py +8 -2
- simo/users/auto_urls.py +2 -1
- simo/users/models.py +123 -14
- simo/users/serializers.py +4 -1
- simo/users/templates/conf/mosquitto_acls.conf +7 -7
- simo/users/views.py +12 -0
- {simo-3.0.5.dist-info → simo-3.1.1.dist-info}/METADATA +1 -1
- {simo-3.0.5.dist-info → simo-3.1.1.dist-info}/RECORD +29 -26
- {simo-3.0.5.dist-info → simo-3.1.1.dist-info}/WHEEL +0 -0
- {simo-3.0.5.dist-info → simo-3.1.1.dist-info}/entry_points.txt +0 -0
- {simo-3.0.5.dist-info → simo-3.1.1.dist-info}/licenses/LICENSE.md +0 -0
- {simo-3.0.5.dist-info → simo-3.1.1.dist-info}/top_level.txt +0 -0
simo/generic/gateways.py
CHANGED
|
@@ -7,9 +7,8 @@ import traceback
|
|
|
7
7
|
from django.conf import settings
|
|
8
8
|
from django.utils import timezone
|
|
9
9
|
import paho.mqtt.client as mqtt
|
|
10
|
-
from simo.core.
|
|
11
|
-
from simo.core.
|
|
12
|
-
from simo.core.middleware import drop_current_instance
|
|
10
|
+
from simo.core.models import Instance, Component
|
|
11
|
+
from simo.core.middleware import introduce_instance, drop_current_instance
|
|
13
12
|
from simo.core.gateways import BaseObjectCommandsGatewayHandler
|
|
14
13
|
from simo.core.forms import BaseGatewayForm
|
|
15
14
|
from simo.core.events import GatewayObjectCommand, get_event_obj
|
|
@@ -176,15 +175,18 @@ class GenericGatewayHandler(
|
|
|
176
175
|
):
|
|
177
176
|
name = "Generic"
|
|
178
177
|
config_form = BaseGatewayForm
|
|
178
|
+
auto_create = True
|
|
179
179
|
info = "Provides generic type components which use other components to operate like " \
|
|
180
180
|
"thermostats, alarm groups, watering programs, alarm clocks," \
|
|
181
181
|
"etc. "
|
|
182
182
|
|
|
183
183
|
running_scripts = {}
|
|
184
184
|
periodic_tasks = (
|
|
185
|
+
('watch_timers', 1),
|
|
185
186
|
('watch_thermostats', 60),
|
|
186
187
|
('watch_alarm_clocks', 30),
|
|
187
188
|
('watch_watering', 60),
|
|
189
|
+
('low_battery_notifications', 60 * 60),
|
|
188
190
|
('watch_main_states', 60),
|
|
189
191
|
('watch_groups', 60)
|
|
190
192
|
)
|
|
@@ -198,6 +200,21 @@ class GenericGatewayHandler(
|
|
|
198
200
|
self.pulsing_switches = {}
|
|
199
201
|
|
|
200
202
|
|
|
203
|
+
def watch_timers(self):
|
|
204
|
+
from simo.core.models import Component
|
|
205
|
+
drop_current_instance()
|
|
206
|
+
for component in Component.objects.filter(
|
|
207
|
+
meta__timer_to__gt=0
|
|
208
|
+
).filter(meta__timer_to__lt=time.time()):
|
|
209
|
+
component.meta['timer_to'] = 0
|
|
210
|
+
component.meta['timer_start'] = 0
|
|
211
|
+
component.save()
|
|
212
|
+
try:
|
|
213
|
+
component.controller._on_timer_end()
|
|
214
|
+
except Exception as e:
|
|
215
|
+
print(traceback.format_exc(), file=sys.stderr)
|
|
216
|
+
|
|
217
|
+
|
|
201
218
|
def watch_thermostats(self):
|
|
202
219
|
from .controllers import Thermostat
|
|
203
220
|
drop_current_instance()
|
|
@@ -206,7 +223,7 @@ class GenericGatewayHandler(
|
|
|
206
223
|
):
|
|
207
224
|
tz = pytz.timezone(thermostat.zone.instance.timezone)
|
|
208
225
|
timezone.activate(tz)
|
|
209
|
-
thermostat._evaluate()
|
|
226
|
+
thermostat.controller._evaluate()
|
|
210
227
|
|
|
211
228
|
|
|
212
229
|
def watch_alarm_clocks(self):
|
|
@@ -217,7 +234,7 @@ class GenericGatewayHandler(
|
|
|
217
234
|
):
|
|
218
235
|
tz = pytz.timezone(alarm_clock.zone.instance.timezone)
|
|
219
236
|
timezone.activate(tz)
|
|
220
|
-
alarm_clock._tick()
|
|
237
|
+
alarm_clock.controller._tick()
|
|
221
238
|
|
|
222
239
|
|
|
223
240
|
def watch_watering(self):
|
|
@@ -227,12 +244,46 @@ class GenericGatewayHandler(
|
|
|
227
244
|
tz = pytz.timezone(watering.zone.instance.timezone)
|
|
228
245
|
timezone.activate(tz)
|
|
229
246
|
if watering.value['status'] == 'running_program':
|
|
230
|
-
watering._set_program_progress(
|
|
247
|
+
watering.controller._set_program_progress(
|
|
231
248
|
watering.value['program_progress'] + 1
|
|
232
249
|
)
|
|
233
250
|
else:
|
|
234
251
|
watering.controller._perform_schedule()
|
|
235
252
|
|
|
253
|
+
def low_battery_notifications(self):
|
|
254
|
+
from simo.notifications.utils import notify_users
|
|
255
|
+
from simo.automation.helpers import be_or_not_to_be
|
|
256
|
+
for instance in Instance.objects.filter(is_active=True):
|
|
257
|
+
timezone.activate(instance.timezone)
|
|
258
|
+
hour = timezone.localtime().hour
|
|
259
|
+
if hour < 7:
|
|
260
|
+
continue
|
|
261
|
+
if hour > 21:
|
|
262
|
+
continue
|
|
263
|
+
|
|
264
|
+
introduce_instance(instance)
|
|
265
|
+
for comp in Component.objects.filter(
|
|
266
|
+
zone__instance=instance,
|
|
267
|
+
battery_level__isnull=False, battery_level__lt=20
|
|
268
|
+
):
|
|
269
|
+
last_warning = comp.meta.get('last_battery_warning', 0)
|
|
270
|
+
notify = be_or_not_to_be(12 * 60 * 60, 72 * 60 * 60,
|
|
271
|
+
last_warning)
|
|
272
|
+
if not notify:
|
|
273
|
+
continue
|
|
274
|
+
|
|
275
|
+
iusers = comp.zone.instance.instance_users.filter(
|
|
276
|
+
is_active=True, role__is_owner=True
|
|
277
|
+
)
|
|
278
|
+
if iusers:
|
|
279
|
+
notify_users(
|
|
280
|
+
'warning',
|
|
281
|
+
f"Low battery ({comp.battery_level}%) on {comp}",
|
|
282
|
+
component=comp, instance_users=iusers
|
|
283
|
+
)
|
|
284
|
+
comp.meta['last_battery_warning'] = time.time()
|
|
285
|
+
comp.save()
|
|
286
|
+
|
|
236
287
|
|
|
237
288
|
def run(self, exit):
|
|
238
289
|
drop_current_instance()
|
|
@@ -340,7 +391,7 @@ class GenericGatewayHandler(
|
|
|
340
391
|
def set_get_day_evening_night_morning(self, state):
|
|
341
392
|
if state.value not in ('day', 'night', 'evening', 'morning'):
|
|
342
393
|
return
|
|
343
|
-
new_state = state._get_day_evening_night_morning()
|
|
394
|
+
new_state = state.controller._get_day_evening_night_morning()
|
|
344
395
|
if new_state == state.value:
|
|
345
396
|
self.last_set_state = state.value
|
|
346
397
|
return
|
|
@@ -377,7 +428,7 @@ class GenericGatewayHandler(
|
|
|
377
428
|
self.sensors_on_watch[state.id][sensor.id] = i_id
|
|
378
429
|
sensor.on_change(self.security_sensor_change)
|
|
379
430
|
|
|
380
|
-
if state._check_is_away(self.last_sensor_actions.get(i_id, 0)):
|
|
431
|
+
if state.controller._check_is_away(self.last_sensor_actions.get(i_id, 0)):
|
|
381
432
|
if state.value != 'away':
|
|
382
433
|
print(f"New main state of "
|
|
383
434
|
f"{state.zone.instance} - away")
|
|
@@ -385,7 +436,7 @@ class GenericGatewayHandler(
|
|
|
385
436
|
else:
|
|
386
437
|
if state.value == 'away':
|
|
387
438
|
try:
|
|
388
|
-
new_state = state._get_day_evening_night_morning()
|
|
439
|
+
new_state = state.controller._get_day_evening_night_morning()
|
|
389
440
|
except:
|
|
390
441
|
new_state = 'day'
|
|
391
442
|
print(f"New main state of "
|
|
@@ -394,14 +445,14 @@ class GenericGatewayHandler(
|
|
|
394
445
|
|
|
395
446
|
if state.config.get('sleeping_phones_hour') is not None:
|
|
396
447
|
if state.value != 'sleep':
|
|
397
|
-
if state._is_sleep_time() and state._owner_phones_on_charge(True):
|
|
448
|
+
if state.controller._is_sleep_time() and state.controller._owner_phones_on_charge(True):
|
|
398
449
|
print(f"New main state of {state.zone.instance} - sleep")
|
|
399
450
|
state.send('sleep')
|
|
400
451
|
else:
|
|
401
|
-
if not state._owner_phones_on_charge(True) \
|
|
402
|
-
and not state._is_sleep_time():
|
|
452
|
+
if not state.controller._owner_phones_on_charge(True) \
|
|
453
|
+
and not state.controller._is_sleep_time():
|
|
403
454
|
try:
|
|
404
|
-
new_state = state._get_day_evening_night_morning()
|
|
455
|
+
new_state = state.controller._get_day_evening_night_morning()
|
|
405
456
|
except:
|
|
406
457
|
new_state = 'day'
|
|
407
458
|
print(f"New main state of "
|
|
@@ -509,9 +560,9 @@ class GenericGatewayHandler(
|
|
|
509
560
|
class DummyGatewayHandler(BaseObjectCommandsGatewayHandler):
|
|
510
561
|
name = "Dummy"
|
|
511
562
|
config_form = BaseGatewayForm
|
|
563
|
+
auto_create = True
|
|
512
564
|
info = "Provides dummy components that do absolutely anything, " \
|
|
513
565
|
"but comes in super handy when configuring custom automations."
|
|
514
566
|
|
|
515
567
|
def perform_value_send(self, component, value):
|
|
516
568
|
component.controller.set(value)
|
|
517
|
-
|
simo/generic/tasks.py
CHANGED
|
@@ -56,25 +56,4 @@ def fire_breach_events(ag_id):
|
|
|
56
56
|
ag.meta['events_triggered'] = [uid]
|
|
57
57
|
else:
|
|
58
58
|
ag.meta['events_triggered'].append(uid)
|
|
59
|
-
ag.save(update_fields=['meta'])
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
@celery_app.task
|
|
63
|
-
def watch_timers():
|
|
64
|
-
from simo.core.models import Component
|
|
65
|
-
drop_current_instance()
|
|
66
|
-
for component in Component.objects.filter(
|
|
67
|
-
meta__timer_to__gt=0
|
|
68
|
-
).filter(meta__timer_to__lt=time.time()):
|
|
69
|
-
component.meta['timer_to'] = 0
|
|
70
|
-
component.meta['timer_start'] = 0
|
|
71
|
-
component.save()
|
|
72
|
-
try:
|
|
73
|
-
component.controller._on_timer_end()
|
|
74
|
-
except Exception as e:
|
|
75
|
-
print(traceback.format_exc(), file=sys.stderr)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
@celery_app.on_after_finalize.connect
|
|
79
|
-
def setup_periodic_tasks(sender, **kwargs):
|
|
80
|
-
sender.add_periodic_task(1, watch_timers.s())
|
|
59
|
+
ag.save(update_fields=['meta'])
|
simo/settings.py
CHANGED
|
@@ -133,6 +133,14 @@ DATABASES = {
|
|
|
133
133
|
'ENGINE': 'simo.core.db_backend',
|
|
134
134
|
'NAME': 'SIMO',
|
|
135
135
|
'ATOMIC_REQUESTS': False,
|
|
136
|
+
'CONN_HEALTH_CHECKS': True,
|
|
137
|
+
'CONN_MAX_AGE': 300,
|
|
138
|
+
'OPTIONS': {
|
|
139
|
+
"keepalives": 1,
|
|
140
|
+
"keepalives_idle": 30,
|
|
141
|
+
"keepalives_interval": 10,
|
|
142
|
+
"keepalives_count": 3,
|
|
143
|
+
}
|
|
136
144
|
}
|
|
137
145
|
}
|
|
138
146
|
|
simo/urls.py
CHANGED
|
@@ -45,7 +45,8 @@ urlpatterns = [
|
|
|
45
45
|
path('logout/', LogoutView.as_view(), name='logout'),
|
|
46
46
|
path('admin/', admin_site.urls),
|
|
47
47
|
path('api-hub-info/', hub_info),
|
|
48
|
-
|
|
48
|
+
# REST API (instance-scoped)
|
|
49
|
+
path('api/<slug:instance_slug>/', include(rest_router.urls)),
|
|
49
50
|
|
|
50
51
|
]
|
|
51
52
|
|
simo/users/api.py
CHANGED
|
@@ -127,9 +127,16 @@ class UsersViewSet(mixins.RetrieveModelMixin,
|
|
|
127
127
|
raise ValidationError(
|
|
128
128
|
'You do not have permission for this!', code=403
|
|
129
129
|
)
|
|
130
|
-
|
|
130
|
+
if InstanceUser.objects.filter(user=user, is_active=True).count() > 1:
|
|
131
|
+
InstanceUser.objects.filter(
|
|
132
|
+
user=user, instance=self.instance
|
|
133
|
+
).delete()
|
|
134
|
+
else:
|
|
135
|
+
user.delete()
|
|
131
136
|
return RESTResponse(status=status.HTTP_204_NO_CONTENT)
|
|
132
137
|
|
|
138
|
+
# (moved to dedicated view at /users/mqtt-credentials/)
|
|
139
|
+
|
|
133
140
|
|
|
134
141
|
class RolesViewsets(InstanceMixin, viewsets.ReadOnlyModelViewSet):
|
|
135
142
|
url = 'users/roles'
|
|
@@ -406,4 +413,3 @@ class FingerprintViewSet(
|
|
|
406
413
|
def destroy(self, request, *args, **kwargs):
|
|
407
414
|
self.check_can_manage_user(request)
|
|
408
415
|
return super().destroy(request, *args, **kwargs)
|
|
409
|
-
|
simo/users/auto_urls.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from django.urls import re_path, path
|
|
2
|
-
from .views import accept_invitation, RolesAutocomplete
|
|
2
|
+
from .views import accept_invitation, RolesAutocomplete, mqtt_credentials
|
|
3
3
|
|
|
4
4
|
urlpatterns = [
|
|
5
5
|
re_path(
|
|
@@ -10,4 +10,5 @@ urlpatterns = [
|
|
|
10
10
|
'autocomplete-roles',
|
|
11
11
|
RolesAutocomplete.as_view(), name='autocomplete-user-roles'
|
|
12
12
|
),
|
|
13
|
+
path('mqtt-credentials/', mqtt_credentials, name='mqtt-credentials'),
|
|
13
14
|
]
|
simo/users/models.py
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import datetime
|
|
2
|
+
import time
|
|
3
|
+
import json
|
|
2
4
|
import requests
|
|
3
5
|
import subprocess
|
|
4
6
|
from django.urls import reverse
|
|
@@ -16,6 +18,7 @@ from actstream import action
|
|
|
16
18
|
from django.contrib.auth.models import (
|
|
17
19
|
AbstractBaseUser, PermissionsMixin, UserManager as DefaultUserManager
|
|
18
20
|
)
|
|
21
|
+
from django.conf import settings
|
|
19
22
|
from django.utils import timezone
|
|
20
23
|
from easy_thumbnails.fields import ThumbnailerImageField
|
|
21
24
|
from location_field.models.plain import PlainLocationField
|
|
@@ -115,7 +118,9 @@ class InstanceUser(DirtyFieldsMixin, models.Model, OnChangeMixin):
|
|
|
115
118
|
objects = ActiveInstanceManager()
|
|
116
119
|
|
|
117
120
|
on_change_fields = (
|
|
118
|
-
'is_active', '
|
|
121
|
+
'is_active', 'role', 'at_home',
|
|
122
|
+
'last_seen', 'last_seen_location', 'last_seen_speed_kmh',
|
|
123
|
+
'phone_on_charge'
|
|
119
124
|
)
|
|
120
125
|
|
|
121
126
|
class Meta:
|
|
@@ -158,13 +163,11 @@ class InstanceUser(DirtyFieldsMixin, models.Model, OnChangeMixin):
|
|
|
158
163
|
|
|
159
164
|
@receiver(post_save, sender=InstanceUser)
|
|
160
165
|
def post_instance_user_save(sender, instance, created, **kwargs):
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
dirty_fields = instance.get_dirty_fields()
|
|
165
|
-
if any([f in dirty_fields.keys() for f in InstanceUser.on_change_fields]):
|
|
166
|
+
from simo.core.events import ObjectChangeEvent, dirty_fields_to_current_values
|
|
167
|
+
dirty_fields_prev = instance.get_dirty_fields()
|
|
168
|
+
if not created and any([f in dirty_fields_prev.keys() for f in InstanceUser.on_change_fields]):
|
|
166
169
|
def post_update():
|
|
167
|
-
if 'at_home' in
|
|
170
|
+
if 'at_home' in dirty_fields_prev:
|
|
168
171
|
if instance.at_home:
|
|
169
172
|
verb = 'came home'
|
|
170
173
|
else:
|
|
@@ -174,12 +177,57 @@ def post_instance_user_save(sender, instance, created, **kwargs):
|
|
|
174
177
|
instance_id=instance.instance.id,
|
|
175
178
|
action_type='user_presence', value=instance.at_home
|
|
176
179
|
)
|
|
180
|
+
# Include key fields so clients can update UI without a refetch
|
|
177
181
|
ObjectChangeEvent(
|
|
178
|
-
instance.instance,
|
|
182
|
+
instance.instance,
|
|
183
|
+
instance,
|
|
184
|
+
dirty_fields=dirty_fields_to_current_values(instance, dirty_fields_prev),
|
|
185
|
+
at_home=instance.at_home,
|
|
186
|
+
last_seen=instance.last_seen,
|
|
187
|
+
last_seen_location=instance.last_seen_location,
|
|
188
|
+
last_seen_speed_kmh=instance.last_seen_speed_kmh,
|
|
189
|
+
phone_on_charge=instance.phone_on_charge,
|
|
190
|
+
is_active=instance.is_active,
|
|
191
|
+
role=instance.role_id,
|
|
179
192
|
).publish()
|
|
193
|
+
# If role changed, notify the affected user to re-fetch states
|
|
194
|
+
if 'role' in dirty_fields_prev:
|
|
195
|
+
from paho.mqtt import publish as mqtt_publish
|
|
196
|
+
topic = f"SIMO/user/{instance.user.id}/perms-changed"
|
|
197
|
+
payload = json.dumps({
|
|
198
|
+
'instance_id': instance.instance.id,
|
|
199
|
+
'timestamp': int(time.time())
|
|
200
|
+
})
|
|
201
|
+
try:
|
|
202
|
+
mqtt_publish.single(
|
|
203
|
+
topic, payload,
|
|
204
|
+
hostname=settings.MQTT_HOST, port=settings.MQTT_PORT,
|
|
205
|
+
auth={'username': 'root', 'password': settings.SECRET_KEY},
|
|
206
|
+
retain=False
|
|
207
|
+
)
|
|
208
|
+
except Exception:
|
|
209
|
+
pass
|
|
210
|
+
# Invalidate cached role lookups
|
|
211
|
+
try:
|
|
212
|
+
cache.delete(f'user-{instance.user.id}_instance-{instance.instance.id}-role-id')
|
|
213
|
+
cache.delete(f'user-{instance.user.id}_instance-{instance.instance.id}_role')
|
|
214
|
+
except Exception:
|
|
215
|
+
pass
|
|
180
216
|
transaction.on_commit(post_update)
|
|
181
|
-
if
|
|
217
|
+
# Rebuild ACLs if user became active/inactive due to this role change
|
|
218
|
+
try:
|
|
219
|
+
if created or ('is_active' in dirty_fields_prev):
|
|
220
|
+
dynamic_settings['core__needs_mqtt_acls_rebuild'] = True
|
|
221
|
+
except Exception:
|
|
222
|
+
pass
|
|
223
|
+
|
|
224
|
+
@receiver(post_delete, sender=InstanceUser)
|
|
225
|
+
def post_instance_user_delete(sender, instance, **kwargs):
|
|
226
|
+
# Deleting role entry may change user's overall is_active; rebuild ACLs
|
|
227
|
+
try:
|
|
182
228
|
dynamic_settings['core__needs_mqtt_acls_rebuild'] = True
|
|
229
|
+
except Exception:
|
|
230
|
+
pass
|
|
183
231
|
|
|
184
232
|
|
|
185
233
|
# DirtyFieldsMixin does not work with AbstractBaseUser model!!!
|
|
@@ -256,6 +304,14 @@ class User(AbstractBaseUser, SimoAdminMixin):
|
|
|
256
304
|
if not org or (org.secret_key != self.secret_key):
|
|
257
305
|
self.update_mqtt_secret()
|
|
258
306
|
|
|
307
|
+
# Rebuild ACLs when a user is created or properties affecting ACLs change
|
|
308
|
+
# (username/email used by Mosquitto + master flag)
|
|
309
|
+
try:
|
|
310
|
+
if (not org) or (org.email != self.email) or (org.is_master != self.is_master):
|
|
311
|
+
dynamic_settings['core__needs_mqtt_acls_rebuild'] = True
|
|
312
|
+
except Exception:
|
|
313
|
+
pass
|
|
314
|
+
|
|
259
315
|
return obj
|
|
260
316
|
|
|
261
317
|
def update_mqtt_secret(self, reload=True):
|
|
@@ -408,6 +464,12 @@ class User(AbstractBaseUser, SimoAdminMixin):
|
|
|
408
464
|
|
|
409
465
|
rebuild_authorized_keys()
|
|
410
466
|
|
|
467
|
+
# Reflect access changes in Mosquitto ACLs
|
|
468
|
+
try:
|
|
469
|
+
dynamic_settings['core__needs_mqtt_acls_rebuild'] = True
|
|
470
|
+
except Exception:
|
|
471
|
+
pass
|
|
472
|
+
|
|
411
473
|
self._is_active = None
|
|
412
474
|
|
|
413
475
|
|
|
@@ -507,8 +569,28 @@ class ComponentPermission(models.Model):
|
|
|
507
569
|
|
|
508
570
|
@receiver(post_save, sender=ComponentPermission)
|
|
509
571
|
def rebuild_mqtt_acls_on_create(sender, instance, created, **kwargs):
|
|
510
|
-
|
|
511
|
-
|
|
572
|
+
# ACLs are per-user prefix; permission changes don't require ACL rebuilds.
|
|
573
|
+
|
|
574
|
+
# Notify affected users to re-sync their subscriptions
|
|
575
|
+
def _notify():
|
|
576
|
+
from paho.mqtt import publish as mqtt_publish
|
|
577
|
+
role = instance.role
|
|
578
|
+
for iu in role.instance.instance_users.filter(role=role, is_active=True).select_related('user'):
|
|
579
|
+
topic = f"SIMO/user/{iu.user.id}/perms-changed"
|
|
580
|
+
payload = json.dumps({
|
|
581
|
+
'instance_id': role.instance.id,
|
|
582
|
+
'timestamp': int(time.time())
|
|
583
|
+
})
|
|
584
|
+
try:
|
|
585
|
+
mqtt_publish.single(
|
|
586
|
+
topic, payload,
|
|
587
|
+
hostname=settings.MQTT_HOST, port=settings.MQTT_PORT,
|
|
588
|
+
auth={'username': 'root', 'password': settings.SECRET_KEY},
|
|
589
|
+
retain=False
|
|
590
|
+
)
|
|
591
|
+
except Exception:
|
|
592
|
+
pass
|
|
593
|
+
transaction.on_commit(_notify)
|
|
512
594
|
|
|
513
595
|
|
|
514
596
|
|
|
@@ -524,7 +606,7 @@ def create_component_permissions_comp(sender, instance, created, **kwargs):
|
|
|
524
606
|
'write': role.is_superuser or role.is_owner
|
|
525
607
|
}
|
|
526
608
|
)
|
|
527
|
-
|
|
609
|
+
# ACLs are per-user prefix; component additions don't require ACL rebuilds.
|
|
528
610
|
|
|
529
611
|
|
|
530
612
|
@receiver(post_save, sender=PermissionsRole)
|
|
@@ -541,6 +623,35 @@ def create_component_permissions_role(sender, instance, created, **kwargs):
|
|
|
541
623
|
}
|
|
542
624
|
)
|
|
543
625
|
|
|
626
|
+
# Permissions topology changed; notify users on this role
|
|
627
|
+
def _notify():
|
|
628
|
+
from paho.mqtt import publish as mqtt_publish
|
|
629
|
+
for iu in instance.instance.instance_users.filter(role=instance, is_active=True).select_related('user'):
|
|
630
|
+
topic = f"SIMO/user/{iu.user.id}/perms-changed"
|
|
631
|
+
payload = json.dumps({
|
|
632
|
+
'instance_id': instance.instance.id,
|
|
633
|
+
'timestamp': int(time.time())
|
|
634
|
+
})
|
|
635
|
+
try:
|
|
636
|
+
mqtt_publish.single(
|
|
637
|
+
topic, payload,
|
|
638
|
+
hostname=settings.MQTT_HOST, port=settings.MQTT_PORT,
|
|
639
|
+
auth={'username': 'root', 'password': settings.SECRET_KEY},
|
|
640
|
+
retain=False
|
|
641
|
+
)
|
|
642
|
+
except Exception:
|
|
643
|
+
pass
|
|
644
|
+
transaction.on_commit(_notify)
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
@receiver(post_delete, sender=User)
|
|
648
|
+
def rebuild_mqtt_acls_on_user_delete(sender, instance, **kwargs):
|
|
649
|
+
# Remove ACL stanza for deleted user
|
|
650
|
+
try:
|
|
651
|
+
dynamic_settings['core__needs_mqtt_acls_rebuild'] = True
|
|
652
|
+
except Exception:
|
|
653
|
+
pass
|
|
654
|
+
|
|
544
655
|
|
|
545
656
|
def get_default_inviation_expire_date():
|
|
546
657
|
return timezone.now() + datetime.timedelta(days=14)
|
|
@@ -607,5 +718,3 @@ class InstanceInvitation(models.Model):
|
|
|
607
718
|
|
|
608
719
|
def get_absolute_url(self):
|
|
609
720
|
return reverse('accept_invitation', kwargs={'token': self.token})
|
|
610
|
-
|
|
611
|
-
|
simo/users/serializers.py
CHANGED
|
@@ -32,7 +32,10 @@ class UserSerializer(serializers.ModelSerializer):
|
|
|
32
32
|
iu = InstanceUser.objects.filter(
|
|
33
33
|
user=obj, instance=get_current_instance()
|
|
34
34
|
).first()
|
|
35
|
-
|
|
35
|
+
try:
|
|
36
|
+
return iu.is_active
|
|
37
|
+
except:
|
|
38
|
+
return False
|
|
36
39
|
|
|
37
40
|
def get_avatar(self, obj):
|
|
38
41
|
if not obj.avatar:
|
|
@@ -2,18 +2,18 @@ user root
|
|
|
2
2
|
topic readwrite #
|
|
3
3
|
|
|
4
4
|
{% for user in users %}
|
|
5
|
+
{% if user.email == 'system@simo.io' %}
|
|
5
6
|
user {{ user.email }}
|
|
6
|
-
{% if user.email == 'system@simo.io' or user.is_master %}
|
|
7
7
|
topic readwrite #
|
|
8
|
-
|
|
8
|
+
{% elif user.is_active %}
|
|
9
|
+
user {{ user.email }}
|
|
10
|
+
{% if user.is_master %}
|
|
9
11
|
topic readwrite #
|
|
10
|
-
topic deny SIMO/#
|
|
11
12
|
{% else %}
|
|
12
|
-
|
|
13
|
-
topic read SIMO/obj-state/{{ perm.component.zone.instance.id }}/Component-{{ perm.component.id }}
|
|
14
|
-
{% endfor %}
|
|
13
|
+
topic readwrite SIMO/user/{{ user.id }}/#
|
|
15
14
|
{% endif %}
|
|
15
|
+
{% endif %}
|
|
16
16
|
{% endfor %}
|
|
17
17
|
|
|
18
18
|
# This affects all clients.
|
|
19
|
-
pattern deny #
|
|
19
|
+
pattern deny #
|
simo/users/views.py
CHANGED
|
@@ -125,3 +125,15 @@ class RolesAutocomplete(autocomplete.Select2QuerySetView):
|
|
|
125
125
|
return qs.distinct()
|
|
126
126
|
|
|
127
127
|
|
|
128
|
+
@login_required
|
|
129
|
+
def mqtt_credentials(request):
|
|
130
|
+
"""Return MQTT credentials for the authenticated user.
|
|
131
|
+
Response payload:
|
|
132
|
+
- username: user's email
|
|
133
|
+
- password: user's MQTT secret
|
|
134
|
+
"""
|
|
135
|
+
return JsonResponse({
|
|
136
|
+
'username': request.user.email,
|
|
137
|
+
'password': request.user.secret_key,
|
|
138
|
+
'user_id': request.user.id
|
|
139
|
+
})
|