simo 2.9.1__py3-none-any.whl → 2.10.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/__pycache__/gateways.cpython-312.pyc +0 -0
- simo/automation/gateways.py +12 -10
- simo/core/__pycache__/controllers.cpython-312.pyc +0 -0
- simo/core/__pycache__/tasks.cpython-312.pyc +0 -0
- simo/core/controllers.py +41 -2
- simo/core/tasks.py +8 -1
- simo/generic/__pycache__/controllers.cpython-312.pyc +0 -0
- simo/generic/__pycache__/forms.cpython-312.pyc +0 -0
- simo/generic/__pycache__/gateways.cpython-312.pyc +0 -0
- simo/generic/controllers.py +99 -43
- simo/generic/forms.py +13 -10
- simo/generic/gateways.py +91 -2
- simo/generic/migrations/0003_auto_20250409_1404.py +33 -0
- simo/generic/migrations/__pycache__/0003_auto_20250409_1404.cpython-312.pyc +0 -0
- simo/users/__pycache__/api.cpython-312.pyc +0 -0
- simo/users/__pycache__/dynamic_settings.cpython-312.pyc +0 -0
- simo/users/api.py +71 -18
- simo/users/dynamic_settings.py +1 -1
- {simo-2.9.1.dist-info → simo-2.10.1.dist-info}/METADATA +1 -1
- {simo-2.9.1.dist-info → simo-2.10.1.dist-info}/RECORD +24 -22
- {simo-2.9.1.dist-info → simo-2.10.1.dist-info}/WHEEL +0 -0
- {simo-2.9.1.dist-info → simo-2.10.1.dist-info}/entry_points.txt +0 -0
- {simo-2.9.1.dist-info → simo-2.10.1.dist-info}/licenses/LICENSE.md +0 -0
- {simo-2.9.1.dist-info → simo-2.10.1.dist-info}/top_level.txt +0 -0
|
Binary file
|
simo/automation/gateways.py
CHANGED
|
@@ -165,7 +165,7 @@ class GatesHandler:
|
|
|
165
165
|
if iu_id != iuser.id:
|
|
166
166
|
continue
|
|
167
167
|
gate = Component.objects.get(id=gate_id)
|
|
168
|
-
if is_out:
|
|
168
|
+
if is_out > 4:
|
|
169
169
|
print(
|
|
170
170
|
f"{iuser.user.name} is out, "
|
|
171
171
|
f"let's see if we must open the gates for him"
|
|
@@ -174,17 +174,17 @@ class GatesHandler:
|
|
|
174
174
|
# he is now coming back and open the gate for him
|
|
175
175
|
if self._is_in_geofence(gate, iuser.last_seen_location):
|
|
176
176
|
print("Yes he is back in a geofence! Open THE GATEEE!!")
|
|
177
|
-
self.gate_iusers[gate_id][iuser.id] =
|
|
178
|
-
|
|
177
|
+
self.gate_iusers[gate_id][iuser.id] = 0
|
|
178
|
+
if iuser.last_seen_speed_kmh > 10:
|
|
179
|
+
gate.open()
|
|
179
180
|
else:
|
|
180
181
|
print("No he is not back yet.")
|
|
181
182
|
else:
|
|
182
183
|
print(f"Check if {iuser.user.name} is out.")
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
print(f"YES {iuser.user.name} is out!")
|
|
184
|
+
if self._is_out_of_geofence(gate, iuser.last_seen_location):
|
|
185
|
+
self.gate_iusers[gate_id][iuser.id] += 1
|
|
186
|
+
if self.gate_iusers[gate_id][iuser.id] > 4:
|
|
187
|
+
print(f"YES {iuser.user.name} is truly out!")
|
|
188
188
|
|
|
189
189
|
def watch_gates(self):
|
|
190
190
|
drop_current_instance()
|
|
@@ -204,9 +204,11 @@ class GatesHandler:
|
|
|
204
204
|
self.gate_iusers[gate.id] = {}
|
|
205
205
|
if iuser.id not in self.gate_iusers[gate.id]:
|
|
206
206
|
if iuser.last_seen_location:
|
|
207
|
-
self.gate_iusers[gate.id][iuser.id] =
|
|
207
|
+
self.gate_iusers[gate.id][iuser.id] = 0
|
|
208
|
+
if self._is_out_of_geofence(
|
|
208
209
|
gate, iuser.last_seen_location
|
|
209
|
-
)
|
|
210
|
+
):
|
|
211
|
+
self.gate_iusers[gate.id][iuser.id] += 1
|
|
210
212
|
iuser.on_change(self.check_gates)
|
|
211
213
|
|
|
212
214
|
|
|
Binary file
|
|
Binary file
|
simo/core/controllers.py
CHANGED
|
@@ -631,6 +631,15 @@ class Dimmer(ControllerBase, TimerMixin, OnOffPokerMixin):
|
|
|
631
631
|
else:
|
|
632
632
|
self.send(self.component.config.get('max', 90))
|
|
633
633
|
|
|
634
|
+
def max_out(self):
|
|
635
|
+
self.send(self.component.config.get('max', 90))
|
|
636
|
+
|
|
637
|
+
def output_percent(self, value):
|
|
638
|
+
min = self.component.config.get('min', 0)
|
|
639
|
+
max = self.component.config.get('max', 100)
|
|
640
|
+
delta = max - min
|
|
641
|
+
self.send(min + delta * value / 100)
|
|
642
|
+
|
|
634
643
|
def toggle(self):
|
|
635
644
|
self.component.refresh_from_db()
|
|
636
645
|
if self.component.value:
|
|
@@ -845,21 +854,51 @@ class Switch(MultiSwitchBase, TimerMixin, OnOffPokerMixin):
|
|
|
845
854
|
default_value = False
|
|
846
855
|
|
|
847
856
|
def turn_on(self):
|
|
857
|
+
if self.component.meta.get('pulse'):
|
|
858
|
+
self.component.meta.pop('pulse')
|
|
859
|
+
self.component.save()
|
|
848
860
|
self.send(True)
|
|
849
861
|
|
|
850
862
|
def turn_off(self):
|
|
863
|
+
if self.component.meta.get('pulse'):
|
|
864
|
+
self.component.meta.pop('pulse')
|
|
865
|
+
self.component.save()
|
|
851
866
|
self.send(False)
|
|
852
867
|
|
|
853
868
|
def toggle(self):
|
|
869
|
+
if self.component.meta.get('pulse'):
|
|
870
|
+
self.component.meta.pop('pulse')
|
|
871
|
+
self.component.save()
|
|
854
872
|
self.send(not self.component.value)
|
|
855
873
|
|
|
856
874
|
def click(self):
|
|
857
875
|
'''
|
|
858
876
|
Gateway specific implementation is very welcome of this!
|
|
859
877
|
'''
|
|
878
|
+
if self.component.meta.get('pulse'):
|
|
879
|
+
self.component.meta.pop('pulse')
|
|
880
|
+
self.component.save()
|
|
860
881
|
self.turn_on()
|
|
861
|
-
|
|
862
|
-
|
|
882
|
+
from .tasks import component_action
|
|
883
|
+
component_action.s(
|
|
884
|
+
self.component.id, 'turn_off'
|
|
885
|
+
).apply_async(countdown=1)
|
|
886
|
+
|
|
887
|
+
def pulse(self, frame_length_s, on_percentage):
|
|
888
|
+
self.component.meta['pulse'] = {
|
|
889
|
+
'frame': frame_length_s, 'duty': on_percentage / 100
|
|
890
|
+
}
|
|
891
|
+
self.component.save()
|
|
892
|
+
from simo.generic.gateways import GenericGatewayHandler
|
|
893
|
+
from .models import Gateway
|
|
894
|
+
generic_gateway = Gateway.objects.filter(
|
|
895
|
+
type=GenericGatewayHandler.uid
|
|
896
|
+
).first()
|
|
897
|
+
if generic_gateway:
|
|
898
|
+
GatewayObjectCommand(
|
|
899
|
+
generic_gateway, self.component,
|
|
900
|
+
pulse=self.component.meta['pulse']
|
|
901
|
+
).publish()
|
|
863
902
|
|
|
864
903
|
|
|
865
904
|
class DoubleSwitch(MultiSwitchBase):
|
simo/core/tasks.py
CHANGED
|
@@ -16,11 +16,18 @@ from django.utils import timezone
|
|
|
16
16
|
from actstream.models import Action
|
|
17
17
|
from simo.conf import dynamic_settings
|
|
18
18
|
from simo.core.utils.helpers import get_self_ip
|
|
19
|
-
from simo.core.middleware import introduce_instance
|
|
19
|
+
from simo.core.middleware import introduce_instance, drop_current_instance
|
|
20
20
|
from simo.users.models import PermissionsRole, InstanceUser
|
|
21
21
|
from .models import Instance, Component, ComponentHistory, HistoryAggregate
|
|
22
22
|
|
|
23
23
|
|
|
24
|
+
@celery_app.task
|
|
25
|
+
def component_action(comp_id, method, args=None, kwargs=None):
|
|
26
|
+
drop_current_instance()
|
|
27
|
+
component = Component.objects.get(id=comp_id)
|
|
28
|
+
getattr(component, method)(*args, **kwargs)
|
|
29
|
+
|
|
30
|
+
|
|
24
31
|
@celery_app.task
|
|
25
32
|
def supervisor_restart():
|
|
26
33
|
time.sleep(2)
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
simo/generic/controllers.py
CHANGED
|
@@ -69,11 +69,16 @@ class Thermostat(ControllerBase):
|
|
|
69
69
|
|
|
70
70
|
@property
|
|
71
71
|
def default_config(self):
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
instance = get_current_instance()
|
|
73
|
+
min = 4
|
|
74
|
+
max = 36
|
|
75
|
+
if instance and instance.units_of_measure == 'imperial':
|
|
76
|
+
min = 40
|
|
77
|
+
max = 95
|
|
74
78
|
return {
|
|
75
79
|
'temperature_sensor': 0, 'heater': 0, 'cooler': 0,
|
|
76
|
-
'
|
|
80
|
+
'engagement': 'dynamic','reaction_difference': 2,
|
|
81
|
+
'min': min, 'max': max,
|
|
77
82
|
'has_real_feel': False,
|
|
78
83
|
'user_config': config_to_dict(self._get_default_user_config())
|
|
79
84
|
}
|
|
@@ -161,12 +166,12 @@ class Thermostat(ControllerBase):
|
|
|
161
166
|
temperature_sensor = Component.objects.filter(
|
|
162
167
|
pk=self.component.config.get('temperature_sensor')
|
|
163
168
|
).first()
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
)
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
)
|
|
169
|
+
heaters = Component.objects.filter(
|
|
170
|
+
pk__in=self.component.config.get('heater')
|
|
171
|
+
)
|
|
172
|
+
coolers = Component.objects.filter(
|
|
173
|
+
pk__in=self.component.config.get('cooler')
|
|
174
|
+
)
|
|
170
175
|
|
|
171
176
|
if not temperature_sensor or not temperature_sensor.alive:
|
|
172
177
|
self.component.error_msg = "No temperature sensor"
|
|
@@ -187,50 +192,100 @@ class Thermostat(ControllerBase):
|
|
|
187
192
|
target_temp = self.get_current_target_temperature()
|
|
188
193
|
mode = self.component.config['user_config'].get('mode', 'auto')
|
|
189
194
|
|
|
195
|
+
low = target_temp - self.component.config['reaction_difference']
|
|
196
|
+
high = target_temp + self.component.config['reaction_difference']
|
|
197
|
+
|
|
198
|
+
heating = False
|
|
199
|
+
cooling = False
|
|
200
|
+
|
|
201
|
+
if self.component.config.get('engagement', 'static'):
|
|
202
|
+
if heaters:
|
|
203
|
+
for heater in heaters:
|
|
204
|
+
if current_temp < low:
|
|
205
|
+
if heater.base_type == 'dimmer':
|
|
206
|
+
heater.max_out()
|
|
207
|
+
else:
|
|
208
|
+
heater.turn_on()
|
|
209
|
+
heating = True
|
|
210
|
+
elif current_temp > high:
|
|
211
|
+
heater.turn_off()
|
|
212
|
+
heating = False
|
|
213
|
+
else:
|
|
214
|
+
if heater.value:
|
|
215
|
+
heating = True
|
|
216
|
+
break
|
|
217
|
+
|
|
218
|
+
if coolers:
|
|
219
|
+
for cooler in coolers:
|
|
220
|
+
if heating: # Do not cool if heating!
|
|
221
|
+
cooler.turn_off()
|
|
222
|
+
else:
|
|
223
|
+
if current_temp > high:
|
|
224
|
+
if heater.base_type == 'dimmer':
|
|
225
|
+
cooler.max_out()
|
|
226
|
+
else:
|
|
227
|
+
cooler.turn_on()
|
|
228
|
+
cooling = True
|
|
229
|
+
elif current_temp < low:
|
|
230
|
+
if cooler.value:
|
|
231
|
+
cooler.turn_off()
|
|
232
|
+
cooling = False
|
|
233
|
+
else:
|
|
234
|
+
if cooler.value:
|
|
235
|
+
cooling = True
|
|
236
|
+
break
|
|
237
|
+
|
|
238
|
+
else:
|
|
239
|
+
window = high - low
|
|
240
|
+
if heaters:
|
|
241
|
+
reach = high - current_temp
|
|
242
|
+
reaction_force = self._get_reaction_force(window, reach)
|
|
243
|
+
if reaction_force:
|
|
244
|
+
heating = True
|
|
245
|
+
self._engage_devices(heaters, reaction_force)
|
|
246
|
+
if coolers:
|
|
247
|
+
if heating: # Do not cool if heating!
|
|
248
|
+
reaction_force = 0
|
|
249
|
+
else:
|
|
250
|
+
reach = current_temp - low
|
|
251
|
+
reaction_force = self._get_reaction_force(window, reach)
|
|
252
|
+
self._engage_devices(coolers, reaction_force)
|
|
253
|
+
|
|
190
254
|
self.component.set({
|
|
191
255
|
'mode': mode,
|
|
192
256
|
'current_temp': current_temp,
|
|
193
257
|
'target_temp': target_temp,
|
|
194
|
-
'heating':
|
|
258
|
+
'heating': heating, 'cooling': cooling
|
|
195
259
|
}, actor=get_system_user())
|
|
196
260
|
|
|
197
|
-
low = target_temp - self.component.config['reaction_difference'] / 2
|
|
198
|
-
high = target_temp + self.component.config['reaction_difference'] / 2
|
|
199
|
-
|
|
200
|
-
if mode in ('auto', 'heater'):
|
|
201
|
-
if (not heater or not heater.alive) and mode == 'heater':
|
|
202
|
-
self.component.error_msg = "No heater"
|
|
203
|
-
self.component.alive = False
|
|
204
|
-
self.component.save()
|
|
205
|
-
return
|
|
206
|
-
if current_temp < low:
|
|
207
|
-
if not heater.value:
|
|
208
|
-
heater.turn_on()
|
|
209
|
-
self.component.value['heating'] = True
|
|
210
|
-
elif current_temp > high:
|
|
211
|
-
if heater.value:
|
|
212
|
-
heater.turn_off()
|
|
213
|
-
self.component.value['heating'] = False
|
|
214
|
-
if mode in ('auto', 'cooler') and cooler:
|
|
215
|
-
if not cooler or not cooler.alive:
|
|
216
|
-
if mode == 'cooler' or (not heater or not heater.alive):
|
|
217
|
-
print(f"No cooler or heater on {self.component}!")
|
|
218
|
-
self.component.alive = False
|
|
219
|
-
self.component.save()
|
|
220
|
-
return
|
|
221
|
-
if current_temp > high:
|
|
222
|
-
if not cooler.value:
|
|
223
|
-
cooler.turn_on()
|
|
224
|
-
self.component.value['cooling'] = True
|
|
225
|
-
elif current_temp < low:
|
|
226
|
-
if cooler.value:
|
|
227
|
-
cooler.turn_off()
|
|
228
|
-
self.component.value['cooling'] = False
|
|
229
|
-
|
|
230
261
|
self.component.error_msg = None
|
|
231
262
|
self.component.alive = True
|
|
232
263
|
self.component.save()
|
|
233
264
|
|
|
265
|
+
|
|
266
|
+
def _get_reaction_force(self, window, reach):
|
|
267
|
+
if reach > window:
|
|
268
|
+
reaction_force = 100
|
|
269
|
+
elif reach <= 0:
|
|
270
|
+
reaction_force = 0
|
|
271
|
+
else:
|
|
272
|
+
reaction_force = reach / window * 100
|
|
273
|
+
return reaction_force
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _engage_devices(self, devices, reaction_force):
|
|
277
|
+
for device in devices:
|
|
278
|
+
if device.base_type == 'dimmer':
|
|
279
|
+
device.output_percent(reaction_force)
|
|
280
|
+
elif device.base_type == 'switch':
|
|
281
|
+
if reaction_force == 100:
|
|
282
|
+
device.turn_on()
|
|
283
|
+
elif reaction_force == 0:
|
|
284
|
+
device.turn_off()
|
|
285
|
+
else:
|
|
286
|
+
device.pulse(30, reaction_force)
|
|
287
|
+
|
|
288
|
+
|
|
234
289
|
def update_user_conf(self, new_conf):
|
|
235
290
|
self.component.refresh_from_db()
|
|
236
291
|
self.component.config['user_config'] = validate_new_conf(
|
|
@@ -241,6 +296,7 @@ class Thermostat(ControllerBase):
|
|
|
241
296
|
self.component.save()
|
|
242
297
|
self.evaluate()
|
|
243
298
|
|
|
299
|
+
|
|
244
300
|
def hold(self, temperature=None):
|
|
245
301
|
if temperature != None:
|
|
246
302
|
self.component.config['user_config']['hard'] = {
|
simo/generic/forms.py
CHANGED
|
@@ -90,33 +90,38 @@ class ThermostatConfigForm(BaseComponentForm):
|
|
|
90
90
|
)
|
|
91
91
|
)
|
|
92
92
|
# TODO: support for multiple heaters
|
|
93
|
-
|
|
93
|
+
heaters = Select2ModelMultipleChoiceField(
|
|
94
94
|
queryset=Component.objects.filter(base_type=Switch.base_type),
|
|
95
95
|
required=False,
|
|
96
96
|
url='autocomplete-component',
|
|
97
97
|
forward=(
|
|
98
98
|
forward.Const([
|
|
99
|
-
Switch.base_type,
|
|
99
|
+
Switch.base_type, Dimmer.base_type
|
|
100
100
|
], 'base_type'),
|
|
101
101
|
)
|
|
102
102
|
)
|
|
103
103
|
# TODO: support for multiple coolers
|
|
104
|
-
|
|
104
|
+
coolers = Select2ModelMultipleChoiceField(
|
|
105
105
|
queryset=Component.objects.filter(base_type=Switch.base_type),
|
|
106
106
|
required=False,
|
|
107
107
|
url='autocomplete-component',
|
|
108
108
|
forward=(
|
|
109
109
|
forward.Const([
|
|
110
|
-
Switch.base_type,
|
|
110
|
+
Switch.base_type, Dimmer.base_type
|
|
111
111
|
], 'base_type'),
|
|
112
112
|
)
|
|
113
113
|
|
|
114
114
|
)
|
|
115
|
-
|
|
116
|
-
choices=(('
|
|
117
|
-
initial='
|
|
115
|
+
engagement = forms.ChoiceField(
|
|
116
|
+
choices=(('dynamic', "Dynamic"), ('static', "Static")),
|
|
117
|
+
initial='dynamic',
|
|
118
|
+
help_text="Dynamic - scales engagement intensity within reaction window <br>"
|
|
119
|
+
"Static - engages/disengages fully within reaction window <br>"
|
|
120
|
+
)
|
|
121
|
+
reaction_difference = forms.FloatField(
|
|
122
|
+
initial=2, min_value=0, max_value=50,
|
|
123
|
+
help_text="Reaction window = target temp +- reaction difference."
|
|
118
124
|
)
|
|
119
|
-
reaction_difference = forms.FloatField(initial=0.5)
|
|
120
125
|
min = forms.IntegerField(initial=3)
|
|
121
126
|
max = forms.IntegerField(initial=36)
|
|
122
127
|
use_real_feel = forms.BooleanField(
|
|
@@ -126,8 +131,6 @@ class ThermostatConfigForm(BaseComponentForm):
|
|
|
126
131
|
def __init__(self, *args, **kwargs):
|
|
127
132
|
super().__init__(*args, **kwargs)
|
|
128
133
|
if self.instance.pk:
|
|
129
|
-
self.fields['mode'].initial = \
|
|
130
|
-
self.instance.config['user_config']['mode']
|
|
131
134
|
temperature_sensor = Component.objects.filter(
|
|
132
135
|
pk=self.instance.config.get('temperature_sensor', 0)
|
|
133
136
|
).first()
|
simo/generic/gateways.py
CHANGED
|
@@ -195,7 +195,7 @@ class GenericGatewayHandler(
|
|
|
195
195
|
self.sensors_on_watch = {}
|
|
196
196
|
self.sleep_is_on = {}
|
|
197
197
|
self.last_set_state = None
|
|
198
|
-
|
|
198
|
+
self.pulsing_switches = {}
|
|
199
199
|
|
|
200
200
|
|
|
201
201
|
def watch_thermostats(self):
|
|
@@ -208,6 +208,7 @@ class GenericGatewayHandler(
|
|
|
208
208
|
timezone.activate(tz)
|
|
209
209
|
thermostat.evaluate()
|
|
210
210
|
|
|
211
|
+
|
|
211
212
|
def watch_alarm_clocks(self):
|
|
212
213
|
from .controllers import AlarmClock
|
|
213
214
|
drop_current_instance()
|
|
@@ -218,6 +219,7 @@ class GenericGatewayHandler(
|
|
|
218
219
|
timezone.activate(tz)
|
|
219
220
|
alarm_clock.tick()
|
|
220
221
|
|
|
222
|
+
|
|
221
223
|
def watch_watering(self):
|
|
222
224
|
drop_current_instance()
|
|
223
225
|
from .controllers import Watering
|
|
@@ -231,13 +233,15 @@ class GenericGatewayHandler(
|
|
|
231
233
|
else:
|
|
232
234
|
watering.controller._perform_schedule()
|
|
233
235
|
|
|
236
|
+
|
|
234
237
|
def run(self, exit):
|
|
235
238
|
drop_current_instance()
|
|
236
239
|
self.exit = exit
|
|
237
240
|
self.logger = get_gw_logger(self.gateway_instance.id)
|
|
238
241
|
for task, period in self.periodic_tasks:
|
|
239
242
|
threading.Thread(
|
|
240
|
-
target=self._run_periodic_task, args=(exit, task, period),
|
|
243
|
+
target=self._run_periodic_task, args=(exit, task, period),
|
|
244
|
+
daemon=True
|
|
241
245
|
).start()
|
|
242
246
|
|
|
243
247
|
from simo.generic.controllers import IPCamera
|
|
@@ -254,6 +258,11 @@ class GenericGatewayHandler(
|
|
|
254
258
|
cam_watch = CameraWatcher(cam.id, exit)
|
|
255
259
|
cam_watch.start()
|
|
256
260
|
|
|
261
|
+
threading.Thread(
|
|
262
|
+
target=self.watch_switch_pulses, args=(exit,),
|
|
263
|
+
daemon=True
|
|
264
|
+
).start()
|
|
265
|
+
|
|
257
266
|
print("GATEWAY STARTED!")
|
|
258
267
|
while not exit.is_set():
|
|
259
268
|
mqtt_client.loop()
|
|
@@ -278,6 +287,8 @@ class GenericGatewayHandler(
|
|
|
278
287
|
self.control_alarm_group(component, payload.get('set_val'))
|
|
279
288
|
# elif component.controller_uid == AudioAlert.uid:
|
|
280
289
|
# self.control_audio_alert(component, payload.get('set_val'))
|
|
290
|
+
elif payload.get('pulse'):
|
|
291
|
+
self.start_pulse(component, payload['pulse'])
|
|
281
292
|
else:
|
|
282
293
|
component.controller.set(payload.get('set_val'))
|
|
283
294
|
except Exception:
|
|
@@ -416,6 +427,84 @@ class GenericGatewayHandler(
|
|
|
416
427
|
] = time.time()
|
|
417
428
|
|
|
418
429
|
|
|
430
|
+
def watch_switch_pulses(self, exit):
|
|
431
|
+
for comp in Component.objects.filter(
|
|
432
|
+
base_type='switch', meta__has_key='pulse'
|
|
433
|
+
):
|
|
434
|
+
comp.send(True)
|
|
435
|
+
self.pulsing_switches[comp.id] = {
|
|
436
|
+
'comp': comp, 'last_toggle': time.time(), 'value': True,
|
|
437
|
+
'pulse': comp.meta['pulse']
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
step = 0
|
|
441
|
+
while not exit.is_set():
|
|
442
|
+
time.sleep(0.1)
|
|
443
|
+
step += 1
|
|
444
|
+
remove_switches = []
|
|
445
|
+
for id, data in self.pulsing_switches.items():
|
|
446
|
+
on_interval = data['pulse']['frame'] * data['pulse']['duty']
|
|
447
|
+
off_interval = data['pulse']['frame'] - on_interval
|
|
448
|
+
|
|
449
|
+
if (
|
|
450
|
+
data['value'] and
|
|
451
|
+
time.time() - data['last_toggle'] > on_interval
|
|
452
|
+
) or (
|
|
453
|
+
not data['value'] and
|
|
454
|
+
time.time() - data['last_toggle'] > off_interval
|
|
455
|
+
):
|
|
456
|
+
data['comp'].refresh_from_db()
|
|
457
|
+
if not data['comp'].meta.get('pulse'):
|
|
458
|
+
remove_switches.append(id)
|
|
459
|
+
continue
|
|
460
|
+
if data['pulse'] != data['comp'].meta['pulse']:
|
|
461
|
+
self.pulsing_switches[id]['pulse'] = data['comp'].meta['pulse']
|
|
462
|
+
continue
|
|
463
|
+
|
|
464
|
+
if data['value']:
|
|
465
|
+
if time.time() - data['last_toggle'] > on_interval:
|
|
466
|
+
data['comp'].send(False)
|
|
467
|
+
self.pulsing_switches[id]['last_toggle'] = time.time()
|
|
468
|
+
self.pulsing_switches[id]['value'] = False
|
|
469
|
+
else:
|
|
470
|
+
if time.time() - data['last_toggle'] > off_interval:
|
|
471
|
+
data['comp'].send(True)
|
|
472
|
+
self.pulsing_switches[id]['last_toggle'] = time.time()
|
|
473
|
+
self.pulsing_switches[id]['value'] = True
|
|
474
|
+
|
|
475
|
+
for id in remove_switches:
|
|
476
|
+
del self.pulsing_switches[id]
|
|
477
|
+
|
|
478
|
+
# Update with db every 5s just in case something is missed.
|
|
479
|
+
if step < 50:
|
|
480
|
+
continue
|
|
481
|
+
step = 0
|
|
482
|
+
|
|
483
|
+
remove_switches = set(self.pulsing_switches.keys())
|
|
484
|
+
for comp in Component.objects.filter(
|
|
485
|
+
base_type='switch', meta__has_key='pulse'
|
|
486
|
+
):
|
|
487
|
+
if comp.id in remove_switches:
|
|
488
|
+
remove_switches.remove(comp.id)
|
|
489
|
+
self.pulsing_switches[comp.id]['pulse'] = comp.meta['pulse']
|
|
490
|
+
continue
|
|
491
|
+
comp.send(True)
|
|
492
|
+
self.pulsing_switches[comp.id] = {
|
|
493
|
+
'comp': comp, 'last_toggle': time.time(), 'value': True,
|
|
494
|
+
'pulse': comp.meta['pulse']
|
|
495
|
+
}
|
|
496
|
+
for id in remove_switches:
|
|
497
|
+
del self.pulsing_switches[id]
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def start_pulse(self, comp, pulse):
|
|
501
|
+
comp.send(True)
|
|
502
|
+
self.pulsing_switches[comp.id] = {
|
|
503
|
+
'comp': comp, 'last_toggle': time.time(), 'value': True,
|
|
504
|
+
'pulse': pulse
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
|
|
419
508
|
|
|
420
509
|
class DummyGatewayHandler(BaseObjectCommandsGatewayHandler):
|
|
421
510
|
name = "Dummy"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Generated by Django 4.2.10 on 2025-04-09 14:04
|
|
2
|
+
|
|
3
|
+
from django.db import migrations
|
|
4
|
+
|
|
5
|
+
def forwards_func(apps, schema_editor):
|
|
6
|
+
|
|
7
|
+
Component = apps.get_model("core", "Component")
|
|
8
|
+
|
|
9
|
+
for thermostat in Component.objects.filter(base_type='thermostat'):
|
|
10
|
+
thermostat.config['heaters'] = []
|
|
11
|
+
heater = thermostat.config.pop('heater')
|
|
12
|
+
if heater:
|
|
13
|
+
thermostat.config['heaters'].append(heater)
|
|
14
|
+
thermostat.config['coolers'] = []
|
|
15
|
+
cooler = thermostat.config.pop('cooler')
|
|
16
|
+
if cooler:
|
|
17
|
+
thermostat.config['coolers'].append(cooler)
|
|
18
|
+
thermostat.save()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def reverse_func(apps, schema_editor):
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Migration(migrations.Migration):
|
|
26
|
+
|
|
27
|
+
dependencies = [
|
|
28
|
+
('generic', '0002_auto_20241126_0726'),
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
operations = [
|
|
32
|
+
migrations.RunPython(forwards_func, reverse_func, elidable=True),
|
|
33
|
+
]
|
|
Binary file
|
|
Binary file
|
simo/users/api.py
CHANGED
|
@@ -194,10 +194,6 @@ class UserDeviceReport(InstanceMixin, viewsets.GenericViewSet):
|
|
|
194
194
|
user_device.users.add(request.user)
|
|
195
195
|
|
|
196
196
|
log_datetime = timezone.now()
|
|
197
|
-
# if request.data.get('timestamp'):
|
|
198
|
-
# log_datetime = datetime.datetime.fromtimestamp(
|
|
199
|
-
# int(float(request.GET['timestamp'])), pytz.utc
|
|
200
|
-
# )
|
|
201
197
|
|
|
202
198
|
relay = None
|
|
203
199
|
if request.META.get('HTTP_HOST', '').endswith('.simo.io'):
|
|
@@ -208,22 +204,26 @@ class UserDeviceReport(InstanceMixin, viewsets.GenericViewSet):
|
|
|
208
204
|
except:
|
|
209
205
|
speed_kmh = 0
|
|
210
206
|
|
|
207
|
+
if speed_kmh < 0:
|
|
208
|
+
speed_kmh = 0
|
|
209
|
+
|
|
211
210
|
avg_speed_kmh = 0
|
|
212
211
|
|
|
213
212
|
if relay:
|
|
214
213
|
location = request.data.get('location')
|
|
215
214
|
if 'null' in location:
|
|
216
215
|
location = None
|
|
217
|
-
|
|
216
|
+
sum = 0
|
|
217
|
+
no_of_points = 0
|
|
218
|
+
for speed in UserDeviceReportLog.objects.filter(
|
|
218
219
|
user_device=user_device, instance=self.instance,
|
|
219
220
|
datetime__lt=log_datetime - datetime.timedelta(seconds=3),
|
|
220
|
-
datetime__gt=log_datetime - datetime.timedelta(seconds=
|
|
221
|
+
datetime__gt=log_datetime - datetime.timedelta(seconds=30),
|
|
221
222
|
at_home=False, location__isnull=False
|
|
222
|
-
).
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
avg_speed_kmh = round(meters_traveled / seconds_passed * 3.6, 0)
|
|
223
|
+
).values('speed_kmh',):
|
|
224
|
+
sum += speed[0]
|
|
225
|
+
sum += speed_kmh
|
|
226
|
+
avg_speed_kmh = round(sum / (no_of_points + 1))
|
|
227
227
|
else:
|
|
228
228
|
location = self.instance.location
|
|
229
229
|
|
|
@@ -248,7 +248,30 @@ class UserDeviceReport(InstanceMixin, viewsets.GenericViewSet):
|
|
|
248
248
|
self.instance.location, location
|
|
249
249
|
) < dynamic_settings['users__at_home_radius']
|
|
250
250
|
|
|
251
|
+
app_open = request.data.get('app_open', False)
|
|
252
|
+
|
|
253
|
+
if self.reject_location_report(
|
|
254
|
+
log_datetime, user_device, location, app_open, relay,
|
|
255
|
+
phone_on_charge, at_home, speed_kmh
|
|
256
|
+
):
|
|
257
|
+
# We respond with success status, so that the device dos not try to
|
|
258
|
+
# report this data point again.
|
|
259
|
+
return RESTResponse({'status': 'success'})
|
|
260
|
+
# return RESTResponse(
|
|
261
|
+
# {'status': 'error', 'msg': 'Duplicate or Bad report!'},
|
|
262
|
+
# status=status.HTTP_400_BAD_REQUEST
|
|
263
|
+
# )
|
|
264
|
+
|
|
265
|
+
UserDeviceReportLog.objects.create(
|
|
266
|
+
user_device=user_device, instance=self.instance,
|
|
267
|
+
app_open=app_open,
|
|
268
|
+
location=location, datetime=log_datetime,
|
|
269
|
+
relay=relay, speed_kmh=speed_kmh, avg_speed_kmh=avg_speed_kmh,
|
|
270
|
+
phone_on_charge=phone_on_charge, at_home=at_home
|
|
271
|
+
)
|
|
272
|
+
|
|
251
273
|
drop_current_instance()
|
|
274
|
+
|
|
252
275
|
for iu in request.user.instance_roles.filter(is_active=True):
|
|
253
276
|
if not relay:
|
|
254
277
|
iu.at_home = True
|
|
@@ -260,19 +283,49 @@ class UserDeviceReport(InstanceMixin, viewsets.GenericViewSet):
|
|
|
260
283
|
iu.last_seen = user_device.last_seen
|
|
261
284
|
if location:
|
|
262
285
|
iu.last_seen_location = location
|
|
263
|
-
iu.last_seen_speed_kmh =
|
|
286
|
+
iu.last_seen_speed_kmh = avg_speed_kmh
|
|
264
287
|
iu.phone_on_charge = phone_on_charge
|
|
265
288
|
iu.save()
|
|
266
289
|
|
|
267
|
-
|
|
290
|
+
return RESTResponse({'status': 'success'})
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def reject_location_report(
|
|
294
|
+
self, log_datetime, user_device, location, app_open, relay,
|
|
295
|
+
phone_on_charge, at_home, speed_kmh
|
|
296
|
+
):
|
|
297
|
+
# Phone's App location repoorting is not always as reliable as we would like to
|
|
298
|
+
# therefore we filter out duplicate reports as they happen quiet often
|
|
299
|
+
# It has been observer that sometimes an app reports locations that are
|
|
300
|
+
# way from past, therefore locations might jump out of the usual pattern,
|
|
301
|
+
# so we try to filter out these anomalies to.
|
|
302
|
+
q = UserDeviceReportLog.objects.filter(
|
|
268
303
|
user_device=user_device, instance=self.instance,
|
|
269
|
-
app_open=
|
|
270
|
-
|
|
271
|
-
relay=relay,
|
|
272
|
-
phone_on_charge=phone_on_charge, at_home=at_home
|
|
304
|
+
app_open=app_open,
|
|
305
|
+
datetime__gt=log_datetime - datetime.timedelta(seconds=20),
|
|
306
|
+
relay=relay, phone_on_charge=phone_on_charge, at_home=at_home
|
|
273
307
|
)
|
|
308
|
+
if location:
|
|
309
|
+
q = q.filter(location__isnull=False)
|
|
310
|
+
last_similar_report = q.last()
|
|
311
|
+
|
|
312
|
+
if not last_similar_report:
|
|
313
|
+
return False
|
|
314
|
+
|
|
315
|
+
if location == last_similar_report.location:
|
|
316
|
+
# This looks like 100% duplicate
|
|
317
|
+
return True
|
|
318
|
+
|
|
319
|
+
from simo.automation.helpers import haversine_distance
|
|
320
|
+
distance = haversine_distance(location, last_similar_report.location)
|
|
321
|
+
seconds_passed = (
|
|
322
|
+
log_datetime - last_similar_report.datetime
|
|
323
|
+
).total_seconds()
|
|
324
|
+
if speed_kmh < 100 and distance / seconds_passed * 3.6 > 300:
|
|
325
|
+
return True
|
|
326
|
+
|
|
327
|
+
return False
|
|
274
328
|
|
|
275
|
-
return RESTResponse({'status': 'success'})
|
|
276
329
|
|
|
277
330
|
|
|
278
331
|
class InvitationsViewSet(InstanceMixin, viewsets.ModelViewSet):
|
simo/users/dynamic_settings.py
CHANGED
|
@@ -12,7 +12,7 @@ users = Section('users')
|
|
|
12
12
|
class AtHomeRadius(IntegerPreference):
|
|
13
13
|
section = users
|
|
14
14
|
name = 'at_home_radius'
|
|
15
|
-
default =
|
|
15
|
+
default = 50
|
|
16
16
|
required = True
|
|
17
17
|
help_text = 'Distance in meters around hub location point that is ' \
|
|
18
18
|
'considered as At Home.'
|
|
@@ -21,7 +21,7 @@ simo/automation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
21
21
|
simo/automation/app_widgets.py,sha256=gaqImMZjuMHm7nIb9a4D-Y3qipz_WhSPAHXcwGx4Uzs,199
|
|
22
22
|
simo/automation/controllers.py,sha256=66aGjlfgigcePXiLmDkbVrxbm-Z26klMKIiOvaMH71k,11545
|
|
23
23
|
simo/automation/forms.py,sha256=jcmiq7A-S5WwoIY3YD7GmWomHXXJAipTawWcgOavuRM,10239
|
|
24
|
-
simo/automation/gateways.py,sha256=
|
|
24
|
+
simo/automation/gateways.py,sha256=GH8StZslRvZvWGz6_q8IjQzQg5zSmIZ7_mALbbKm5OY,16539
|
|
25
25
|
simo/automation/helpers.py,sha256=iP-fxxB8HsFQy3k2CjFubu86aMqvWgmh-p24DiyOrek,4330
|
|
26
26
|
simo/automation/models.py,sha256=zt-jkzyq5ddqGT864OkJzCsvov2vZ0nO4ez3hAeZkXg,934
|
|
27
27
|
simo/automation/serializers.py,sha256=Pg-hMaASQPB5_BTAMkfqM6z4jdHWH8xMYWOvDxIvmx8,2126
|
|
@@ -34,7 +34,7 @@ simo/automation/__pycache__/controllers.cpython-312.pyc,sha256=QV63g3UlpMAA-yCaZ
|
|
|
34
34
|
simo/automation/__pycache__/controllers.cpython-38.pyc,sha256=CL-0Tq9B4-E36fYfWT1XEBTq1dkq1W8003f6MrBnQU0,8391
|
|
35
35
|
simo/automation/__pycache__/forms.cpython-312.pyc,sha256=kmw04yujs7dVHNHvzr9Q8gXc0e6BAZHg_URPXjLbrfU,12419
|
|
36
36
|
simo/automation/__pycache__/forms.cpython-38.pyc,sha256=cpA5hA2Iz3JsPC0Dq01ki1I7S9c5DKRcXveHApI1dJo,7772
|
|
37
|
-
simo/automation/__pycache__/gateways.cpython-312.pyc,sha256=
|
|
37
|
+
simo/automation/__pycache__/gateways.cpython-312.pyc,sha256=SvCnrrJGw727sredFOl3JEeljBcNrno3e5Yqdai6xI8,22686
|
|
38
38
|
simo/automation/__pycache__/gateways.cpython-38.pyc,sha256=nHujqChMCqqxHbZezP3MisavjKDhczqzFGurO10h-lc,11113
|
|
39
39
|
simo/automation/__pycache__/helpers.cpython-312.pyc,sha256=v6abo_h8qfWD3isbJvTO3X9sfUREJxTlc_P1ZhN8ZCs,5853
|
|
40
40
|
simo/automation/__pycache__/helpers.cpython-38.pyc,sha256=fNjSyn4Mfq7-JQx-bdsnj-rSxgu20dVJ9-5ZEMT6yiM,3627
|
|
@@ -98,7 +98,7 @@ simo/core/auto_urls.py,sha256=fM9Tqzt0OfJ2FNnePGp7LcbJAWzgEwaNAJy7FNXHY-o,1299
|
|
|
98
98
|
simo/core/autocomplete_views.py,sha256=x3MKOZvXYS3xVQ-V1S7Liv_U5bxr-uc0gePa85wv5nA,4561
|
|
99
99
|
simo/core/base_types.py,sha256=WypW8hTfzveuTQtruGjLYAGQZIuczxTlW-SdRk3iQug,666
|
|
100
100
|
simo/core/context.py,sha256=LKw1I4iIRnlnzoTCuSLLqDX7crHdBnMo3hjqYvVmzFc,1557
|
|
101
|
-
simo/core/controllers.py,sha256=
|
|
101
|
+
simo/core/controllers.py,sha256=1TdJXaPcHIo7OfRXrqe8swxhXthpO2-Pji45StCxlC0,39350
|
|
102
102
|
simo/core/dynamic_settings.py,sha256=bUs58XEZOCIEhg1TigR3LmYggli13KMryBZ9pC7ugAQ,1872
|
|
103
103
|
simo/core/events.py,sha256=hnv17g37kqwpfrkMUStfk_Rk_-G28MU1HEjolwWNNFg,4978
|
|
104
104
|
simo/core/filters.py,sha256=6wbn8C2WvKTTjtfMwwLBp2Fib1V0-DMpS4iqJd6jJQo,2540
|
|
@@ -115,7 +115,7 @@ simo/core/serializers.py,sha256=Yeu6YRGMIZzAgnfIgm0_v4pWXDgsBCSlwWaDws_bMUg,2310
|
|
|
115
115
|
simo/core/signal_receivers.py,sha256=PsAHAvdFKuNLjxNbb7UqjrX7VHtjdPAhP2y3vbhVGXs,7114
|
|
116
116
|
simo/core/socket_consumers.py,sha256=UlxV9OvTUUXaoKKYT3-qf1TyGxyOPxIpFH5cPFepH1o,9584
|
|
117
117
|
simo/core/storage.py,sha256=_5igjaoWZAiExGWFEJMElxUw55DzJG1jqFty33xe8BE,342
|
|
118
|
-
simo/core/tasks.py,sha256
|
|
118
|
+
simo/core/tasks.py,sha256=Y9gXMd1QBjpO_4EAOuMSoHfK4H4qVcsg-36mPcx2sPU,17322
|
|
119
119
|
simo/core/todos.py,sha256=eYVXfLGiapkxKK57XuviSNe3WsUYyIWZ0hgQJk7ThKo,665
|
|
120
120
|
simo/core/types.py,sha256=WJEq48mIbFi_5Alt4wxWMGXxNxUTXqfQU5koH7wqHHI,1108
|
|
121
121
|
simo/core/views.py,sha256=BBh8U2P3rS_WccKN9hN6dhtiyXIfCX9I80zNwejqfe4,3666
|
|
@@ -142,7 +142,7 @@ simo/core/__pycache__/base_types.cpython-312.pyc,sha256=Lnq2NL9B5hfwJARJYC447Rdv
|
|
|
142
142
|
simo/core/__pycache__/base_types.cpython-38.pyc,sha256=CX-qlF7CefRi_mCE954wYa9rUFR88mOl6g7fybDRu7g,803
|
|
143
143
|
simo/core/__pycache__/context.cpython-312.pyc,sha256=8rsN2Er-Sx3rrVmO0Gk4cem3euGh0kTELXj667GGZ5E,2193
|
|
144
144
|
simo/core/__pycache__/context.cpython-38.pyc,sha256=NlTHt2GvXxA21AhBkeyOLfRFUuXw7wmwqyNhhcDl2cw,1373
|
|
145
|
-
simo/core/__pycache__/controllers.cpython-312.pyc,sha256=
|
|
145
|
+
simo/core/__pycache__/controllers.cpython-312.pyc,sha256=XovbuP57Sn0QBCGvpOoHaDGLPBBMa0INLQ0n7iWkQf4,56673
|
|
146
146
|
simo/core/__pycache__/controllers.cpython-38.pyc,sha256=LtrQQ8egOIOuQbAckeM-z8OfbzS4W8VQ3vBnryAm3iU,32086
|
|
147
147
|
simo/core/__pycache__/dynamic_settings.cpython-312.pyc,sha256=WUZ6XF4kZb6zPf541PkKmiQaBIw-r5C6F3EUUZiTEnE,3331
|
|
148
148
|
simo/core/__pycache__/dynamic_settings.cpython-38.pyc,sha256=wGpnscX1DxFpRl54MQURhjz2aD3NJohSzw9JCFnzh2Y,2384
|
|
@@ -176,7 +176,7 @@ simo/core/__pycache__/socket_consumers.cpython-312.pyc,sha256=Yph9SQTj6c3xr2HXKn
|
|
|
176
176
|
simo/core/__pycache__/socket_consumers.cpython-38.pyc,sha256=Mr1-x-vGjBNffbz0S6AKpJCuzHJgRm8kXefv3qVVY_E,8397
|
|
177
177
|
simo/core/__pycache__/storage.cpython-312.pyc,sha256=VOD9via7BtpTbXe2jWsRovCIkeJofoX7Ts_Eyl9I_a4,934
|
|
178
178
|
simo/core/__pycache__/storage.cpython-38.pyc,sha256=9R1Xu0FJDflfRXUPsqEgt0SpwiP7FGk7HaR8s8XRyI8,721
|
|
179
|
-
simo/core/__pycache__/tasks.cpython-312.pyc,sha256=
|
|
179
|
+
simo/core/__pycache__/tasks.cpython-312.pyc,sha256=YNRlJSItp3_hbF6uhcksjS3Px1LQOVbjkAaAXRu1Znw,22818
|
|
180
180
|
simo/core/__pycache__/tasks.cpython-38.pyc,sha256=-J2is-l5XsfhamreN2TPQDTK-Jw6XGYL81bcVfjXsU8,11213
|
|
181
181
|
simo/core/__pycache__/todos.cpython-312.pyc,sha256=bqguSv-oCCdlvzbMsm-7jAB-g0AvMFux1GL97XNJjyk,263
|
|
182
182
|
simo/core/__pycache__/todos.cpython-38.pyc,sha256=lOqGZ58siHM3isoJV4r7sg8igrfE9fFd-jSfeBa0AQI,253
|
|
@@ -10606,9 +10606,9 @@ simo/fleet/templates/fleet/controllers_info/RoomZonePresenceSensor.md,sha256=Nun
|
|
|
10606
10606
|
simo/generic/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10607
10607
|
simo/generic/app_widgets.py,sha256=y8W3jR76Hh26O9pPQyg2SophMbYIOtAWD33MPKbB8Mg,856
|
|
10608
10608
|
simo/generic/base_types.py,sha256=u3SlfpNYaCwkVBwomWgso4ODzL71ay9MhiAW-bxgnDU,341
|
|
10609
|
-
simo/generic/controllers.py,sha256=
|
|
10610
|
-
simo/generic/forms.py,sha256=
|
|
10611
|
-
simo/generic/gateways.py,sha256=
|
|
10609
|
+
simo/generic/controllers.py,sha256=mtMkWH-76KlmXNwzHSfgbCEuvK1D4S9R1yvIdkjtl4E,48202
|
|
10610
|
+
simo/generic/forms.py,sha256=dItnnUTwc5EtTfDAHcL8Ihy_b9DVVA5ajFd8FXpA_dQ,26568
|
|
10611
|
+
simo/generic/gateways.py,sha256=Vth-jSq1i81cm5G7cbuOlFJ4xKjmjLtvo2hn_djfPR4,19445
|
|
10612
10612
|
simo/generic/models.py,sha256=59fkYowOX0imviIhA6uwupvuharrpBykmBm674rJNoI,7279
|
|
10613
10613
|
simo/generic/routing.py,sha256=elQVZmgnPiieEuti4sJ7zITk1hlRxpgbotcutJJgC60,228
|
|
10614
10614
|
simo/generic/socket_consumers.py,sha256=qesKZVhI56Kh7vdIUDD3hzDUi0FcXwIfcmE_a3YS6JQ,1772
|
|
@@ -10619,11 +10619,11 @@ simo/generic/__pycache__/app_widgets.cpython-312.pyc,sha256=ywoKk91YSEZxpyt9haG5
|
|
|
10619
10619
|
simo/generic/__pycache__/app_widgets.cpython-38.pyc,sha256=D9b13pbMlirgHmjDnQhfLIDGSVINoSouHb4SWOeCRrs,1642
|
|
10620
10620
|
simo/generic/__pycache__/base_types.cpython-312.pyc,sha256=h8Mwu49i-zmwTbL33JaLJfRDGOgkQh2_VqrfzEc4UQ4,616
|
|
10621
10621
|
simo/generic/__pycache__/base_types.cpython-38.pyc,sha256=aV5NdIuvXR-ItKpI__MwcyPZHD6Z882TFdgYkPCkr1I,493
|
|
10622
|
-
simo/generic/__pycache__/controllers.cpython-312.pyc,sha256=
|
|
10622
|
+
simo/generic/__pycache__/controllers.cpython-312.pyc,sha256=mPRJKmyjAiayb9ccDF1Wdm7-yI1q5DPvgqAdVGv1pf0,54042
|
|
10623
10623
|
simo/generic/__pycache__/controllers.cpython-38.pyc,sha256=jJjwKVaDYyazrRGNjUFoY74nr_jX_DEnsC9KjyxZCgc,30427
|
|
10624
|
-
simo/generic/__pycache__/forms.cpython-312.pyc,sha256
|
|
10624
|
+
simo/generic/__pycache__/forms.cpython-312.pyc,sha256=kjg_rV74sV2V3qg5qYuAPRySVHLbHrJeLQzlAIgajNs,35047
|
|
10625
10625
|
simo/generic/__pycache__/forms.cpython-38.pyc,sha256=k8lz3taXdWAg5P9jcnw66mWH51pCc4SOsg32kVEtBCg,19416
|
|
10626
|
-
simo/generic/__pycache__/gateways.cpython-312.pyc,sha256=
|
|
10626
|
+
simo/generic/__pycache__/gateways.cpython-312.pyc,sha256=naa_lnv2qQO9EALDYGajGAdU0wPqnaKjWE0ZKFjYJKQ,23195
|
|
10627
10627
|
simo/generic/__pycache__/gateways.cpython-38.pyc,sha256=GIeMT51oZU2OCFD4eUDFdSRRYE0Qf14AcOr_gdUqG94,12705
|
|
10628
10628
|
simo/generic/__pycache__/models.cpython-312.pyc,sha256=ggaeX6BQa-0-KG50HadpRCWeW84Fbog0muT2gBkqLNQ,10190
|
|
10629
10629
|
simo/generic/__pycache__/models.cpython-38.pyc,sha256=MZpum7syAFxuulf47K7gtUlJJ7xRD-IBUBAwUM1ZRnw,5825
|
|
@@ -10634,11 +10634,13 @@ simo/generic/__pycache__/socket_consumers.cpython-38.pyc,sha256=FaVCf_uJI2uwj1Zz
|
|
|
10634
10634
|
simo/generic/__pycache__/tasks.cpython-312.pyc,sha256=zEyNpFVmEJoZdeYKNZ7cEmPtIyZMTwEqzZJZZwMYl-o,4494
|
|
10635
10635
|
simo/generic/migrations/0001_initial.py,sha256=7FpPcfpRU5ya0b8s2KbxR5a3npf92YruvZltUybjzys,676
|
|
10636
10636
|
simo/generic/migrations/0002_auto_20241126_0726.py,sha256=SX38JwP732QooOm5HM1Xo7Th_Mv_6YZloT3eozULOhs,922
|
|
10637
|
+
simo/generic/migrations/0003_auto_20250409_1404.py,sha256=Jf8fFspN_V6woeUVvez36KVAcYR5tb23arLguagnm7Q,875
|
|
10637
10638
|
simo/generic/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10638
10639
|
simo/generic/migrations/__pycache__/0001_initial.cpython-312.pyc,sha256=n0KtsGZlJ5Tr7sPAgxQB9-PFGc_wRYMaCMV9Sp3cLEo,1267
|
|
10639
10640
|
simo/generic/migrations/__pycache__/0001_initial.cpython-38.pyc,sha256=xy_fN8vnebC_X8hArqHAOLYHLm3puD_stzJ-LvUOhgk,1009
|
|
10640
10641
|
simo/generic/migrations/__pycache__/0002_auto_20241126_0726.cpython-312.pyc,sha256=Iqo7MhrkP7TTV5_BdI4DoEn_6yqXqyGO9mSjWjWmmRA,1676
|
|
10641
10642
|
simo/generic/migrations/__pycache__/0002_auto_20241126_0726.cpython-38.pyc,sha256=vfPHUDBG6UlfdqsIi35ZcdQ0NfsSeawvTNzZ7YTKQd0,1130
|
|
10643
|
+
simo/generic/migrations/__pycache__/0003_auto_20250409_1404.cpython-312.pyc,sha256=TIh9Y-e8_9-wot9-o97-MjyR36HtREwqE-eVPyz78DI,1651
|
|
10642
10644
|
simo/generic/migrations/__pycache__/__init__.cpython-312.pyc,sha256=1n3ngIGcB4TVFUtZfXMMaVfYZnAEWPtIxwI-88in_Mc,178
|
|
10643
10645
|
simo/generic/migrations/__pycache__/__init__.cpython-38.pyc,sha256=nJV0NkIT8MuONj1hUX-V6aCU2lX3BXHyPjisapnBsPA,172
|
|
10644
10646
|
simo/generic/static/weather_icons/01d@2x.png,sha256=TZfWi6Rfddb2P-oldWWcjUiuCHiU9Yrc5hyrQAhF26I,948
|
|
@@ -10752,11 +10754,11 @@ simo/notifications/migrations/__pycache__/__init__.cpython-312.pyc,sha256=YMFc4Q
|
|
|
10752
10754
|
simo/notifications/migrations/__pycache__/__init__.cpython-38.pyc,sha256=YMBRHVon2nWDtIUbghckjnC12sIg_ykPWhV5aM0tto4,178
|
|
10753
10755
|
simo/users/__init__.py,sha256=6a7uBpCWB_DR7p54rbHusc0xvi1qfT1ZCCQGb6TiBh8,52
|
|
10754
10756
|
simo/users/admin.py,sha256=2J_ZwrwAPw03Stb6msx5GvRpGCsDSC39RPpYBLGf8UY,7441
|
|
10755
|
-
simo/users/api.py,sha256=
|
|
10757
|
+
simo/users/api.py,sha256=K0hGtuHkjwlLiWjne6jruI-lfswHrPXfCHSLP-xqOfY,15050
|
|
10756
10758
|
simo/users/apps.py,sha256=cq0A8-U1HALEwev0TicgFhr4CAu7Icz8rwq0HfOaL4E,207
|
|
10757
10759
|
simo/users/auth_backends.py,sha256=HxRp9iFvU1KqUhE7pA9YKEjqtBCJDbDqk_UMCD2Dwww,4361
|
|
10758
10760
|
simo/users/auto_urls.py,sha256=RSUW3ai5LbMTknS8M7M5aOnG_YlFOVQrnNVNH-fkwlg,357
|
|
10759
|
-
simo/users/dynamic_settings.py,sha256=
|
|
10761
|
+
simo/users/dynamic_settings.py,sha256=yDtjpcEKA5uS2fcma6e-Zznh2iyMT3x8N7aRqNCtzSM,569
|
|
10760
10762
|
simo/users/managers.py,sha256=OHgEP85MBtdkdYxdstBd8RavTBT8F_2WyDxUJ9aCqqM,246
|
|
10761
10763
|
simo/users/middleware.py,sha256=tNPmnzo0eTrJ25SLHP7NotqYKI2cKnmv8hf6v5DLOWo,427
|
|
10762
10764
|
simo/users/models.py,sha256=TKHs7mVjVC84WK-qXDz--4YfaD_TsUDrxxZ-oEXF-Pc,20527
|
|
@@ -10771,7 +10773,7 @@ simo/users/__pycache__/__init__.cpython-312.pyc,sha256=n0GE5zxjujBUkv1t1CswZ9Gby
|
|
|
10771
10773
|
simo/users/__pycache__/__init__.cpython-38.pyc,sha256=VFoDJE_SKKaPqqYaaBYd1Ndb1hjakkTo_u0EG_XJ1GM,211
|
|
10772
10774
|
simo/users/__pycache__/admin.cpython-312.pyc,sha256=lTVuZQwkuw1bzSn0A0kt-8H6ruKas_YPb_61AxSYSoo,11465
|
|
10773
10775
|
simo/users/__pycache__/admin.cpython-38.pyc,sha256=uL8TwAipkatZxanvQtBKKcOv8Fm3UvinBxsP0okrOZg,8443
|
|
10774
|
-
simo/users/__pycache__/api.cpython-312.pyc,sha256=
|
|
10776
|
+
simo/users/__pycache__/api.cpython-312.pyc,sha256=UxOSngKsHG8FD0d0BE0734v_uj9Ynz5kL9j5zXB91Vs,19424
|
|
10775
10777
|
simo/users/__pycache__/api.cpython-38.pyc,sha256=zZ4DfNktgeVvLAtMpaPUv7AoAgbKr7SCt-4ghJk1zp4,10386
|
|
10776
10778
|
simo/users/__pycache__/apps.cpython-312.pyc,sha256=6KefztC11cSg6VHJ7-sVDvOA-oq_jNMB8jMvmhcVZD4,707
|
|
10777
10779
|
simo/users/__pycache__/apps.cpython-38.pyc,sha256=dgbWL8CxzzISJQTmq_4IztPJ2UzykNVdqA2Ae1PmeGk,605
|
|
@@ -10779,7 +10781,7 @@ simo/users/__pycache__/auth_backends.cpython-312.pyc,sha256=NrdX57hAzX0sRI7BHiSx
|
|
|
10779
10781
|
simo/users/__pycache__/auth_backends.cpython-38.pyc,sha256=jYS2hlbTZh_ZtPeWcN50pc0IpyfCSO7_MvIbuVwEp8M,3144
|
|
10780
10782
|
simo/users/__pycache__/auto_urls.cpython-312.pyc,sha256=nRaBVD1YnibleMC8AdZdk1ZpwEgMyIPm9VW0quIuj5E,565
|
|
10781
10783
|
simo/users/__pycache__/auto_urls.cpython-38.pyc,sha256=bc6BOgghLdMzUroGMj7eB6YX9kIDWB9JvTc0iJQrEa4,478
|
|
10782
|
-
simo/users/__pycache__/dynamic_settings.cpython-312.pyc,sha256=
|
|
10784
|
+
simo/users/__pycache__/dynamic_settings.cpython-312.pyc,sha256=TZ6YtXDJzsdO0qJLtNhvottM2kK1WYugwG3DaXt1CF0,981
|
|
10783
10785
|
simo/users/__pycache__/dynamic_settings.cpython-38.pyc,sha256=6F8JBjZkHykySnmZjNEzjS0ijbmPdcp9yUAZ5kqq_Fo,864
|
|
10784
10786
|
simo/users/__pycache__/managers.cpython-312.pyc,sha256=A9-yF1dilDc1H_-BtrIw9USpH6MYwkUI02gVmDOxsOA,821
|
|
10785
10787
|
simo/users/__pycache__/managers.cpython-38.pyc,sha256=O0Y8ABp42RAosrbODmYsPMaj9AyOPyJ-aqzuO0Qpi2s,679
|
|
@@ -10943,9 +10945,9 @@ simo/users/templates/invitations/expired_msg.html,sha256=47DEQpj8HBSa-_TImW-5JCe
|
|
|
10943
10945
|
simo/users/templates/invitations/expired_suggestion.html,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10944
10946
|
simo/users/templates/invitations/taken_msg.html,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10945
10947
|
simo/users/templates/invitations/taken_suggestion.html,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10946
|
-
simo-2.
|
|
10947
|
-
simo-2.
|
|
10948
|
-
simo-2.
|
|
10949
|
-
simo-2.
|
|
10950
|
-
simo-2.
|
|
10951
|
-
simo-2.
|
|
10948
|
+
simo-2.10.1.dist-info/licenses/LICENSE.md,sha256=M7wm1EmMGDtwPRdg7kW4d00h1uAXjKOT3HFScYQMeiE,34916
|
|
10949
|
+
simo-2.10.1.dist-info/METADATA,sha256=9c6MSVrGuXjWD5IIrfhZ5CDlKqLEoZFxAnC20XDyNEg,2028
|
|
10950
|
+
simo-2.10.1.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
|
10951
|
+
simo-2.10.1.dist-info/entry_points.txt,sha256=S9PwnUYmTSW7681GKDCxUbL0leRJIaRk6fDQIKgbZBA,135
|
|
10952
|
+
simo-2.10.1.dist-info/top_level.txt,sha256=GmS1hrAbpVqn9OWZh6UX82eIOdRLgYA82RG9fe8v4Rs,5
|
|
10953
|
+
simo-2.10.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|