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
|
simo/asgi.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import importlib, datetime, sys
|
|
1
|
+
import importlib, importlib.util, datetime, sys, traceback
|
|
2
2
|
from django.core.asgi import get_asgi_application
|
|
3
3
|
from channels.auth import AuthMiddlewareStack
|
|
4
4
|
from channels.routing import ProtocolTypeRouter, URLRouter
|
|
@@ -13,13 +13,32 @@ for name, app in apps.app_configs.items():
|
|
|
13
13
|
'staticfiles'
|
|
14
14
|
):
|
|
15
15
|
continue
|
|
16
|
+
|
|
17
|
+
module_path = f"{app.name}.routing"
|
|
18
|
+
|
|
19
|
+
# Only attempt import if the module exists
|
|
20
|
+
spec = importlib.util.find_spec(module_path)
|
|
21
|
+
if spec is None:
|
|
22
|
+
continue
|
|
23
|
+
|
|
24
|
+
# Import and collect urlpatterns; on failure, print full traceback and continue
|
|
25
|
+
try:
|
|
26
|
+
routing = importlib.import_module(module_path)
|
|
27
|
+
except Exception:
|
|
28
|
+
print(
|
|
29
|
+
f"Failed to import {module_path}:\n{traceback.format_exc()}"
|
|
30
|
+
)
|
|
31
|
+
continue
|
|
32
|
+
|
|
16
33
|
try:
|
|
17
|
-
|
|
18
|
-
|
|
34
|
+
app_urlpatterns = getattr(routing, 'urlpatterns', None)
|
|
35
|
+
if isinstance(app_urlpatterns, list):
|
|
36
|
+
urlpatterns.extend(app_urlpatterns)
|
|
37
|
+
except Exception:
|
|
38
|
+
print(
|
|
39
|
+
f"Error while processing urlpatterns from {module_path}:\n{traceback.format_exc()}"
|
|
40
|
+
)
|
|
19
41
|
continue
|
|
20
|
-
for var_name, item in routing.__dict__.items():
|
|
21
|
-
if isinstance(item, list) and var_name == 'urlpatterns':
|
|
22
|
-
urlpatterns.extend(item)
|
|
23
42
|
|
|
24
43
|
|
|
25
44
|
class TimestampedStream:
|
|
Binary file
|
simo/automation/controllers.py
CHANGED
|
@@ -63,24 +63,38 @@ class Script(ControllerBase, TimerMixin):
|
|
|
63
63
|
return 'stopped'
|
|
64
64
|
|
|
65
65
|
def start(self, new_code=None):
|
|
66
|
+
"""Start the script process (optionally updating source code).
|
|
67
|
+
|
|
68
|
+
Parameters:
|
|
69
|
+
- new_code (str|None): Optional Python script to persist before start.
|
|
70
|
+
"""
|
|
66
71
|
if new_code:
|
|
67
72
|
self.component.new_code = new_code
|
|
68
73
|
self.send('start')
|
|
69
74
|
|
|
70
75
|
def play(self):
|
|
76
|
+
"""Alias for `start()` to harmonize with media-like controls."""
|
|
71
77
|
return self.start()
|
|
72
78
|
|
|
73
79
|
def stop(self):
|
|
80
|
+
"""Stop the running script process."""
|
|
74
81
|
self.send('stop')
|
|
75
82
|
|
|
76
83
|
def toggle(self):
|
|
84
|
+
"""Toggle script run state between running and stopped."""
|
|
77
85
|
self.component.refresh_from_db()
|
|
78
86
|
if self.component.value == 'running':
|
|
79
87
|
self.send('stop')
|
|
80
88
|
else:
|
|
81
89
|
self.send('start')
|
|
82
90
|
|
|
83
|
-
def ai_assistant(self, wish):
|
|
91
|
+
def ai_assistant(self, wish, current_code=None):
|
|
92
|
+
"""Request an AI-generated script for the given natural-language wish.
|
|
93
|
+
|
|
94
|
+
Parameters:
|
|
95
|
+
- wish (str): User intent in natural language.
|
|
96
|
+
Returns: dict with status, generated script, and description.
|
|
97
|
+
"""
|
|
84
98
|
try:
|
|
85
99
|
request_data = {
|
|
86
100
|
'hub_uid': dynamic_settings['core__hub_uid'],
|
|
@@ -88,6 +102,7 @@ class Script(ControllerBase, TimerMixin):
|
|
|
88
102
|
'instance_uid': get_current_instance().uid,
|
|
89
103
|
'system_data': json.dumps(get_current_state()),
|
|
90
104
|
'wish': wish,
|
|
105
|
+
'current_code': current_code
|
|
91
106
|
}
|
|
92
107
|
except Exception as e:
|
|
93
108
|
print(traceback.format_exc(), file=sys.stderr)
|
|
@@ -97,7 +112,7 @@ class Script(ControllerBase, TimerMixin):
|
|
|
97
112
|
request_data['current_user'] = UserSerializer(user, many=False).data
|
|
98
113
|
try:
|
|
99
114
|
response = requests.post(
|
|
100
|
-
'https://simo.io/
|
|
115
|
+
'https://simo.io/ai/scripts/', json=request_data
|
|
101
116
|
)
|
|
102
117
|
except:
|
|
103
118
|
return {'status': 'error', 'result': "Connection error"}
|
|
@@ -121,6 +136,7 @@ class PresenceLighting(Script):
|
|
|
121
136
|
masters_only = False
|
|
122
137
|
name = _("Presence lighting")
|
|
123
138
|
config_form = PresenceLightingConfigForm
|
|
139
|
+
accepts_value = False
|
|
124
140
|
|
|
125
141
|
def __init__(self, *args, **kwargs):
|
|
126
142
|
super().__init__(*args, **kwargs)
|
simo/automation/forms.py
CHANGED
|
@@ -33,13 +33,12 @@ class ScriptConfigForm(BaseComponentForm):
|
|
|
33
33
|
"in my living room when it get's dark."
|
|
34
34
|
}
|
|
35
35
|
),
|
|
36
|
-
help_text="
|
|
37
|
-
"you want to happen with this scenario script. <br>"
|
|
38
|
-
"The more defined, exact and clear is your description the more "
|
|
36
|
+
help_text="The more defined, exact and clear is your description the more "
|
|
39
37
|
"accurate automation script SIMO.io AI assistanw will generate.<br>"
|
|
40
|
-
"Use component, zone and category
|
|
41
|
-
"SIMO.io AI will re-generate your automation code and update it's description
|
|
42
|
-
"every time
|
|
38
|
+
"Use component, zone and category ID's for best accuracy. <br>"
|
|
39
|
+
"SIMO.io AI will re-generate your automation code and update it's description "
|
|
40
|
+
"every time you enter something in this field. <br>"
|
|
41
|
+
"Takes up to 60s to do it. <br>"
|
|
43
42
|
"Actual script code can only be edited via SIMO.io Admin.",
|
|
44
43
|
)
|
|
45
44
|
code = forms.CharField(widget=PythonCode, required=False)
|
|
@@ -85,24 +84,16 @@ class ScriptConfigForm(BaseComponentForm):
|
|
|
85
84
|
|
|
86
85
|
|
|
87
86
|
def clean(self):
|
|
88
|
-
if self.cleaned_data
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
resp = self.instance.ai_assistant(
|
|
99
|
-
self.cleaned_data['assistant_request'],
|
|
100
|
-
)
|
|
101
|
-
if resp['status'] == 'success':
|
|
102
|
-
self._ai_resp = resp
|
|
103
|
-
elif resp['status'] == 'error':
|
|
104
|
-
self.add_error('assistant_request', resp['result'])
|
|
105
|
-
|
|
87
|
+
if self.cleaned_data['assistant_request']:
|
|
88
|
+
resp = self.instance.ai_assistant(
|
|
89
|
+
self.cleaned_data['assistant_request'],
|
|
90
|
+
self.instance.config.get('code')
|
|
91
|
+
)
|
|
92
|
+
if resp['status'] == 'success':
|
|
93
|
+
self._ai_resp = resp
|
|
94
|
+
self.cleaned_data['assistant_request'] = None
|
|
95
|
+
elif resp['status'] == 'error':
|
|
96
|
+
self.add_error('assistant_request', resp['result'])
|
|
106
97
|
return self.cleaned_data
|
|
107
98
|
|
|
108
99
|
def save(self, commit=True):
|
simo/automation/gateways.py
CHANGED
|
@@ -257,23 +257,43 @@ class AutomationsGatewayHandler(GatesHandler, BaseObjectCommandsGatewayHandler):
|
|
|
257
257
|
# however the process is actually still running
|
|
258
258
|
process.kill()
|
|
259
259
|
self.last_death = time.time()
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
if id not in self.terminating_scripts: # was not intentionaly terminated
|
|
265
|
-
if comp:
|
|
260
|
+
# If component exists and is marked running, attempt to persist error
|
|
261
|
+
# BEFORE removing from in-memory tracking, so we can retry if DB is down.
|
|
262
|
+
if comp and comp.value == 'running' and id not in self.terminating_scripts:
|
|
263
|
+
try:
|
|
266
264
|
tz = pytz.timezone(comp.zone.instance.timezone)
|
|
267
265
|
timezone.activate(tz)
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
266
|
+
logger = get_component_logger(comp)
|
|
267
|
+
logger.log(logging.INFO, "-------DEAD!-------")
|
|
268
|
+
comp.value = 'error'
|
|
269
|
+
comp.save()
|
|
270
|
+
except Exception:
|
|
271
|
+
# Leave entry in running_scripts to retry on next tick
|
|
272
|
+
continue
|
|
273
|
+
# For any other case or after successful DB update, drop tracking entry
|
|
274
|
+
self.running_scripts.pop(id, None)
|
|
272
275
|
|
|
273
276
|
if self.last_death and time.time() - self.last_death < 5:
|
|
274
277
|
# give 10s air before we wake these dead scripts up!
|
|
275
278
|
return
|
|
276
279
|
|
|
280
|
+
# Reconcile scripts marked as 'running' in DB but not tracked or with dead PID
|
|
281
|
+
for comp in Component.objects.filter(base_type='script', value='running'):
|
|
282
|
+
if comp.id in self.running_scripts:
|
|
283
|
+
continue
|
|
284
|
+
pid = None
|
|
285
|
+
try:
|
|
286
|
+
pid = int(comp.meta.get('pid')) if comp.meta and 'pid' in comp.meta else None
|
|
287
|
+
except Exception:
|
|
288
|
+
pid = None
|
|
289
|
+
is_pid_alive = bool(pid) and os.path.exists(f"/proc/{pid}")
|
|
290
|
+
if not is_pid_alive:
|
|
291
|
+
try:
|
|
292
|
+
comp.value = 'error'
|
|
293
|
+
comp.save(update_fields=['value'])
|
|
294
|
+
except Exception:
|
|
295
|
+
pass
|
|
296
|
+
|
|
277
297
|
for script in Component.objects.filter(
|
|
278
298
|
base_type='script', config__keep_alive=True
|
|
279
299
|
).exclude(value__in=('running', 'stopped', 'finished')):
|
|
@@ -357,13 +377,9 @@ class AutomationsGatewayHandler(GatesHandler, BaseObjectCommandsGatewayHandler):
|
|
|
357
377
|
print("START SCRIPT %s" % str(component))
|
|
358
378
|
|
|
359
379
|
if component.id in self.running_scripts:
|
|
360
|
-
#
|
|
361
|
-
# so we make sure it has correct value, do nothing else and return!
|
|
380
|
+
# Script appears to be healthy; do nothing and return.
|
|
362
381
|
if component.id not in self.terminating_scripts \
|
|
363
382
|
and self.running_scripts[component.id]['proc'].is_alive():
|
|
364
|
-
if component.value != 'running':
|
|
365
|
-
component.value = 'running'
|
|
366
|
-
component.save()
|
|
367
383
|
return
|
|
368
384
|
|
|
369
385
|
# script is in terminating state or is no longer alive
|
|
@@ -429,4 +445,4 @@ class AutomationsGatewayHandler(GatesHandler, BaseObjectCommandsGatewayHandler):
|
|
|
429
445
|
self.running_scripts.pop(component.id, None)
|
|
430
446
|
logger.handlers = []
|
|
431
447
|
|
|
432
|
-
threading.Thread(target=kill, daemon=True).start()
|
|
448
|
+
threading.Thread(target=kill, daemon=True).start()
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
simo/core/admin.py
CHANGED
|
@@ -352,9 +352,10 @@ class ComponentAdmin(EasyObjectsDeleteMixin, admin.ModelAdmin):
|
|
|
352
352
|
|
|
353
353
|
ctx['is_last'] = True
|
|
354
354
|
ctx['current_step'] = 3
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
)
|
|
355
|
+
# Normalize controller base type to slug for display
|
|
356
|
+
bt = getattr(controller_cls, 'base_type', None)
|
|
357
|
+
slug = bt if isinstance(bt, str) else getattr(bt, 'slug', None)
|
|
358
|
+
ctx['selected_type'] = ALL_BASE_TYPES.get(slug or bt, slug or bt)
|
|
358
359
|
ctx['info'] = controller_cls.info(controller_cls)
|
|
359
360
|
if request.method == 'POST':
|
|
360
361
|
ctx['form'] = add_form(
|
|
@@ -506,4 +507,4 @@ class ComponentAdmin(EasyObjectsDeleteMixin, admin.ModelAdmin):
|
|
|
506
507
|
'value_history': obj.history.filter(type='value').order_by('-date')[:50],
|
|
507
508
|
'arm_status_history': obj.history.filter(type='security').order_by('-date')[:50]
|
|
508
509
|
}
|
|
509
|
-
)
|
|
510
|
+
)
|
simo/core/base_types.py
CHANGED
|
@@ -1,23 +1,196 @@
|
|
|
1
1
|
"""
|
|
2
|
-
|
|
2
|
+
Component base types as first-class, self-describing classes.
|
|
3
|
+
|
|
4
|
+
Apps can define their base types in their own `<app>.base_types` module
|
|
5
|
+
by subclassing `BaseComponentType`. For backward compatibility, each
|
|
6
|
+
module should also export a `BASE_TYPES` mapping `{slug: name}`; this
|
|
7
|
+
file exports such mapping automatically from declared classes.
|
|
3
8
|
"""
|
|
4
9
|
|
|
10
|
+
from abc import ABC
|
|
5
11
|
from django.utils.translation import gettext_lazy as _
|
|
6
12
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
'
|
|
21
|
-
'
|
|
22
|
-
'
|
|
23
|
-
|
|
13
|
+
|
|
14
|
+
class BaseComponentType(ABC):
|
|
15
|
+
"""Abstract base for component base types.
|
|
16
|
+
|
|
17
|
+
Subclasses should set:
|
|
18
|
+
- slug: string identifier stored in Component.base_type
|
|
19
|
+
- name: human-friendly name (lazy-translated)
|
|
20
|
+
- description: short explanation of what this type represents
|
|
21
|
+
- purpose: when/why to use this type
|
|
22
|
+
- required_methods: tuple of controller method names that must be
|
|
23
|
+
implemented by controllers of this base type (optional).
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
slug: str = ''
|
|
27
|
+
name: str = ''
|
|
28
|
+
description: str = ''
|
|
29
|
+
purpose: str = ''
|
|
30
|
+
required_methods: tuple = tuple()
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def describe(cls) -> dict:
|
|
34
|
+
return {
|
|
35
|
+
'slug': cls.slug,
|
|
36
|
+
'name': cls.name,
|
|
37
|
+
'description': cls.description,
|
|
38
|
+
'purpose': cls.purpose,
|
|
39
|
+
'required_methods': list(cls.required_methods or ()),
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def validate_controller(cls, controller_cls):
|
|
44
|
+
"""Validate that a controller satisfies this type's contract.
|
|
45
|
+
|
|
46
|
+
Raises TypeError with a helpful message on mismatch.
|
|
47
|
+
"""
|
|
48
|
+
# If there are no explicit requirements, nothing to validate.
|
|
49
|
+
if not cls.required_methods:
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
missing = []
|
|
53
|
+
for method in cls.required_methods:
|
|
54
|
+
attr = getattr(controller_cls, method, None)
|
|
55
|
+
if not callable(attr):
|
|
56
|
+
missing.append(method)
|
|
57
|
+
if missing:
|
|
58
|
+
reqs = ', '.join(cls.required_methods)
|
|
59
|
+
raise TypeError(
|
|
60
|
+
f"Controller {controller_cls.__module__}.{controller_cls.__name__} "
|
|
61
|
+
f"for base type '{cls.slug}' is missing required method(s): "
|
|
62
|
+
f"{', '.join(missing)}. Expected: {reqs}"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# ---- Core base types -------------------------------------------------
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class NumericSensorType(BaseComponentType):
|
|
70
|
+
slug = 'numeric-sensor'
|
|
71
|
+
name = _("Numeric sensor")
|
|
72
|
+
description = _("Represents a single numeric value that changes over time.")
|
|
73
|
+
purpose = _("Use for temperature, humidity, light level, etc.")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class MultiSensorType(BaseComponentType):
|
|
77
|
+
slug = 'multi-sensor'
|
|
78
|
+
name = _("Multi sensor")
|
|
79
|
+
description = _("Represents several labeled readings in one component.")
|
|
80
|
+
purpose = _("Use when a single device reports multiple values.")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class BinarySensorType(BaseComponentType):
|
|
84
|
+
slug = 'binary-sensor'
|
|
85
|
+
name = _("Binary sensor")
|
|
86
|
+
description = _("A boolean on/off style sensor.")
|
|
87
|
+
purpose = _("Use for motion, door, presence, and similar states.")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class ButtonType(BaseComponentType):
|
|
91
|
+
slug = 'button'
|
|
92
|
+
name = _("Button")
|
|
93
|
+
description = _("Momentary button events like click, double-click, hold.")
|
|
94
|
+
purpose = _("Use to model input-only button devices.")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class SwitchType(BaseComponentType):
|
|
98
|
+
slug = 'switch'
|
|
99
|
+
name = _("Switch")
|
|
100
|
+
description = _("Binary on/off actuator.")
|
|
101
|
+
purpose = _("Use to control relays, power sockets, or generic toggles.")
|
|
102
|
+
required_methods = ('turn_on', 'turn_off', 'toggle')
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class DoubleSwitchType(BaseComponentType):
|
|
106
|
+
slug = 'switch-double'
|
|
107
|
+
name = _("Switch Double")
|
|
108
|
+
description = _("Two-channel on/off actuator.")
|
|
109
|
+
purpose = _("Use to control two separate loads in one device.")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class TripleSwitchType(BaseComponentType):
|
|
113
|
+
slug = 'switch-triple'
|
|
114
|
+
name = _("Switch Triple")
|
|
115
|
+
description = _("Three-channel on/off actuator.")
|
|
116
|
+
purpose = _("Use to control three separate loads in one device.")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class QuadrupleSwitchType(BaseComponentType):
|
|
120
|
+
slug = 'switch-quadruple'
|
|
121
|
+
name = _("Switch Quadruple")
|
|
122
|
+
description = _("Four-channel on/off actuator.")
|
|
123
|
+
purpose = _("Use to control four loads in one device.")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class QuintupleSwitchType(BaseComponentType):
|
|
127
|
+
slug = 'switch-quintuple'
|
|
128
|
+
name = _("Switch Quintuple")
|
|
129
|
+
description = _("Five-channel on/off actuator.")
|
|
130
|
+
purpose = _("Use to control five loads in one device.")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class DimmerType(BaseComponentType):
|
|
134
|
+
slug = 'dimmer'
|
|
135
|
+
name = _("Dimmer")
|
|
136
|
+
description = _("Continuous actuator with settable output level.")
|
|
137
|
+
purpose = _("Use to control lights or devices with variable output.")
|
|
138
|
+
required_methods = ('turn_on', 'turn_off', 'toggle', 'output_percent', 'max_out')
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class DimmerPlusType(BaseComponentType):
|
|
142
|
+
slug = 'dimmer-plus'
|
|
143
|
+
name = _("Dimmer Plus")
|
|
144
|
+
description = _("Multi-channel dimmer with main and secondary outputs.")
|
|
145
|
+
purpose = _("Use for fixtures with multiple dimmable channels.")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class RGBWLightType(BaseComponentType):
|
|
149
|
+
slug = 'rgbw-light'
|
|
150
|
+
name = _("RGB(W) light")
|
|
151
|
+
description = _("Color-capable light with optional white channel.")
|
|
152
|
+
purpose = _("Use for RGB/RGBW lighting control.")
|
|
153
|
+
required_methods = ('turn_on', 'turn_off', 'toggle')
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class LockType(BaseComponentType):
|
|
157
|
+
slug = 'lock'
|
|
158
|
+
name = _("Lock")
|
|
159
|
+
description = _("Door lock actuator with state reporting.")
|
|
160
|
+
purpose = _("Use to control smart locks and display their status.")
|
|
161
|
+
required_methods = ('lock', 'unlock')
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class GateType(BaseComponentType):
|
|
165
|
+
slug = 'gate'
|
|
166
|
+
name = _("Gate")
|
|
167
|
+
description = _("Gate/door opener with open/close/call commands.")
|
|
168
|
+
purpose = _("Use to manage gates with impulse or directional control.")
|
|
169
|
+
required_methods = ('open', 'close', 'call')
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class BlindsType(BaseComponentType):
|
|
173
|
+
slug = 'blinds'
|
|
174
|
+
name = _("Blinds")
|
|
175
|
+
description = _("Window coverings with position and optional angle control.")
|
|
176
|
+
purpose = _("Use to control roller blinds, shades, or venetians.")
|
|
177
|
+
required_methods = ('open', 'close', 'stop')
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _export_base_types_dict():
|
|
181
|
+
"""Derive legacy BASE_TYPES mapping from declared classes.
|
|
182
|
+
|
|
183
|
+
Returns {slug: name} for compatibility with legacy loaders.
|
|
184
|
+
"""
|
|
185
|
+
import inspect as _inspect
|
|
186
|
+
mapping = {}
|
|
187
|
+
g = globals()
|
|
188
|
+
for _name, _obj in g.items():
|
|
189
|
+
if _inspect.isclass(_obj) and issubclass(_obj, BaseComponentType) \
|
|
190
|
+
and _obj is not BaseComponentType and getattr(_obj, 'slug', None):
|
|
191
|
+
mapping[_obj.slug] = _obj.name
|
|
192
|
+
return mapping
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
# Backwards-compatible export for code still expecting a dict.
|
|
196
|
+
BASE_TYPES = _export_base_types_dict()
|