simo 2.5.3__py3-none-any.whl → 2.5.4__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.

Files changed (47) hide show
  1. simo/core/__pycache__/app_widgets.cpython-38.pyc +0 -0
  2. simo/core/__pycache__/base_types.cpython-38.pyc +0 -0
  3. simo/core/__pycache__/controllers.cpython-38.pyc +0 -0
  4. simo/core/__pycache__/signal_receivers.cpython-38.pyc +0 -0
  5. simo/core/__pycache__/tasks.cpython-38.pyc +0 -0
  6. simo/core/app_widgets.py +19 -1
  7. simo/core/base_types.py +2 -0
  8. simo/core/controllers.py +154 -4
  9. simo/core/management/_hub_template/hub/supervisor.conf +4 -0
  10. simo/core/signal_receivers.py +8 -5
  11. simo/core/tasks.py +1 -1
  12. simo/fleet/__pycache__/controllers.cpython-38.pyc +0 -0
  13. simo/fleet/__pycache__/forms.cpython-38.pyc +0 -0
  14. simo/fleet/controllers.py +20 -119
  15. simo/fleet/forms.py +101 -0
  16. simo/generic/__pycache__/app_widgets.cpython-38.pyc +0 -0
  17. simo/generic/__pycache__/base_types.cpython-38.pyc +0 -0
  18. simo/generic/__pycache__/controllers.cpython-38.pyc +0 -0
  19. simo/generic/__pycache__/forms.cpython-38.pyc +0 -0
  20. simo/generic/__pycache__/gateways.cpython-38.pyc +0 -0
  21. simo/generic/app_widgets.py +0 -18
  22. simo/generic/base_types.py +0 -2
  23. simo/generic/controllers.py +2 -262
  24. simo/generic/forms.py +0 -49
  25. simo/generic/gateways.py +5 -118
  26. simo/generic/scripting/__pycache__/helpers.cpython-38.pyc +0 -0
  27. simo/generic/scripting/example.py +67 -0
  28. simo/generic/scripting/helpers.py +66 -10
  29. simo/users/__pycache__/admin.cpython-38.pyc +0 -0
  30. simo/users/__pycache__/api.cpython-38.pyc +0 -0
  31. simo/users/__pycache__/models.cpython-38.pyc +0 -0
  32. simo/users/admin.py +25 -5
  33. simo/users/api.py +25 -11
  34. simo/users/migrations/0035_instanceuser_last_seen_speed_kmh_and_more.py +23 -0
  35. simo/users/migrations/0036_instanceuser_phone_on_charge_user_phone_on_charge.py +23 -0
  36. simo/users/migrations/0037_rename_last_seen_location_datetime_instanceuser_last_seen_and_more.py +53 -0
  37. simo/users/migrations/__pycache__/0035_instanceuser_last_seen_speed_kmh_and_more.cpython-38.pyc +0 -0
  38. simo/users/migrations/__pycache__/0036_instanceuser_phone_on_charge_user_phone_on_charge.cpython-38.pyc +0 -0
  39. simo/users/migrations/__pycache__/0037_rename_last_seen_location_datetime_instanceuser_last_seen_and_more.cpython-38.pyc +0 -0
  40. simo/users/models.py +12 -55
  41. {simo-2.5.3.dist-info → simo-2.5.4.dist-info}/METADATA +1 -1
  42. {simo-2.5.3.dist-info → simo-2.5.4.dist-info}/RECORD +46 -40
  43. {simo-2.5.3.dist-info → simo-2.5.4.dist-info}/WHEEL +1 -1
  44. simo/scripting.py +0 -39
  45. {simo-2.5.3.dist-info → simo-2.5.4.dist-info}/LICENSE.md +0 -0
  46. {simo-2.5.3.dist-info → simo-2.5.4.dist-info}/entry_points.txt +0 -0
  47. {simo-2.5.3.dist-info → simo-2.5.4.dist-info}/top_level.txt +0 -0
@@ -37,14 +37,14 @@ from simo.core.utils.config_values import (
37
37
  from .gateways import GenericGatewayHandler, DummyGatewayHandler
38
38
  from .app_widgets import (
39
39
  ScriptWidget, ThermostatWidget, AlarmGroupWidget, IPCameraWidget,
40
- WeatherForecastWidget, GateWidget, BlindsWidget, SlidesWidget,
40
+ WeatherForecastWidget,
41
41
  WateringWidget, StateSelectWidget, AlarmClockWidget
42
42
  )
43
43
  from .forms import (
44
44
  ScriptConfigForm, PresenceLightingConfigForm,
45
45
  ThermostatConfigForm, AlarmGroupConfigForm,
46
46
  IPCameraConfigForm, WeatherForecastForm, GateConfigForm,
47
- BlindsConfigForm, WateringConfigForm, StateSelectForm,
47
+ WateringConfigForm, StateSelectForm,
48
48
  AlarmClockConfigForm
49
49
  )
50
50
  from .scripting import get_current_state
@@ -558,266 +558,6 @@ class IPCamera(ControllerBase):
558
558
  )
559
559
 
560
560
 
561
- class Gate(ControllerBase, TimerMixin):
562
- name = _("Gate")
563
- base_type = 'gate'
564
- gateway_class = GenericGatewayHandler
565
- app_widget = GateWidget
566
- config_form = GateConfigForm
567
- admin_widget_template = 'admin/controller_widgets/gate.html'
568
- default_config = {}
569
-
570
- @property
571
- def default_value(self):
572
- return 'closed'
573
-
574
- def _validate_val(self, value, occasion=None):
575
- if occasion == BEFORE_SEND:
576
- if self.component.config.get('action_method') == 'click':
577
- if value != 'call':
578
- raise ValidationError(
579
- 'Gate component understands only one command: '
580
- '"call". You have provided: "%s"' % (str(value))
581
- )
582
- else:
583
- if value not in ('call', 'open', 'close'):
584
- raise ValidationError(
585
- 'This gate component understands only 3 commands: '
586
- '"open", "close" and "call". You have provided: "%s"' %
587
- (str(value))
588
- )
589
- elif occasion == BEFORE_SET and value not in (
590
- 'closed', 'open', 'open_moving', 'closed_moving'
591
- ):
592
- raise ValidationError(
593
- 'Gate component can only be in 4 states: '
594
- '"closed", "closed", "open_moving", "closed_moving". '
595
- 'You have provided: "%s"' % (str(value))
596
- )
597
- return value
598
-
599
- def _set_on_the_move(self):
600
- def cancel_move():
601
- start_value = self.component.value
602
- start_sensor_value = self.component.config.get('sensor_value')
603
- move_duration = self.component.config.get(
604
- 'gate_open_duration', 30
605
- ) * 1000
606
- # stay in moving state for user defined amount of seconds
607
- time.sleep(move_duration / 1000)
608
- self.component.refresh_from_db()
609
- if time.time() - self.component.config.get('last_call', 0) \
610
- < move_duration / 1000:
611
- # There was another call in between of this wait,
612
- # so we must skip this in favor of the new cancel_move function
613
- # that is currently running in parallel.
614
- return
615
-
616
- # If it is no longer on the move this process becomes obsolete
617
- # For example when open/close binary sensor detects closed event
618
- # gate value is immediately set to closed.
619
- if not self.component.value.endswith('moving'):
620
- return
621
-
622
- # Started from closed, sensor already picked up open event
623
- # therefore this must now be considered as open.
624
- if start_value.startswith('closed') \
625
- and self.component.value == 'open_moving' \
626
- and self.component.config.get('sensor_value'):
627
- self.component.set('open')
628
- return
629
-
630
- # In all other occasions we wait for another move_duration
631
- # and finish move anyways.
632
- time.sleep(move_duration / 1000)
633
- self.component.refresh_from_db()
634
- if self.component.value.endswith('moving'):
635
- self.component.set(self.component.value[:-7])
636
-
637
- self.component.refresh_from_db()
638
- self.component.config['last_call'] = time.time()
639
- self.component.save(update_fields=['config'])
640
-
641
- if not self.component.value.endswith('_moving'):
642
- self.component.set(self.component.value + '_moving')
643
- threading.Thread(target=cancel_move, daemon=True).start()
644
-
645
- def open(self):
646
- self.send('open')
647
-
648
- def close(self):
649
- self.send('close')
650
-
651
- def call(self):
652
- self.send('call')
653
-
654
- # TODO: This was in gateway class, however it
655
- # needs to be moved here or part of it back to the gateway
656
- # as we no longer have Event object.
657
- # if msg.topic == Event.TOPIC:
658
- # if isinstance(component.controller, Switch):
659
- # value_change = payload['data'].get('value')
660
- # if not value_change:
661
- # return
662
- #
663
- # # Handle Gate switches
664
- # for gate in Component.objects.filter(
665
- # controller_uid=Gate.uid, config__action_switch=component.id
666
- # ):
667
- # if gate.config.get('action_method') == 'toggle':
668
- # gate.controller._set_on_the_move()
669
- # else:
670
- # if value_change.get('new') == False:
671
- # # Button released
672
- # # set stopped position if it was moving, or set moving if not.
673
- # if gate.value.endswith('moving'):
674
- # if gate.config.get('sensor_value'):
675
- # gate.set('open')
676
- # else:
677
- # gate.set('closed')
678
- # else:
679
- # gate.controller._set_on_the_move()
680
- #
681
- # return
682
- #
683
- # elif isinstance(component.controller, BinarySensor):
684
- # value_change = payload['data'].get('value')
685
- # if not value_change:
686
- # return
687
- # # Handle Gate binary sensors
688
- # for gate in Component.objects.filter(
689
- # controller_uid=Gate.uid,
690
- # config__open_closed_sensor=component.id
691
- # ):
692
- # gate.config['sensor_value'] = component.value
693
- # gate.save(update_fields=['config'])
694
- # # If sensor goes from False to True, while gate is moving
695
- # # it usually means that gate just started the move and must stay in the move
696
- # # user defined amount of seconds to represent actual gate movement.
697
- # # Open state therefore is reached only after user defined duration.
698
- # # If it was not in the move, then it simply means that it was
699
- # # opened in some other way and we set it to open immediately.
700
- # if component.value:
701
- # if gate.value.endswith('moving'):
702
- # print("SET OPEN MOVING!")
703
- # gate.set('open_moving')
704
- # else:
705
- # gate.set('open')
706
- # # if binary sensor detects gate close event
707
- # # we set gate value to closed immediately as it means that
708
- # # gate is now truly closed and no longer moving.
709
- # else:
710
- # gate.set('closed')
711
-
712
-
713
- class Blinds(ControllerBase, TimerMixin):
714
- name = _("Blind")
715
- base_type = 'blinds'
716
- gateway_class = GenericGatewayHandler
717
- config_form = BlindsConfigForm
718
- admin_widget_template = 'admin/controller_widgets/blinds.html'
719
- default_config = {}
720
-
721
- @property
722
- def app_widget(self):
723
- if self.component.config.get('control_mode') == 'slide':
724
- return SlidesWidget
725
- else:
726
- return BlindsWidget
727
-
728
- @property
729
- def default_value(self):
730
- # target and current positions in milliseconds, angle in degrees (0 - 180)
731
- return {'target': 0, 'position': 0, 'angle': 0}
732
-
733
- def _validate_val(self, value, occasion=None):
734
-
735
- if occasion == BEFORE_SEND:
736
- if isinstance(value, int) or isinstance(value, float):
737
- # legacy support
738
- value = {'target': int(value)}
739
- if 'target' not in value:
740
- raise ValidationError("Target value is required!")
741
- target = value.get('target')
742
- if type(target) not in (float, int):
743
- raise ValidationError(
744
- "Bad target position for blinds to go."
745
- )
746
- if target > self.component.config.get('open_duration') * 1000:
747
- raise ValidationError(
748
- "Target value lower than %d expected, "
749
- "%d received instead" % (
750
- self.component.config['open_duration'] * 1000,
751
- target
752
- )
753
- )
754
- if 'angle' in value:
755
- try:
756
- angle = int(value['angle'])
757
- except:
758
- raise ValidationError(
759
- "Integer between 0 - 180 is required for blinds angle."
760
- )
761
- if angle < 0 or angle > 180:
762
- raise ValidationError(
763
- "Integer between 0 - 180 is required for blinds angle."
764
- )
765
- else:
766
- value['angle'] = self.component.value.get('angle', 0)
767
-
768
- elif occasion == BEFORE_SET:
769
- if not isinstance(value, dict):
770
- raise ValidationError("Dictionary is expected")
771
- for key, val in value.items():
772
- if key not in ('target', 'position', 'angle'):
773
- raise ValidationError(
774
- "'target', 'position' or 'angle' parameters are expected."
775
- )
776
- if key == 'position':
777
- if val < 0:
778
- raise ValidationError(
779
- "Positive integer expected for blind position"
780
- )
781
- if val > self.component.config.get('open_duration') * 1000:
782
- raise ValidationError(
783
- "Positive value is to big. Must be lower than %d, "
784
- "but you have provided %d" % (
785
- self.component.config.get('open_duration') * 1000, val
786
- )
787
- )
788
-
789
- self.component.refresh_from_db()
790
- if 'target' not in value:
791
- value['target'] = self.component.value.get('target')
792
- if 'position' not in value:
793
- value['position'] = self.component.value.get('position')
794
- if 'angle' not in value:
795
- value['angle'] = self.component.value.get('angle')
796
-
797
- return value
798
-
799
- def open(self):
800
- send_val = {'target': 0}
801
- angle = self.component.value.get('angle')
802
- if angle is not None and 0 <= angle <= 180:
803
- send_val['angle'] = angle
804
- self.send(send_val)
805
-
806
- def close(self):
807
- send_val = {'target': self.component.config['open_duration'] * 1000}
808
- angle = self.component.value.get('angle')
809
- if angle is not None and 0 <= angle <= 180:
810
- send_val['angle'] = angle
811
- self.send(send_val)
812
-
813
- def stop(self):
814
- send_val = {'target': -1}
815
- angle = self.component.value.get('angle')
816
- if angle is not None and 0 <= angle <= 180:
817
- send_val['angle'] = angle
818
- self.send(send_val)
819
-
820
-
821
561
  class Watering(ControllerBase):
822
562
  STATUS_CHOICES = (
823
563
  'stopped', 'running_program', 'running_custom',
simo/generic/forms.py CHANGED
@@ -620,55 +620,6 @@ class GateConfigForm(BaseComponentForm):
620
620
  )
621
621
 
622
622
 
623
- class BlindsConfigForm(BaseComponentForm):
624
- open_switch = forms.ModelChoiceField(
625
- Component.objects.filter(base_type=Switch.base_type),
626
- widget=autocomplete.ModelSelect2(
627
- url='autocomplete-component', attrs={'data-html': True},
628
- forward=(
629
- forward.Const([Switch.base_type], 'base_type'),
630
- )
631
- )
632
- )
633
- close_switch = forms.ModelChoiceField(
634
- Component.objects.filter(base_type=Switch.base_type),
635
- widget=autocomplete.ModelSelect2(
636
- url='autocomplete-component', attrs={'data-html': True},
637
- forward=(
638
- forward.Const([Switch.base_type], 'base_type'),
639
- )
640
- )
641
- )
642
- open_direction = forms.ChoiceField(
643
- label='Closed > Open direction',
644
- required=True, choices=(
645
- ('up', "Up"), ('down', "Down"),
646
- ('right', "Right"), ('left', "Left")
647
- ),
648
- help_text="Move direction from fully closed to fully open."
649
-
650
- )
651
- open_duration = forms.FloatField(
652
- label='Open duration', min_value=0.001, max_value=360000,
653
- initial=30,
654
- help_text="Time in seconds it takes for your blinds to go "
655
- "from fully closed to fully open."
656
- )
657
- slats_angle_duration = forms.FloatField(
658
- label='Slats angle duration', min_value=0.01, max_value=360000,
659
- required=False,
660
- help_text="Takes effect only with App control mode - 'Slide', "
661
- "can be used with slat blinds to control slats angle. <br>"
662
- "Time in seconds it takes "
663
- "to go from fully closed to the start of open movement. <br>"
664
- "Usually it's in between of 1 - 3 seconds."
665
- )
666
- control_mode = forms.ChoiceField(
667
- label="App control mode", required=True, choices=(
668
- ('click', "Click"), ('hold', "Hold"), ('slide', "Slide")
669
- ),
670
- )
671
-
672
623
 
673
624
  class ContourForm(forms.Form):
674
625
  uid = forms.CharField(widget=forms.HiddenInput(), required=False)
simo/generic/gateways.py CHANGED
@@ -20,62 +20,6 @@ from simo.core.events import GatewayObjectCommand, get_event_obj
20
20
  from simo.core.loggers import get_gw_logger, get_component_logger
21
21
 
22
22
 
23
-
24
- class BlindsRunner(threading.Thread):
25
-
26
- def __init__(self, blinds, *args, **kwargs):
27
- self.blinds = blinds
28
- self.target = self.blinds.value['target']
29
- self.position = self.blinds.value['position']
30
- self.open_duration = self.blinds.config.get('open_duration', 0) * 1000
31
- assert self.target >= -1
32
- assert self.target <= self.open_duration
33
- self.exit = multiprocessing.Event()
34
- super().__init__(*args, **kwargs)
35
-
36
- def run(self):
37
- try:
38
- self.open_switch = Component.objects.get(
39
- pk=self.blinds.config.get('open_switch')
40
- )
41
- self.close_switch = Component.objects.get(
42
- pk=self.blinds.config.get('close_switch')
43
- )
44
- except:
45
- self.done = True
46
- return
47
- self.start_position = self.blinds.value['position']
48
- self.position = self.blinds.value['position']
49
- self.start_time = time.time()
50
- self.last_save = time.time()
51
- while not self.exit.is_set():
52
- change = (time.time() - self.start_time) * 1000
53
- if self.target > self.start_position:
54
- self.position = self.start_position + change
55
- if self.position >= self.target:
56
- self.blinds.set(
57
- {'position': self.target, 'target': -1}
58
- )
59
- self.open_switch.turn_off()
60
- self.close_switch.turn_off()
61
- return
62
- else:
63
- self.position = self.start_position - change
64
- if self.position < self.target:
65
- self.blinds.set({'position': self.target, 'target': -1})
66
- self.open_switch.turn_off()
67
- self.close_switch.turn_off()
68
- return
69
-
70
- if self.last_save < time.time() - 1:
71
- self.blinds.set({'position': self.position})
72
- self.last_save = time.time()
73
- time.sleep(0.01)
74
-
75
- def terminate(self):
76
- self.exit.set()
77
-
78
-
79
23
  class CameraWatcher(threading.Thread):
80
24
 
81
25
  def __init__(self, component_id, exit, *args, **kwargs):
@@ -174,7 +118,6 @@ class GenericGatewayHandler(BaseObjectCommandsGatewayHandler):
174
118
  config_form = BaseGatewayForm
175
119
 
176
120
  running_scripts = {}
177
- blinds_runners = {}
178
121
  periodic_tasks = (
179
122
  ('watch_thermostats', 60),
180
123
  ('watch_alarm_clocks', 30),
@@ -207,6 +150,9 @@ class GenericGatewayHandler(BaseObjectCommandsGatewayHandler):
207
150
  dead_processes = []
208
151
  for id, process in self.running_scripts.items():
209
152
  if process.is_alive():
153
+ if not Component.objects.filter(id=id).count():
154
+ # script is deleted.
155
+ process.terminate()
210
156
  continue
211
157
  component = Component.objects.filter(id=id).exclude(
212
158
  value__in=('error', 'finished')
@@ -270,7 +216,7 @@ class GenericGatewayHandler(BaseObjectCommandsGatewayHandler):
270
216
  component.save()
271
217
 
272
218
  # Start scripts that are designed to be autostarted
273
- # as well as those who are designed to be kept alive, but
219
+ # as well as those that are designed to be kept alive, but
274
220
  # got terminated unexpectedly
275
221
  for script in Component.objects.filter(
276
222
  base_type='script',
@@ -291,9 +237,6 @@ class GenericGatewayHandler(BaseObjectCommandsGatewayHandler):
291
237
  mqtt_client.loop()
292
238
  mqtt_client.disconnect()
293
239
 
294
- for id, runner in self.blinds_runners.items():
295
- runner.terminate()
296
-
297
240
  script_ids = [id for id in self.running_scripts.keys()]
298
241
  for id in script_ids:
299
242
  self.stop_script(
@@ -310,7 +253,7 @@ class GenericGatewayHandler(BaseObjectCommandsGatewayHandler):
310
253
  def on_mqtt_message(self, client, userdata, msg):
311
254
  print("Mqtt message: ", msg.payload)
312
255
  from simo.generic.controllers import (
313
- Script, Blinds, AlarmGroup, Gate
256
+ Script, AlarmGroup
314
257
  )
315
258
  payload = json.loads(msg.payload)
316
259
  component = get_event_obj(payload, Component)
@@ -323,12 +266,8 @@ class GenericGatewayHandler(BaseObjectCommandsGatewayHandler):
323
266
  elif payload.get('set_val') == 'stop':
324
267
  self.stop_script(component)
325
268
  return
326
- elif component.controller_uid == Blinds.uid:
327
- self.control_blinds(component, payload.get('set_val'))
328
269
  elif component.controller_uid == AlarmGroup.uid:
329
270
  self.control_alarm_group(component, payload.get('set_val'))
330
- elif component.controller_uid == Gate:
331
- self.control_gate(component, payload.get('set_val'))
332
271
  else:
333
272
  component.controller.set(payload.get('set_val'))
334
273
  except Exception:
@@ -385,43 +324,6 @@ class GenericGatewayHandler(BaseObjectCommandsGatewayHandler):
385
324
 
386
325
  threading.Thread(target=kill, daemon=True).start()
387
326
 
388
- def control_blinds(self, blinds, target):
389
- try:
390
- open_switch = Component.objects.get(
391
- pk=blinds.config['open_switch']
392
- )
393
- close_switch = Component.objects.get(
394
- pk=blinds.config['close_switch']
395
- )
396
- except:
397
- return
398
-
399
- blinds.set({'target': target})
400
-
401
- blinds_runner = self.blinds_runners.get(blinds.id)
402
- if blinds_runner:
403
- blinds_runner.terminate()
404
-
405
- if target == -1:
406
- open_switch.turn_off()
407
- close_switch.turn_off()
408
-
409
- elif target != blinds.value['position']:
410
- try:
411
- self.blinds_runners[blinds.id] = BlindsRunner(blinds)
412
- self.blinds_runners[blinds.id].daemon = True
413
- except:
414
- pass
415
- else:
416
- if target > blinds.value['position']:
417
- close_switch.turn_off()
418
- open_switch.turn_on()
419
- else:
420
- open_switch.turn_off()
421
- close_switch.turn_on()
422
-
423
- self.blinds_runners[blinds.id].start()
424
-
425
327
  def control_alarm_group(self, alarm_group, value):
426
328
  from simo.generic.controllers import AlarmGroup
427
329
 
@@ -464,21 +366,6 @@ class GenericGatewayHandler(BaseObjectCommandsGatewayHandler):
464
366
  other_group.refresh_status()
465
367
 
466
368
 
467
- def control_gate(self, gate, value):
468
- switch = Component.objects.filter(
469
- pk=gate.config.get('action_switch')
470
- ).first()
471
- if not switch:
472
- return
473
-
474
- if gate.config.get('action_method') == 'click':
475
- switch.click()
476
- else:
477
- if value == 'open':
478
- switch.turn_on()
479
- else:
480
- switch.turn_off()
481
-
482
369
  def watch_alarm_events(self):
483
370
  from .controllers import AlarmGroup
484
371
  for alarm in Component.objects.filter(
@@ -0,0 +1,67 @@
1
+ import time
2
+ import random
3
+ import pytz
4
+ from datetime import datetime
5
+ from django.utils import timezone
6
+ from simo.core.middleware import get_current_instance
7
+ from simo.core.models import Component
8
+ from simo.users.models import InstanceUser
9
+ from simo.generic.scripting.helpers import LocalSun
10
+
11
+
12
+ class Automation:
13
+ REZIMAS_COMPONENT_ID = 130
14
+
15
+ def __init__(self):
16
+ self.instance = get_current_instance()
17
+ self.rezimas = Component.objects.get(id=self.REZIMAS_COMPONENT_ID)
18
+ self.sun = LocalSun(self.instance.location)
19
+ self.night_is_on = False
20
+
21
+ def check_owner_phones(self, rezimas, instance_users, datetime):
22
+ if not self.night_is_on:
23
+ if not (datetime.hour >= 22 or datetime.hour < 6):
24
+ return
25
+
26
+ for iuser in instance_users:
27
+ # skipping users that are not at home
28
+ if not iuser.at_home:
29
+ continue
30
+ if not iuser.phone_on_charge:
31
+ # at least one user's phone is not yet on charge
32
+ return
33
+ self.night_is_on = True
34
+ return 'night'
35
+ else:
36
+ # return new_rezimas diena only if there are still users
37
+ # at home, none of them have their phones on charge
38
+ # and current rezimas is still night
39
+ for iuser in instance_users:
40
+ # skipping users that are not at home
41
+ if not iuser.at_home:
42
+ continue
43
+ if iuser.phone_on_charge:
44
+ # at least one user's phone is still on charge
45
+ return
46
+ else:
47
+ self.night_is_on = False
48
+ if not self.night_is_on and rezimas.value == 'night':
49
+ return 'day'
50
+
51
+ def run(self):
52
+ while True:
53
+ instance_users = InstanceUser.objects.filter(
54
+ is_active=True, role__is_owner=True
55
+ )
56
+ self.rezimas.refresh_from_db()
57
+ new_rezimas = self.check_owner_phones(
58
+ self.rezimas, instance_users, timezone.localtime()
59
+ )
60
+ if new_rezimas:
61
+ self.rezimas.send(new_rezimas)
62
+
63
+ # randomize script load
64
+ time.sleep(random.randint(20, 40))
65
+
66
+
67
+ def test(self):
@@ -1,3 +1,5 @@
1
+ import pytz
2
+ import math
1
3
  from django.utils import timezone
2
4
  from suntime import Sun
3
5
  from simo.core.models import Instance
@@ -5,10 +7,12 @@ from simo.core.models import Instance
5
7
 
6
8
  class LocalSun(Sun):
7
9
 
8
- def __init__(self, instance=None):
9
- if not instance:
10
+ def __init__(self, location=None):
11
+ if not location:
10
12
  instance = Instance.objects.all().first()
11
- coordinates = instance.location.split(',')
13
+ coordinates = instance.location.split(',')
14
+ else:
15
+ coordinates = location.split(',')
12
16
  try:
13
17
  lat = float(coordinates[0])
14
18
  except:
@@ -19,17 +23,69 @@ class LocalSun(Sun):
19
23
  lon = 0
20
24
  super().__init__(lat, lon)
21
25
 
22
- def is_night(self):
23
- if timezone.now() > self.get_sunset_time():
26
+ def get_sunrise_time(self, localdatetime=None):
27
+ sunrise = super().get_sunrise_time(date=localdatetime)
28
+ if not localdatetime or not localdatetime.tzinfo:
29
+ return sunrise
30
+ return sunrise.astimezone(localdatetime.tzinfo)
31
+
32
+ def get_sunset_time(self, localdatetime=None):
33
+ sunset = super().get_sunset_time(date=localdatetime)
34
+ if not localdatetime or not localdatetime.tzinfo:
35
+ return sunset
36
+ return sunset.astimezone(localdatetime.tzinfo)
37
+
38
+ def _get_utc_datetime(self, localdatetime=None):
39
+ if not localdatetime:
40
+ utc_datetime = timezone.now()
41
+ else:
42
+ utc_datetime = localdatetime.astimezone(pytz.utc)
43
+ return utc_datetime
44
+
45
+ def is_night(self, localdatetime=None):
46
+ utc_datetime = self._get_utc_datetime(localdatetime)
47
+ if utc_datetime > self.get_sunset_time(utc_datetime):
24
48
  return True
25
- if timezone.now() < self.get_sunrise_time():
49
+ if utc_datetime < self.get_sunrise_time(utc_datetime):
26
50
  return True
27
51
  return False
28
52
 
29
- def seconds_to_sunset(self):
30
- return (self.get_sunset_time() - timezone.now()).total_seconds()
53
+ def seconds_to_sunset(self, localdatetime=None):
54
+ utc_datetime = self._get_utc_datetime(localdatetime)
55
+ return (self.get_sunset_time(utc_datetime) - utc_datetime).total_seconds()
56
+
57
+ def seconds_to_sunrise(self, localdatetime=None):
58
+ utc_datetime = self._get_utc_datetime(localdatetime)
59
+ return (self.get_sunrise_time(utc_datetime) - utc_datetime).total_seconds()
60
+
61
+
62
+ def haversine_distance(location1, location2, units_of_measure='metric'):
63
+ # Radius of Earth in meters
64
+ R = 6371000
65
+
66
+ # Unpack coordinates
67
+ lat1, lon1 = location1.split(',')
68
+ lat2, lon2 = location2.split(',')
69
+ lat1, lon1, lat2, lon2 = float(lat1), float(lon1), float(lat2), float(lon2)
70
+
71
+ # Convert latitude and longitude from degrees to radians
72
+ phi1 = math.radians(lat1)
73
+ phi2 = math.radians(lat2)
74
+ delta_phi = math.radians(lat2 - lat1)
75
+ delta_lambda = math.radians(lon2 - lon1)
76
+
77
+ # Haversine formula
78
+ a = math.sin(delta_phi / 2) ** 2 + math.cos(phi1) * math.cos(
79
+ phi2) * math.sin(delta_lambda / 2) ** 2
80
+ c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
31
81
 
32
- def seconds_to_sunrise(self):
33
- return (self.get_sunrise_time() - timezone.now()).total_seconds()
82
+ # Distance in meters
83
+ distance_meters = R * c
34
84
 
85
+ # Convert to feet if 'imperial' is chosen
86
+ if units_of_measure == 'imperial':
87
+ distance = distance_meters * 3.28084 # Convert meters to feet
88
+ else:
89
+ distance = distance_meters # Keep in meters for 'metric'
35
90
 
91
+ return distance