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.
- 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 +154 -4
- simo/core/management/_hub_template/hub/supervisor.conf +4 -0
- 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 +5 -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.3.dist-info → simo-2.5.4.dist-info}/METADATA +1 -1
- {simo-2.5.3.dist-info → simo-2.5.4.dist-info}/RECORD +46 -40
- {simo-2.5.3.dist-info → simo-2.5.4.dist-info}/WHEEL +1 -1
- simo/scripting.py +0 -39
- {simo-2.5.3.dist-info → simo-2.5.4.dist-info}/LICENSE.md +0 -0
- {simo-2.5.3.dist-info → simo-2.5.4.dist-info}/entry_points.txt +0 -0
- {simo-2.5.3.dist-info → simo-2.5.4.dist-info}/top_level.txt +0 -0
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):
|
|
@@ -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
|
|
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,
|
|
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(
|
|
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):
|
|
@@ -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,
|
|
9
|
-
if not
|
|
10
|
+
def __init__(self, location=None):
|
|
11
|
+
if not location:
|
|
10
12
|
instance = Instance.objects.all().first()
|
|
11
|
-
|
|
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
|
|
23
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
33
|
-
|
|
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
|