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.
- simo/core/__pycache__/app_widgets.cpython-38.pyc +0 -0
- simo/core/__pycache__/base_types.cpython-38.pyc +0 -0
- simo/core/__pycache__/controllers.cpython-38.pyc +0 -0
- simo/core/__pycache__/signal_receivers.cpython-38.pyc +0 -0
- simo/core/__pycache__/tasks.cpython-38.pyc +0 -0
- simo/core/app_widgets.py +19 -1
- simo/core/base_types.py +2 -0
- simo/core/controllers.py +157 -6
- simo/core/management/_hub_template/hub/supervisor.conf +4 -0
- simo/core/management/update.py +5 -3
- simo/core/signal_receivers.py +8 -5
- simo/core/tasks.py +1 -1
- simo/fleet/__pycache__/controllers.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/forms.cpython-38.pyc +0 -0
- simo/fleet/controllers.py +20 -119
- simo/fleet/forms.py +101 -0
- simo/generic/__pycache__/app_widgets.cpython-38.pyc +0 -0
- simo/generic/__pycache__/base_types.cpython-38.pyc +0 -0
- simo/generic/__pycache__/controllers.cpython-38.pyc +0 -0
- simo/generic/__pycache__/forms.cpython-38.pyc +0 -0
- simo/generic/__pycache__/gateways.cpython-38.pyc +0 -0
- simo/generic/app_widgets.py +0 -18
- simo/generic/base_types.py +0 -2
- simo/generic/controllers.py +2 -262
- simo/generic/forms.py +0 -49
- simo/generic/gateways.py +8 -118
- simo/generic/scripting/__pycache__/helpers.cpython-38.pyc +0 -0
- simo/generic/scripting/example.py +67 -0
- simo/generic/scripting/helpers.py +66 -10
- simo/users/__pycache__/admin.cpython-38.pyc +0 -0
- simo/users/__pycache__/api.cpython-38.pyc +0 -0
- simo/users/__pycache__/models.cpython-38.pyc +0 -0
- simo/users/admin.py +25 -5
- simo/users/api.py +25 -11
- simo/users/migrations/0035_instanceuser_last_seen_speed_kmh_and_more.py +23 -0
- simo/users/migrations/0036_instanceuser_phone_on_charge_user_phone_on_charge.py +23 -0
- simo/users/migrations/0037_rename_last_seen_location_datetime_instanceuser_last_seen_and_more.py +53 -0
- simo/users/migrations/__pycache__/0035_instanceuser_last_seen_speed_kmh_and_more.cpython-38.pyc +0 -0
- simo/users/migrations/__pycache__/0036_instanceuser_phone_on_charge_user_phone_on_charge.cpython-38.pyc +0 -0
- simo/users/migrations/__pycache__/0037_rename_last_seen_location_datetime_instanceuser_last_seen_and_more.cpython-38.pyc +0 -0
- simo/users/models.py +12 -55
- {simo-2.5.2.dist-info → simo-2.5.4.dist-info}/METADATA +1 -1
- {simo-2.5.2.dist-info → simo-2.5.4.dist-info}/RECORD +47 -41
- {simo-2.5.2.dist-info → simo-2.5.4.dist-info}/WHEEL +1 -1
- simo/scripting.py +0 -39
- {simo-2.5.2.dist-info → simo-2.5.4.dist-info}/LICENSE.md +0 -0
- {simo-2.5.2.dist-info → simo-2.5.4.dist-info}/entry_points.txt +0 -0
- {simo-2.5.2.dist-info → simo-2.5.4.dist-info}/top_level.txt +0 -0
simo/generic/app_widgets.py
CHANGED
|
@@ -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')
|
simo/generic/base_types.py
CHANGED
|
@@ -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"),
|
simo/generic/controllers.py
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
|
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,
|
|
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(
|
|
Binary file
|
|
@@ -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):
|