simo 2.6.9__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 +18 -1
- simo/automation/gateways.py +130 -1
- 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_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__/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/forms.py +52 -4
- 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/views.py +20 -1
- {simo-2.6.9.dist-info → simo-2.7.1.dist-info}/METADATA +1 -1
- {simo-2.6.9.dist-info → simo-2.7.1.dist-info}/RECORD +51 -49
- {simo-2.6.9.dist-info → simo-2.7.1.dist-info}/LICENSE.md +0 -0
- {simo-2.6.9.dist-info → simo-2.7.1.dist-info}/WHEEL +0 -0
- {simo-2.6.9.dist-info → simo-2.7.1.dist-info}/entry_points.txt +0 -0
- {simo-2.6.9.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
|
|
@@ -252,6 +265,8 @@ class PresenceLighting(Script):
|
|
|
252
265
|
on_val = bool(on_val)
|
|
253
266
|
print(f"Send {on_val} to {comp}!")
|
|
254
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]
|
|
255
270
|
comp.controller.send(on_val)
|
|
256
271
|
return
|
|
257
272
|
|
|
@@ -287,6 +302,8 @@ class PresenceLighting(Script):
|
|
|
287
302
|
else:
|
|
288
303
|
off_val = self.light_org_values.get(comp.id, 0)
|
|
289
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]
|
|
290
307
|
comp.send(off_val)
|
|
291
308
|
|
|
292
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
|
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,
|
simo/core/signal_receivers.py
CHANGED
|
@@ -81,100 +81,24 @@ def create_instance_defaults(sender, instance, created, **kwargs):
|
|
|
81
81
|
)
|
|
82
82
|
weather_icon = Icon.objects.get(slug='cloud-bolt-sun')
|
|
83
83
|
|
|
84
|
+
from simo.generic.controllers import Weather, MainState
|
|
84
85
|
Component.objects.create(
|
|
85
86
|
name='Weather', icon=weather_icon,
|
|
86
87
|
zone=other_zone,
|
|
87
88
|
category=climate_category,
|
|
88
89
|
gateway=generic, base_type='weather',
|
|
89
|
-
controller_uid=
|
|
90
|
+
controller_uid=Weather.uid,
|
|
90
91
|
config={'is_main': True}
|
|
91
92
|
)
|
|
92
93
|
|
|
93
|
-
|
|
94
|
-
name='State', icon=Icon.objects.get(slug='home'),
|
|
94
|
+
Component.objects.create(
|
|
95
|
+
name='Main State', icon=Icon.objects.get(slug='home'),
|
|
95
96
|
zone=other_zone,
|
|
96
97
|
category=other_category,
|
|
97
|
-
gateway=generic, base_type=
|
|
98
|
-
controller_uid=
|
|
98
|
+
gateway=generic, base_type=MainState.base_type,
|
|
99
|
+
controller_uid=MainState.uid,
|
|
99
100
|
value='day',
|
|
100
|
-
config=
|
|
101
|
-
{
|
|
102
|
-
"icon": "sunrise", "name": "Morning", "slug": "morning",
|
|
103
|
-
'help_text': "6:00 AM to sunrise. Activates only in dark time of a year."
|
|
104
|
-
},
|
|
105
|
-
{
|
|
106
|
-
"icon": "house-day", "name": "Day", "slug": "day",
|
|
107
|
-
'help_text': "From sunrise to sunset."
|
|
108
|
-
},
|
|
109
|
-
{
|
|
110
|
-
"icon": "house-night", "name": "Evening", "slug": "evening",
|
|
111
|
-
'help_text': "From sunrise to midnight"
|
|
112
|
-
},
|
|
113
|
-
{
|
|
114
|
-
"icon": "moon-cloud", "name": "Night", "slug": "night",
|
|
115
|
-
'help_text': "From midnight to sunrise or 6:00 AM."
|
|
116
|
-
},
|
|
117
|
-
{"icon": "snooze", "name": "Sleep time", "slug": "sleep"},
|
|
118
|
-
{"icon": "house-person-leave", "name": "Away", "slug": "away"},
|
|
119
|
-
{"icon": "island-tropical", "name": "Vacation", "slug": "vacation"}
|
|
120
|
-
], "is_main": True}
|
|
121
|
-
)
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
auto_state_code = render_to_string(
|
|
125
|
-
'automations/auto_state_script.py', {'state_comp_id': state_comp.id}
|
|
126
|
-
)
|
|
127
|
-
Component.objects.create(
|
|
128
|
-
name='Auto state', icon=Icon.objects.get(slug='bolt'),
|
|
129
|
-
zone=other_zone,
|
|
130
|
-
category=other_category, show_in_app=False,
|
|
131
|
-
gateway=automation, base_type='script',
|
|
132
|
-
controller_uid='simo.automation.controllers.Script',
|
|
133
|
-
config={
|
|
134
|
-
"code": auto_state_code, 'autostart': True, 'keep_alive': True,
|
|
135
|
-
"notes": f"""
|
|
136
|
-
The script automatically controls the states of the "State" component (ID:{state_comp.id}) — 'morning', 'day', 'evening', 'night'.
|
|
137
|
-
|
|
138
|
-
"""
|
|
139
|
-
}
|
|
140
|
-
)
|
|
141
|
-
|
|
142
|
-
code = render_to_string(
|
|
143
|
-
'automations/phones_sleep_script.py', {'state_comp_id': state_comp.id}
|
|
144
|
-
)
|
|
145
|
-
Component.objects.create(
|
|
146
|
-
name='Sleep mode when owner phones are charge',
|
|
147
|
-
icon=Icon.objects.get(slug='bolt'), zone=other_zone,
|
|
148
|
-
category=other_category, show_in_app=False,
|
|
149
|
-
gateway=automation, base_type='script',
|
|
150
|
-
controller_uid='simo.automation.controllers.Script',
|
|
151
|
-
config={
|
|
152
|
-
"code": code, 'autostart': True, 'keep_alive': True,
|
|
153
|
-
"notes": f"""
|
|
154
|
-
Automatically sets State component (ID: {state_comp.id}) to "Sleep" if it is later than 10pm and all home owners phones who are at home are put on charge.
|
|
155
|
-
Sets State component back to regular state as soon as none of the home owners phones are on charge and it is 6am or later.
|
|
156
|
-
|
|
157
|
-
"""
|
|
158
|
-
}
|
|
159
|
-
)
|
|
160
|
-
|
|
161
|
-
code = render_to_string(
|
|
162
|
-
'automations/auto_away.py', {'state_comp_id': state_comp.id}
|
|
163
|
-
)
|
|
164
|
-
Component.objects.create(
|
|
165
|
-
name='Auto Away State',
|
|
166
|
-
icon=Icon.objects.get(slug='bolt'), zone=other_zone,
|
|
167
|
-
category=other_category, show_in_app=False,
|
|
168
|
-
gateway=automation, base_type='script',
|
|
169
|
-
controller_uid='simo.automation.controllers.Script',
|
|
170
|
-
config={
|
|
171
|
-
"code": code, 'autostart': True, 'keep_alive': True,
|
|
172
|
-
"notes": f"""
|
|
173
|
-
Automatically set mode to "Away" there are no users at home and there was no motion for more than 30 seconds.
|
|
174
|
-
Set it back to a regular mode as soon as somebody comes back home or motion is detected.
|
|
175
|
-
|
|
176
|
-
"""
|
|
177
|
-
}
|
|
101
|
+
config=MainState.default_config
|
|
178
102
|
)
|
|
179
103
|
|
|
180
104
|
# Create default User permission roles
|
|
Binary file
|