simo 2.11.4__py3-none-any.whl → 3.0.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/__pycache__/settings.cpython-312.pyc +0 -0
- simo/asgi.py +25 -6
- simo/automation/__pycache__/controllers.cpython-312.pyc +0 -0
- simo/automation/controllers.py +18 -2
- simo/automation/forms.py +15 -24
- simo/automation/gateways.py +32 -16
- simo/core/__pycache__/admin.cpython-312.pyc +0 -0
- simo/core/__pycache__/base_types.cpython-312.pyc +0 -0
- simo/core/__pycache__/controllers.cpython-312.pyc +0 -0
- simo/core/__pycache__/forms.cpython-312.pyc +0 -0
- simo/core/__pycache__/models.cpython-312.pyc +0 -0
- simo/core/__pycache__/serializers.cpython-312.pyc +0 -0
- simo/core/__pycache__/signal_receivers.cpython-312.pyc +0 -0
- simo/core/__pycache__/tasks.cpython-312.pyc +0 -0
- simo/core/admin.py +5 -4
- simo/core/base_types.py +191 -18
- simo/core/controllers.py +259 -26
- simo/core/forms.py +10 -2
- simo/core/management/_hub_template/hub/nginx.conf +23 -50
- simo/core/management/_hub_template/hub/supervisor.conf +15 -0
- simo/core/mcp.py +154 -0
- simo/core/migrations/0051_instance_ai_memory.py +18 -0
- simo/core/migrations/__pycache__/0051_instance_ai_memory.cpython-312.pyc +0 -0
- simo/core/models.py +3 -0
- simo/core/serializers.py +120 -0
- simo/core/signal_receivers.py +1 -1
- simo/core/tasks.py +1 -3
- simo/core/utils/__pycache__/type_constants.cpython-312.pyc +0 -0
- simo/core/utils/type_constants.py +78 -17
- simo/fleet/__pycache__/admin.cpython-312.pyc +0 -0
- simo/fleet/__pycache__/api.cpython-312.pyc +0 -0
- simo/fleet/__pycache__/base_types.cpython-312.pyc +0 -0
- simo/fleet/__pycache__/controllers.cpython-312.pyc +0 -0
- simo/fleet/__pycache__/forms.cpython-312.pyc +0 -0
- simo/fleet/__pycache__/gateways.cpython-312.pyc +0 -0
- simo/fleet/__pycache__/models.cpython-312.pyc +0 -0
- simo/fleet/__pycache__/serializers.cpython-312.pyc +0 -0
- simo/fleet/admin.py +5 -1
- simo/fleet/api.py +2 -27
- simo/fleet/base_types.py +35 -4
- simo/fleet/controllers.py +162 -156
- simo/fleet/forms.py +58 -88
- simo/fleet/gateways.py +8 -15
- simo/fleet/migrations/0055_colonel_is_vo_active_colonel_last_wake_and_more.py +28 -0
- simo/fleet/migrations/0056_delete_customdalidevice.py +16 -0
- simo/fleet/migrations/__pycache__/0055_colonel_is_vo_active_colonel_last_wake_and_more.cpython-312.pyc +0 -0
- simo/fleet/migrations/__pycache__/0056_delete_customdalidevice.cpython-312.pyc +0 -0
- simo/fleet/models.py +13 -72
- simo/fleet/serializers.py +1 -48
- simo/fleet/socket_consumers.py +100 -39
- simo/fleet/tasks.py +2 -22
- simo/fleet/voice_assistant.py +903 -0
- simo/generic/__pycache__/base_types.cpython-312.pyc +0 -0
- simo/generic/__pycache__/controllers.cpython-312.pyc +0 -0
- simo/generic/__pycache__/gateways.cpython-312.pyc +0 -0
- simo/generic/base_types.py +70 -10
- simo/generic/controllers.py +104 -17
- simo/generic/gateways.py +10 -10
- simo/mcp_server/__init__.py +0 -0
- simo/mcp_server/__pycache__/__init__.cpython-312.pyc +0 -0
- simo/mcp_server/__pycache__/admin.cpython-312.pyc +0 -0
- simo/mcp_server/__pycache__/models.cpython-312.pyc +0 -0
- simo/mcp_server/admin.py +18 -0
- simo/mcp_server/app.py +4 -0
- simo/mcp_server/auth.py +34 -0
- simo/mcp_server/dummy.py +22 -0
- simo/mcp_server/migrations/0001_initial.py +30 -0
- simo/mcp_server/migrations/0002_alter_instanceaccesstoken_date_expired.py +18 -0
- simo/mcp_server/migrations/0003_instanceaccesstoken_issuer.py +18 -0
- simo/mcp_server/migrations/__init__.py +0 -0
- simo/mcp_server/migrations/__pycache__/0001_initial.cpython-312.pyc +0 -0
- simo/mcp_server/migrations/__pycache__/0002_alter_instanceaccesstoken_date_expired.cpython-312.pyc +0 -0
- simo/mcp_server/migrations/__pycache__/0003_instanceaccesstoken_issuer.cpython-312.pyc +0 -0
- simo/mcp_server/migrations/__pycache__/__init__.cpython-312.pyc +0 -0
- simo/mcp_server/models.py +27 -0
- simo/mcp_server/server.py +60 -0
- simo/mcp_server/tasks.py +19 -0
- simo/multimedia/__pycache__/base_types.cpython-312.pyc +0 -0
- simo/multimedia/__pycache__/controllers.cpython-312.pyc +0 -0
- simo/multimedia/base_types.py +29 -4
- simo/multimedia/controllers.py +66 -19
- simo/settings.py +1 -0
- simo/users/__pycache__/utils.cpython-312.pyc +0 -0
- simo/users/utils.py +10 -0
- {simo-2.11.4.dist-info → simo-3.0.4.dist-info}/METADATA +11 -4
- {simo-2.11.4.dist-info → simo-3.0.4.dist-info}/RECORD +90 -64
- simo/fleet/custom_dali_operations.py +0 -287
- {simo-2.11.4.dist-info → simo-3.0.4.dist-info}/WHEEL +0 -0
- {simo-2.11.4.dist-info → simo-3.0.4.dist-info}/entry_points.txt +0 -0
- {simo-2.11.4.dist-info → simo-3.0.4.dist-info}/licenses/LICENSE.md +0 -0
- {simo-2.11.4.dist-info → simo-3.0.4.dist-info}/top_level.txt +0 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
simo/generic/base_types.py
CHANGED
|
@@ -1,12 +1,72 @@
|
|
|
1
1
|
from django.utils.translation import gettext_lazy as _
|
|
2
|
+
from simo.core.base_types import BaseComponentType
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
'
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
4
|
+
|
|
5
|
+
class ScriptType(BaseComponentType):
|
|
6
|
+
slug = 'script'
|
|
7
|
+
name = _("Script")
|
|
8
|
+
description = _("Runs user-defined logic or actions.")
|
|
9
|
+
purpose = _("Use for programmable behaviors or routines.")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ThermostatType(BaseComponentType):
|
|
13
|
+
slug = 'thermostat'
|
|
14
|
+
name = _("Thermostat")
|
|
15
|
+
description = _("Thermal control orchestrating heaters/coolers.")
|
|
16
|
+
purpose = _("Use to maintain target temperature based on sensors.")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AlarmGroupType(BaseComponentType):
|
|
20
|
+
slug = 'alarm-group'
|
|
21
|
+
name = _("Alarm Group")
|
|
22
|
+
description = _("Aggregates security components into a single state.")
|
|
23
|
+
purpose = _("Use to arm/disarm and observe overall security status.")
|
|
24
|
+
required_methods = ('arm', 'disarm')
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class IPCameraType(BaseComponentType):
|
|
28
|
+
slug = 'ip-camera'
|
|
29
|
+
name = _("IP Camera")
|
|
30
|
+
description = _("Network camera streaming and snapshots.")
|
|
31
|
+
purpose = _("Use to integrate RTSP-capable cameras.")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class WeatherType(BaseComponentType):
|
|
35
|
+
slug = 'weather'
|
|
36
|
+
name = _("Weather")
|
|
37
|
+
description = _("Aggregated weather information for the instance.")
|
|
38
|
+
purpose = _("Use to display external weather data.")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class WateringType(BaseComponentType):
|
|
42
|
+
slug = 'watering'
|
|
43
|
+
name = _("Watering")
|
|
44
|
+
description = _("Irrigation control with programs and manual runs.")
|
|
45
|
+
purpose = _("Use to drive valves/pumps for garden irrigation.")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class StateSelectType(BaseComponentType):
|
|
49
|
+
slug = 'state-select'
|
|
50
|
+
name = _("State Select")
|
|
51
|
+
description = _("Pick one of predefined states.")
|
|
52
|
+
purpose = _("Use to represent and switch between modes/states.")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class AlarmClockType(BaseComponentType):
|
|
56
|
+
slug = 'alarm-clock'
|
|
57
|
+
name = _("Alarm Clock")
|
|
58
|
+
description = _("User-configurable alarm schedule and events.")
|
|
59
|
+
purpose = _("Use to trigger actions at specific times.")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _export_base_types_dict():
|
|
63
|
+
import inspect as _inspect
|
|
64
|
+
mapping = {}
|
|
65
|
+
for _name, _obj in globals().items():
|
|
66
|
+
if _inspect.isclass(_obj) and issubclass(_obj, BaseComponentType) \
|
|
67
|
+
and _obj is not BaseComponentType and getattr(_obj, 'slug', None):
|
|
68
|
+
mapping[_obj.slug] = _obj.name
|
|
69
|
+
return mapping
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
BASE_TYPES = _export_base_types_dict()
|
simo/generic/controllers.py
CHANGED
|
@@ -18,6 +18,10 @@ from simo.core.controllers import (
|
|
|
18
18
|
RGBWLight,
|
|
19
19
|
DoubleSwitch, TripleSwitch, QuadrupleSwitch, QuintupleSwitch
|
|
20
20
|
)
|
|
21
|
+
from .base_types import (
|
|
22
|
+
ThermostatType, AlarmGroupType, WeatherType, IPCameraType,
|
|
23
|
+
WateringType, StateSelectType, AlarmClockType
|
|
24
|
+
)
|
|
21
25
|
from simo.core.utils.config_values import (
|
|
22
26
|
BooleanConfigValue, FloatConfigValue,
|
|
23
27
|
TimeTempConfigValue, ThermostatModeConfigValue,
|
|
@@ -57,7 +61,7 @@ class SwitchGroup(Switch):
|
|
|
57
61
|
|
|
58
62
|
class Thermostat(ControllerBase):
|
|
59
63
|
name = _("Thermostat")
|
|
60
|
-
base_type =
|
|
64
|
+
base_type = ThermostatType
|
|
61
65
|
gateway_class = GenericGatewayHandler
|
|
62
66
|
app_widget = ThermostatWidget
|
|
63
67
|
config_form = ThermostatConfigForm
|
|
@@ -66,6 +70,7 @@ class Thermostat(ControllerBase):
|
|
|
66
70
|
'current_temp': 21, 'target_temp': 22,
|
|
67
71
|
'heating': False, 'cooling': False
|
|
68
72
|
}
|
|
73
|
+
accepts_value = False
|
|
69
74
|
|
|
70
75
|
@property
|
|
71
76
|
def default_config(self):
|
|
@@ -142,12 +147,18 @@ class Thermostat(ControllerBase):
|
|
|
142
147
|
target_temp = sorted_options[-1][1]
|
|
143
148
|
for timestr, target in sorted_options:
|
|
144
149
|
start_second = int(timestr.split(':')[0]) * 3600 \
|
|
145
|
-
+ int(timestr.split(':')[1] * 60
|
|
150
|
+
+ int(timestr.split(':')[1]) * 60
|
|
146
151
|
if start_second < current_second:
|
|
147
152
|
target_temp = target
|
|
148
153
|
return target_temp
|
|
149
154
|
|
|
150
155
|
def get_current_target_temperature(self):
|
|
156
|
+
"""Return active target temperature from user config.
|
|
157
|
+
|
|
158
|
+
Computes the target based on hard/daily/weekly schedules and the
|
|
159
|
+
current local time.
|
|
160
|
+
Returns: float temperature.
|
|
161
|
+
"""
|
|
151
162
|
data = self.component.config['user_config']
|
|
152
163
|
if data['hard']['active']:
|
|
153
164
|
return data['hard']['target']
|
|
@@ -157,8 +168,7 @@ class Thermostat(ControllerBase):
|
|
|
157
168
|
return self._get_target_from_options(
|
|
158
169
|
data['weekly'][str(localtime.weekday() + 1)])
|
|
159
170
|
|
|
160
|
-
def
|
|
161
|
-
|
|
171
|
+
def _evaluate(self):
|
|
162
172
|
from simo.core.models import Component
|
|
163
173
|
self.component.refresh_from_db()
|
|
164
174
|
tz = pytz.timezone(self.component.zone.instance.timezone)
|
|
@@ -341,6 +351,12 @@ class Thermostat(ControllerBase):
|
|
|
341
351
|
|
|
342
352
|
|
|
343
353
|
def update_user_conf(self, new_conf):
|
|
354
|
+
"""Update thermostat user configuration.
|
|
355
|
+
|
|
356
|
+
Parameters:
|
|
357
|
+
- new_conf (dict): Partial or full user_config; validated and merged
|
|
358
|
+
with defaults. Triggers re-evaluation after save.
|
|
359
|
+
"""
|
|
344
360
|
self.component.refresh_from_db()
|
|
345
361
|
self.component.config['user_config'] = validate_new_conf(
|
|
346
362
|
new_conf,
|
|
@@ -348,10 +364,16 @@ class Thermostat(ControllerBase):
|
|
|
348
364
|
self._get_default_user_config()
|
|
349
365
|
)
|
|
350
366
|
self.component.save()
|
|
351
|
-
self.
|
|
367
|
+
self._evaluate()
|
|
352
368
|
|
|
353
369
|
|
|
354
370
|
def hold(self, temperature=None):
|
|
371
|
+
"""Hold a temporary target temperature.
|
|
372
|
+
|
|
373
|
+
Parameters:
|
|
374
|
+
- temperature (float|None): If provided, enables hard hold at this
|
|
375
|
+
target; if None, disables hard hold to resume schedules.
|
|
376
|
+
"""
|
|
355
377
|
if temperature != None:
|
|
356
378
|
self.component.config['user_config']['hard'] = {
|
|
357
379
|
'active': True, 'target': temperature
|
|
@@ -363,7 +385,7 @@ class Thermostat(ControllerBase):
|
|
|
363
385
|
|
|
364
386
|
class AlarmGroup(ControllerBase):
|
|
365
387
|
name = _("Alarm Group")
|
|
366
|
-
base_type =
|
|
388
|
+
base_type = AlarmGroupType
|
|
367
389
|
gateway_class = GenericGatewayHandler
|
|
368
390
|
app_widget = AlarmGroupWidget
|
|
369
391
|
config_form = AlarmGroupConfigForm
|
|
@@ -386,18 +408,31 @@ class AlarmGroup(ControllerBase):
|
|
|
386
408
|
)
|
|
387
409
|
return value
|
|
388
410
|
|
|
411
|
+
def send(self, value):
|
|
412
|
+
"""Set group state.
|
|
413
|
+
|
|
414
|
+
Parameters:
|
|
415
|
+
- value (str): 'armed', 'disarmed' (or 'breached' by system).
|
|
416
|
+
Prefer using `arm()` and `disarm()` helpers.
|
|
417
|
+
"""
|
|
418
|
+
return super().send(value)
|
|
419
|
+
|
|
389
420
|
def arm(self):
|
|
421
|
+
"""Arm the entire group (children remain in their states)."""
|
|
390
422
|
self.send('armed')
|
|
391
423
|
|
|
392
424
|
def disarm(self):
|
|
425
|
+
"""Disarm the entire group."""
|
|
393
426
|
self.send('disarmed')
|
|
394
427
|
|
|
395
428
|
def get_children(self):
|
|
429
|
+
"""Return the queryset of child components that form this group."""
|
|
396
430
|
return Component.objects.filter(
|
|
397
431
|
pk__in=self.component.config['components']
|
|
398
432
|
)
|
|
399
433
|
|
|
400
434
|
def refresh_status(self):
|
|
435
|
+
"""Recompute and persist the group's aggregated security status."""
|
|
401
436
|
stats = {
|
|
402
437
|
'disarmed': 0, 'pending-arm': 0, 'armed': 0, 'breached': 0
|
|
403
438
|
}
|
|
@@ -435,7 +470,7 @@ class AlarmGroup(ControllerBase):
|
|
|
435
470
|
|
|
436
471
|
class Weather(ControllerBase):
|
|
437
472
|
name = _("Weather")
|
|
438
|
-
base_type =
|
|
473
|
+
base_type = WeatherType
|
|
439
474
|
gateway_class = GenericGatewayHandler
|
|
440
475
|
config_form = WeatherForm
|
|
441
476
|
app_widget = WeatherWidget
|
|
@@ -443,6 +478,7 @@ class Weather(ControllerBase):
|
|
|
443
478
|
default_config = {}
|
|
444
479
|
default_value = {}
|
|
445
480
|
manual_add = False
|
|
481
|
+
accepts_value = False
|
|
446
482
|
|
|
447
483
|
def _validate_val(self, value, occasion=None):
|
|
448
484
|
return value
|
|
@@ -450,18 +486,20 @@ class Weather(ControllerBase):
|
|
|
450
486
|
|
|
451
487
|
class IPCamera(ControllerBase):
|
|
452
488
|
name = _("IP Camera")
|
|
453
|
-
base_type =
|
|
489
|
+
base_type = IPCameraType
|
|
454
490
|
gateway_class = GenericGatewayHandler
|
|
455
491
|
app_widget = IPCameraWidget
|
|
456
492
|
config_form = IPCameraConfigForm
|
|
457
493
|
admin_widget_template = 'admin/controller_widgets/ip_camera.html'
|
|
458
494
|
default_config = {'rtsp_address': ''}
|
|
459
495
|
default_value = ''
|
|
496
|
+
accepts_value = False
|
|
460
497
|
|
|
461
498
|
def _validate_val(self, value, occasion=None):
|
|
462
499
|
raise ValidationError("This component type does not accept set value!")
|
|
463
500
|
|
|
464
501
|
def get_stream_socket_url(self):
|
|
502
|
+
"""Return the Channels WebSocket URL for live RTSP streaming."""
|
|
465
503
|
return reverse_lazy(
|
|
466
504
|
'ws-cam-stream', kwargs={'component_id': self.component.id},
|
|
467
505
|
urlconf=settings.CHANNELS_URLCONF
|
|
@@ -474,7 +512,7 @@ class Watering(ControllerBase):
|
|
|
474
512
|
'paused_program', 'paused_custom'
|
|
475
513
|
)
|
|
476
514
|
name = _("Watering")
|
|
477
|
-
base_type =
|
|
515
|
+
base_type = WateringType
|
|
478
516
|
gateway_class = GenericGatewayHandler
|
|
479
517
|
config_form = WateringConfigForm
|
|
480
518
|
app_widget = WateringWidget
|
|
@@ -525,8 +563,19 @@ class Watering(ControllerBase):
|
|
|
525
563
|
)
|
|
526
564
|
return value
|
|
527
565
|
|
|
566
|
+
def send(self, value):
|
|
567
|
+
"""Control watering.
|
|
568
|
+
|
|
569
|
+
Parameters:
|
|
570
|
+
- value (str): 'start', 'pause', 'reset', or
|
|
571
|
+
- value (dict): {'status': <status>, 'program_progress': <minute>}
|
|
572
|
+
Prefer `start()`, `pause()`, `reset()`, `set_program_progress()`.
|
|
573
|
+
"""
|
|
574
|
+
return super().send(value)
|
|
575
|
+
|
|
528
576
|
|
|
529
577
|
def start(self):
|
|
578
|
+
"""Start the watering program at current or last progress point."""
|
|
530
579
|
self.component.refresh_from_db()
|
|
531
580
|
if not self.component.value.get('program_progress', 0):
|
|
532
581
|
self.component.meta['last_run'] = timezone.now().timestamp()
|
|
@@ -538,9 +587,11 @@ class Watering(ControllerBase):
|
|
|
538
587
|
self.set_program_progress(self.component.value['program_progress'])
|
|
539
588
|
|
|
540
589
|
def play(self):
|
|
590
|
+
"""Alias for `start()` (for consistency with media-like controls)."""
|
|
541
591
|
return self.start()
|
|
542
592
|
|
|
543
593
|
def pause(self):
|
|
594
|
+
"""Pause the watering program and disengage all contours."""
|
|
544
595
|
self.component.refresh_from_db()
|
|
545
596
|
self.set({
|
|
546
597
|
'status': 'paused_program',
|
|
@@ -549,13 +600,15 @@ class Watering(ControllerBase):
|
|
|
549
600
|
self.disengage_all()
|
|
550
601
|
|
|
551
602
|
def reset(self):
|
|
603
|
+
"""Stop the watering program and reset progress to 0."""
|
|
552
604
|
self.set({'status': 'stopped', 'program_progress': 0})
|
|
553
605
|
self.disengage_all()
|
|
554
606
|
|
|
555
607
|
def stop(self):
|
|
608
|
+
"""Alias for `reset()` to stop the program."""
|
|
556
609
|
return self.reset()
|
|
557
610
|
|
|
558
|
-
def
|
|
611
|
+
def _set_program_progress(self, program_minute, run=True):
|
|
559
612
|
engaged_contours = []
|
|
560
613
|
for flow_data in self.component.config['program']['flow']:
|
|
561
614
|
if flow_data['minute'] <= program_minute:
|
|
@@ -588,6 +641,11 @@ class Watering(ControllerBase):
|
|
|
588
641
|
)
|
|
589
642
|
|
|
590
643
|
def ai_assist_update(self, data):
|
|
644
|
+
"""Update AI-assistant computed watering parameters.
|
|
645
|
+
|
|
646
|
+
Parameters:
|
|
647
|
+
- data (dict): Partial program/schedule/contour updates from AI.
|
|
648
|
+
"""
|
|
591
649
|
for key, val in data.items():
|
|
592
650
|
assert key in ('ai_assist', 'soil_type', 'ai_assist_level')
|
|
593
651
|
if key == 'ai_assist':
|
|
@@ -602,6 +660,11 @@ class Watering(ControllerBase):
|
|
|
602
660
|
self.component.save()
|
|
603
661
|
|
|
604
662
|
def contours_update(self, contours):
|
|
663
|
+
"""Replace contours config and rebuild program accordingly.
|
|
664
|
+
|
|
665
|
+
Parameters:
|
|
666
|
+
- contours (list[dict]): Contour entries with uid and runtime updates.
|
|
667
|
+
"""
|
|
605
668
|
current_contours = {
|
|
606
669
|
c['uid']: c
|
|
607
670
|
for c in self.component.config.get('contours')
|
|
@@ -618,6 +681,7 @@ class Watering(ControllerBase):
|
|
|
618
681
|
self.component.save()
|
|
619
682
|
|
|
620
683
|
def schedule_update(self, new_schedule):
|
|
684
|
+
"""Replace schedule config and rebuild program accordingly."""
|
|
621
685
|
self.component.refresh_from_db()
|
|
622
686
|
self.component.config['schedule'] = validate_new_conf(
|
|
623
687
|
new_schedule,
|
|
@@ -686,6 +750,7 @@ class Watering(ControllerBase):
|
|
|
686
750
|
return {'duration': 0, 'flow': []}
|
|
687
751
|
|
|
688
752
|
def disengage_all(self):
|
|
753
|
+
"""Turn off all configured watering contours immediately."""
|
|
689
754
|
for contour_data in self.component.config['contours']:
|
|
690
755
|
try:
|
|
691
756
|
switch = Component.objects.get(pk=contour_data['switch'])
|
|
@@ -784,7 +849,7 @@ class Watering(ControllerBase):
|
|
|
784
849
|
|
|
785
850
|
class AlarmClock(ControllerBase):
|
|
786
851
|
name = _("Alarm Clock")
|
|
787
|
-
base_type =
|
|
852
|
+
base_type = AlarmClockType
|
|
788
853
|
gateway_class = GenericGatewayHandler
|
|
789
854
|
config_form = AlarmClockConfigForm
|
|
790
855
|
app_widget = AlarmClockWidget
|
|
@@ -795,6 +860,7 @@ class AlarmClock(ControllerBase):
|
|
|
795
860
|
'events_triggered': [],
|
|
796
861
|
'alarm_timestamp': None
|
|
797
862
|
}
|
|
863
|
+
accepts_value = False
|
|
798
864
|
|
|
799
865
|
def _validate_val(self, value, occasion=None):
|
|
800
866
|
# this component does not accept value set.
|
|
@@ -1058,13 +1124,14 @@ class AlarmClock(ControllerBase):
|
|
|
1058
1124
|
|
|
1059
1125
|
return current_value
|
|
1060
1126
|
|
|
1061
|
-
def
|
|
1127
|
+
def _tick(self):
|
|
1062
1128
|
self.component.value = self._check_alarm(
|
|
1063
1129
|
self.component.meta, self.component.value
|
|
1064
1130
|
)
|
|
1065
1131
|
self.component.save()
|
|
1066
1132
|
|
|
1067
1133
|
def play_all(self):
|
|
1134
|
+
"""Execute all enabled alarm events immediately for the current alarm."""
|
|
1068
1135
|
alarms = self.component.meta
|
|
1069
1136
|
current_value = self.component.value
|
|
1070
1137
|
|
|
@@ -1100,6 +1167,7 @@ class AlarmClock(ControllerBase):
|
|
|
1100
1167
|
|
|
1101
1168
|
|
|
1102
1169
|
def cancel_all(self):
|
|
1170
|
+
"""Cancel all enabled events of the current alarm and move to next alarm."""
|
|
1103
1171
|
alarms = self.component.meta
|
|
1104
1172
|
current_value = self.component.value
|
|
1105
1173
|
|
|
@@ -1133,6 +1201,12 @@ class AlarmClock(ControllerBase):
|
|
|
1133
1201
|
return self.component.value
|
|
1134
1202
|
|
|
1135
1203
|
def snooze(self, mins):
|
|
1204
|
+
"""Delay the current alarm by a number of minutes.
|
|
1205
|
+
|
|
1206
|
+
Parameters:
|
|
1207
|
+
- mins (int): Minutes to postpone both the alarm and all its events.
|
|
1208
|
+
Returns updated clock state.
|
|
1209
|
+
"""
|
|
1136
1210
|
current_value = self.component.value
|
|
1137
1211
|
localtime = timezone.localtime()
|
|
1138
1212
|
if not current_value.get('in_alarm'):
|
|
@@ -1165,6 +1239,11 @@ class AudioAlert(Switch):
|
|
|
1165
1239
|
config_form = AudioAlertConfigForm
|
|
1166
1240
|
|
|
1167
1241
|
def send(self, value):
|
|
1242
|
+
"""Trigger or cancel an alert on all configured player components.
|
|
1243
|
+
|
|
1244
|
+
Parameters:
|
|
1245
|
+
- value (bool): True to play alert; False to cancel.
|
|
1246
|
+
"""
|
|
1168
1247
|
for player in Component.objects.filter(
|
|
1169
1248
|
id__in=self.component.config['players']
|
|
1170
1249
|
):
|
|
@@ -1178,7 +1257,7 @@ class AudioAlert(Switch):
|
|
|
1178
1257
|
class StateSelect(ControllerBase):
|
|
1179
1258
|
gateway_class = GenericGatewayHandler
|
|
1180
1259
|
name = _("State select")
|
|
1181
|
-
base_type =
|
|
1260
|
+
base_type = StateSelectType
|
|
1182
1261
|
app_widget = StateSelectWidget
|
|
1183
1262
|
config_form = StateSelectForm
|
|
1184
1263
|
|
|
@@ -1197,6 +1276,14 @@ class StateSelect(ControllerBase):
|
|
|
1197
1276
|
raise ValidationError("Unsupported value!")
|
|
1198
1277
|
return value
|
|
1199
1278
|
|
|
1279
|
+
def send(self, value):
|
|
1280
|
+
"""Select state by slug.
|
|
1281
|
+
|
|
1282
|
+
Parameters:
|
|
1283
|
+
- value (str): Must match one of configured state slugs.
|
|
1284
|
+
"""
|
|
1285
|
+
return super().send(value)
|
|
1286
|
+
|
|
1200
1287
|
|
|
1201
1288
|
class MainState(StateSelect):
|
|
1202
1289
|
name = _("Main State")
|
|
@@ -1232,7 +1319,7 @@ class MainState(StateSelect):
|
|
|
1232
1319
|
]
|
|
1233
1320
|
}
|
|
1234
1321
|
|
|
1235
|
-
def
|
|
1322
|
+
def _get_day_evening_night_morning(self):
|
|
1236
1323
|
from simo.automation.helpers import LocalSun
|
|
1237
1324
|
sun = LocalSun(self.component.zone.instance.location)
|
|
1238
1325
|
timezone.activate(self.component.zone.instance.timezone)
|
|
@@ -1257,7 +1344,7 @@ class MainState(StateSelect):
|
|
|
1257
1344
|
return 'night'
|
|
1258
1345
|
|
|
1259
1346
|
|
|
1260
|
-
def
|
|
1347
|
+
def _check_is_away(self, last_sensor_action):
|
|
1261
1348
|
away_on_no_action = self.component.config.get('away_on_no_action')
|
|
1262
1349
|
if not away_on_no_action:
|
|
1263
1350
|
return False
|
|
@@ -1271,7 +1358,7 @@ class MainState(StateSelect):
|
|
|
1271
1358
|
return (time.time() - last_sensor_action) // 60 >= away_on_no_action
|
|
1272
1359
|
|
|
1273
1360
|
|
|
1274
|
-
def
|
|
1361
|
+
def _is_sleep_time(self):
|
|
1275
1362
|
timezone.activate(self.component.zone.instance.timezone)
|
|
1276
1363
|
localtime = timezone.localtime()
|
|
1277
1364
|
if localtime.weekday() < 5:
|
|
@@ -1289,7 +1376,7 @@ class MainState(StateSelect):
|
|
|
1289
1376
|
return False
|
|
1290
1377
|
|
|
1291
1378
|
|
|
1292
|
-
def
|
|
1379
|
+
def _owner_phones_on_charge(self, all_phones=False):
|
|
1293
1380
|
sleeping_phones_hour = self.component.config.get('sleeping_phones_hour')
|
|
1294
1381
|
if sleeping_phones_hour is None:
|
|
1295
1382
|
return False
|
simo/generic/gateways.py
CHANGED
|
@@ -206,7 +206,7 @@ class GenericGatewayHandler(
|
|
|
206
206
|
):
|
|
207
207
|
tz = pytz.timezone(thermostat.zone.instance.timezone)
|
|
208
208
|
timezone.activate(tz)
|
|
209
|
-
thermostat.
|
|
209
|
+
thermostat._evaluate()
|
|
210
210
|
|
|
211
211
|
|
|
212
212
|
def watch_alarm_clocks(self):
|
|
@@ -217,7 +217,7 @@ class GenericGatewayHandler(
|
|
|
217
217
|
):
|
|
218
218
|
tz = pytz.timezone(alarm_clock.zone.instance.timezone)
|
|
219
219
|
timezone.activate(tz)
|
|
220
|
-
alarm_clock.
|
|
220
|
+
alarm_clock._tick()
|
|
221
221
|
|
|
222
222
|
|
|
223
223
|
def watch_watering(self):
|
|
@@ -227,7 +227,7 @@ class GenericGatewayHandler(
|
|
|
227
227
|
tz = pytz.timezone(watering.zone.instance.timezone)
|
|
228
228
|
timezone.activate(tz)
|
|
229
229
|
if watering.value['status'] == 'running_program':
|
|
230
|
-
watering.
|
|
230
|
+
watering._set_program_progress(
|
|
231
231
|
watering.value['program_progress'] + 1
|
|
232
232
|
)
|
|
233
233
|
else:
|
|
@@ -340,7 +340,7 @@ class GenericGatewayHandler(
|
|
|
340
340
|
def set_get_day_evening_night_morning(self, state):
|
|
341
341
|
if state.value not in ('day', 'night', 'evening', 'morning'):
|
|
342
342
|
return
|
|
343
|
-
new_state = state.
|
|
343
|
+
new_state = state._get_day_evening_night_morning()
|
|
344
344
|
if new_state == state.value:
|
|
345
345
|
self.last_set_state = state.value
|
|
346
346
|
return
|
|
@@ -377,7 +377,7 @@ class GenericGatewayHandler(
|
|
|
377
377
|
self.sensors_on_watch[state.id][sensor.id] = i_id
|
|
378
378
|
sensor.on_change(self.security_sensor_change)
|
|
379
379
|
|
|
380
|
-
if state.
|
|
380
|
+
if state._check_is_away(self.last_sensor_actions.get(i_id, 0)):
|
|
381
381
|
if state.value != 'away':
|
|
382
382
|
print(f"New main state of "
|
|
383
383
|
f"{state.zone.instance} - away")
|
|
@@ -385,7 +385,7 @@ class GenericGatewayHandler(
|
|
|
385
385
|
else:
|
|
386
386
|
if state.value == 'away':
|
|
387
387
|
try:
|
|
388
|
-
new_state = state.
|
|
388
|
+
new_state = state._get_day_evening_night_morning()
|
|
389
389
|
except:
|
|
390
390
|
new_state = 'day'
|
|
391
391
|
print(f"New main state of "
|
|
@@ -394,14 +394,14 @@ class GenericGatewayHandler(
|
|
|
394
394
|
|
|
395
395
|
if state.config.get('sleeping_phones_hour') is not None:
|
|
396
396
|
if state.value != 'sleep':
|
|
397
|
-
if state.
|
|
397
|
+
if state._is_sleep_time() and state._owner_phones_on_charge(True):
|
|
398
398
|
print(f"New main state of {state.zone.instance} - sleep")
|
|
399
399
|
state.send('sleep')
|
|
400
400
|
else:
|
|
401
|
-
if not state.
|
|
402
|
-
and not state.
|
|
401
|
+
if not state._owner_phones_on_charge(True) \
|
|
402
|
+
and not state._is_sleep_time():
|
|
403
403
|
try:
|
|
404
|
-
new_state = state.
|
|
404
|
+
new_state = state._get_day_evening_night_morning()
|
|
405
405
|
except:
|
|
406
406
|
new_state = 'day'
|
|
407
407
|
print(f"New main state of "
|
|
File without changes
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
simo/mcp_server/admin.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from django.contrib import admin
|
|
2
|
+
from django.utils.safestring import mark_safe
|
|
3
|
+
from django.templatetags.static import static
|
|
4
|
+
from .models import InstanceAccessToken
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@admin.register(InstanceAccessToken)
|
|
8
|
+
class InstanceAccessTokenAdmin(admin.ModelAdmin):
|
|
9
|
+
list_display = 'token', 'instance', 'user', 'is_valid'
|
|
10
|
+
|
|
11
|
+
def is_valid(self, obj):
|
|
12
|
+
if obj.date_expired:
|
|
13
|
+
return mark_safe(
|
|
14
|
+
'<img src="%s" alt="False">' % static('admin/img/icon-no.svg')
|
|
15
|
+
)
|
|
16
|
+
return mark_safe(
|
|
17
|
+
'<img src="%s" alt="True">' % static('admin/img/icon-yes.svg')
|
|
18
|
+
)
|
simo/mcp_server/app.py
ADDED
simo/mcp_server/auth.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from typing import Any, Optional
|
|
2
|
+
from asgiref.sync import sync_to_async
|
|
3
|
+
from simo.mcp_server.models import InstanceAccessToken
|
|
4
|
+
from simo.core.middleware import introduce_instance
|
|
5
|
+
from fastmcp.server.auth.auth import AccessToken, TokenVerifier
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DjangoTokenVerifier(TokenVerifier):
|
|
10
|
+
|
|
11
|
+
async def verify_token(self, token: str) -> Optional[AccessToken]:
|
|
12
|
+
|
|
13
|
+
def _load():
|
|
14
|
+
access_token = InstanceAccessToken.objects.select_related(
|
|
15
|
+
"instance"
|
|
16
|
+
).filter(
|
|
17
|
+
token=token, date_expired=None
|
|
18
|
+
).first()
|
|
19
|
+
if not access_token:
|
|
20
|
+
return
|
|
21
|
+
introduce_instance(access_token.instance)
|
|
22
|
+
return access_token
|
|
23
|
+
|
|
24
|
+
access_token = await sync_to_async(_load, thread_sensitive=True)()
|
|
25
|
+
if not access_token:
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
# Build a minimal AccessToken; scopes optional
|
|
29
|
+
return AccessToken(
|
|
30
|
+
token = token,
|
|
31
|
+
client_id=str(access_token.instance.id),
|
|
32
|
+
scopes=["simo:read", "simo:write"],
|
|
33
|
+
claims={"instance_id": access_token.instance.id},
|
|
34
|
+
)
|
simo/mcp_server/dummy.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dummy MCP server to run mcp inspector
|
|
3
|
+
mcp dev simo/mcp_server/dummy.py
|
|
4
|
+
"""
|
|
5
|
+
import uvicorn
|
|
6
|
+
from mcp.server.fastmcp import FastMCP
|
|
7
|
+
|
|
8
|
+
mcp = FastMCP("Dummy MCP")
|
|
9
|
+
|
|
10
|
+
def create_app():
|
|
11
|
+
return mcp.streamable_http_app()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
if __name__ == "__main__":
|
|
15
|
+
uvicorn.run(
|
|
16
|
+
"simo.mcp_server.run:create_app",
|
|
17
|
+
host="0.0.0.0",
|
|
18
|
+
port=3333,
|
|
19
|
+
factory=True,
|
|
20
|
+
log_config=None
|
|
21
|
+
)
|
|
22
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Generated by Django 4.2.10 on 2025-09-23 06:24
|
|
2
|
+
|
|
3
|
+
from django.conf import settings
|
|
4
|
+
from django.db import migrations, models
|
|
5
|
+
import django.db.models.deletion
|
|
6
|
+
import simo.mcp_server.models
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Migration(migrations.Migration):
|
|
10
|
+
|
|
11
|
+
initial = True
|
|
12
|
+
|
|
13
|
+
dependencies = [
|
|
14
|
+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
15
|
+
('core', '0050_componenthistory_alive'),
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
operations = [
|
|
19
|
+
migrations.CreateModel(
|
|
20
|
+
name='InstanceAccessToken',
|
|
21
|
+
fields=[
|
|
22
|
+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
23
|
+
('date_created', models.DateTimeField(auto_now_add=True, db_index=True)),
|
|
24
|
+
('token', models.CharField(db_index=True, default=simo.mcp_server.models.get_new_token, max_length=20, unique=True)),
|
|
25
|
+
('date_expired', models.DateTimeField(db_index=True, null=True)),
|
|
26
|
+
('instance', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.instance')),
|
|
27
|
+
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
|
|
28
|
+
],
|
|
29
|
+
),
|
|
30
|
+
]
|