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
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Generated by Django 4.2.10 on 2025-09-24 07:04
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
('mcp_server', '0001_initial'),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.AlterField(
|
|
14
|
+
model_name='instanceaccesstoken',
|
|
15
|
+
name='date_expired',
|
|
16
|
+
field=models.DateTimeField(blank=True, db_index=True, null=True),
|
|
17
|
+
),
|
|
18
|
+
]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Generated by Django 4.2.10 on 2025-09-24 08:07
|
|
2
|
+
|
|
3
|
+
from django.db import migrations, models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
('mcp_server', '0002_alter_instanceaccesstoken_date_expired'),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.AddField(
|
|
14
|
+
model_name='instanceaccesstoken',
|
|
15
|
+
name='issuer',
|
|
16
|
+
field=models.CharField(db_index=True, editable=False, null=True),
|
|
17
|
+
),
|
|
18
|
+
]
|
|
File without changes
|
|
Binary file
|
simo/mcp_server/migrations/__pycache__/0002_alter_instanceaccesstoken_date_expired.cpython-312.pyc
ADDED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from django.db import models
|
|
2
|
+
from simo.core.models import Instance
|
|
3
|
+
from simo.core.utils.helpers import get_random_string
|
|
4
|
+
from simo.core.middleware import get_current_instance
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_new_token():
|
|
8
|
+
token = get_random_string(size=20)
|
|
9
|
+
instance = get_current_instance()
|
|
10
|
+
if InstanceAccessToken.objects.filter(
|
|
11
|
+
instance=instance, token=token
|
|
12
|
+
).first():
|
|
13
|
+
return get_new_token()
|
|
14
|
+
return token
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class InstanceAccessToken(models.Model):
|
|
18
|
+
instance = models.ForeignKey(Instance, on_delete=models.CASCADE)
|
|
19
|
+
date_created = models.DateTimeField(auto_now_add=True, db_index=True)
|
|
20
|
+
token = models.CharField(
|
|
21
|
+
max_length=20, unique=True, db_index=True, default=get_new_token
|
|
22
|
+
)
|
|
23
|
+
date_expired = models.DateTimeField(null=True, blank=True, db_index=True)
|
|
24
|
+
user = models.ForeignKey(
|
|
25
|
+
'users.User', null=True, blank=True, on_delete=models.SET_NULL
|
|
26
|
+
)
|
|
27
|
+
issuer = models.CharField(db_index=True, editable=False, null=True)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import logging
|
|
3
|
+
import django
|
|
4
|
+
|
|
5
|
+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings")
|
|
6
|
+
django.setup()
|
|
7
|
+
|
|
8
|
+
from django.apps import apps
|
|
9
|
+
from simo.mcp_server.app import mcp
|
|
10
|
+
from starlette.middleware import Middleware
|
|
11
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
12
|
+
from fastmcp.server.http import create_streamable_http_app
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
log = logging.getLogger("simo")
|
|
16
|
+
logging.basicConfig(
|
|
17
|
+
level=logging.INFO,
|
|
18
|
+
format="%(asctime)s %(levelname).1s %(name)s: %(message)s",
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def load_tools_from_apps() -> None:
|
|
23
|
+
import importlib.util
|
|
24
|
+
|
|
25
|
+
for cfg in apps.get_app_configs():
|
|
26
|
+
mod_name = f"{cfg.name}.mcp"
|
|
27
|
+
|
|
28
|
+
# Only attempt import if module exists
|
|
29
|
+
if importlib.util.find_spec(mod_name) is None:
|
|
30
|
+
continue
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
importlib.import_module(mod_name)
|
|
34
|
+
log.info("Loaded MCP tools: %s", mod_name)
|
|
35
|
+
except Exception:
|
|
36
|
+
# Keep the server up; log full traceback and continue
|
|
37
|
+
log.exception("Failed to import %s", mod_name)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class LogExceptions(BaseHTTPMiddleware):
|
|
41
|
+
async def dispatch(self, request, call_next):
|
|
42
|
+
try:
|
|
43
|
+
return await call_next(request)
|
|
44
|
+
except Exception:
|
|
45
|
+
log.exception("Unhandled exception in %s %s", request.method, request.url.path)
|
|
46
|
+
raise # Let Starlette/Uvicorn still return 500
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def create_app():
|
|
50
|
+
load_tools_from_apps()
|
|
51
|
+
app = create_streamable_http_app(
|
|
52
|
+
server=mcp,
|
|
53
|
+
streamable_http_path="/",
|
|
54
|
+
auth=mcp.auth,
|
|
55
|
+
json_response=True,
|
|
56
|
+
stateless_http=True,
|
|
57
|
+
debug=True,
|
|
58
|
+
middleware=[Middleware(LogExceptions)],
|
|
59
|
+
)
|
|
60
|
+
return app
|
simo/mcp_server/tasks.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
from django.utils import timezone
|
|
3
|
+
from celeryc import celery_app
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@celery_app.task
|
|
7
|
+
def auto_expire_tokens():
|
|
8
|
+
"""Auto expire instance access tokens that are older than one day"""
|
|
9
|
+
from .models import InstanceAccessToken
|
|
10
|
+
InstanceAccessToken.objects.filter(
|
|
11
|
+
date_created__lt=timezone.now() - datetime.timedelta(days=1),
|
|
12
|
+
date_expired=None
|
|
13
|
+
).update(date_expired=timezone.now())
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@celery_app.on_after_finalize.connect
|
|
18
|
+
def setup_periodic_tasks(sender, **kwargs):
|
|
19
|
+
sender.add_periodic_task(60 * 60, auto_expire_tokens.s()) # hourly cron
|
|
Binary file
|
|
Binary file
|
simo/multimedia/base_types.py
CHANGED
|
@@ -1,6 +1,31 @@
|
|
|
1
1
|
from django.utils.translation import gettext_lazy as _
|
|
2
|
+
from simo.core.base_types import BaseComponentType
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
'
|
|
6
|
-
|
|
4
|
+
|
|
5
|
+
class AudioPlayerType(BaseComponentType):
|
|
6
|
+
slug = 'audio-player'
|
|
7
|
+
name = _("Audio Player")
|
|
8
|
+
description = _("Playback control for audio sources.")
|
|
9
|
+
purpose = _("Use to play/pause/stop audio and adjust playback.")
|
|
10
|
+
required_methods = ('play', 'pause', 'stop')
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class VideoPlayerType(BaseComponentType):
|
|
14
|
+
slug = 'video-player'
|
|
15
|
+
name = _("Video Player")
|
|
16
|
+
description = _("Playback control for video sources.")
|
|
17
|
+
purpose = _("Use to control video playback in the app.")
|
|
18
|
+
required_methods = ('play', 'pause', 'stop')
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _export_base_types_dict():
|
|
22
|
+
import inspect as _inspect
|
|
23
|
+
mapping = {}
|
|
24
|
+
for _name, _obj in globals().items():
|
|
25
|
+
if _inspect.isclass(_obj) and issubclass(_obj, BaseComponentType) \
|
|
26
|
+
and _obj is not BaseComponentType and getattr(_obj, 'slug', None):
|
|
27
|
+
mapping[_obj.slug] = _obj.name
|
|
28
|
+
return mapping
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
BASE_TYPES = _export_base_types_dict()
|
simo/multimedia/controllers.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from django.utils.translation import gettext_lazy as _
|
|
2
2
|
from simo.core.controllers import Switch, TimerMixin
|
|
3
3
|
from .app_widgets import AudioPlayerWidget, VideoPlayerWidget
|
|
4
|
+
from .base_types import AudioPlayerType, VideoPlayerType
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
class BasePlayer(Switch):
|
|
@@ -32,60 +33,103 @@ class BasePlayer(Switch):
|
|
|
32
33
|
def _validate_val(self, value, occasion=None):
|
|
33
34
|
return value
|
|
34
35
|
|
|
36
|
+
def send(self, value):
|
|
37
|
+
"""Control playback.
|
|
38
|
+
|
|
39
|
+
Parameters:
|
|
40
|
+
- value (str): one of 'play', 'pause', 'stop', 'next', 'previous', or
|
|
41
|
+
- value (dict): one of {'seek': seconds}, {'set_volume': 0-100},
|
|
42
|
+
{'shuffle': bool}, {'loop': bool}, {'alert': id|None},
|
|
43
|
+
{'play_from_library': id, 'volume': int|None, 'fade_in': seconds|None},
|
|
44
|
+
{'play_uri': uri, 'volume': int|None}.
|
|
45
|
+
Prefer using the convenience methods (`play()`, `pause()`, `seek()`, ...).
|
|
46
|
+
"""
|
|
47
|
+
return super().send(value)
|
|
48
|
+
|
|
35
49
|
def play(self):
|
|
50
|
+
"""Start or resume playback."""
|
|
36
51
|
self.send('play')
|
|
37
52
|
|
|
38
53
|
def pause(self):
|
|
54
|
+
"""Pause playback (keeps current position)."""
|
|
39
55
|
self.send('pause')
|
|
40
56
|
|
|
41
57
|
def stop(self):
|
|
58
|
+
"""Stop playback and reset position to start (if supported)."""
|
|
42
59
|
self.send('stop')
|
|
43
60
|
|
|
44
61
|
def seek(self, second):
|
|
62
|
+
"""Seek to the specified position in seconds.
|
|
63
|
+
|
|
64
|
+
Parameters:
|
|
65
|
+
- second (int|float): Absolute position from start.
|
|
66
|
+
"""
|
|
45
67
|
self.send({'seek': second})
|
|
46
68
|
|
|
47
69
|
def next(self):
|
|
70
|
+
"""Skip to the next item in the queue/playlist."""
|
|
48
71
|
self.send('next')
|
|
49
72
|
|
|
50
73
|
def previous(self):
|
|
74
|
+
"""Return to the previous item in the queue/playlist."""
|
|
51
75
|
self.send('previous')
|
|
52
76
|
|
|
53
77
|
def set_volume(self, val):
|
|
78
|
+
"""Set output volume.
|
|
79
|
+
|
|
80
|
+
Parameters:
|
|
81
|
+
- val (int): Volume percent 0–100.
|
|
82
|
+
"""
|
|
54
83
|
assert 0 <= val <= 100
|
|
55
84
|
self.component.meta['volume'] = val
|
|
56
85
|
self.component.save()
|
|
57
86
|
self.send({'set_volume': val})
|
|
58
87
|
|
|
59
88
|
def get_volume(self):
|
|
60
|
-
|
|
89
|
+
"""Get the last known volume (0–100).
|
|
90
|
+
|
|
91
|
+
Note: This reads cached meta; device may differ if gateway does not
|
|
92
|
+
report volume changes.
|
|
93
|
+
"""
|
|
61
94
|
return self.component.meta['volume']
|
|
62
95
|
|
|
63
96
|
def set_shuffle_play(self, val):
|
|
97
|
+
"""Enable or disable shuffle mode.
|
|
98
|
+
|
|
99
|
+
Parameters:
|
|
100
|
+
- val (bool|int): Truthy to enable, falsy to disable.
|
|
101
|
+
"""
|
|
64
102
|
self.component.meta['shuffle'] = bool(val)
|
|
65
103
|
self.component.save()
|
|
66
104
|
self.send({'shuffle': bool(val)})
|
|
67
105
|
|
|
68
106
|
def set_loop_play(self, val):
|
|
107
|
+
"""Enable or disable loop/repeat mode.
|
|
108
|
+
|
|
109
|
+
Parameters:
|
|
110
|
+
- val (bool|int): Truthy to enable, falsy to disable.
|
|
111
|
+
"""
|
|
69
112
|
self.component.meta['loop'] = bool(val)
|
|
70
113
|
self.component.save()
|
|
71
114
|
self.send({'loop': bool(val)})
|
|
72
115
|
|
|
73
116
|
def play_library_item(self, id, volume=None, fade_in=None):
|
|
74
|
-
'
|
|
75
|
-
|
|
76
|
-
:
|
|
77
|
-
|
|
78
|
-
:
|
|
79
|
-
|
|
117
|
+
"""Play an item from the controller's library.
|
|
118
|
+
|
|
119
|
+
Parameters:
|
|
120
|
+
- id: Library item identifier as provided by the gateway.
|
|
121
|
+
- volume (int|None): Optional volume 0–100; keep current if None.
|
|
122
|
+
- fade_in (int|float|None): Optional seconds to fade in.
|
|
123
|
+
"""
|
|
80
124
|
self.send({'play_from_library': id, 'volume': volume, 'fade_in': fade_in})
|
|
81
125
|
|
|
82
126
|
def play_uri(self, uri, volume=None):
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
:
|
|
86
|
-
|
|
87
|
-
:
|
|
88
|
-
|
|
127
|
+
"""Replace the queue with a single URI and play immediately.
|
|
128
|
+
|
|
129
|
+
Parameters:
|
|
130
|
+
- uri (str): Playable URI/URL (e.g. file://, http://, spotify:...).
|
|
131
|
+
- volume (int|None): Optional volume 0–100; keep current if None.
|
|
132
|
+
"""
|
|
89
133
|
if volume:
|
|
90
134
|
assert 0 <= volume <= 100
|
|
91
135
|
self.send({"play_uri": uri, 'volume': volume})
|
|
@@ -105,13 +149,19 @@ class BasePlayer(Switch):
|
|
|
105
149
|
|
|
106
150
|
|
|
107
151
|
def play_alert(self, id):
|
|
152
|
+
"""Play a one-shot alert sound by id, then resume previous playback.
|
|
153
|
+
|
|
154
|
+
Parameters:
|
|
155
|
+
- id: Alert sound identifier.
|
|
156
|
+
"""
|
|
108
157
|
self.send({"alert": id})
|
|
109
158
|
|
|
110
159
|
def cancel_alert(self):
|
|
111
|
-
|
|
160
|
+
"""Cancel an in-progress alert and resume previous playback."""
|
|
112
161
|
self.send({"alert": None})
|
|
113
162
|
|
|
114
163
|
def toggle(self):
|
|
164
|
+
"""Toggle between play and pause based on current state."""
|
|
115
165
|
if self.component.value == 'playing':
|
|
116
166
|
self.pause()
|
|
117
167
|
else:
|
|
@@ -121,15 +171,12 @@ class BasePlayer(Switch):
|
|
|
121
171
|
class BaseAudioPlayer(BasePlayer):
|
|
122
172
|
"""Base class for audio players"""
|
|
123
173
|
name = _("Audio Player")
|
|
124
|
-
base_type =
|
|
174
|
+
base_type = AudioPlayerType
|
|
125
175
|
app_widget = AudioPlayerWidget
|
|
126
176
|
|
|
127
177
|
|
|
128
178
|
class BaseVideoPlayer(BasePlayer):
|
|
129
179
|
"""Base class for video players"""
|
|
130
180
|
name = _("Video Player")
|
|
131
|
-
base_type =
|
|
181
|
+
base_type = VideoPlayerType
|
|
132
182
|
app_widget = VideoPlayerWidget
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
simo/settings.py
CHANGED
|
Binary file
|
simo/users/utils.py
CHANGED
|
@@ -30,6 +30,16 @@ def get_device_user():
|
|
|
30
30
|
return device
|
|
31
31
|
|
|
32
32
|
|
|
33
|
+
def get_ai_user():
|
|
34
|
+
from .models import User
|
|
35
|
+
device, new = User.objects.get_or_create(
|
|
36
|
+
email='ai@simo.io', defaults={
|
|
37
|
+
'name': "AI"
|
|
38
|
+
}
|
|
39
|
+
)
|
|
40
|
+
return device
|
|
41
|
+
|
|
42
|
+
|
|
33
43
|
def rebuild_authorized_keys():
|
|
34
44
|
from .models import User
|
|
35
45
|
try:
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: simo
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.4
|
|
4
4
|
Summary: Smart Home Supremacy
|
|
5
|
-
Author-email:
|
|
5
|
+
Author-email: "Simon V." <simon@simo.io>
|
|
6
6
|
Project-URL: Homepage, https://simo.io
|
|
7
|
-
Project-URL: Issues, https://github.com/
|
|
7
|
+
Project-URL: Issues, https://github.com/SIMO-io/simo/issues
|
|
8
8
|
Classifier: Programming Language :: Python :: 3
|
|
9
9
|
Classifier: License :: OSI Approved :: MIT License
|
|
10
10
|
Classifier: Operating System :: OS Independent
|
|
11
|
-
Requires-Python: >=3.
|
|
11
|
+
Requires-Python: >=3.12
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
13
|
License-File: LICENSE.md
|
|
14
14
|
Requires-Dist: Django==4.2.10
|
|
@@ -44,14 +44,21 @@ Requires-Dist: django-location-field==2.1.0
|
|
|
44
44
|
Requires-Dist: slugify==0.0.1
|
|
45
45
|
Requires-Dist: django-countries==7.5.1
|
|
46
46
|
Requires-Dist: librosa==0.10.1
|
|
47
|
+
Requires-Dist: lameenc==1.8.1
|
|
47
48
|
Requires-Dist: daphne==4.1.2
|
|
48
49
|
Requires-Dist: Pillow==9.5.0
|
|
49
50
|
Requires-Dist: django-markdownify==0.9.5
|
|
50
51
|
Requires-Dist: django-activity-stream==2.0.0
|
|
51
52
|
Requires-Dist: gunicorn==23.0.0
|
|
52
53
|
Requires-Dist: python-crontab==3.2.0
|
|
54
|
+
Requires-Dist: pydub==0.25.1
|
|
53
55
|
Requires-Dist: django-object-actions==4.3.0
|
|
54
56
|
Requires-Dist: tqdm==4.67.0
|
|
55
57
|
Requires-Dist: bs4==0.0.2
|
|
56
58
|
Requires-Dist: lxml==5.3.0
|
|
59
|
+
Requires-Dist: websockets==15.0.1
|
|
60
|
+
Requires-Dist: lameenc==1.8.1
|
|
61
|
+
Requires-Dist: pydub==0.25.1
|
|
62
|
+
Requires-Dist: fastmcp==2.12.3
|
|
63
|
+
Requires-Dist: uvicorn==0.35.0
|
|
57
64
|
Dynamic: license-file
|