simo 2.6.8__py3-none-any.whl → 2.7.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/__pycache__/settings.cpython-38.pyc +0 -0
- simo/automation/__pycache__/controllers.cpython-38.pyc +0 -0
- simo/automation/__pycache__/gateways.cpython-38.pyc +0 -0
- simo/automation/controllers.py +21 -2
- simo/automation/gateways.py +130 -1
- simo/core/__pycache__/api.cpython-38.pyc +0 -0
- simo/core/__pycache__/api_meta.cpython-38.pyc +0 -0
- simo/core/__pycache__/autocomplete_views.cpython-38.pyc +0 -0
- simo/core/__pycache__/controllers.cpython-38.pyc +0 -0
- simo/core/__pycache__/form_fields.cpython-38.pyc +0 -0
- simo/core/__pycache__/forms.cpython-38.pyc +0 -0
- simo/core/__pycache__/gateways.cpython-38.pyc +0 -0
- simo/core/__pycache__/middleware.cpython-38.pyc +0 -0
- simo/core/__pycache__/serializers.cpython-38.pyc +0 -0
- simo/core/__pycache__/signal_receivers.cpython-38.pyc +0 -0
- simo/core/api.py +4 -1
- simo/core/api_meta.py +6 -2
- simo/core/autocomplete_views.py +4 -3
- simo/core/controllers.py +13 -4
- simo/core/form_fields.py +92 -1
- simo/core/forms.py +10 -4
- simo/core/gateways.py +11 -1
- simo/core/serializers.py +8 -1
- simo/core/signal_receivers.py +7 -83
- simo/core/utils/__pycache__/converters.cpython-38.pyc +0 -0
- simo/core/utils/converters.py +59 -0
- simo/fleet/__pycache__/auto_urls.cpython-38.pyc +0 -0
- 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__/views.cpython-38.pyc +0 -0
- simo/fleet/auto_urls.py +5 -0
- simo/fleet/controllers.py +4 -26
- simo/fleet/forms.py +52 -4
- simo/fleet/migrations/__pycache__/0043_auto_20241203_0930.cpython-38.pyc +0 -0
- simo/fleet/views.py +37 -6
- 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/controllers.py +120 -1
- simo/generic/forms.py +77 -9
- simo/generic/gateways.py +81 -2
- simo/users/__pycache__/admin.cpython-38.pyc +0 -0
- simo/users/__pycache__/api.cpython-38.pyc +0 -0
- simo/users/__pycache__/auto_urls.cpython-38.pyc +0 -0
- simo/users/__pycache__/views.cpython-38.pyc +0 -0
- simo/users/admin.py +9 -0
- simo/users/api.py +2 -0
- simo/users/auto_urls.py +6 -3
- simo/users/migrations/0039_auto_20241117_1039.py +25 -24
- simo/users/migrations/__pycache__/0039_auto_20241117_1039.cpython-38.pyc +0 -0
- simo/users/views.py +20 -1
- {simo-2.6.8.dist-info → simo-2.7.1.dist-info}/METADATA +1 -1
- {simo-2.6.8.dist-info → simo-2.7.1.dist-info}/RECORD +58 -55
- {simo-2.6.8.dist-info → simo-2.7.1.dist-info}/LICENSE.md +0 -0
- {simo-2.6.8.dist-info → simo-2.7.1.dist-info}/WHEEL +0 -0
- {simo-2.6.8.dist-info → simo-2.7.1.dist-info}/entry_points.txt +0 -0
- {simo-2.6.8.dist-info → simo-2.7.1.dist-info}/top_level.txt +0 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
simo/automation/controllers.py
CHANGED
|
@@ -133,6 +133,7 @@ class PresenceLighting(Script):
|
|
|
133
133
|
self.last_presence = 0
|
|
134
134
|
self.hold_time = 60
|
|
135
135
|
self.conditions = []
|
|
136
|
+
self.expected_light_values = {}
|
|
136
137
|
|
|
137
138
|
def _run(self):
|
|
138
139
|
self.hold_time = self.component.config.get('hold_time', 0) * 10
|
|
@@ -166,6 +167,16 @@ class PresenceLighting(Script):
|
|
|
166
167
|
self.condition_comps[comp.id] = comp
|
|
167
168
|
|
|
168
169
|
while True:
|
|
170
|
+
# Resend expected values if they have failed to reach
|
|
171
|
+
# corresponding light
|
|
172
|
+
for c_id, [timestamp, expected_val] in self.expected_light_values.items():
|
|
173
|
+
if time.time() - timestamp < 5:
|
|
174
|
+
continue
|
|
175
|
+
comp = Component.objects.filter(id=c_id).first()
|
|
176
|
+
if not comp:
|
|
177
|
+
continue
|
|
178
|
+
print(f"Resending [{expected_val}] to {comp}")
|
|
179
|
+
comp.send(expected_val)
|
|
169
180
|
self._regulate()
|
|
170
181
|
time.sleep(random.randint(5, 15))
|
|
171
182
|
|
|
@@ -182,8 +193,10 @@ class PresenceLighting(Script):
|
|
|
182
193
|
self._regulate(on_condition_change=True)
|
|
183
194
|
|
|
184
195
|
def _on_light_change(self, light):
|
|
196
|
+
# If we were expecting some value change from the light
|
|
197
|
+
# We have received something. So we stop demanding it!
|
|
198
|
+
self.expected_light_values.pop(light.id, None)
|
|
185
199
|
# change original value if it has been changed to something different
|
|
186
|
-
# than this script does.
|
|
187
200
|
if self.is_on and light.value != self.light_send_values[light.id]:
|
|
188
201
|
self.light_send_values[light.id] = light.value
|
|
189
202
|
self.light_org_values[light.id] = light.value
|
|
@@ -198,6 +211,9 @@ class PresenceLighting(Script):
|
|
|
198
211
|
if must_on and on_sensor:
|
|
199
212
|
print("Presence detected!")
|
|
200
213
|
|
|
214
|
+
if must_on:
|
|
215
|
+
self.last_presence = 0
|
|
216
|
+
|
|
201
217
|
additional_conditions_met = True
|
|
202
218
|
for condition in self.conditions:
|
|
203
219
|
|
|
@@ -226,7 +242,6 @@ class PresenceLighting(Script):
|
|
|
226
242
|
additional_conditions_met = False
|
|
227
243
|
break
|
|
228
244
|
|
|
229
|
-
|
|
230
245
|
if not self.is_on:
|
|
231
246
|
if not must_on:
|
|
232
247
|
return
|
|
@@ -250,6 +265,8 @@ class PresenceLighting(Script):
|
|
|
250
265
|
on_val = bool(on_val)
|
|
251
266
|
print(f"Send {on_val} to {comp}!")
|
|
252
267
|
self.light_send_values[comp.id] = on_val
|
|
268
|
+
if comp.value != on_val:
|
|
269
|
+
self.expected_light_values[comp.id] = [time.time(), on_val]
|
|
253
270
|
comp.controller.send(on_val)
|
|
254
271
|
return
|
|
255
272
|
|
|
@@ -285,6 +302,8 @@ class PresenceLighting(Script):
|
|
|
285
302
|
else:
|
|
286
303
|
off_val = self.light_org_values.get(comp.id, 0)
|
|
287
304
|
print(f"Send {off_val} to {comp}!")
|
|
305
|
+
if comp.value != off_val:
|
|
306
|
+
self.expected_light_values[comp.id] = [time.time(), off_val]
|
|
288
307
|
comp.send(off_val)
|
|
289
308
|
|
|
290
309
|
|
simo/automation/gateways.py
CHANGED
|
@@ -17,8 +17,11 @@ from simo.core.middleware import introduce_instance, drop_current_instance
|
|
|
17
17
|
from simo.core.gateways import BaseObjectCommandsGatewayHandler
|
|
18
18
|
from simo.core.forms import BaseGatewayForm
|
|
19
19
|
from simo.core.utils.logs import StreamToLogger
|
|
20
|
+
from simo.core.utils.converters import input_to_meters
|
|
20
21
|
from simo.core.events import GatewayObjectCommand, get_event_obj
|
|
21
22
|
from simo.core.loggers import get_gw_logger, get_component_logger
|
|
23
|
+
from simo.users.models import InstanceUser
|
|
24
|
+
from .helpers import haversine_distance
|
|
22
25
|
|
|
23
26
|
|
|
24
27
|
class ScriptRunHandler(multiprocessing.Process):
|
|
@@ -77,7 +80,132 @@ class ScriptRunHandler(multiprocessing.Process):
|
|
|
77
80
|
return
|
|
78
81
|
|
|
79
82
|
|
|
80
|
-
class
|
|
83
|
+
class GatesHandler:
|
|
84
|
+
'''
|
|
85
|
+
Handles automatic gates openning
|
|
86
|
+
'''
|
|
87
|
+
# users are considered out of gate geofence, when they
|
|
88
|
+
# go out at least this amount of meters away from the gate
|
|
89
|
+
GEOFENCE_CROSS_ZONE = 200
|
|
90
|
+
|
|
91
|
+
def __init__(self, *args, **kwargs):
|
|
92
|
+
super().__init__(*args, **kwargs)
|
|
93
|
+
self.gate_iusers = {}
|
|
94
|
+
|
|
95
|
+
def _is_out_of_geofence(self, gate, location):
|
|
96
|
+
'''
|
|
97
|
+
Returns True if given location is out of geofencing zone
|
|
98
|
+
'''
|
|
99
|
+
|
|
100
|
+
auto_open_distance = gate.config.get('auto_open_distance')
|
|
101
|
+
if not auto_open_distance:
|
|
102
|
+
return False
|
|
103
|
+
auto_open_distance = input_to_meters(auto_open_distance)
|
|
104
|
+
|
|
105
|
+
gate_location = gate.config.get('location')
|
|
106
|
+
try:
|
|
107
|
+
distance_meters = haversine_distance(
|
|
108
|
+
gate_location,
|
|
109
|
+
location, units_of_measure='metric'
|
|
110
|
+
)
|
|
111
|
+
except:
|
|
112
|
+
gate_location = gate.zone.instance.location
|
|
113
|
+
try:
|
|
114
|
+
distance_meters = haversine_distance(
|
|
115
|
+
gate_location,
|
|
116
|
+
location, units_of_measure='metric'
|
|
117
|
+
)
|
|
118
|
+
except:
|
|
119
|
+
print(f"Bad location of {gate}!")
|
|
120
|
+
return False
|
|
121
|
+
print(f"Distance from {gate} : {distance_meters}m")
|
|
122
|
+
return distance_meters > (
|
|
123
|
+
auto_open_distance + self.GEOFENCE_CROSS_ZONE
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
def _is_in_geofence(self, gate, location):
|
|
127
|
+
'''
|
|
128
|
+
Returns True if given location is within geofencing zone
|
|
129
|
+
'''
|
|
130
|
+
auto_open_distance = gate.config.get('auto_open_distance')
|
|
131
|
+
if not auto_open_distance:
|
|
132
|
+
return False
|
|
133
|
+
auto_open_distance = input_to_meters(auto_open_distance)
|
|
134
|
+
gate_location = gate.config.get('location')
|
|
135
|
+
try:
|
|
136
|
+
distance_meters = haversine_distance(
|
|
137
|
+
gate_location,
|
|
138
|
+
location, units_of_measure='metric'
|
|
139
|
+
)
|
|
140
|
+
except:
|
|
141
|
+
gate_location = gate.zone.instance.location
|
|
142
|
+
try:
|
|
143
|
+
distance_meters = haversine_distance(
|
|
144
|
+
gate_location,
|
|
145
|
+
location, units_of_measure='metric'
|
|
146
|
+
)
|
|
147
|
+
except:
|
|
148
|
+
print(f"Bad location of {gate}!")
|
|
149
|
+
return False
|
|
150
|
+
|
|
151
|
+
print(f"Distance from {gate} : {distance_meters}m")
|
|
152
|
+
return distance_meters <= auto_open_distance
|
|
153
|
+
|
|
154
|
+
def check_gates(self, iuser):
|
|
155
|
+
if not iuser.last_seen_location:
|
|
156
|
+
print("User's last seen location is unknown")
|
|
157
|
+
return
|
|
158
|
+
for gate_id, geofence_data in self.gate_iusers.items():
|
|
159
|
+
for iu_id, is_out in geofence_data.items():
|
|
160
|
+
if iu_id != iuser.id:
|
|
161
|
+
continue
|
|
162
|
+
gate = Component.objects.get(id=gate_id)
|
|
163
|
+
if is_out:
|
|
164
|
+
print(
|
|
165
|
+
f"{iuser.user.name} is out, "
|
|
166
|
+
f"let's see if we must open the gates for him"
|
|
167
|
+
)
|
|
168
|
+
# user was fully out, we must check if
|
|
169
|
+
# he is now coming back and open the gate for him
|
|
170
|
+
if self._is_in_geofence(gate, iuser.last_seen_location):
|
|
171
|
+
print("Yes he is back in a geofence! Open THE GATEEE!!")
|
|
172
|
+
self.gate_iusers[gate_id][iuser.id] = False
|
|
173
|
+
gate.open()
|
|
174
|
+
else:
|
|
175
|
+
print("No he is not back yet.")
|
|
176
|
+
else:
|
|
177
|
+
print(f"Check if {iuser.user.name} is out.")
|
|
178
|
+
self.gate_iusers[gate_id][iuser.id] = self._is_out_of_geofence(
|
|
179
|
+
gate, iuser.last_seen_location
|
|
180
|
+
)
|
|
181
|
+
if self.gate_iusers[gate_id][iuser.id]:
|
|
182
|
+
print(f"YES {iuser.user.name} is out!")
|
|
183
|
+
|
|
184
|
+
def watch_gates(self):
|
|
185
|
+
drop_current_instance()
|
|
186
|
+
for gate in Component.objects.filter(base_type='gate').select_related(
|
|
187
|
+
'zone', 'zone__instance'
|
|
188
|
+
):
|
|
189
|
+
if not gate.config.get('auto_open_distance'):
|
|
190
|
+
continue
|
|
191
|
+
# Track new users as they appear in the system
|
|
192
|
+
for iuser in InstanceUser.objects.filter(
|
|
193
|
+
is_active=True, instance=gate.zone.instance
|
|
194
|
+
):
|
|
195
|
+
if gate.config.get('auto_open_for'):
|
|
196
|
+
if iuser.role.id not in gate.config['auto_open_for']:
|
|
197
|
+
continue
|
|
198
|
+
if gate.id not in self.gate_iusers:
|
|
199
|
+
self.gate_iusers[gate.id] = {}
|
|
200
|
+
if iuser.id not in self.gate_iusers[gate.id]:
|
|
201
|
+
if iuser.last_seen_location:
|
|
202
|
+
self.gate_iusers[gate.id][iuser.id] = self._is_out_of_geofence(
|
|
203
|
+
gate, iuser.last_seen_location
|
|
204
|
+
)
|
|
205
|
+
iuser.on_change(self.check_gates)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class AutomationsGatewayHandler(GatesHandler, BaseObjectCommandsGatewayHandler):
|
|
81
209
|
name = "Automation"
|
|
82
210
|
config_form = BaseGatewayForm
|
|
83
211
|
info = "Provides various types of automation capabilities"
|
|
@@ -85,6 +213,7 @@ class AutomationsGatewayHandler(BaseObjectCommandsGatewayHandler):
|
|
|
85
213
|
running_scripts = {}
|
|
86
214
|
periodic_tasks = (
|
|
87
215
|
('watch_scripts', 10),
|
|
216
|
+
('watch_gates', 60)
|
|
88
217
|
)
|
|
89
218
|
|
|
90
219
|
terminating_scripts = set()
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
simo/core/api.py
CHANGED
|
@@ -734,11 +734,12 @@ class ControllerTypes(InstanceMixin, viewsets.GenericViewSet):
|
|
|
734
734
|
for uid, cls in get_controller_types_map(user=request.user).items():
|
|
735
735
|
if cls.gateway_class.name not in data:
|
|
736
736
|
data[cls.gateway_class.name] = []
|
|
737
|
+
if not cls.manual_add:
|
|
738
|
+
continue
|
|
737
739
|
data[cls.gateway_class.name].append({
|
|
738
740
|
'uid': uid,
|
|
739
741
|
'name': cls.name,
|
|
740
742
|
'is_discoverable': cls.is_discoverable,
|
|
741
|
-
'manual_add': cls.manual_add,
|
|
742
743
|
'discovery_msg': cls.discovery_msg,
|
|
743
744
|
'info': cls.info(cls)
|
|
744
745
|
})
|
|
@@ -767,6 +768,8 @@ class GWControllerTypes(InstanceMixin, viewsets.GenericViewSet):
|
|
|
767
768
|
'info': cls.gateway_class.info,
|
|
768
769
|
'controllers': []
|
|
769
770
|
}
|
|
771
|
+
if not cls.manual_add:
|
|
772
|
+
continue
|
|
770
773
|
data[cls.gateway_class.uid]['controllers'].append({
|
|
771
774
|
'uid': uid,
|
|
772
775
|
'name': cls.name,
|
simo/core/api_meta.py
CHANGED
|
@@ -8,7 +8,7 @@ from simo.core.models import Icon, Instance, Category, Zone
|
|
|
8
8
|
from simo.core.middleware import introduce_instance
|
|
9
9
|
from .serializers import (
|
|
10
10
|
HiddenSerializerField, ComponentManyToManyRelatedField,
|
|
11
|
-
TextAreaSerializerField, Component
|
|
11
|
+
TextAreaSerializerField, Component, LocationSerializer
|
|
12
12
|
)
|
|
13
13
|
|
|
14
14
|
|
|
@@ -41,6 +41,7 @@ class SIMOAPIMetadata(SimpleMetadata):
|
|
|
41
41
|
ComponentManyToManyRelatedField: 'many related objects',
|
|
42
42
|
HiddenSerializerField: 'hidden',
|
|
43
43
|
TextAreaSerializerField: 'textarea',
|
|
44
|
+
LocationSerializer: 'location',
|
|
44
45
|
})
|
|
45
46
|
|
|
46
47
|
def determine_metadata(self, request, view):
|
|
@@ -84,7 +85,7 @@ class SIMOAPIMetadata(SimpleMetadata):
|
|
|
84
85
|
'read_only', 'label', 'help_text',
|
|
85
86
|
'min_length', 'max_length',
|
|
86
87
|
'min_value', 'max_value',
|
|
87
|
-
'initial'
|
|
88
|
+
'initial',
|
|
88
89
|
]
|
|
89
90
|
|
|
90
91
|
for attr in attrs:
|
|
@@ -115,6 +116,9 @@ class SIMOAPIMetadata(SimpleMetadata):
|
|
|
115
116
|
zone__instance=self.instance
|
|
116
117
|
)
|
|
117
118
|
|
|
119
|
+
if form_field and hasattr(form_field, 'zoom'):
|
|
120
|
+
field_info['zoom'] = form_field.zoom
|
|
121
|
+
|
|
118
122
|
if not field_info.get('read_only') and hasattr(field, 'choices'):# and not hasattr(form_field, 'forward'):
|
|
119
123
|
field_info['choices'] = [
|
|
120
124
|
{
|
simo/core/autocomplete_views.py
CHANGED
|
@@ -3,6 +3,7 @@ from dal.views import BaseQuerySetView
|
|
|
3
3
|
from django.db.models import Q
|
|
4
4
|
from django.template.loader import render_to_string
|
|
5
5
|
from simo.core.utils.helpers import search_queryset
|
|
6
|
+
from simo.core.middleware import get_current_instance
|
|
6
7
|
from .models import Icon, Category, Zone, Component
|
|
7
8
|
|
|
8
9
|
|
|
@@ -53,7 +54,7 @@ class IconModelAutocomplete(autocomplete.Select2QuerySetView):
|
|
|
53
54
|
class CategoryAutocomplete(autocomplete.Select2QuerySetView):
|
|
54
55
|
|
|
55
56
|
def get_queryset(self):
|
|
56
|
-
qs = Category.objects.
|
|
57
|
+
qs = Category.objects.filter(instance=get_current_instance(self.request))
|
|
57
58
|
|
|
58
59
|
if self.forwarded.get("id"):
|
|
59
60
|
return qs.filter(pk=self.forwarded.get("id"))
|
|
@@ -79,7 +80,7 @@ class CategoryAutocomplete(autocomplete.Select2QuerySetView):
|
|
|
79
80
|
class ZoneAutocomplete(autocomplete.Select2QuerySetView):
|
|
80
81
|
|
|
81
82
|
def get_queryset(self):
|
|
82
|
-
qs = Zone.objects.
|
|
83
|
+
qs = Zone.objects.filter(instance=get_current_instance(self.request))
|
|
83
84
|
|
|
84
85
|
if self.forwarded.get("id"):
|
|
85
86
|
return qs.filter(pk=self.forwarded.get("id"))
|
|
@@ -104,7 +105,7 @@ class ZoneAutocomplete(autocomplete.Select2QuerySetView):
|
|
|
104
105
|
class ComponentAutocomplete(autocomplete.Select2QuerySetView):
|
|
105
106
|
|
|
106
107
|
def get_queryset(self):
|
|
107
|
-
qs = Component.objects.
|
|
108
|
+
qs = Component.objects.filter(zone__instance=get_current_instance(self.request))
|
|
108
109
|
|
|
109
110
|
if self.forwarded.get("id"):
|
|
110
111
|
if isinstance(self.forwarded['id'], list):
|
simo/core/controllers.py
CHANGED
|
@@ -233,7 +233,12 @@ class ControllerBase(ABC):
|
|
|
233
233
|
return vals
|
|
234
234
|
|
|
235
235
|
def send(self, value):
|
|
236
|
-
|
|
236
|
+
from .models import Component
|
|
237
|
+
try:
|
|
238
|
+
self.component.refresh_from_db()
|
|
239
|
+
except Component.DoesNotExist:
|
|
240
|
+
return
|
|
241
|
+
|
|
237
242
|
|
|
238
243
|
# Bulk send if it is a switch or dimmer and has slaves
|
|
239
244
|
if self.component.base_type in ('switch', 'dimmer') \
|
|
@@ -241,7 +246,7 @@ class ControllerBase(ABC):
|
|
|
241
246
|
bulk_send_map = {self.component: value}
|
|
242
247
|
for slave in self.component.slaves.all():
|
|
243
248
|
bulk_send_map[slave] = value
|
|
244
|
-
|
|
249
|
+
|
|
245
250
|
Component.objects.bulk_send(bulk_send_map)
|
|
246
251
|
return
|
|
247
252
|
|
|
@@ -1027,7 +1032,10 @@ class Gate(ControllerBase, TimerMixin):
|
|
|
1027
1032
|
base_type = 'gate'
|
|
1028
1033
|
app_widget = GateWidget
|
|
1029
1034
|
admin_widget_template = 'admin/controller_widgets/gate.html'
|
|
1030
|
-
default_config = {
|
|
1035
|
+
default_config = {
|
|
1036
|
+
'auto_open_distance': '150 m',
|
|
1037
|
+
'auto_open_for': [],
|
|
1038
|
+
}
|
|
1031
1039
|
|
|
1032
1040
|
@property
|
|
1033
1041
|
def default_value(self):
|
|
@@ -1065,4 +1073,5 @@ class Gate(ControllerBase, TimerMixin):
|
|
|
1065
1073
|
self.send('close')
|
|
1066
1074
|
|
|
1067
1075
|
def call(self):
|
|
1068
|
-
self.send('call')
|
|
1076
|
+
self.send('call')
|
|
1077
|
+
|
simo/core/form_fields.py
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import copy
|
|
2
|
+
import json
|
|
3
|
+
import six
|
|
4
|
+
from django.template.loader import render_to_string
|
|
5
|
+
from django.utils.safestring import mark_safe
|
|
2
6
|
from django import forms
|
|
7
|
+
from django.conf import settings
|
|
3
8
|
from dal import autocomplete
|
|
4
9
|
|
|
5
10
|
|
|
@@ -91,4 +96,90 @@ class Select2ModelMultipleChoiceField(
|
|
|
91
96
|
class Select2ListMultipleChoiceField(
|
|
92
97
|
Select2MultipleMixin, forms.MultipleChoiceField
|
|
93
98
|
):
|
|
94
|
-
pass
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class LocationWidget(forms.widgets.TextInput):
|
|
103
|
+
def __init__(self, **kwargs):
|
|
104
|
+
attrs = kwargs.pop('attrs', None)
|
|
105
|
+
|
|
106
|
+
self.options = dict(settings.LOCATION_FIELD)
|
|
107
|
+
self.options['map.zoom'] = kwargs.get('zoom')
|
|
108
|
+
self.options['field_options'] = {
|
|
109
|
+
'based_fields': kwargs.pop('based_fields')
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
super(LocationWidget, self).__init__(attrs)
|
|
113
|
+
|
|
114
|
+
def render(self, name, value, attrs=None, renderer=None):
|
|
115
|
+
if value is not None:
|
|
116
|
+
try:
|
|
117
|
+
if isinstance(value, six.string_types):
|
|
118
|
+
lat, lng = value.split(',')
|
|
119
|
+
else:
|
|
120
|
+
lng = value.x
|
|
121
|
+
lat = value.y
|
|
122
|
+
|
|
123
|
+
value = '%s,%s' % (
|
|
124
|
+
float(lat),
|
|
125
|
+
float(lng),
|
|
126
|
+
)
|
|
127
|
+
except ValueError:
|
|
128
|
+
value = ''
|
|
129
|
+
else:
|
|
130
|
+
value = ''
|
|
131
|
+
|
|
132
|
+
if '-' not in name:
|
|
133
|
+
prefix = ''
|
|
134
|
+
else:
|
|
135
|
+
prefix = name[:name.rindex('-') + 1]
|
|
136
|
+
|
|
137
|
+
self.options['field_options']['prefix'] = prefix
|
|
138
|
+
|
|
139
|
+
attrs = attrs or {}
|
|
140
|
+
attrs['data-location-field-options'] = json.dumps(self.options)
|
|
141
|
+
|
|
142
|
+
# Django added renderer parameter in 1.11, made it mandatory in 2.1
|
|
143
|
+
kwargs = {}
|
|
144
|
+
if renderer is not None:
|
|
145
|
+
kwargs['renderer'] = renderer
|
|
146
|
+
text_input = super(LocationWidget, self).render(name, value, attrs=attrs, **kwargs)
|
|
147
|
+
|
|
148
|
+
return render_to_string('location_field/map_widget.html', {
|
|
149
|
+
'field_name': name,
|
|
150
|
+
'field_input': mark_safe(text_input)
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def media(self):
|
|
155
|
+
return forms.Media(**settings.LOCATION_FIELD['resources.media'])
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class PlainLocationField(forms.fields.CharField):
|
|
160
|
+
|
|
161
|
+
zoom = 13
|
|
162
|
+
|
|
163
|
+
def __init__(self, based_fields=None, zoom=None, suffix='', *args, **kwargs):
|
|
164
|
+
self.zoom = zoom
|
|
165
|
+
if not based_fields:
|
|
166
|
+
based_fields = []
|
|
167
|
+
if not self.zoom:
|
|
168
|
+
self.zoom = settings.LOCATION_FIELD['map.zoom']
|
|
169
|
+
self.widget = LocationWidget(based_fields=based_fields, zoom=self.zoom,
|
|
170
|
+
suffix=suffix, **kwargs)
|
|
171
|
+
|
|
172
|
+
dwargs = {
|
|
173
|
+
'required': True,
|
|
174
|
+
'label': None,
|
|
175
|
+
'initial': None,
|
|
176
|
+
'help_text': None,
|
|
177
|
+
'error_messages': None,
|
|
178
|
+
'show_hidden_initial': False,
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
for attr in dwargs:
|
|
182
|
+
if attr in kwargs:
|
|
183
|
+
dwargs[attr] = kwargs[attr]
|
|
184
|
+
|
|
185
|
+
super(PlainLocationField, self).__init__(*args, **dwargs)
|
simo/core/forms.py
CHANGED
|
@@ -79,10 +79,11 @@ class ConfigFieldsMixin:
|
|
|
79
79
|
if field_name in self.model_fields:
|
|
80
80
|
continue
|
|
81
81
|
self.config_fields.append(field_name)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
82
|
+
|
|
83
|
+
for field_name in self.config_fields:
|
|
84
|
+
if field_name not in self.instance.config:
|
|
85
|
+
continue
|
|
86
|
+
if self.instance.pk:
|
|
86
87
|
if hasattr(self.fields[field_name], 'queryset'):
|
|
87
88
|
if isinstance(self.instance.config.get(field_name), list):
|
|
88
89
|
self.fields[field_name].initial = \
|
|
@@ -97,6 +98,11 @@ class ConfigFieldsMixin:
|
|
|
97
98
|
else:
|
|
98
99
|
self.fields[field_name].initial = \
|
|
99
100
|
self.instance.config.get(field_name)
|
|
101
|
+
else:
|
|
102
|
+
if self.instance.config.get(field_name):
|
|
103
|
+
self.fields[field_name].initial = self.instance.config.get(field_name)
|
|
104
|
+
|
|
105
|
+
|
|
100
106
|
|
|
101
107
|
def save(self, commit=True):
|
|
102
108
|
for field_name in self.config_fields:
|
simo/core/gateways.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import threading
|
|
2
2
|
import time
|
|
3
3
|
import json
|
|
4
|
+
import random
|
|
4
5
|
import paho.mqtt.client as mqtt
|
|
5
6
|
from django.conf import settings
|
|
6
7
|
from django.template.loader import render_to_string
|
|
@@ -78,13 +79,22 @@ class BaseObjectCommandsGatewayHandler(BaseGatewayHandler):
|
|
|
78
79
|
self.mqtt_client.loop_stop()
|
|
79
80
|
|
|
80
81
|
def _run_periodic_task(self, exit, task, period):
|
|
82
|
+
first_run = True
|
|
81
83
|
while not exit.is_set():
|
|
82
84
|
try:
|
|
83
85
|
#print(f"Run periodic task {task}!")
|
|
84
86
|
getattr(self, task)()
|
|
85
87
|
except Exception as e:
|
|
86
88
|
self.logger.error(e, exc_info=True)
|
|
87
|
-
|
|
89
|
+
# spread tasks around so that they do not happen all
|
|
90
|
+
# at once all the time
|
|
91
|
+
if first_run:
|
|
92
|
+
first_run = False
|
|
93
|
+
randomized_sleep = random.randint(0, period) + random.random()
|
|
94
|
+
time.sleep(randomized_sleep)
|
|
95
|
+
else:
|
|
96
|
+
time.sleep(period)
|
|
97
|
+
|
|
88
98
|
|
|
89
99
|
def _on_mqtt_connect(self, mqtt_client, userdata, flags, rc):
|
|
90
100
|
print("MQTT Connected!")
|
simo/core/serializers.py
CHANGED
|
@@ -15,7 +15,8 @@ from actstream.models import Action
|
|
|
15
15
|
from simo.core.forms import HiddenField, FormsetField
|
|
16
16
|
from simo.core.form_fields import (
|
|
17
17
|
Select2ListChoiceField, Select2ModelChoiceField,
|
|
18
|
-
Select2ListMultipleChoiceField, Select2ModelMultipleChoiceField
|
|
18
|
+
Select2ListMultipleChoiceField, Select2ModelMultipleChoiceField,
|
|
19
|
+
PlainLocationField
|
|
19
20
|
)
|
|
20
21
|
from simo.core.models import Component
|
|
21
22
|
from rest_framework.relations import PrimaryKeyRelatedField, ManyRelatedField
|
|
@@ -28,6 +29,10 @@ from .forms import ComponentAdminForm
|
|
|
28
29
|
from .models import Category, Zone, Icon, ComponentHistory
|
|
29
30
|
|
|
30
31
|
|
|
32
|
+
class LocationSerializer(serializers.CharField):
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
31
36
|
|
|
32
37
|
class TimestampField(serializers.Field):
|
|
33
38
|
|
|
@@ -139,6 +144,7 @@ class ComponentFormsetField(FormSerializer):
|
|
|
139
144
|
form = forms.Form
|
|
140
145
|
field_mapping = {
|
|
141
146
|
HiddenField: HiddenSerializerField,
|
|
147
|
+
PlainLocationField: LocationSerializer,
|
|
142
148
|
Select2ListChoiceField: serializers.ChoiceField,
|
|
143
149
|
forms.ModelChoiceField: FormsetPrimaryKeyRelatedField,
|
|
144
150
|
Select2ModelChoiceField: FormsetPrimaryKeyRelatedField,
|
|
@@ -293,6 +299,7 @@ class ComponentSerializer(FormSerializer):
|
|
|
293
299
|
forms.TypedChoiceField: serializers.ChoiceField,
|
|
294
300
|
forms.FloatField: serializers.FloatField,
|
|
295
301
|
forms.SlugField: serializers.CharField,
|
|
302
|
+
PlainLocationField: LocationSerializer,
|
|
296
303
|
forms.ModelChoiceField: ComponentPrimaryKeyRelatedField,
|
|
297
304
|
Select2ModelChoiceField: ComponentPrimaryKeyRelatedField,
|
|
298
305
|
forms.ModelMultipleChoiceField: ComponentManyToManyRelatedField,
|