simo 2.8.15__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__/admin.cpython-312.pyc +0 -0
- simo/core/__pycache__/auto_urls.cpython-312.pyc +0 -0
- simo/core/__pycache__/controllers.cpython-312.pyc +0 -0
- simo/core/__pycache__/models.cpython-312.pyc +0 -0
- simo/core/__pycache__/serializers.cpython-312.pyc +0 -0
- simo/core/__pycache__/tasks.cpython-312.pyc +0 -0
- simo/core/__pycache__/views.cpython-312.pyc +0 -0
- simo/core/admin.py +5 -2
- simo/core/auto_urls.py +4 -1
- simo/core/controllers.py +42 -5
- simo/core/models.py +32 -16
- simo/core/serializers.py +2 -2
- simo/core/tasks.py +8 -1
- simo/core/templates/admin/core/component_change_form.html +1 -1
- simo/core/templates/admin/wizard/discovery.html +3 -4
- simo/core/templates/admin/wizard/wizard_add.html +1 -1
- simo/core/views.py +26 -2
- simo/fleet/__pycache__/api.cpython-312.pyc +0 -0
- simo/fleet/__pycache__/base_types.cpython-312.pyc +0 -0
- simo/fleet/__pycache__/controllers.cpython-312.pyc +0 -0
- simo/fleet/__pycache__/forms.cpython-312.pyc +0 -0
- simo/fleet/__pycache__/managers.cpython-312.pyc +0 -0
- simo/fleet/__pycache__/models.cpython-312.pyc +0 -0
- simo/fleet/__pycache__/serializers.cpython-312.pyc +0 -0
- simo/fleet/__pycache__/socket_consumers.cpython-312.pyc +0 -0
- simo/fleet/api.py +26 -3
- simo/fleet/base_types.py +1 -0
- simo/fleet/controllers.py +240 -7
- simo/fleet/custom_dali_operations.py +275 -0
- simo/fleet/forms.py +132 -3
- simo/fleet/managers.py +3 -1
- simo/fleet/migrations/0045_alter_colonel_type_customdalidevice.py +29 -0
- simo/fleet/migrations/0046_delete_customdalidevice.py +16 -0
- simo/fleet/migrations/0047_customdalidevice.py +28 -0
- simo/fleet/migrations/0048_remove_customdalidevice_colonel_and_more.py +28 -0
- simo/fleet/migrations/__pycache__/0045_alter_colonel_type_customdalidevice.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0046_delete_customdalidevice.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0047_customdalidevice.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0048_remove_customdalidevice_colonel_and_more.cpython-312.pyc +0 -0
- simo/fleet/models.py +54 -9
- simo/fleet/serializers.py +15 -1
- simo/fleet/socket_consumers.py +6 -0
- simo/fleet/tasks.py +22 -2
- simo/fleet/templates/fleet/controllers_info/RoomZonePresenceSensor.md +8 -0
- 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.8.15.dist-info → simo-2.10.1.dist-info}/METADATA +1 -1
- {simo-2.8.15.dist-info → simo-2.10.1.dist-info}/RECORD +64 -52
- {simo-2.8.15.dist-info → simo-2.10.1.dist-info}/WHEEL +0 -0
- {simo-2.8.15.dist-info → simo-2.10.1.dist-info}/entry_points.txt +0 -0
- {simo-2.8.15.dist-info → simo-2.10.1.dist-info}/licenses/LICENSE.md +0 -0
- {simo-2.8.15.dist-info → simo-2.10.1.dist-info}/top_level.txt +0 -0
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.'
|