simo 2.5.41__py3-none-any.whl → 2.6.2__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/__pycache__/settings.cpython-38.pyc +0 -0
- simo/automation/__pycache__/__init__.cpython-38.pyc +0 -0
- simo/automation/__pycache__/app_widgets.cpython-38.pyc +0 -0
- simo/automation/__pycache__/controllers.cpython-38.pyc +0 -0
- simo/automation/__pycache__/forms.cpython-38.pyc +0 -0
- simo/automation/__pycache__/gateways.cpython-38.pyc +0 -0
- simo/automation/__pycache__/helpers.cpython-38.pyc +0 -0
- simo/{generic/scripting → automation}/__pycache__/serializers.cpython-38.pyc +0 -0
- simo/{generic/scripting/__pycache__/__init__.cpython-38.pyc → automation/__pycache__/state.cpython-38.pyc} +0 -0
- simo/automation/app_widgets.py +8 -0
- simo/automation/controllers.py +273 -0
- simo/automation/forms.py +290 -0
- simo/automation/gateways.py +257 -0
- simo/automation/migrations/0001_initial.py +39 -0
- simo/automation/migrations/0002_update_helpers_in_scripts.py +29 -0
- simo/automation/migrations/__init__.py +0 -0
- simo/automation/migrations/__pycache__/0001_initial.cpython-38.pyc +0 -0
- simo/automation/migrations/__pycache__/0002_update_helpers_in_scripts.cpython-38.pyc +0 -0
- simo/automation/migrations/__pycache__/__init__.cpython-38.pyc +0 -0
- simo/automation/templates/automations/auto_away.py +55 -0
- simo/automation/templates/automations/auto_state_script.py +31 -0
- simo/{core/templates/core/auto_night_day_script.py → automation/templates/automations/phones_sleep_script.py} +25 -13
- simo/core/__pycache__/admin.cpython-38.pyc +0 -0
- simo/core/__pycache__/api.cpython-38.pyc +0 -0
- simo/core/__pycache__/filters.cpython-38.pyc +0 -0
- simo/core/__pycache__/models.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/admin.py +7 -4
- simo/core/api.py +8 -1
- simo/core/filters.py +61 -0
- simo/core/management/_hub_template/hub/supervisor.conf +0 -1
- simo/core/signal_receivers.py +50 -17
- simo/core/utils/__pycache__/type_constants.cpython-38.pyc +0 -0
- simo/core/utils/type_constants.py +1 -1
- simo/fleet/__pycache__/api.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/controllers.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/forms.cpython-38.pyc +0 -0
- simo/fleet/__pycache__/serializers.cpython-38.pyc +0 -0
- simo/fleet/api.py +6 -0
- simo/fleet/controllers.py +1 -0
- simo/fleet/forms.py +22 -3
- simo/fleet/serializers.py +9 -1
- simo/generic/__pycache__/app_widgets.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 -6
- simo/generic/controllers.py +4 -260
- simo/generic/forms.py +2 -269
- simo/generic/gateways.py +4 -193
- simo/generic/migrations/0002_auto_20241126_0726.py +34 -0
- simo/generic/migrations/__pycache__/0002_auto_20241126_0726.cpython-38.pyc +0 -0
- simo/notifications/__pycache__/api.cpython-38.pyc +0 -0
- simo/notifications/api.py +1 -1
- simo/settings.py +1 -0
- simo/users/__pycache__/api.cpython-38.pyc +0 -0
- simo/users/api.py +1 -2
- {simo-2.5.41.dist-info → simo-2.6.2.dist-info}/METADATA +1 -1
- {simo-2.5.41.dist-info → simo-2.6.2.dist-info}/RECORD +69 -51
- simo/core/templates/core/auto_state_script.py +0 -78
- simo/generic/scripting/__pycache__/helpers.cpython-38.pyc +0 -0
- /simo/{generic/scripting/example.py → automation/__init__.py} +0 -0
- /simo/{generic/scripting → automation}/helpers.py +0 -0
- /simo/{generic/scripting → automation}/serializers.py +0 -0
- /simo/{generic/scripting/__init__.py → automation/state.py} +0 -0
- /simo/{generic → automation}/templates/admin/controller_widgets/script.html +0 -0
- {simo-2.5.41.dist-info → simo-2.6.2.dist-info}/LICENSE.md +0 -0
- {simo-2.5.41.dist-info → simo-2.6.2.dist-info}/WHEEL +0 -0
- {simo-2.5.41.dist-info → simo-2.6.2.dist-info}/entry_points.txt +0 -0
- {simo-2.5.41.dist-info → simo-2.6.2.dist-info}/top_level.txt +0 -0
simo/core/signal_receivers.py
CHANGED
|
@@ -76,6 +76,9 @@ def create_instance_defaults(sender, instance, created, **kwargs):
|
|
|
76
76
|
dummy, new = Gateway.objects.get_or_create(
|
|
77
77
|
type='simo.generic.gateways.DummyGatewayHandler'
|
|
78
78
|
)
|
|
79
|
+
automation, new = Gateway.objects.get_or_create(
|
|
80
|
+
type='simo.automation.gateways.AutomationsGatewayHandler'
|
|
81
|
+
)
|
|
79
82
|
weather_icon = Icon.objects.get(slug='cloud-bolt-sun')
|
|
80
83
|
|
|
81
84
|
Component.objects.create(
|
|
@@ -95,9 +98,23 @@ def create_instance_defaults(sender, instance, created, **kwargs):
|
|
|
95
98
|
controller_uid='simo.generic.controllers.StateSelect',
|
|
96
99
|
value='day',
|
|
97
100
|
config={"states": [
|
|
98
|
-
{
|
|
99
|
-
|
|
100
|
-
|
|
101
|
+
{
|
|
102
|
+
"icon": "sunrise", "name": "Morning", "slug": "morning",
|
|
103
|
+
'help_text': "6:00 AM to sunrise. Activates only in dark time of a year."
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"icon": "house-day", "name": "Day", "slug": "day",
|
|
107
|
+
'help_text': "From sunrise to sunset."
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"icon": "house-night", "name": "Evening", "slug": "evening",
|
|
111
|
+
'help_text': "From sunrise to midnight"
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"icon": "moon-cloud", "name": "Night", "slug": "night",
|
|
115
|
+
'help_text': "From midnight to sunrise or 6:00 AM."
|
|
116
|
+
},
|
|
117
|
+
{"icon": "snooze", "name": "Sleep time", "slug": "sleep"},
|
|
101
118
|
{"icon": "house-person-leave", "name": "Away", "slug": "away"},
|
|
102
119
|
{"icon": "island-tropical", "name": "Vacation", "slug": "vacation"}
|
|
103
120
|
], "is_main": True}
|
|
@@ -105,45 +122,61 @@ def create_instance_defaults(sender, instance, created, **kwargs):
|
|
|
105
122
|
|
|
106
123
|
|
|
107
124
|
auto_state_code = render_to_string(
|
|
108
|
-
'
|
|
125
|
+
'automations/auto_state_script.py', {'state_comp_id': state_comp.id}
|
|
109
126
|
)
|
|
110
127
|
Component.objects.create(
|
|
111
128
|
name='Auto state', icon=Icon.objects.get(slug='bolt'),
|
|
112
129
|
zone=other_zone,
|
|
113
130
|
category=other_category, show_in_app=False,
|
|
114
|
-
gateway=
|
|
115
|
-
controller_uid='simo.
|
|
131
|
+
gateway=automation, base_type='script',
|
|
132
|
+
controller_uid='simo.automation.controllers.Script',
|
|
116
133
|
config={
|
|
117
134
|
"code": auto_state_code, 'autostart': True, 'keep_alive': True,
|
|
118
135
|
"notes": f"""
|
|
119
|
-
The script automatically controls the states of the "State" component (ID:{state_comp.id}) — 'day,
|
|
120
|
-
|
|
121
|
-
If no one is home, the 'away' state is activated.
|
|
122
|
-
If a different state, such as 'vacation,' is selected, the script stops running and waits until the State is switched back to one of the controlled states.
|
|
123
|
-
If one of the controlled states is manually selected, the script waits until that state is reached automatically and, once aligned with the manually set state, resumes its operation in normal mode.
|
|
136
|
+
The script automatically controls the states of the "State" component (ID:{state_comp.id}) — 'morning', 'day', 'evening', 'night'.
|
|
137
|
+
|
|
124
138
|
"""
|
|
125
139
|
}
|
|
126
140
|
)
|
|
127
141
|
|
|
128
142
|
code = render_to_string(
|
|
129
|
-
'
|
|
143
|
+
'automations/phones_sleep_script.py', {'state_comp_id': state_comp.id}
|
|
130
144
|
)
|
|
131
145
|
Component.objects.create(
|
|
132
|
-
name='
|
|
146
|
+
name='Sleep mode when owner phones are charge',
|
|
133
147
|
icon=Icon.objects.get(slug='bolt'), zone=other_zone,
|
|
134
148
|
category=other_category, show_in_app=False,
|
|
135
|
-
gateway=
|
|
136
|
-
controller_uid='simo.
|
|
149
|
+
gateway=automation, base_type='script',
|
|
150
|
+
controller_uid='simo.automation.controllers.Script',
|
|
137
151
|
config={
|
|
138
152
|
"code": code, 'autostart': True, 'keep_alive': True,
|
|
139
153
|
"notes": f"""
|
|
140
|
-
Automatically sets State component (ID: {state_comp.id}) to "
|
|
141
|
-
Sets State component to
|
|
154
|
+
Automatically sets State component (ID: {state_comp.id}) to "Sleep" if it is later than 10pm and all home owners phones who are at home are put on charge.
|
|
155
|
+
Sets State component back to regular state as soon as none of the home owners phones are on charge and it is 6am or later.
|
|
142
156
|
|
|
143
157
|
"""
|
|
144
158
|
}
|
|
145
159
|
)
|
|
146
160
|
|
|
161
|
+
code = render_to_string(
|
|
162
|
+
'automations/auto_away.py', {'state_comp_id': state_comp.id}
|
|
163
|
+
)
|
|
164
|
+
Component.objects.create(
|
|
165
|
+
name='Auto Away State',
|
|
166
|
+
icon=Icon.objects.get(slug='bolt'), zone=other_zone,
|
|
167
|
+
category=other_category, show_in_app=False,
|
|
168
|
+
gateway=automation, base_type='script',
|
|
169
|
+
controller_uid='simo.automation.controllers.Script',
|
|
170
|
+
config={
|
|
171
|
+
"code": code, 'autostart': True, 'keep_alive': True,
|
|
172
|
+
"notes": f"""
|
|
173
|
+
Automatically set mode to "Away" there are no users at home and there was no motion for more than 30 seconds.
|
|
174
|
+
Set it back to a regular mode as soon as somebody comes back home or motion is detected.
|
|
175
|
+
|
|
176
|
+
"""
|
|
177
|
+
}
|
|
178
|
+
)
|
|
179
|
+
|
|
147
180
|
# Create default User permission roles
|
|
148
181
|
|
|
149
182
|
PermissionsRole.objects.create(
|
|
Binary file
|
|
@@ -51,7 +51,7 @@ CONTROLLER_TYPES_MAP = get_controller_types_map()
|
|
|
51
51
|
def get_controller_types_choices(gateway=None):
|
|
52
52
|
choices = []
|
|
53
53
|
for controller_cls in get_controller_types_map(gateway).values():
|
|
54
|
-
choices.append((controller_cls.uid, controller_cls.name))
|
|
54
|
+
choices.append((controller_cls.uid, f"{controller_cls.gateway_class.name} | {controller_cls.name}"))
|
|
55
55
|
return choices
|
|
56
56
|
|
|
57
57
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
simo/fleet/api.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from django.db.models import Count
|
|
2
2
|
from django.utils.translation import gettext_lazy as _
|
|
3
3
|
from rest_framework import viewsets
|
|
4
|
+
from rest_framework.response import Response as RESTResponse
|
|
4
5
|
from rest_framework.decorators import action
|
|
5
6
|
from rest_framework.exceptions import ValidationError as APIValidationError
|
|
6
7
|
from simo.core.api import InstanceMixin
|
|
@@ -37,6 +38,7 @@ class ColonelsViewSet(InstanceMixin, viewsets.ModelViewSet):
|
|
|
37
38
|
def check_for_upgrade(self, request, pk=None, *args, **kwargs):
|
|
38
39
|
colonel = self.get_object()
|
|
39
40
|
colonel.check_for_upgrade()
|
|
41
|
+
return RESTResponse({'status': 'success'})
|
|
40
42
|
|
|
41
43
|
@action(detail=True, methods=['post'])
|
|
42
44
|
def upgrade(self, request, pk=None, *args, **kwargs):
|
|
@@ -45,16 +47,19 @@ class ColonelsViewSet(InstanceMixin, viewsets.ModelViewSet):
|
|
|
45
47
|
colonel.update_firmware(colonel.major_upgrade_available)
|
|
46
48
|
elif colonel.minor_upgrade_available:
|
|
47
49
|
colonel.update_firmware(colonel.minor_upgrade_available)
|
|
50
|
+
return RESTResponse({'status': 'success'})
|
|
48
51
|
|
|
49
52
|
@action(detail=True, methods=['post'])
|
|
50
53
|
def restart(self, request, pk=None, *args, **kwargs):
|
|
51
54
|
colonel = self.get_object()
|
|
52
55
|
colonel.restart()
|
|
56
|
+
return RESTResponse({'status': 'success'})
|
|
53
57
|
|
|
54
58
|
@action(detail=True, methods=['post'])
|
|
55
59
|
def update_config(self, request, pk=None, *args, **kwargs):
|
|
56
60
|
colonel = self.get_object()
|
|
57
61
|
colonel.update_config()
|
|
62
|
+
return RESTResponse({'status': 'success'})
|
|
58
63
|
|
|
59
64
|
@action(detail=True, methods=['post'])
|
|
60
65
|
def move_to(self, request, pk, *args, **kwargs):
|
|
@@ -68,6 +73,7 @@ class ColonelsViewSet(InstanceMixin, viewsets.ModelViewSet):
|
|
|
68
73
|
if not target:
|
|
69
74
|
raise APIValidationError(_('Invalid target.'), code=400)
|
|
70
75
|
colonel.move_to(target)
|
|
76
|
+
return RESTResponse({'status': 'success'})
|
|
71
77
|
|
|
72
78
|
|
|
73
79
|
class InterfaceViewSet(
|
simo/fleet/controllers.py
CHANGED
simo/fleet/forms.py
CHANGED
|
@@ -1459,6 +1459,18 @@ class TTLockConfigForm(ColonelComponentForm):
|
|
|
1459
1459
|
), required=False,
|
|
1460
1460
|
help_text="Quickens up lock status reporting on open/close if provided."
|
|
1461
1461
|
)
|
|
1462
|
+
auto_lock = forms.IntegerField(
|
|
1463
|
+
min_value=0, max_value=60, required=False,
|
|
1464
|
+
help_text="Lock the lock after given amount of seconds."
|
|
1465
|
+
)
|
|
1466
|
+
inverse = forms.BooleanField(
|
|
1467
|
+
required=False,
|
|
1468
|
+
help_text="Inverse operation (if supported by the lock)."
|
|
1469
|
+
)
|
|
1470
|
+
|
|
1471
|
+
def __init__(self, *args, **kwargs):
|
|
1472
|
+
super().__init__(*args, **kwargs)
|
|
1473
|
+
self.basic_fields.extend(['auto_lock', 'inverse'])
|
|
1462
1474
|
|
|
1463
1475
|
def clean(self):
|
|
1464
1476
|
if not self.instance or not self.instance.pk:
|
|
@@ -1475,10 +1487,17 @@ class TTLockConfigForm(ColonelComponentForm):
|
|
|
1475
1487
|
|
|
1476
1488
|
def save(self, commit=True):
|
|
1477
1489
|
obj = super(ColonelComponentForm, self).save(commit)
|
|
1478
|
-
if commit
|
|
1490
|
+
if commit:
|
|
1491
|
+
if 'door_sensor' in self.cleaned_data:
|
|
1492
|
+
GatewayObjectCommand(
|
|
1493
|
+
self.instance.gateway, self.cleaned_data['door_sensor'],
|
|
1494
|
+
command='watch_lock_sensor'
|
|
1495
|
+
).publish()
|
|
1479
1496
|
GatewayObjectCommand(
|
|
1480
|
-
|
|
1481
|
-
command='
|
|
1497
|
+
obj.gateway, self.cleaned_data['colonel'], id=obj.id,
|
|
1498
|
+
command='call', method='update_config', args=[
|
|
1499
|
+
obj.controller._get_colonel_config()
|
|
1500
|
+
]
|
|
1482
1501
|
).publish()
|
|
1483
1502
|
return obj
|
|
1484
1503
|
|
simo/fleet/serializers.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from rest_framework import serializers
|
|
2
|
+
from simo.core.serializers import TimestampField
|
|
2
3
|
from .models import InstanceOptions, Colonel, ColonelPin, Interface
|
|
3
4
|
|
|
4
5
|
|
|
@@ -43,6 +44,8 @@ class ColonelSerializer(serializers.ModelSerializer):
|
|
|
43
44
|
pins = serializers.SerializerMethodField()
|
|
44
45
|
interfaces = serializers.SerializerMethodField()
|
|
45
46
|
newer_firmware_available = serializers.SerializerMethodField()
|
|
47
|
+
last_seen = TimestampField()
|
|
48
|
+
is_empty = serializers.SerializerMethodField()
|
|
46
49
|
|
|
47
50
|
class Meta:
|
|
48
51
|
model = Colonel
|
|
@@ -51,10 +54,12 @@ class ColonelSerializer(serializers.ModelSerializer):
|
|
|
51
54
|
'firmware_version', 'firmware_auto_update',
|
|
52
55
|
'newer_firmware_available',
|
|
53
56
|
'socket_connected', 'last_seen', 'pins', 'interfaces',
|
|
57
|
+
'is_empty'
|
|
54
58
|
)
|
|
55
59
|
read_only_fields = [
|
|
56
60
|
'uid', 'type', 'firmware_version', 'newer_firmware_available',
|
|
57
|
-
'socket_connected', 'last_seen', 'pins', 'interfaces'
|
|
61
|
+
'socket_connected', 'last_seen', 'pins', 'interfaces',
|
|
62
|
+
'is_empty'
|
|
58
63
|
]
|
|
59
64
|
|
|
60
65
|
def get_pins(self, obj):
|
|
@@ -72,6 +77,9 @@ class ColonelSerializer(serializers.ModelSerializer):
|
|
|
72
77
|
def get_newer_firmware_available(self, obj):
|
|
73
78
|
return obj.newer_firmware_available()
|
|
74
79
|
|
|
80
|
+
def get_is_empty(self, obj):
|
|
81
|
+
return not bool(obj.components.all().count())
|
|
82
|
+
|
|
75
83
|
def update(self, instance, validated_data):
|
|
76
84
|
instance = super().update(instance, validated_data)
|
|
77
85
|
instance.update_config()
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
simo/generic/app_widgets.py
CHANGED
|
@@ -2,12 +2,6 @@ from django.utils.translation import gettext_lazy as _
|
|
|
2
2
|
from simo.core.app_widgets import BaseAppWidget
|
|
3
3
|
|
|
4
4
|
|
|
5
|
-
class ScriptWidget(BaseAppWidget):
|
|
6
|
-
uid = 'script'
|
|
7
|
-
name = _("Script")
|
|
8
|
-
size = [2, 1]
|
|
9
|
-
|
|
10
|
-
|
|
11
5
|
class ThermostatWidget(BaseAppWidget):
|
|
12
6
|
uid = 'thermostat'
|
|
13
7
|
name = _("Thermostat")
|
simo/generic/controllers.py
CHANGED
|
@@ -1,31 +1,21 @@
|
|
|
1
|
-
import time
|
|
2
|
-
import threading
|
|
3
1
|
import pytz
|
|
4
2
|
import datetime
|
|
5
3
|
import json
|
|
6
|
-
import requests
|
|
7
|
-
import traceback
|
|
8
|
-
import sys
|
|
9
|
-
import random
|
|
10
|
-
from bs4 import BeautifulSoup
|
|
11
4
|
from django.core.exceptions import ValidationError
|
|
12
5
|
from django.utils import timezone
|
|
13
6
|
from django.utils.functional import cached_property
|
|
14
7
|
from django.utils.translation import gettext_lazy as _
|
|
15
8
|
from django.conf import settings
|
|
16
9
|
from django.urls import reverse_lazy
|
|
17
|
-
from simo.conf import dynamic_settings
|
|
18
10
|
from simo.users.middleware import get_current_user, introduce
|
|
19
11
|
from simo.users.utils import get_system_user
|
|
20
|
-
from simo.core.
|
|
21
|
-
from simo.core.models import RUN_STATUS_CHOICES_MAP, Component
|
|
12
|
+
from simo.core.models import Component
|
|
22
13
|
from simo.core.utils.helpers import get_random_string
|
|
23
|
-
from simo.core.utils.operations import OPERATIONS
|
|
24
14
|
from simo.core.middleware import get_current_instance
|
|
25
15
|
from simo.core.controllers import (
|
|
26
|
-
BEFORE_SEND,
|
|
16
|
+
BEFORE_SEND, ControllerBase,
|
|
27
17
|
BinarySensor, NumericSensor, MultiSensor, Switch, Dimmer, DimmerPlus,
|
|
28
|
-
RGBWLight,
|
|
18
|
+
RGBWLight,
|
|
29
19
|
DoubleSwitch, TripleSwitch, QuadrupleSwitch, QuintupleSwitch
|
|
30
20
|
)
|
|
31
21
|
from simo.core.utils.config_values import (
|
|
@@ -37,265 +27,19 @@ from simo.core.utils.config_values import (
|
|
|
37
27
|
)
|
|
38
28
|
from .gateways import GenericGatewayHandler, DummyGatewayHandler
|
|
39
29
|
from .app_widgets import (
|
|
40
|
-
|
|
30
|
+
ThermostatWidget, AlarmGroupWidget, IPCameraWidget,
|
|
41
31
|
WateringWidget, StateSelectWidget, AlarmClockWidget,
|
|
42
32
|
WeatherWidget
|
|
43
33
|
)
|
|
44
34
|
from .forms import (
|
|
45
|
-
ScriptConfigForm, PresenceLightingConfigForm,
|
|
46
35
|
ThermostatConfigForm, AlarmGroupConfigForm,
|
|
47
36
|
IPCameraConfigForm, WeatherForm,
|
|
48
37
|
WateringConfigForm, StateSelectForm,
|
|
49
38
|
AlarmClockConfigForm
|
|
50
39
|
)
|
|
51
|
-
from .scripting import get_current_state
|
|
52
|
-
from .scripting.serializers import UserSerializer
|
|
53
40
|
|
|
54
41
|
# ----------- Generic controllers -----------------------------
|
|
55
42
|
|
|
56
|
-
|
|
57
|
-
class Script(ControllerBase, TimerMixin):
|
|
58
|
-
name = _("AI Script")
|
|
59
|
-
base_type = 'script'
|
|
60
|
-
gateway_class = GenericGatewayHandler
|
|
61
|
-
app_widget = ScriptWidget
|
|
62
|
-
config_form = ScriptConfigForm
|
|
63
|
-
admin_widget_template = 'admin/controller_widgets/script.html'
|
|
64
|
-
default_config = {'autostart': True, 'autorestart': True}
|
|
65
|
-
default_value = 'stopped'
|
|
66
|
-
|
|
67
|
-
def _validate_val(self, value, occasion=None):
|
|
68
|
-
if occasion == BEFORE_SEND:
|
|
69
|
-
if value not in ('start', 'stop'):
|
|
70
|
-
raise ValidationError("Must be 'start' or 'stop'")
|
|
71
|
-
elif occasion == BEFORE_SET:
|
|
72
|
-
if value not in RUN_STATUS_CHOICES_MAP.keys():
|
|
73
|
-
raise ValidationError(
|
|
74
|
-
"Invalid script controller status!"
|
|
75
|
-
)
|
|
76
|
-
return value
|
|
77
|
-
|
|
78
|
-
def _prepare_for_send(self, value):
|
|
79
|
-
if value == 'start':
|
|
80
|
-
new_code = getattr(self.component, 'new_code', None)
|
|
81
|
-
if new_code:
|
|
82
|
-
self.component.new_code = None
|
|
83
|
-
self.component.refresh_from_db()
|
|
84
|
-
self.component.config['code'] = new_code
|
|
85
|
-
self.component.save(update_fields=['config'])
|
|
86
|
-
return value
|
|
87
|
-
|
|
88
|
-
def _val_to_success(self, value):
|
|
89
|
-
if value == 'start':
|
|
90
|
-
return 'running'
|
|
91
|
-
else:
|
|
92
|
-
return 'stopped'
|
|
93
|
-
|
|
94
|
-
def start(self, new_code=None):
|
|
95
|
-
if new_code:
|
|
96
|
-
self.component.new_code = new_code
|
|
97
|
-
self.send('start')
|
|
98
|
-
|
|
99
|
-
def play(self):
|
|
100
|
-
return self.start()
|
|
101
|
-
|
|
102
|
-
def stop(self):
|
|
103
|
-
self.send('stop')
|
|
104
|
-
|
|
105
|
-
def toggle(self):
|
|
106
|
-
self.component.refresh_from_db()
|
|
107
|
-
if self.component.value == 'running':
|
|
108
|
-
self.send('stop')
|
|
109
|
-
else:
|
|
110
|
-
self.send('start')
|
|
111
|
-
|
|
112
|
-
def ai_assistant(self, wish):
|
|
113
|
-
try:
|
|
114
|
-
request_data = {
|
|
115
|
-
'hub_uid': dynamic_settings['core__hub_uid'],
|
|
116
|
-
'hub_secret': dynamic_settings['core__hub_secret'],
|
|
117
|
-
'instance_uid': get_current_instance().uid,
|
|
118
|
-
'system_data': json.dumps(get_current_state()),
|
|
119
|
-
'wish': wish,
|
|
120
|
-
}
|
|
121
|
-
except Exception as e:
|
|
122
|
-
print(traceback.format_exc(), file=sys.stderr)
|
|
123
|
-
return {'status': 'error', 'result': f"Internal error: {e}"}
|
|
124
|
-
user = get_current_user()
|
|
125
|
-
if user:
|
|
126
|
-
request_data['current_user'] = UserSerializer(user, many=False).data
|
|
127
|
-
try:
|
|
128
|
-
response = requests.post(
|
|
129
|
-
'https://simo.io/hubs/ai-assist/scripts/', json=request_data
|
|
130
|
-
)
|
|
131
|
-
except:
|
|
132
|
-
return {'status': 'error', 'result': "Connection error"}
|
|
133
|
-
|
|
134
|
-
if response.status_code != 200:
|
|
135
|
-
content = response.content.decode()
|
|
136
|
-
if '<html' in content:
|
|
137
|
-
# Parse the HTML content
|
|
138
|
-
soup = BeautifulSoup(response.content, 'html.parser')
|
|
139
|
-
content = F"Server error {response.status_code}: {soup.title.string}"
|
|
140
|
-
return {'status': 'error', 'result': content}
|
|
141
|
-
|
|
142
|
-
return {
|
|
143
|
-
'status': 'success',
|
|
144
|
-
'result': response.json()['script'],
|
|
145
|
-
'description': response.json()['description']
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
class PresenceLighting(Script):
|
|
150
|
-
masters_only = False
|
|
151
|
-
name = _("Presence lighting")
|
|
152
|
-
config_form = PresenceLightingConfigForm
|
|
153
|
-
|
|
154
|
-
# script specific variables
|
|
155
|
-
sensors = {}
|
|
156
|
-
condition_comps = {}
|
|
157
|
-
light_org_values = {}
|
|
158
|
-
is_on = False
|
|
159
|
-
turn_off_task = None
|
|
160
|
-
last_presence = 0
|
|
161
|
-
hold_time = 60
|
|
162
|
-
conditions = []
|
|
163
|
-
|
|
164
|
-
def _run(self):
|
|
165
|
-
self.hold_time = self.component.config.get('hold_time', 0) * 10
|
|
166
|
-
for id in self.component.config['presence_sensors']:
|
|
167
|
-
sensor = Component.objects.filter(id=id).first()
|
|
168
|
-
if sensor:
|
|
169
|
-
sensor.on_change(self._on_sensor)
|
|
170
|
-
self.sensors[id] = sensor
|
|
171
|
-
|
|
172
|
-
if self.component.config['off_value'] != 0:
|
|
173
|
-
for id in self.component.config['lights']:
|
|
174
|
-
light = Component.objects.filter(id=id).first()
|
|
175
|
-
if not light or not light.controller:
|
|
176
|
-
continue
|
|
177
|
-
light.on_change(self._on_light_change)
|
|
178
|
-
|
|
179
|
-
for condition in self.component.config.get('conditions', []):
|
|
180
|
-
comp = Component.objects.filter(
|
|
181
|
-
id=condition.get('component', 0)
|
|
182
|
-
).first()
|
|
183
|
-
if comp:
|
|
184
|
-
condition['component'] = comp
|
|
185
|
-
self.conditions.append(condition)
|
|
186
|
-
comp.on_change(self._on_condition)
|
|
187
|
-
self.condition_comps[comp.id] = comp
|
|
188
|
-
|
|
189
|
-
while True:
|
|
190
|
-
self._regulate(on_val_change=False)
|
|
191
|
-
time.sleep(random.randint(5, 15))
|
|
192
|
-
|
|
193
|
-
def _on_sensor(self, sensor=None):
|
|
194
|
-
if sensor:
|
|
195
|
-
self.sensors[sensor.id] = sensor
|
|
196
|
-
self._regulate()
|
|
197
|
-
|
|
198
|
-
def _on_condition(self, condition_comp=None):
|
|
199
|
-
if condition_comp:
|
|
200
|
-
for condition in self.conditions:
|
|
201
|
-
if condition['component'].id == condition_comp.id:
|
|
202
|
-
condition['component'] = condition_comp
|
|
203
|
-
self._regulate()
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
def _on_light_change(self, light):
|
|
207
|
-
if self.is_on:
|
|
208
|
-
self.light_org_values[light.id] = light.value
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
def _regulate(self, on_val_change=True):
|
|
212
|
-
presence_values = [s.value for id, s in self.sensors.items()]
|
|
213
|
-
if self.component.config.get('act_on', 0) == 0:
|
|
214
|
-
must_on = any(presence_values)
|
|
215
|
-
else:
|
|
216
|
-
must_on = all(presence_values)
|
|
217
|
-
|
|
218
|
-
if must_on and on_val_change:
|
|
219
|
-
print("Presence detected!")
|
|
220
|
-
|
|
221
|
-
additional_conditions_met = True
|
|
222
|
-
for condition in self.conditions:
|
|
223
|
-
|
|
224
|
-
comp = condition['component']
|
|
225
|
-
|
|
226
|
-
op = OPERATIONS.get(condition.get('op'))
|
|
227
|
-
if not op:
|
|
228
|
-
continue
|
|
229
|
-
|
|
230
|
-
if condition['op'] == 'in':
|
|
231
|
-
if comp.value not in self._string_to_vals(condition['value']):
|
|
232
|
-
if must_on and on_val_change:
|
|
233
|
-
print(
|
|
234
|
-
f"Condition not met: [{comp} value:{comp.value} "
|
|
235
|
-
f"{condition['op']} {condition['value']}]"
|
|
236
|
-
)
|
|
237
|
-
additional_conditions_met = False
|
|
238
|
-
break
|
|
239
|
-
|
|
240
|
-
if not op(comp.value, condition['value']):
|
|
241
|
-
if must_on and on_val_change:
|
|
242
|
-
print(
|
|
243
|
-
f"Condition not met: [{comp} value:{comp.value} "
|
|
244
|
-
f"{condition['op']} {condition['value']}]"
|
|
245
|
-
)
|
|
246
|
-
additional_conditions_met = False
|
|
247
|
-
break
|
|
248
|
-
|
|
249
|
-
if must_on and additional_conditions_met and not self.is_on:
|
|
250
|
-
print("Turn the lights ON!")
|
|
251
|
-
self.is_on = True
|
|
252
|
-
self.light_org_values = {}
|
|
253
|
-
for id in self.component.config['lights']:
|
|
254
|
-
comp = Component.objects.filter(id=id).first()
|
|
255
|
-
if not comp or not comp.controller:
|
|
256
|
-
continue
|
|
257
|
-
self.light_org_values[comp.id] = comp.value
|
|
258
|
-
comp.controller.send(self.component.config['on_value'])
|
|
259
|
-
return
|
|
260
|
-
|
|
261
|
-
if self.is_on:
|
|
262
|
-
if not additional_conditions_met:
|
|
263
|
-
return self._turn_it_off()
|
|
264
|
-
if not any(presence_values):
|
|
265
|
-
if not self.component.config.get('hold_time', 0):
|
|
266
|
-
return self._turn_it_off()
|
|
267
|
-
|
|
268
|
-
if not self.last_presence:
|
|
269
|
-
self.last_presence = time.time()
|
|
270
|
-
|
|
271
|
-
if self.hold_time and (
|
|
272
|
-
time.time() - self.hold_time > self.last_presence
|
|
273
|
-
):
|
|
274
|
-
self._turn_it_off()
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
def _turn_it_off(self):
|
|
278
|
-
print("Turn the lights OFF!")
|
|
279
|
-
self.is_on = False
|
|
280
|
-
self.last_presence = 0
|
|
281
|
-
for id in self.component.config['lights']:
|
|
282
|
-
comp = Component.objects.filter(id=id).first()
|
|
283
|
-
if not comp or not comp.controller:
|
|
284
|
-
continue
|
|
285
|
-
if self.component.config['off_value'] == 0:
|
|
286
|
-
comp.send(0)
|
|
287
|
-
else:
|
|
288
|
-
comp.send(self.light_org_values.get(comp.id, 0))
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
# TODO: Night lighting
|
|
292
|
-
#
|
|
293
|
-
# Lights: components (switches, dimmers)
|
|
294
|
-
# On value: 40
|
|
295
|
-
# Sunset offset (mins): negative = earlier, positive = later
|
|
296
|
-
# Save energy at night: 1 - 6 turn the lights completely off at night.
|
|
297
|
-
|
|
298
|
-
|
|
299
43
|
class Thermostat(ControllerBase):
|
|
300
44
|
name = _("Thermostat")
|
|
301
45
|
base_type = 'thermostat'
|