python-library-automation 0.1.16__tar.gz
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.
- python_library_automation-0.1.16/.gitignore +11 -0
- python_library_automation-0.1.16/PKG-INFO +8 -0
- python_library_automation-0.1.16/automation/__init__.py +25 -0
- python_library_automation-0.1.16/automation/assistant.py +141 -0
- python_library_automation-0.1.16/automation/builtins/__init__.py +27 -0
- python_library_automation-0.1.16/automation/builtins/action/__init__.py +0 -0
- python_library_automation-0.1.16/automation/builtins/action/call_entity_method.py +35 -0
- python_library_automation-0.1.16/automation/builtins/action/delay.py +18 -0
- python_library_automation-0.1.16/automation/builtins/action/log.py +20 -0
- python_library_automation-0.1.16/automation/builtins/action/set_attribute.py +27 -0
- python_library_automation-0.1.16/automation/builtins/entity/__init__.py +0 -0
- python_library_automation-0.1.16/automation/builtins/entity/time.py +29 -0
- python_library_automation-0.1.16/automation/builtins/entity/variable.py +81 -0
- python_library_automation-0.1.16/automation/builtins/event/__init__.py +0 -0
- python_library_automation-0.1.16/automation/builtins/event/_scheduled.py +34 -0
- python_library_automation-0.1.16/automation/builtins/event/at.py +27 -0
- python_library_automation-0.1.16/automation/builtins/event/callback.py +65 -0
- python_library_automation-0.1.16/automation/builtins/event/every.py +32 -0
- python_library_automation-0.1.16/automation/builtins/event/state_changed.py +58 -0
- python_library_automation-0.1.16/automation/core/__init__.py +13 -0
- python_library_automation-0.1.16/automation/core/action.py +22 -0
- python_library_automation-0.1.16/automation/core/base.py +59 -0
- python_library_automation-0.1.16/automation/core/composite_action.py +47 -0
- python_library_automation-0.1.16/automation/core/entity.py +178 -0
- python_library_automation-0.1.16/automation/core/event.py +87 -0
- python_library_automation-0.1.16/automation/core/event_context.py +10 -0
- python_library_automation-0.1.16/automation/core/trigger.py +151 -0
- python_library_automation-0.1.16/automation/errors.py +42 -0
- python_library_automation-0.1.16/automation/executor.py +46 -0
- python_library_automation-0.1.16/automation/hub.py +52 -0
- python_library_automation-0.1.16/automation/listeners/__init__.py +17 -0
- python_library_automation-0.1.16/automation/listeners/base.py +23 -0
- python_library_automation-0.1.16/automation/listeners/console.py +82 -0
- python_library_automation-0.1.16/automation/listeners/instance_schema.py +26 -0
- python_library_automation-0.1.16/automation/listeners/record.py +29 -0
- python_library_automation-0.1.16/automation/listeners/trace.py +70 -0
- python_library_automation-0.1.16/automation/listeners/type_schema.py +26 -0
- python_library_automation-0.1.16/automation/loader.py +131 -0
- python_library_automation-0.1.16/automation/renderer.py +311 -0
- python_library_automation-0.1.16/automation/schema.py +142 -0
- python_library_automation-0.1.16/automation/updater.py +84 -0
- python_library_automation-0.1.16/example-simple.bat +10 -0
- python_library_automation-0.1.16/examples/simple/__main__.py +27 -0
- python_library_automation-0.1.16/pyproject.toml +17 -0
- python_library_automation-0.1.16/test.bat +9 -0
- python_library_automation-0.1.16/tests/support.py +20 -0
- python_library_automation-0.1.16/tests/test_assistant.py +31 -0
- python_library_automation-0.1.16/tests/test_builder.py +43 -0
- python_library_automation-0.1.16/tests/test_expression_condition.py +103 -0
- python_library_automation-0.1.16/tests/test_expression_parser.py +38 -0
- python_library_automation-0.1.16/tests/test_hub.py +20 -0
- python_library_automation-0.1.16/tests/test_schema.py +179 -0
- python_library_automation-0.1.16/tests/test_trigger_flow.py +92 -0
- python_library_automation-0.1.16/update.bat +9 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from .core import (
|
|
2
|
+
Action,
|
|
3
|
+
Entity,
|
|
4
|
+
Trigger,
|
|
5
|
+
Event,
|
|
6
|
+
)
|
|
7
|
+
from .assistant import Assistant
|
|
8
|
+
from .listeners import BaseListener, ConsoleListener, TraceListener, TypeSchemaListener, InstanceSchemaListener
|
|
9
|
+
from .renderer import Renderer
|
|
10
|
+
|
|
11
|
+
from . import builtins # noqa: F401 触发自动注册
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"Action",
|
|
15
|
+
"Entity",
|
|
16
|
+
"Trigger",
|
|
17
|
+
"Event",
|
|
18
|
+
"Assistant",
|
|
19
|
+
"BaseListener",
|
|
20
|
+
"ConsoleListener",
|
|
21
|
+
"TraceListener",
|
|
22
|
+
"TypeSchemaListener",
|
|
23
|
+
"InstanceSchemaListener",
|
|
24
|
+
"Renderer",
|
|
25
|
+
]
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from configlib import load_config
|
|
9
|
+
from watch_config import WatchConfig
|
|
10
|
+
|
|
11
|
+
from automation.hub import Hub, State
|
|
12
|
+
from automation.core import Entity, Event, Trigger, BaseAutomation
|
|
13
|
+
from automation.core.composite_action import CompositeAction
|
|
14
|
+
from automation import loader, updater, schema
|
|
15
|
+
from automation.listeners import BaseListener
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Assistant:
|
|
21
|
+
def __init__(self, listeners: list[BaseListener] | BaseListener | None = None) -> None:
|
|
22
|
+
self._hub = Hub()
|
|
23
|
+
if listeners is not None:
|
|
24
|
+
if isinstance(listeners, BaseListener):
|
|
25
|
+
listeners = [listeners]
|
|
26
|
+
self._hub.listeners = listeners
|
|
27
|
+
self._watcher: WatchConfig[dict] | None = None
|
|
28
|
+
self._loop: asyncio.AbstractEventLoop | None = None
|
|
29
|
+
self._reload_lock = asyncio.Lock()
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def entities(self) -> dict[str, Entity]:
|
|
33
|
+
return self._hub.entities
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def events(self) -> dict[str, Event]:
|
|
37
|
+
return self._hub.events
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def actions(self) -> dict[str, CompositeAction]:
|
|
41
|
+
return self._hub.actions
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def triggers(self) -> dict[str, Trigger]:
|
|
45
|
+
return self._hub.triggers
|
|
46
|
+
|
|
47
|
+
def section(self, name: str) -> dict[str, BaseAutomation]:
|
|
48
|
+
return self._hub.section(name)
|
|
49
|
+
|
|
50
|
+
async def load(
|
|
51
|
+
self, source: str | Path | dict, watch: bool = False
|
|
52
|
+
) -> Assistant:
|
|
53
|
+
if watch and isinstance(source, dict):
|
|
54
|
+
raise TypeError(
|
|
55
|
+
"watch=True requires a file path, dict is not supported"
|
|
56
|
+
)
|
|
57
|
+
config = _read_source(source)
|
|
58
|
+
await loader.load(self._hub, config)
|
|
59
|
+
self._hub.notify("on_loaded", self._hub)
|
|
60
|
+
if watch:
|
|
61
|
+
self._watcher = WatchConfig(Path(source), dict)
|
|
62
|
+
self._watcher(self._on_config_change)
|
|
63
|
+
return self
|
|
64
|
+
|
|
65
|
+
async def start(self) -> Assistant:
|
|
66
|
+
if self._hub.state == State.RUNNING:
|
|
67
|
+
return self
|
|
68
|
+
self._hub.state = State.RUNNING
|
|
69
|
+
self._hub.stop_event.clear()
|
|
70
|
+
self._loop = asyncio.get_running_loop()
|
|
71
|
+
for section_name in self._hub.AUTOMATION_SECTIONS:
|
|
72
|
+
for obj in self._hub.section(section_name).values():
|
|
73
|
+
await obj.on_start()
|
|
74
|
+
if self._watcher is not None:
|
|
75
|
+
self._watcher.start()
|
|
76
|
+
self._hub.notify("on_start")
|
|
77
|
+
return self
|
|
78
|
+
|
|
79
|
+
async def run(self) -> Assistant:
|
|
80
|
+
await self.start()
|
|
81
|
+
await self._hub.stop_event.wait()
|
|
82
|
+
return self
|
|
83
|
+
|
|
84
|
+
async def stop(self) -> None:
|
|
85
|
+
if self._hub.state != State.RUNNING:
|
|
86
|
+
return
|
|
87
|
+
self._hub.state = State.STOPPED
|
|
88
|
+
if self._watcher:
|
|
89
|
+
self._watcher.stop()
|
|
90
|
+
for section_name in reversed(self._hub.AUTOMATION_SECTIONS):
|
|
91
|
+
for obj in self._hub.section(section_name).values():
|
|
92
|
+
await obj.on_stop()
|
|
93
|
+
self._hub.stop_event.set()
|
|
94
|
+
self._hub.notify("on_stop")
|
|
95
|
+
|
|
96
|
+
async def update(self, source: str | Path | dict) -> None:
|
|
97
|
+
"""手动热更新"""
|
|
98
|
+
new_config = _read_source(source)
|
|
99
|
+
await self._apply_reload(new_config)
|
|
100
|
+
|
|
101
|
+
def watch(self, path: str | Path, interval: float = 2.0) -> Assistant:
|
|
102
|
+
"""设置文件监控"""
|
|
103
|
+
if self._watcher:
|
|
104
|
+
self._watcher.stop()
|
|
105
|
+
self._watcher = WatchConfig(Path(path), dict, interval=interval)
|
|
106
|
+
self._watcher(self._on_config_change)
|
|
107
|
+
if self._hub.state == State.RUNNING:
|
|
108
|
+
self._watcher.start()
|
|
109
|
+
return self
|
|
110
|
+
|
|
111
|
+
def _on_config_change(self, cfg: dict, changelog) -> None:
|
|
112
|
+
if self._loop is None or self._loop.is_closed():
|
|
113
|
+
return
|
|
114
|
+
asyncio.run_coroutine_threadsafe(
|
|
115
|
+
self._apply_reload(cfg), self._loop
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
async def _apply_reload(self, new_config: dict) -> None:
|
|
119
|
+
async with self._reload_lock:
|
|
120
|
+
old_config = self._hub.config
|
|
121
|
+
if old_config == new_config:
|
|
122
|
+
return
|
|
123
|
+
await updater.apply_diff(self._hub, old_config, new_config)
|
|
124
|
+
self._hub.config = new_config
|
|
125
|
+
self._hub.notify("on_loaded", self._hub)
|
|
126
|
+
|
|
127
|
+
@staticmethod
|
|
128
|
+
def export_schema() -> dict:
|
|
129
|
+
from automation.schema import export_type_schema
|
|
130
|
+
return export_type_schema()
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _read_source(source: str | Path | dict) -> dict[str, Any]:
|
|
134
|
+
if isinstance(source, (str, Path)):
|
|
135
|
+
data = load_config(str(source))
|
|
136
|
+
if not isinstance(data, dict):
|
|
137
|
+
raise TypeError(
|
|
138
|
+
f"Config root must be a dict, got {type(data).__name__}"
|
|
139
|
+
)
|
|
140
|
+
return data
|
|
141
|
+
return source
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import pkgutil
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
def _auto_import():
|
|
6
|
+
"""自动导入 builtins 下所有子模块,触发类的注册"""
|
|
7
|
+
package_dir = Path(__file__).resolve().parent
|
|
8
|
+
for sub_pkg in package_dir.iterdir():
|
|
9
|
+
if not sub_pkg.is_dir() or sub_pkg.name.startswith("_"):
|
|
10
|
+
continue
|
|
11
|
+
sub_package = f"{__name__}.{sub_pkg.name}"
|
|
12
|
+
try:
|
|
13
|
+
pkg = importlib.import_module(sub_package)
|
|
14
|
+
except ImportError:
|
|
15
|
+
continue
|
|
16
|
+
pkg_path = getattr(pkg, "__path__", None)
|
|
17
|
+
if pkg_path is None:
|
|
18
|
+
continue
|
|
19
|
+
for _finder, module_name, _is_pkg in pkgutil.walk_packages(
|
|
20
|
+
pkg_path, prefix=f"{sub_package}."
|
|
21
|
+
):
|
|
22
|
+
try:
|
|
23
|
+
importlib.import_module(module_name)
|
|
24
|
+
except ImportError:
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
_auto_import()
|
|
File without changes
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import inspect
|
|
3
|
+
from typing import ClassVar, TYPE_CHECKING
|
|
4
|
+
from pydantic import Field
|
|
5
|
+
from automation.core import Action
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from automation.renderer import Renderer
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CallEntityMethod(Action):
|
|
12
|
+
_abstract: ClassVar[bool] = False
|
|
13
|
+
_type: ClassVar[str] = "call_entity_method"
|
|
14
|
+
|
|
15
|
+
entity: str = Field(description="实体名称")
|
|
16
|
+
method: str = Field(description="方法名称")
|
|
17
|
+
args: dict = Field(default_factory=dict, description="方法参数")
|
|
18
|
+
|
|
19
|
+
async def execute(self, renderer: Renderer) -> None:
|
|
20
|
+
hub = renderer._hub
|
|
21
|
+
if self.entity not in hub.entities:
|
|
22
|
+
raise ValueError(f"Entity {self.entity!r} not found")
|
|
23
|
+
entity = hub.entities[self.entity]
|
|
24
|
+
if not hasattr(entity, self.method):
|
|
25
|
+
raise ValueError(
|
|
26
|
+
f"Entity {self.entity!r} has no method {self.method!r}"
|
|
27
|
+
)
|
|
28
|
+
method = getattr(entity, self.method)
|
|
29
|
+
if not callable(method):
|
|
30
|
+
raise ValueError(
|
|
31
|
+
f"Entity {self.entity!r}.{self.method} is not callable"
|
|
32
|
+
)
|
|
33
|
+
result = method(**self.args)
|
|
34
|
+
if inspect.isawaitable(result):
|
|
35
|
+
await result
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import ClassVar, TYPE_CHECKING
|
|
3
|
+
import asyncio
|
|
4
|
+
from pydantic import Field
|
|
5
|
+
from automation.core import Action
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from automation.renderer import Renderer
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DelayAction(Action):
|
|
12
|
+
_type: ClassVar[str] = "delay"
|
|
13
|
+
_abstract: ClassVar[bool] = False
|
|
14
|
+
|
|
15
|
+
seconds: float = Field(ge=0, description="等待秒数")
|
|
16
|
+
|
|
17
|
+
async def execute(self, renderer: Renderer) -> None:
|
|
18
|
+
await asyncio.sleep(self.seconds)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import ClassVar, TYPE_CHECKING
|
|
3
|
+
import logging
|
|
4
|
+
from pydantic import Field
|
|
5
|
+
from automation.core import Action
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from automation.renderer import Renderer
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class LogAction(Action):
|
|
14
|
+
_type: ClassVar[str] = "log"
|
|
15
|
+
_abstract: ClassVar[bool] = False
|
|
16
|
+
|
|
17
|
+
info: str = Field(description="日志内容")
|
|
18
|
+
|
|
19
|
+
async def execute(self, renderer: Renderer) -> None:
|
|
20
|
+
logger.info(self.info)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Any, ClassVar, TYPE_CHECKING
|
|
3
|
+
from pydantic import Field
|
|
4
|
+
from automation.core import Action
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from automation.renderer import Renderer
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SetAttributeAction(Action):
|
|
11
|
+
_abstract: ClassVar[bool] = False
|
|
12
|
+
_type: ClassVar[str] = "set_attribute"
|
|
13
|
+
|
|
14
|
+
entity: str = Field(description="实体名称")
|
|
15
|
+
attribute: str = Field(description="属性名称")
|
|
16
|
+
value: Any = Field(description="新值")
|
|
17
|
+
|
|
18
|
+
async def execute(self, renderer: Renderer) -> None:
|
|
19
|
+
hub = renderer._hub
|
|
20
|
+
if self.entity not in hub.entities:
|
|
21
|
+
raise ValueError(f"Entity {self.entity!r} not found")
|
|
22
|
+
entity = hub.entities[self.entity]
|
|
23
|
+
if not hasattr(entity, self.attribute):
|
|
24
|
+
raise ValueError(
|
|
25
|
+
f"Entity {self.entity!r} has no attribute {self.attribute!r}"
|
|
26
|
+
)
|
|
27
|
+
setattr(entity, self.attribute, self.value)
|
|
File without changes
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import ClassVar
|
|
4
|
+
from automation.core import Entity
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TimeEntity(Entity):
|
|
8
|
+
_abstract: ClassVar[bool] = False
|
|
9
|
+
_type: ClassVar[str] = "time"
|
|
10
|
+
|
|
11
|
+
@property
|
|
12
|
+
def hour(self) -> int:
|
|
13
|
+
"""当前小时 (0-23)"""
|
|
14
|
+
return datetime.now().hour
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def minute(self) -> int:
|
|
18
|
+
"""当前分钟 (0-59)"""
|
|
19
|
+
return datetime.now().minute
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def second(self) -> int:
|
|
23
|
+
"""当前秒 (0-59)"""
|
|
24
|
+
return datetime.now().second
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def weekday(self) -> int:
|
|
28
|
+
"""星期几 (0=周一, 6=周日)"""
|
|
29
|
+
return datetime.now().weekday()
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Any, ClassVar, TYPE_CHECKING
|
|
3
|
+
from pydantic import Field, PrivateAttr
|
|
4
|
+
from automation.core import Entity
|
|
5
|
+
from automation.core.entity import AttributeInfo
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from automation.hub import Hub
|
|
9
|
+
|
|
10
|
+
TYPE_MAP = {"int": int, "float": float, "str": str, "bool": bool}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class VariableEntity(Entity):
|
|
14
|
+
_abstract: ClassVar[bool] = False
|
|
15
|
+
_type: ClassVar[str] = "variable"
|
|
16
|
+
|
|
17
|
+
properties: dict[str, dict[str, Any]] = Field(
|
|
18
|
+
default_factory=dict,
|
|
19
|
+
description="变量定义:{name: {type: str, default: value}}",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
_values: dict[str, Any] = PrivateAttr(default_factory=dict)
|
|
23
|
+
|
|
24
|
+
async def on_validate(self, hub: Hub) -> None:
|
|
25
|
+
for name, spec in self.properties.items():
|
|
26
|
+
type_name = spec.get("type", "str")
|
|
27
|
+
value = spec.get("value")
|
|
28
|
+
if type_name == "list":
|
|
29
|
+
self._values[name] = hub.renderer.render_value(value or [])
|
|
30
|
+
else:
|
|
31
|
+
self._values[name] = self._cast(type_name, value)
|
|
32
|
+
|
|
33
|
+
def get_attributes(self) -> tuple[AttributeInfo, ...]:
|
|
34
|
+
return tuple(
|
|
35
|
+
AttributeInfo(
|
|
36
|
+
name=name,
|
|
37
|
+
type=spec.get("type", "str"),
|
|
38
|
+
description=spec.get("description", ""),
|
|
39
|
+
readonly=False,
|
|
40
|
+
default=spec.get("default"),
|
|
41
|
+
)
|
|
42
|
+
for name, spec in self.properties.items()
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def get_attribute_values(self) -> dict[str, Any]:
|
|
46
|
+
return dict(self._values)
|
|
47
|
+
|
|
48
|
+
def __getattr__(self, name: str) -> Any:
|
|
49
|
+
if not name.startswith("_"):
|
|
50
|
+
priv = self.__dict__.get("__pydantic_private__")
|
|
51
|
+
if priv and "_values" in priv and name in priv["_values"]:
|
|
52
|
+
return priv["_values"][name]
|
|
53
|
+
return super().__getattr__(name)
|
|
54
|
+
|
|
55
|
+
def __setattr__(self, name: str, value: Any) -> None:
|
|
56
|
+
priv = self.__dict__.get("__pydantic_private__")
|
|
57
|
+
if priv is not None and "_values" in priv and name in priv["_values"]:
|
|
58
|
+
old = priv["_values"][name]
|
|
59
|
+
type_name = self.properties[name].get("type", "str")
|
|
60
|
+
if type_name == "list":
|
|
61
|
+
new = list(value) if not isinstance(value, list) else value
|
|
62
|
+
else:
|
|
63
|
+
new = self._cast(type_name, value)
|
|
64
|
+
priv["_values"][name] = new
|
|
65
|
+
try:
|
|
66
|
+
changed = old != new
|
|
67
|
+
except Exception:
|
|
68
|
+
changed = True
|
|
69
|
+
if changed:
|
|
70
|
+
self._notify_change(name, old, new)
|
|
71
|
+
return
|
|
72
|
+
super().__setattr__(name, value)
|
|
73
|
+
|
|
74
|
+
@staticmethod
|
|
75
|
+
def _cast(type_name: str, value: Any) -> Any:
|
|
76
|
+
if value is None:
|
|
77
|
+
return None
|
|
78
|
+
caster = TYPE_MAP.get(type_name)
|
|
79
|
+
if caster is not None:
|
|
80
|
+
return caster(value)
|
|
81
|
+
return value
|
|
File without changes
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import ClassVar, TYPE_CHECKING, Any
|
|
4
|
+
|
|
5
|
+
from pydantic import PrivateAttr
|
|
6
|
+
|
|
7
|
+
from automation.core import Event
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from automation.hub import Hub
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ScheduledEvent(Event):
|
|
14
|
+
_abstract: ClassVar[bool] = True
|
|
15
|
+
|
|
16
|
+
_job: Any = PrivateAttr(default=None)
|
|
17
|
+
|
|
18
|
+
def _create_job(self) -> Any:
|
|
19
|
+
raise NotImplementedError
|
|
20
|
+
|
|
21
|
+
async def on_activate(self, hub: Hub) -> None:
|
|
22
|
+
if self._job:
|
|
23
|
+
await self._job.stop()
|
|
24
|
+
job = self._create_job()
|
|
25
|
+
job.add(self.fire)
|
|
26
|
+
self._job = job
|
|
27
|
+
|
|
28
|
+
async def on_start(self) -> None:
|
|
29
|
+
if self._job:
|
|
30
|
+
await self._job.start()
|
|
31
|
+
|
|
32
|
+
async def on_stop(self) -> None:
|
|
33
|
+
if self._job:
|
|
34
|
+
await self._job.stop()
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import ClassVar
|
|
4
|
+
|
|
5
|
+
from pydantic import Field
|
|
6
|
+
|
|
7
|
+
from ._scheduled import ScheduledEvent
|
|
8
|
+
from scheduler import At
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AtEvent(ScheduledEvent):
|
|
12
|
+
_type: ClassVar[str] = "at"
|
|
13
|
+
|
|
14
|
+
weekday: int | None = Field(default=None, ge=0, le=6)
|
|
15
|
+
hour: int = Field(default=0, ge=0, le=23)
|
|
16
|
+
minute: int = Field(default=0, ge=0, le=59)
|
|
17
|
+
second: int = Field(default=0, ge=0, le=59)
|
|
18
|
+
max_runs: int | None = Field(default=None, ge=1)
|
|
19
|
+
|
|
20
|
+
def _create_job(self) -> At:
|
|
21
|
+
return At(
|
|
22
|
+
weekday=self.weekday,
|
|
23
|
+
hour=self.hour,
|
|
24
|
+
minute=self.minute,
|
|
25
|
+
second=self.second,
|
|
26
|
+
max_runs=self.max_runs,
|
|
27
|
+
)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Any, ClassVar, TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
from pydantic import Field, PrivateAttr
|
|
5
|
+
|
|
6
|
+
from automation.core.event import Event
|
|
7
|
+
from automation.core.event_context import EventContext
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from automation.hub import Hub
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CallbackEvent(Event):
|
|
14
|
+
_abstract: ClassVar[bool] = False
|
|
15
|
+
_type: ClassVar[str] = "callback"
|
|
16
|
+
|
|
17
|
+
entity_type: str = Field(description="实体类型名")
|
|
18
|
+
callback: str = Field(description="实体上的回调属性名")
|
|
19
|
+
|
|
20
|
+
_entity_ref: Any = PrivateAttr(default=None)
|
|
21
|
+
_original_callbacks: dict[str, Any] = PrivateAttr(default_factory=dict)
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def entity(self) -> Any:
|
|
25
|
+
"""触发后可引用的实际 entity 实例"""
|
|
26
|
+
return self._entity_ref
|
|
27
|
+
|
|
28
|
+
def _find_entities(self, hub: Hub) -> list:
|
|
29
|
+
return [
|
|
30
|
+
e for e in hub.entities.values()
|
|
31
|
+
if e._type == self.entity_type
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
async def on_validate(self, hub: Hub) -> None:
|
|
35
|
+
if not self._find_entities(hub):
|
|
36
|
+
raise ValueError(
|
|
37
|
+
f"No entity of type {self.entity_type!r} found"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
async def on_activate(self, hub: Hub) -> None:
|
|
41
|
+
event_ref = self
|
|
42
|
+
self._original_callbacks.clear()
|
|
43
|
+
|
|
44
|
+
for entity in self._find_entities(hub):
|
|
45
|
+
self._original_callbacks[entity.instance_name] = getattr(
|
|
46
|
+
entity, self.callback, None
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
async def wrapper(_entity=entity, **kwargs):
|
|
50
|
+
event_ref._entity_ref = _entity
|
|
51
|
+
context = EventContext(
|
|
52
|
+
event_name=event_ref.instance_name,
|
|
53
|
+
data={"entity": _entity, **kwargs},
|
|
54
|
+
)
|
|
55
|
+
await event_ref.fire(context)
|
|
56
|
+
|
|
57
|
+
setattr(entity, self.callback, wrapper)
|
|
58
|
+
|
|
59
|
+
async def on_stop(self) -> None:
|
|
60
|
+
for name, original in self._original_callbacks.items():
|
|
61
|
+
entity = self._hub.entities.get(name)
|
|
62
|
+
if entity is not None and original is not None:
|
|
63
|
+
setattr(entity, self.callback, original)
|
|
64
|
+
self._original_callbacks.clear()
|
|
65
|
+
self._entity_ref = None
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import ClassVar
|
|
3
|
+
|
|
4
|
+
from pydantic import Field
|
|
5
|
+
|
|
6
|
+
from ._scheduled import ScheduledEvent
|
|
7
|
+
from scheduler import Every
|
|
8
|
+
|
|
9
|
+
class EveryEvent(ScheduledEvent):
|
|
10
|
+
_type: ClassVar[str] = "every"
|
|
11
|
+
|
|
12
|
+
seconds: float = Field(default=0, ge=0)
|
|
13
|
+
minutes: float = Field(default=0, ge=0)
|
|
14
|
+
hours: float = Field(default=0, ge=0)
|
|
15
|
+
days: float = Field(default=0, ge=0)
|
|
16
|
+
immediate: bool = Field(default=False)
|
|
17
|
+
max_runs: int | None = Field(default=None, ge=1)
|
|
18
|
+
|
|
19
|
+
async def on_validate(self, hub) -> None:
|
|
20
|
+
total = self.seconds + self.minutes * 60 + self.hours * 3600 + self.days * 86400
|
|
21
|
+
if total <= 0:
|
|
22
|
+
raise ValueError("EveryEvent requires at least one interval > 0")
|
|
23
|
+
|
|
24
|
+
def _create_job(self) -> Every:
|
|
25
|
+
return Every(
|
|
26
|
+
seconds=self.seconds,
|
|
27
|
+
minutes=self.minutes,
|
|
28
|
+
hours=self.hours,
|
|
29
|
+
days=self.days,
|
|
30
|
+
immediate=self.immediate,
|
|
31
|
+
max_runs=self.max_runs,
|
|
32
|
+
)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Any, ClassVar, TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
from pydantic import Field, PrivateAttr
|
|
5
|
+
|
|
6
|
+
from automation.core.event import Event
|
|
7
|
+
from automation.core.event_context import EventContext
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from automation.hub import Hub
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class StateChangedEvent(Event):
|
|
14
|
+
_abstract: ClassVar[bool] = False
|
|
15
|
+
_type: ClassVar[str] = "state_changed"
|
|
16
|
+
|
|
17
|
+
entity_type: str | None = Field(default=None, description="过滤实体类型(可选)")
|
|
18
|
+
entity_name: str | None = Field(default=None, description="过滤实体实例名(可选)")
|
|
19
|
+
attribute: str | None = Field(default=None, description="过滤属性名(可选)")
|
|
20
|
+
|
|
21
|
+
_entity_ref: Any = PrivateAttr(default=None)
|
|
22
|
+
_handler: Any = PrivateAttr(default=None)
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def entity(self) -> Any:
|
|
26
|
+
return self._entity_ref
|
|
27
|
+
|
|
28
|
+
async def on_activate(self, hub: Hub) -> None:
|
|
29
|
+
event_ref = self
|
|
30
|
+
|
|
31
|
+
async def handler(entity, attr, old, new):
|
|
32
|
+
if event_ref.entity_type and entity._type != event_ref.entity_type:
|
|
33
|
+
return
|
|
34
|
+
if event_ref.entity_name and entity.instance_name != event_ref.entity_name:
|
|
35
|
+
return
|
|
36
|
+
if event_ref.attribute and attr != event_ref.attribute:
|
|
37
|
+
return
|
|
38
|
+
event_ref._entity_ref = entity
|
|
39
|
+
context = EventContext(
|
|
40
|
+
event_name=event_ref.instance_name,
|
|
41
|
+
data={
|
|
42
|
+
"entity": entity,
|
|
43
|
+
"attribute": attr,
|
|
44
|
+
"old": old,
|
|
45
|
+
"new": new,
|
|
46
|
+
},
|
|
47
|
+
)
|
|
48
|
+
await event_ref.fire(context)
|
|
49
|
+
|
|
50
|
+
self._handler = handler
|
|
51
|
+
hub._on_state_changed.append(handler)
|
|
52
|
+
|
|
53
|
+
async def on_stop(self) -> None:
|
|
54
|
+
if self._handler and self._hub:
|
|
55
|
+
try:
|
|
56
|
+
self._hub._on_state_changed.remove(self._handler)
|
|
57
|
+
except ValueError:
|
|
58
|
+
pass
|