simo 2.5.2__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 (48) 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 +157 -6
  9. simo/core/management/_hub_template/hub/supervisor.conf +4 -0
  10. simo/core/management/update.py +5 -3
  11. simo/core/signal_receivers.py +8 -5
  12. simo/core/tasks.py +1 -1
  13. simo/fleet/__pycache__/controllers.cpython-38.pyc +0 -0
  14. simo/fleet/__pycache__/forms.cpython-38.pyc +0 -0
  15. simo/fleet/controllers.py +20 -119
  16. simo/fleet/forms.py +101 -0
  17. simo/generic/__pycache__/app_widgets.cpython-38.pyc +0 -0
  18. simo/generic/__pycache__/base_types.cpython-38.pyc +0 -0
  19. simo/generic/__pycache__/controllers.cpython-38.pyc +0 -0
  20. simo/generic/__pycache__/forms.cpython-38.pyc +0 -0
  21. simo/generic/__pycache__/gateways.cpython-38.pyc +0 -0
  22. simo/generic/app_widgets.py +0 -18
  23. simo/generic/base_types.py +0 -2
  24. simo/generic/controllers.py +2 -262
  25. simo/generic/forms.py +0 -49
  26. simo/generic/gateways.py +8 -118
  27. simo/generic/scripting/__pycache__/helpers.cpython-38.pyc +0 -0
  28. simo/generic/scripting/example.py +67 -0
  29. simo/generic/scripting/helpers.py +66 -10
  30. simo/users/__pycache__/admin.cpython-38.pyc +0 -0
  31. simo/users/__pycache__/api.cpython-38.pyc +0 -0
  32. simo/users/__pycache__/models.cpython-38.pyc +0 -0
  33. simo/users/admin.py +25 -5
  34. simo/users/api.py +25 -11
  35. simo/users/migrations/0035_instanceuser_last_seen_speed_kmh_and_more.py +23 -0
  36. simo/users/migrations/0036_instanceuser_phone_on_charge_user_phone_on_charge.py +23 -0
  37. simo/users/migrations/0037_rename_last_seen_location_datetime_instanceuser_last_seen_and_more.py +53 -0
  38. simo/users/migrations/__pycache__/0035_instanceuser_last_seen_speed_kmh_and_more.cpython-38.pyc +0 -0
  39. simo/users/migrations/__pycache__/0036_instanceuser_phone_on_charge_user_phone_on_charge.cpython-38.pyc +0 -0
  40. simo/users/migrations/__pycache__/0037_rename_last_seen_location_datetime_instanceuser_last_seen_and_more.cpython-38.pyc +0 -0
  41. simo/users/models.py +12 -55
  42. {simo-2.5.2.dist-info → simo-2.5.4.dist-info}/METADATA +1 -1
  43. {simo-2.5.2.dist-info → simo-2.5.4.dist-info}/RECORD +47 -41
  44. {simo-2.5.2.dist-info → simo-2.5.4.dist-info}/WHEEL +1 -1
  45. simo/scripting.py +0 -39
  46. {simo-2.5.2.dist-info → simo-2.5.4.dist-info}/LICENSE.md +0 -0
  47. {simo-2.5.2.dist-info → simo-2.5.4.dist-info}/entry_points.txt +0 -0
  48. {simo-2.5.2.dist-info → simo-2.5.4.dist-info}/top_level.txt +0 -0
@@ -38,24 +38,6 @@ class WeatherForecastWidget(BaseAppWidget):
38
38
  size = [4, 2]
39
39
 
40
40
 
41
- class GateWidget(BaseAppWidget):
42
- uid = 'gate'
43
- name = _('Gate')
44
- size = [2, 1]
45
-
46
-
47
- class BlindsWidget(BaseAppWidget):
48
- uid = 'blinds'
49
- name = _('Blinds')
50
- size = [4, 1]
51
-
52
-
53
- class SlidesWidget(BaseAppWidget):
54
- uid = 'slides'
55
- name = _('Slides')
56
- size = [2, 1]
57
-
58
-
59
41
  class WateringWidget(BaseAppWidget):
60
42
  uid = 'watering'
61
43
  name = _('Watering')
@@ -6,8 +6,6 @@ BASE_TYPES = {
6
6
  'alarm-group': _("Alarm Group"),
7
7
  'ip-camera': _("IP Camera"),
8
8
  'weather-forecast': _("Weather Forecast"),
9
- 'gate': _("Gate"),
10
- 'blinds': _("Blinds"),
11
9
  'watering': _("Watering"),
12
10
  'state-select': _("State Select"),
13
11
  'alarm-clock': _("Alarm Clock"),
@@ -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):
@@ -145,7 +89,10 @@ class ScriptRunHandler(multiprocessing.Process):
145
89
  else:
146
90
  code = self.component.config.get('code')
147
91
  def run_code():
92
+ start = time.time()
148
93
  exec(code, globals())
94
+ if 'class Automation:' in code and time.time() - start < 1:
95
+ Automation().run()
149
96
 
150
97
  if not code:
151
98
  self.component.value = 'finished'
@@ -171,7 +118,6 @@ class GenericGatewayHandler(BaseObjectCommandsGatewayHandler):
171
118
  config_form = BaseGatewayForm
172
119
 
173
120
  running_scripts = {}
174
- blinds_runners = {}
175
121
  periodic_tasks = (
176
122
  ('watch_thermostats', 60),
177
123
  ('watch_alarm_clocks', 30),
@@ -204,6 +150,9 @@ class GenericGatewayHandler(BaseObjectCommandsGatewayHandler):
204
150
  dead_processes = []
205
151
  for id, process in self.running_scripts.items():
206
152
  if process.is_alive():
153
+ if not Component.objects.filter(id=id).count():
154
+ # script is deleted.
155
+ process.terminate()
207
156
  continue
208
157
  component = Component.objects.filter(id=id).exclude(
209
158
  value__in=('error', 'finished')
@@ -267,7 +216,7 @@ class GenericGatewayHandler(BaseObjectCommandsGatewayHandler):
267
216
  component.save()
268
217
 
269
218
  # Start scripts that are designed to be autostarted
270
- # 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
271
220
  # got terminated unexpectedly
272
221
  for script in Component.objects.filter(
273
222
  base_type='script',
@@ -288,9 +237,6 @@ class GenericGatewayHandler(BaseObjectCommandsGatewayHandler):
288
237
  mqtt_client.loop()
289
238
  mqtt_client.disconnect()
290
239
 
291
- for id, runner in self.blinds_runners.items():
292
- runner.terminate()
293
-
294
240
  script_ids = [id for id in self.running_scripts.keys()]
295
241
  for id in script_ids:
296
242
  self.stop_script(
@@ -307,7 +253,7 @@ class GenericGatewayHandler(BaseObjectCommandsGatewayHandler):
307
253
  def on_mqtt_message(self, client, userdata, msg):
308
254
  print("Mqtt message: ", msg.payload)
309
255
  from simo.generic.controllers import (
310
- Script, Blinds, AlarmGroup, Gate
256
+ Script, AlarmGroup
311
257
  )
312
258
  payload = json.loads(msg.payload)
313
259
  component = get_event_obj(payload, Component)
@@ -320,12 +266,8 @@ class GenericGatewayHandler(BaseObjectCommandsGatewayHandler):
320
266
  elif payload.get('set_val') == 'stop':
321
267
  self.stop_script(component)
322
268
  return
323
- elif component.controller_uid == Blinds.uid:
324
- self.control_blinds(component, payload.get('set_val'))
325
269
  elif component.controller_uid == AlarmGroup.uid:
326
270
  self.control_alarm_group(component, payload.get('set_val'))
327
- elif component.controller_uid == Gate:
328
- self.control_gate(component, payload.get('set_val'))
329
271
  else:
330
272
  component.controller.set(payload.get('set_val'))
331
273
  except Exception:
@@ -382,43 +324,6 @@ class GenericGatewayHandler(BaseObjectCommandsGatewayHandler):
382
324
 
383
325
  threading.Thread(target=kill, daemon=True).start()
384
326
 
385
- def control_blinds(self, blinds, target):
386
- try:
387
- open_switch = Component.objects.get(
388
- pk=blinds.config['open_switch']
389
- )
390
- close_switch = Component.objects.get(
391
- pk=blinds.config['close_switch']
392
- )
393
- except:
394
- return
395
-
396
- blinds.set({'target': target})
397
-
398
- blinds_runner = self.blinds_runners.get(blinds.id)
399
- if blinds_runner:
400
- blinds_runner.terminate()
401
-
402
- if target == -1:
403
- open_switch.turn_off()
404
- close_switch.turn_off()
405
-
406
- elif target != blinds.value['position']:
407
- try:
408
- self.blinds_runners[blinds.id] = BlindsRunner(blinds)
409
- self.blinds_runners[blinds.id].daemon = True
410
- except:
411
- pass
412
- else:
413
- if target > blinds.value['position']:
414
- close_switch.turn_off()
415
- open_switch.turn_on()
416
- else:
417
- open_switch.turn_off()
418
- close_switch.turn_on()
419
-
420
- self.blinds_runners[blinds.id].start()
421
-
422
327
  def control_alarm_group(self, alarm_group, value):
423
328
  from simo.generic.controllers import AlarmGroup
424
329
 
@@ -461,21 +366,6 @@ class GenericGatewayHandler(BaseObjectCommandsGatewayHandler):
461
366
  other_group.refresh_status()
462
367
 
463
368
 
464
- def control_gate(self, gate, value):
465
- switch = Component.objects.filter(
466
- pk=gate.config.get('action_switch')
467
- ).first()
468
- if not switch:
469
- return
470
-
471
- if gate.config.get('action_method') == 'click':
472
- switch.click()
473
- else:
474
- if value == 'open':
475
- switch.turn_on()
476
- else:
477
- switch.turn_off()
478
-
479
369
  def watch_alarm_events(self):
480
370
  from .controllers import AlarmGroup
481
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):