enapter 0.6.3__py3-none-any.whl → 0.12.1__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.
- enapter/__init__.py +4 -4
- enapter/async_/__init__.py +1 -4
- enapter/async_/generator.py +5 -2
- enapter/async_/routine.py +21 -53
- enapter/http/__init__.py +3 -0
- enapter/http/api/__init__.py +5 -0
- enapter/http/api/client.py +31 -0
- enapter/http/api/config.py +23 -0
- enapter/http/api/devices/__init__.py +21 -0
- enapter/http/api/devices/authorized_role.py +11 -0
- enapter/http/api/devices/client.py +25 -0
- enapter/http/api/devices/communication_config.py +45 -0
- enapter/http/api/devices/device.py +32 -0
- enapter/http/api/devices/device_type.py +10 -0
- enapter/http/api/devices/mqtt_credentials.py +13 -0
- enapter/http/api/devices/mqtt_protocol.py +7 -0
- enapter/http/api/devices/mqtts_credentials.py +18 -0
- enapter/http/api/devices/time_sync_protocol.py +6 -0
- enapter/log/json_formatter.py +14 -4
- enapter/mdns/resolver.py +14 -11
- enapter/mqtt/__init__.py +5 -12
- enapter/mqtt/api/__init__.py +6 -0
- enapter/mqtt/api/client.py +62 -0
- enapter/mqtt/api/config.py +77 -0
- enapter/mqtt/api/device/__init__.py +21 -0
- enapter/mqtt/api/device/channel.py +56 -0
- enapter/mqtt/api/device/command_request.py +38 -0
- enapter/mqtt/api/device/command_response.py +28 -0
- enapter/mqtt/api/device/command_state.py +8 -0
- enapter/mqtt/api/device/log.py +31 -0
- enapter/mqtt/api/device/log_severity.py +9 -0
- enapter/mqtt/api/device/message.py +24 -0
- enapter/mqtt/api/device/properties.py +24 -0
- enapter/mqtt/api/device/telemetry.py +28 -0
- enapter/mqtt/client.py +88 -119
- enapter/mqtt/errors.py +3 -0
- enapter/mqtt/message.py +3 -0
- enapter/standalone/__init__.py +25 -0
- enapter/standalone/config.py +218 -0
- enapter/standalone/device.py +59 -0
- enapter/standalone/device_protocol.py +33 -0
- enapter/standalone/logger.py +21 -0
- enapter/standalone/mqtt_adapter.py +134 -0
- enapter/standalone/run.py +39 -0
- enapter/standalone/ucm.py +28 -0
- enapter-0.12.1.dist-info/METADATA +74 -0
- enapter-0.12.1.dist-info/RECORD +52 -0
- {enapter-0.6.3.dist-info → enapter-0.12.1.dist-info}/WHEEL +1 -1
- {enapter-0.6.3.dist-info → enapter-0.12.1.dist-info}/top_level.txt +0 -1
- enapter/mqtt/command.py +0 -45
- enapter/mqtt/config.py +0 -48
- enapter/mqtt/device_channel.py +0 -83
- enapter/types.py +0 -3
- enapter/vucm/__init__.py +0 -12
- enapter/vucm/app.py +0 -48
- enapter/vucm/config.py +0 -59
- enapter/vucm/device.py +0 -92
- enapter/vucm/logger.py +0 -39
- enapter/vucm/ucm.py +0 -30
- enapter-0.6.3.dist-info/METADATA +0 -111
- enapter-0.6.3.dist-info/RECORD +0 -27
- tests/test_async.py +0 -152
- tests/test_log.py +0 -69
- /tests/__init__.py → /enapter/py.typed +0 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import dataclasses
|
|
3
|
+
import enum
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
from typing import Any, MutableMapping, Self
|
|
8
|
+
|
|
9
|
+
from enapter import mqtt
|
|
10
|
+
|
|
11
|
+
LOGGER = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclasses.dataclass
|
|
15
|
+
class Config:
|
|
16
|
+
|
|
17
|
+
communication_config: "CommunicationConfig"
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def communication(self) -> "CommunicationConfig":
|
|
21
|
+
return self.communication_config
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def from_env(
|
|
25
|
+
cls, env: MutableMapping[str, str] = os.environ, namespace: str = "ENAPTER_"
|
|
26
|
+
) -> Self:
|
|
27
|
+
communication_config = CommunicationConfig.from_env(env, namespace=namespace)
|
|
28
|
+
return cls(communication_config=communication_config)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclasses.dataclass
|
|
32
|
+
class CommunicationConfig:
|
|
33
|
+
|
|
34
|
+
mqtt_api_config: mqtt.api.Config
|
|
35
|
+
hardware_id: str
|
|
36
|
+
channel_id: str
|
|
37
|
+
ucm_needed: bool
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def mqtt_api(self) -> mqtt.api.Config:
|
|
41
|
+
return self.mqtt_api_config
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def from_env(
|
|
45
|
+
cls, env: MutableMapping[str, str] = os.environ, namespace: str = "ENAPTER_"
|
|
46
|
+
) -> Self:
|
|
47
|
+
prefix = namespace + "STANDALONE_COMMUNICATION_"
|
|
48
|
+
try:
|
|
49
|
+
blob = env[namespace + "VUCM_BLOB"]
|
|
50
|
+
LOGGER.warn(
|
|
51
|
+
"`%s` is deprecated and will be removed soon. Please use `%s`.",
|
|
52
|
+
namespace + "VUCM_BLOB",
|
|
53
|
+
prefix + "CONFIG",
|
|
54
|
+
)
|
|
55
|
+
except KeyError:
|
|
56
|
+
blob = env[prefix + "CONFIG"]
|
|
57
|
+
config = cls.from_blob(blob)
|
|
58
|
+
override_mqtt_api_host = env.get(prefix + "OVERRIDE_MQTT_API_HOST")
|
|
59
|
+
if override_mqtt_api_host is not None:
|
|
60
|
+
config.mqtt_api.host = override_mqtt_api_host
|
|
61
|
+
return config
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def from_blob(cls, blob: str) -> Self:
|
|
65
|
+
dto = json.loads(base64.b64decode(blob))
|
|
66
|
+
if "ucm_id" in dto:
|
|
67
|
+
config_v1 = CommunicationConfigV1.from_dto(dto)
|
|
68
|
+
return cls.from_config_v1(config_v1)
|
|
69
|
+
else:
|
|
70
|
+
config_v3 = CommunicationConfigV3.from_dto(dto)
|
|
71
|
+
return cls.from_config_v3(config_v3)
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def from_config_v1(cls, config: "CommunicationConfigV1") -> Self:
|
|
75
|
+
mqtt_api_config = mqtt.api.Config(
|
|
76
|
+
host=config.mqtt_host,
|
|
77
|
+
port=config.mqtt_port,
|
|
78
|
+
tls_config=mqtt.api.TLSConfig(
|
|
79
|
+
secret_key=config.mqtt_private_key,
|
|
80
|
+
cert=config.mqtt_cert,
|
|
81
|
+
ca_cert=config.mqtt_ca,
|
|
82
|
+
),
|
|
83
|
+
)
|
|
84
|
+
return cls(
|
|
85
|
+
mqtt_api_config=mqtt_api_config,
|
|
86
|
+
hardware_id=config.ucm_id,
|
|
87
|
+
channel_id=config.channel_id,
|
|
88
|
+
ucm_needed=True,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
@classmethod
|
|
92
|
+
def from_config_v3(cls, config: "CommunicationConfigV3") -> Self:
|
|
93
|
+
mqtt_api_config: mqtt.api.Config | None = None
|
|
94
|
+
match config.mqtt_protocol:
|
|
95
|
+
case CommunicationConfigV3.MQTTProtocol.MQTT:
|
|
96
|
+
assert isinstance(
|
|
97
|
+
config.mqtt_credentials, CommunicationConfigV3.MQTTCredentials
|
|
98
|
+
)
|
|
99
|
+
mqtt_api_config = mqtt.api.Config(
|
|
100
|
+
host=config.mqtt_host,
|
|
101
|
+
port=config.mqtt_port,
|
|
102
|
+
user=config.mqtt_credentials.username,
|
|
103
|
+
password=config.mqtt_credentials.password,
|
|
104
|
+
)
|
|
105
|
+
case CommunicationConfigV3.MQTTProtocol.MQTTS:
|
|
106
|
+
assert isinstance(
|
|
107
|
+
config.mqtt_credentials, CommunicationConfigV3.MQTTSCredentials
|
|
108
|
+
)
|
|
109
|
+
mqtt_api_config = mqtt.api.Config(
|
|
110
|
+
host=config.mqtt_host,
|
|
111
|
+
port=config.mqtt_port,
|
|
112
|
+
tls_config=mqtt.api.TLSConfig(
|
|
113
|
+
secret_key=config.mqtt_credentials.private_key,
|
|
114
|
+
cert=config.mqtt_credentials.certificate,
|
|
115
|
+
ca_cert=config.mqtt_credentials.ca_chain,
|
|
116
|
+
),
|
|
117
|
+
)
|
|
118
|
+
case _:
|
|
119
|
+
raise NotImplementedError(config.mqtt_protocol)
|
|
120
|
+
assert mqtt_api_config is not None
|
|
121
|
+
return cls(
|
|
122
|
+
mqtt_api_config=mqtt_api_config,
|
|
123
|
+
hardware_id=config.hardware_id,
|
|
124
|
+
channel_id=config.channel_id,
|
|
125
|
+
ucm_needed=False,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@dataclasses.dataclass
|
|
130
|
+
class CommunicationConfigV1:
|
|
131
|
+
|
|
132
|
+
mqtt_host: str
|
|
133
|
+
mqtt_port: int
|
|
134
|
+
mqtt_ca: str
|
|
135
|
+
mqtt_cert: str
|
|
136
|
+
mqtt_private_key: str
|
|
137
|
+
ucm_id: str
|
|
138
|
+
channel_id: str
|
|
139
|
+
|
|
140
|
+
@classmethod
|
|
141
|
+
def from_dto(cls, dto: dict[str, Any]) -> Self:
|
|
142
|
+
return cls(
|
|
143
|
+
mqtt_host=dto["mqtt_host"],
|
|
144
|
+
mqtt_port=int(dto["mqtt_port"]),
|
|
145
|
+
mqtt_ca=dto["mqtt_ca"],
|
|
146
|
+
mqtt_cert=dto["mqtt_cert"],
|
|
147
|
+
mqtt_private_key=dto["mqtt_private_key"],
|
|
148
|
+
ucm_id=dto["ucm_id"],
|
|
149
|
+
channel_id=dto["channel_id"],
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@dataclasses.dataclass
|
|
154
|
+
class CommunicationConfigV3:
|
|
155
|
+
|
|
156
|
+
class MQTTProtocol(enum.Enum):
|
|
157
|
+
|
|
158
|
+
MQTT = "MQTT"
|
|
159
|
+
MQTTS = "MQTTS"
|
|
160
|
+
|
|
161
|
+
@dataclasses.dataclass
|
|
162
|
+
class MQTTCredentials:
|
|
163
|
+
|
|
164
|
+
username: str
|
|
165
|
+
password: str
|
|
166
|
+
|
|
167
|
+
@classmethod
|
|
168
|
+
def from_dto(cls, dto: dict[str, Any]) -> Self:
|
|
169
|
+
return cls(username=dto["username"], password=dto["password"])
|
|
170
|
+
|
|
171
|
+
@dataclasses.dataclass
|
|
172
|
+
class MQTTSCredentials:
|
|
173
|
+
|
|
174
|
+
private_key: str
|
|
175
|
+
certificate: str
|
|
176
|
+
ca_chain: str
|
|
177
|
+
|
|
178
|
+
@classmethod
|
|
179
|
+
def from_dto(cls, dto: dict[str, Any]) -> Self:
|
|
180
|
+
return cls(
|
|
181
|
+
private_key=dto["private_key"],
|
|
182
|
+
certificate=dto["certificate"],
|
|
183
|
+
ca_chain=dto["ca_chain"],
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
mqtt_host: str
|
|
187
|
+
mqtt_port: int
|
|
188
|
+
mqtt_protocol: MQTTProtocol
|
|
189
|
+
mqtt_credentials: MQTTCredentials | MQTTSCredentials
|
|
190
|
+
hardware_id: str
|
|
191
|
+
channel_id: str
|
|
192
|
+
|
|
193
|
+
@classmethod
|
|
194
|
+
def from_dto(cls, dto: dict[str, Any]) -> Self:
|
|
195
|
+
mqtt_protocol = cls.MQTTProtocol(dto["mqtt_protocol"])
|
|
196
|
+
mqtt_credentials: (
|
|
197
|
+
CommunicationConfigV3.MQTTCredentials
|
|
198
|
+
| CommunicationConfigV3.MQTTSCredentials
|
|
199
|
+
| None
|
|
200
|
+
) = None
|
|
201
|
+
match mqtt_protocol:
|
|
202
|
+
case cls.MQTTProtocol.MQTT:
|
|
203
|
+
mqtt_credentials = cls.MQTTCredentials.from_dto(dto["mqtt_credentials"])
|
|
204
|
+
case cls.MQTTProtocol.MQTTS:
|
|
205
|
+
mqtt_credentials = cls.MQTTSCredentials.from_dto(
|
|
206
|
+
dto["mqtt_credentials"]
|
|
207
|
+
)
|
|
208
|
+
case _:
|
|
209
|
+
raise NotImplementedError(mqtt_protocol)
|
|
210
|
+
assert mqtt_credentials is not None
|
|
211
|
+
return cls(
|
|
212
|
+
mqtt_host=dto["mqtt_host"],
|
|
213
|
+
mqtt_port=int(dto["mqtt_port"]),
|
|
214
|
+
mqtt_credentials=mqtt_credentials,
|
|
215
|
+
mqtt_protocol=mqtt_protocol,
|
|
216
|
+
hardware_id=dto["hardware_id"],
|
|
217
|
+
channel_id=dto["channel_id"],
|
|
218
|
+
)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import asyncio
|
|
3
|
+
from typing import AsyncGenerator
|
|
4
|
+
|
|
5
|
+
from .device_protocol import CommandArgs, CommandResult, Log, Properties, Telemetry
|
|
6
|
+
from .logger import Logger
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Device(abc.ABC):
|
|
10
|
+
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
properties_queue_size: int = 1,
|
|
14
|
+
telemetry_queue_size: int = 1,
|
|
15
|
+
log_queue_size: int = 1,
|
|
16
|
+
command_prefix: str = "cmd_",
|
|
17
|
+
) -> None:
|
|
18
|
+
self._properties_queue: asyncio.Queue[Properties] = asyncio.Queue(
|
|
19
|
+
properties_queue_size
|
|
20
|
+
)
|
|
21
|
+
self._telemetry_queue: asyncio.Queue[Telemetry] = asyncio.Queue(
|
|
22
|
+
telemetry_queue_size
|
|
23
|
+
)
|
|
24
|
+
self._log_queue: asyncio.Queue[Log] = asyncio.Queue(log_queue_size)
|
|
25
|
+
self._logger = Logger(self._log_queue)
|
|
26
|
+
self._command_prefix = command_prefix
|
|
27
|
+
|
|
28
|
+
@abc.abstractmethod
|
|
29
|
+
async def run(self) -> None:
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def logger(self) -> Logger:
|
|
34
|
+
return self._logger
|
|
35
|
+
|
|
36
|
+
async def send_properties(self, properties: Properties) -> None:
|
|
37
|
+
await self._properties_queue.put(properties.copy())
|
|
38
|
+
|
|
39
|
+
async def send_telemetry(self, telemetry: Telemetry) -> None:
|
|
40
|
+
await self._telemetry_queue.put(telemetry.copy())
|
|
41
|
+
|
|
42
|
+
async def stream_properties(self) -> AsyncGenerator[Properties, None]:
|
|
43
|
+
while True:
|
|
44
|
+
yield await self._properties_queue.get()
|
|
45
|
+
|
|
46
|
+
async def stream_telemetry(self) -> AsyncGenerator[Telemetry, None]:
|
|
47
|
+
while True:
|
|
48
|
+
yield await self._telemetry_queue.get()
|
|
49
|
+
|
|
50
|
+
async def stream_logs(self) -> AsyncGenerator[Log, None]:
|
|
51
|
+
while True:
|
|
52
|
+
yield await self._log_queue.get()
|
|
53
|
+
|
|
54
|
+
async def execute_command(self, name: str, args: CommandArgs) -> CommandResult:
|
|
55
|
+
try:
|
|
56
|
+
command = getattr(self, self._command_prefix + name)
|
|
57
|
+
except AttributeError:
|
|
58
|
+
raise NotImplementedError() from None
|
|
59
|
+
return {"result": await command(**args)}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
from typing import Any, AsyncGenerator, Literal, Protocol, TypeAlias
|
|
3
|
+
|
|
4
|
+
Properties: TypeAlias = dict[str, Any]
|
|
5
|
+
Telemetry: TypeAlias = dict[str, Any]
|
|
6
|
+
CommandArgs: TypeAlias = dict[str, Any]
|
|
7
|
+
CommandResult: TypeAlias = dict[str, Any]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclasses.dataclass
|
|
11
|
+
class Log:
|
|
12
|
+
|
|
13
|
+
severity: Literal["debug", "info", "warning", "error"]
|
|
14
|
+
message: str
|
|
15
|
+
persist: bool
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class DeviceProtocol(Protocol):
|
|
19
|
+
|
|
20
|
+
async def run(self) -> None:
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
async def stream_properties(self) -> AsyncGenerator[Properties, None]:
|
|
24
|
+
yield {}
|
|
25
|
+
|
|
26
|
+
async def stream_telemetry(self) -> AsyncGenerator[Telemetry, None]:
|
|
27
|
+
yield {}
|
|
28
|
+
|
|
29
|
+
async def stream_logs(self) -> AsyncGenerator[Log, None]:
|
|
30
|
+
yield Log("debug", "", False)
|
|
31
|
+
|
|
32
|
+
async def execute_command(self, name: str, args: CommandArgs) -> CommandResult:
|
|
33
|
+
pass
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
from .device_protocol import Log
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Logger:
|
|
7
|
+
|
|
8
|
+
def __init__(self, queue: asyncio.Queue[Log]) -> None:
|
|
9
|
+
self._queue = queue
|
|
10
|
+
|
|
11
|
+
async def debug(self, msg: str, persist: bool = False) -> None:
|
|
12
|
+
await self._queue.put(Log("debug", msg, persist))
|
|
13
|
+
|
|
14
|
+
async def info(self, msg: str, persist: bool = False) -> None:
|
|
15
|
+
await self._queue.put(Log("info", msg, persist))
|
|
16
|
+
|
|
17
|
+
async def warning(self, msg: str, persist: bool = False) -> None:
|
|
18
|
+
await self._queue.put(Log("warning", msg, persist))
|
|
19
|
+
|
|
20
|
+
async def error(self, msg: str, persist: bool = False) -> None:
|
|
21
|
+
await self._queue.put(Log("error", msg, persist))
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import contextlib
|
|
3
|
+
import logging
|
|
4
|
+
import time
|
|
5
|
+
import traceback
|
|
6
|
+
|
|
7
|
+
from enapter import async_, mqtt
|
|
8
|
+
|
|
9
|
+
from .device_protocol import DeviceProtocol
|
|
10
|
+
|
|
11
|
+
LOGGER = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MQTTAdapter(async_.Routine):
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
hardware_id: str,
|
|
19
|
+
channel_id: str,
|
|
20
|
+
mqtt_api_client: mqtt.api.Client,
|
|
21
|
+
device: DeviceProtocol,
|
|
22
|
+
task_group: asyncio.TaskGroup | None,
|
|
23
|
+
) -> None:
|
|
24
|
+
super().__init__(task_group=task_group)
|
|
25
|
+
self._logger = logging.LoggerAdapter(
|
|
26
|
+
LOGGER, extra={"hardware_id": hardware_id, "channel_id": channel_id}
|
|
27
|
+
)
|
|
28
|
+
self._device_channel = mqtt_api_client.device_channel(hardware_id, channel_id)
|
|
29
|
+
self._device = device
|
|
30
|
+
|
|
31
|
+
async def _run(self) -> None:
|
|
32
|
+
async with asyncio.TaskGroup() as tg:
|
|
33
|
+
tg.create_task(self._device.run())
|
|
34
|
+
tg.create_task(self._stream_properties())
|
|
35
|
+
tg.create_task(self._stream_telemetry())
|
|
36
|
+
tg.create_task(self._stream_logs())
|
|
37
|
+
tg.create_task(self._execute_commands())
|
|
38
|
+
|
|
39
|
+
async def _stream_properties(self) -> None:
|
|
40
|
+
async with contextlib.aclosing(self._device.stream_properties()) as stream:
|
|
41
|
+
async for properties in stream:
|
|
42
|
+
properties = properties.copy()
|
|
43
|
+
timestamp = properties.pop("timestamp", int(time.time()))
|
|
44
|
+
await self._publish_properties(
|
|
45
|
+
mqtt.api.device.Properties(timestamp=timestamp, values=properties)
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
async def _publish_properties(self, properties: mqtt.api.device.Properties) -> None:
|
|
49
|
+
try:
|
|
50
|
+
await self._device_channel.publish_properties(properties=properties)
|
|
51
|
+
except Exception as e:
|
|
52
|
+
self._logger.error("failed to publish properties: %s", e)
|
|
53
|
+
|
|
54
|
+
async def _stream_telemetry(self) -> None:
|
|
55
|
+
async with contextlib.aclosing(self._device.stream_telemetry()) as stream:
|
|
56
|
+
async for telemetry in stream:
|
|
57
|
+
telemetry = telemetry.copy()
|
|
58
|
+
timestamp = telemetry.pop("timestamp", int(time.time()))
|
|
59
|
+
alerts = telemetry.pop("alerts", None)
|
|
60
|
+
await self._publish_telemetry(
|
|
61
|
+
mqtt.api.device.Telemetry(
|
|
62
|
+
timestamp=timestamp, alerts=alerts, values=telemetry
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
async def _publish_telemetry(self, telemetry: mqtt.api.device.Telemetry) -> None:
|
|
67
|
+
try:
|
|
68
|
+
await self._device_channel.publish_telemetry(telemetry=telemetry)
|
|
69
|
+
except Exception as e:
|
|
70
|
+
self._logger.error("failed to publish telemetry: %s", e)
|
|
71
|
+
|
|
72
|
+
async def _stream_logs(self) -> None:
|
|
73
|
+
async with contextlib.aclosing(self._device.stream_logs()) as stream:
|
|
74
|
+
async for log in stream:
|
|
75
|
+
match log.severity:
|
|
76
|
+
case "debug":
|
|
77
|
+
self._logger.debug(log.message)
|
|
78
|
+
case "info":
|
|
79
|
+
self._logger.info(log.message)
|
|
80
|
+
case "warning":
|
|
81
|
+
self._logger.warning(log.message)
|
|
82
|
+
case "error":
|
|
83
|
+
self._logger.error(log.message)
|
|
84
|
+
case _:
|
|
85
|
+
raise NotImplementedError(log.severity)
|
|
86
|
+
await self._publish_log(
|
|
87
|
+
mqtt.api.device.Log(
|
|
88
|
+
timestamp=int(time.time()),
|
|
89
|
+
severity=mqtt.api.device.LogSeverity(log.severity),
|
|
90
|
+
message=log.message,
|
|
91
|
+
persist=log.persist,
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
async def _publish_log(self, log: mqtt.api.device.Log) -> None:
|
|
96
|
+
try:
|
|
97
|
+
await self._device_channel.publish_log(log=log)
|
|
98
|
+
except Exception as e:
|
|
99
|
+
self._logger.error("failed to publish log: %s", e)
|
|
100
|
+
|
|
101
|
+
async def _execute_commands(self) -> None:
|
|
102
|
+
async with asyncio.TaskGroup() as tg:
|
|
103
|
+
async with self._device_channel.subscribe_to_command_requests() as requests:
|
|
104
|
+
async for request in requests:
|
|
105
|
+
tg.create_task(self._execute_command(request))
|
|
106
|
+
|
|
107
|
+
async def _execute_command(self, request: mqtt.api.device.CommandRequest) -> None:
|
|
108
|
+
await self._device_channel.publish_command_response(
|
|
109
|
+
request.new_response(
|
|
110
|
+
mqtt.api.device.CommandState.LOG, {"message": "Executing command..."}
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
try:
|
|
114
|
+
payload = await self._device.execute_command(
|
|
115
|
+
request.name, request.arguments
|
|
116
|
+
)
|
|
117
|
+
except NotImplementedError:
|
|
118
|
+
await self._device_channel.publish_command_response(
|
|
119
|
+
request.new_response(
|
|
120
|
+
mqtt.api.device.CommandState.ERROR,
|
|
121
|
+
{"message": "Command handler not implemented."},
|
|
122
|
+
)
|
|
123
|
+
)
|
|
124
|
+
except Exception:
|
|
125
|
+
await self._device_channel.publish_command_response(
|
|
126
|
+
request.new_response(
|
|
127
|
+
mqtt.api.device.CommandState.ERROR,
|
|
128
|
+
{"message": traceback.format_exc()},
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
else:
|
|
132
|
+
await self._device_channel.publish_command_response(
|
|
133
|
+
request.new_response(mqtt.api.device.CommandState.COMPLETED, payload)
|
|
134
|
+
)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import contextlib
|
|
3
|
+
|
|
4
|
+
from enapter import log, mqtt
|
|
5
|
+
|
|
6
|
+
from .config import Config
|
|
7
|
+
from .device_protocol import DeviceProtocol
|
|
8
|
+
from .mqtt_adapter import MQTTAdapter
|
|
9
|
+
from .ucm import UCM
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
async def run(device: DeviceProtocol) -> None:
|
|
13
|
+
log.configure(level=log.LEVEL or "info")
|
|
14
|
+
config = Config.from_env()
|
|
15
|
+
async with contextlib.AsyncExitStack() as stack:
|
|
16
|
+
task_group = await stack.enter_async_context(asyncio.TaskGroup())
|
|
17
|
+
mqtt_api_client = await stack.enter_async_context(
|
|
18
|
+
mqtt.api.Client(config=config.communication.mqtt_api, task_group=task_group)
|
|
19
|
+
)
|
|
20
|
+
_ = await stack.enter_async_context(
|
|
21
|
+
MQTTAdapter(
|
|
22
|
+
hardware_id=config.communication.hardware_id,
|
|
23
|
+
channel_id=config.communication.channel_id,
|
|
24
|
+
mqtt_api_client=mqtt_api_client,
|
|
25
|
+
device=device,
|
|
26
|
+
task_group=task_group,
|
|
27
|
+
)
|
|
28
|
+
)
|
|
29
|
+
if config.communication.ucm_needed:
|
|
30
|
+
_ = await stack.enter_async_context(
|
|
31
|
+
MQTTAdapter(
|
|
32
|
+
hardware_id=config.communication.hardware_id,
|
|
33
|
+
channel_id="ucm",
|
|
34
|
+
mqtt_api_client=mqtt_api_client,
|
|
35
|
+
device=UCM(),
|
|
36
|
+
task_group=task_group,
|
|
37
|
+
)
|
|
38
|
+
)
|
|
39
|
+
await asyncio.Event().wait()
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
from .device import Device
|
|
4
|
+
from .device_protocol import CommandResult
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class UCM(Device):
|
|
8
|
+
|
|
9
|
+
async def run(self) -> None:
|
|
10
|
+
async with asyncio.TaskGroup() as tg:
|
|
11
|
+
tg.create_task(self.properties_sender())
|
|
12
|
+
tg.create_task(self.telemetry_sender())
|
|
13
|
+
|
|
14
|
+
async def properties_sender(self) -> None:
|
|
15
|
+
while True:
|
|
16
|
+
await self.send_properties({"virtual": True, "lua_api_ver": 1})
|
|
17
|
+
await asyncio.sleep(30)
|
|
18
|
+
|
|
19
|
+
async def telemetry_sender(self) -> None:
|
|
20
|
+
while True:
|
|
21
|
+
await self.send_telemetry({})
|
|
22
|
+
await asyncio.sleep(1)
|
|
23
|
+
|
|
24
|
+
async def cmd_reboot(self, *args, **kwargs) -> CommandResult:
|
|
25
|
+
raise NotImplementedError
|
|
26
|
+
|
|
27
|
+
async def cmd_upload_lua_script(self, *args, **kwargs) -> CommandResult:
|
|
28
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: enapter
|
|
3
|
+
Version: 0.12.1
|
|
4
|
+
Summary: Enapter Python SDK
|
|
5
|
+
Home-page: https://github.com/Enapter/python-sdk
|
|
6
|
+
Author: Roman Novatorov
|
|
7
|
+
Author-email: rnovatorov@enapter.com
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: aiomqtt==2.4.*
|
|
10
|
+
Requires-Dist: dnspython==2.8.*
|
|
11
|
+
Requires-Dist: json-log-formatter==1.1.*
|
|
12
|
+
Requires-Dist: httpx==0.28.*
|
|
13
|
+
Dynamic: author
|
|
14
|
+
Dynamic: author-email
|
|
15
|
+
Dynamic: description
|
|
16
|
+
Dynamic: description-content-type
|
|
17
|
+
Dynamic: home-page
|
|
18
|
+
Dynamic: requires-dist
|
|
19
|
+
Dynamic: summary
|
|
20
|
+
|
|
21
|
+
# Enapter Python SDK
|
|
22
|
+
|
|
23
|
+
[](https://github.com/Enapter/python-sdk/actions/workflows/ci.yml)
|
|
24
|
+
[](https://pypi.org/project/enapter)
|
|
25
|
+
[](https://github.com/python/black)
|
|
26
|
+
|
|
27
|
+
A software development kit (SDK) for building applications and integrations
|
|
28
|
+
with Enapter using Python.
|
|
29
|
+
|
|
30
|
+
## Features
|
|
31
|
+
|
|
32
|
+
- [Standalone
|
|
33
|
+
Devices](https://v3.developers.enapter.com/docs/standalone/introduction)
|
|
34
|
+
framework
|
|
35
|
+
- [MQTT
|
|
36
|
+
API](https://v3.developers.enapter.com/reference/device_integration/mqtt_api/)
|
|
37
|
+
client
|
|
38
|
+
- [HTTP API](https://v3.developers.enapter.com/reference/http/intro) client
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
> [!IMPORTANT]
|
|
43
|
+
> Requires **Python 3.11+**.
|
|
44
|
+
|
|
45
|
+
> [!WARNING]
|
|
46
|
+
> The API is still under development and may change at any time. It is
|
|
47
|
+
> recommended to pin the package version when installing.
|
|
48
|
+
|
|
49
|
+
Install from PyPI:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pip install enapter==0.12.0
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Usage
|
|
56
|
+
|
|
57
|
+
Explore the examples:
|
|
58
|
+
|
|
59
|
+
- [Standalone Devices](examples/standalone)
|
|
60
|
+
- [MQTT API](examples/mqtt)
|
|
61
|
+
- [HTTP API](examples/http)
|
|
62
|
+
|
|
63
|
+
These provide a good overview of the available features and should give you
|
|
64
|
+
enough to get started.
|
|
65
|
+
|
|
66
|
+
> [!TIP]
|
|
67
|
+
> Don't hesitate to peek into the source code - it's meant to be easy to
|
|
68
|
+
> follow.
|
|
69
|
+
|
|
70
|
+
## Help
|
|
71
|
+
|
|
72
|
+
If you feel lost or confused, reach us on
|
|
73
|
+
[Discord](https://discord.com/invite/TCaEZs3qpe) or just [file a
|
|
74
|
+
bug](https://github.com/Enapter/python-sdk/issues/new). We'd be glad to help.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
enapter/__init__.py,sha256=Tl2rL_JwFFrogMeXuJ61iIqUMC9tD2PXPAi4Zs2RRNc,208
|
|
2
|
+
enapter/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
enapter/async_/__init__.py,sha256=9bi9f3OEu71WRUHQei-FiIbz_LEy8lY44n5y_I4WEfo,98
|
|
4
|
+
enapter/async_/generator.py,sha256=BQPPtPCYdsZIjgVy0UlC81DqId3yIDrpEyrmJ2cHdc8,497
|
|
5
|
+
enapter/async_/routine.py,sha256=oNgq2rgMSOEK1cr7gCVeLLc_hlrbKpY8-tzE3uAQObA,1035
|
|
6
|
+
enapter/http/__init__.py,sha256=dvicm4uAqVhr7d9QdhLRuTq3qUcs03JikShjsBSzgdE,37
|
|
7
|
+
enapter/http/api/__init__.py,sha256=tKDag5EZBTcNc9pkVltA_MHFTvJNfZgbNmaIKQFxJFI,119
|
|
8
|
+
enapter/http/api/client.py,sha256=nvnlMZUzmR4QJlW9evqlKXyO5nbQkPz43ZnyWWaKYsQ,741
|
|
9
|
+
enapter/http/api/config.py,sha256=L0yZzgfbuK9YzKe-EVfcgu2W2mCVY9CcUwSauG_YUbs,648
|
|
10
|
+
enapter/http/api/devices/__init__.py,sha256=aBetjJCQEZ8YgxmyN551gx3hQ0X5xtjbpEpjWLJGO7c,572
|
|
11
|
+
enapter/http/api/devices/authorized_role.py,sha256=KWiwiXmZ7lICqaXxKy0Bwb5nlgMcF0M1arl3pCZaTx8,184
|
|
12
|
+
enapter/http/api/devices/client.py,sha256=dhB7KGn5vY6bHlGstmLhG5CFPu4LlYuB9IdeVzvsb5g,859
|
|
13
|
+
enapter/http/api/devices/communication_config.py,sha256=OuexZYjR4K3wVZMdhqUWSmakxL1gOyuYfKHpcqtoFio,1618
|
|
14
|
+
enapter/http/api/devices/device.py,sha256=07R20IxAFO5xRYkXqK7l5nfd6nrn7h4eKBiBl0qFJmU,808
|
|
15
|
+
enapter/http/api/devices/device_type.py,sha256=64pl62rzblNKdstcBX5nUw8NisYx4v0VAbM6VLnpUn4,180
|
|
16
|
+
enapter/http/api/devices/mqtt_credentials.py,sha256=8q1zjJEE4WDo2tjp0rB0gf-eQ4qHACdRzFqtSl7JpCc,274
|
|
17
|
+
enapter/http/api/devices/mqtt_protocol.py,sha256=tJuxuiTpMj__-OeVraTEeCWyOwikcAoDAx_0h2thCI0,84
|
|
18
|
+
enapter/http/api/devices/mqtts_credentials.py,sha256=92Qfp0ynGbLmhpzybM_3gUVED1stsAezUPnbnejNhiA,384
|
|
19
|
+
enapter/http/api/devices/time_sync_protocol.py,sha256=627RxrmxX95LOse3QD7Ub_kNvqRndGKRMnKhE9StlNA,66
|
|
20
|
+
enapter/log/__init__.py,sha256=n1sWMDKJSs_ebZzjbTrVdfg-oi0V1tvliTxgIV-msJ0,600
|
|
21
|
+
enapter/log/json_formatter.py,sha256=TlKjdnYQvVAbSpMwjJbkzLrpelPcBiRbyeBQOHBX30k,974
|
|
22
|
+
enapter/mdns/__init__.py,sha256=uwsg8wJ0Lcsr9oOEW1PkEV3jVgWzgA7RG2ur_MRLtM0,55
|
|
23
|
+
enapter/mdns/resolver.py,sha256=lr1Z1Fa6gC-v1BK9SszynNpQrX1tEIib5AH2QEQo8kg,1434
|
|
24
|
+
enapter/mqtt/__init__.py,sha256=0cZIMl62Racl30n3mR31sFEqDRrkVH7VzTf657k3-y4,165
|
|
25
|
+
enapter/mqtt/client.py,sha256=F589ZfL8QWdCGQerJ1G-dokw4uyZpOxHUFODgnIDtNc,4221
|
|
26
|
+
enapter/mqtt/errors.py,sha256=KGDPc7FwYmKV1Rknl8Ngm7219TQ2G1mIqTQyokNwLP8,42
|
|
27
|
+
enapter/mqtt/message.py,sha256=KO6XndORPDHC3YYJyXXXNlDa3hJRZq153WRmjQ8tNPQ,42
|
|
28
|
+
enapter/mqtt/api/__init__.py,sha256=XiDOzEL1H-1NBWsfPs90cs_7huycvTzt23vcmvZ6s6E,157
|
|
29
|
+
enapter/mqtt/api/client.py,sha256=-D32m8cd9aqR01LL_Cwy90-gEprWch9hmvy3u4gwJ9g,1919
|
|
30
|
+
enapter/mqtt/api/config.py,sha256=UMRX9CBtJDnYDd3zaL3i3aFBzwefk6bJUBjNXZG-bnM,2189
|
|
31
|
+
enapter/mqtt/api/device/__init__.py,sha256=F2RyxMJKrfR1uj-lOlY1meVHN1F2hOJmDD2CmZQrLcY,490
|
|
32
|
+
enapter/mqtt/api/device/channel.py,sha256=Czgol_EPOcR0LYn83r-_iAYLYFPDtgruUe51w-sx8XU,2058
|
|
33
|
+
enapter/mqtt/api/device/command_request.py,sha256=tonIhOVFyiT0fAmCmL0xGcdrV-0zcFBW_5fyMCM5uUk,876
|
|
34
|
+
enapter/mqtt/api/device/command_response.py,sha256=7yBiFy365b4p4G8qC1oEbYiYouQuk9vOxAsc9V0OQKo,627
|
|
35
|
+
enapter/mqtt/api/device/command_state.py,sha256=rsAUyb3YZ1SiRWZc1oKa9Ik3968LJvHnUCIr85w8V9Q,110
|
|
36
|
+
enapter/mqtt/api/device/log.py,sha256=1LbAztBIcmj7tJXh-2fmnaIu0HebKmBUilSe1Dn9Fkk,733
|
|
37
|
+
enapter/mqtt/api/device/log_severity.py,sha256=5icvVG3XfIxmOqqwfcvHB1foewAPq13bXjda7xmVCn0,127
|
|
38
|
+
enapter/mqtt/api/device/message.py,sha256=xoIL4B_im15tmRAv1nctt4KsSk9hOf_yrUM-f0_ww2Y,486
|
|
39
|
+
enapter/mqtt/api/device/properties.py,sha256=0vvYIpIivNcSR8jbh1-bUlZH9wCAFVPs8Z7HaDOV0Ic,683
|
|
40
|
+
enapter/mqtt/api/device/telemetry.py,sha256=pssdpreLZ02JnD-4ovOD5YUlocsBBk4mlnQj2W7H6PM,844
|
|
41
|
+
enapter/standalone/__init__.py,sha256=gLWomm_p8GrIZ07Pvea9HoHJpOVEFgDfhCrlfICvZkQ,407
|
|
42
|
+
enapter/standalone/config.py,sha256=83kAmexJNgpGBzuWnjCLLWERq-Islo4XSpJ5QjlGr9o,6789
|
|
43
|
+
enapter/standalone/device.py,sha256=fX0coeOxl1hFnNmqsUjz7uoXxdbffxwlUI8o-665z1Y,1917
|
|
44
|
+
enapter/standalone/device_protocol.py,sha256=oL5oQJfFmA4r4DKMCldA9qHyL0-UFpgoKMTqhUR6eao,843
|
|
45
|
+
enapter/standalone/logger.py,sha256=DF5KUbOYmb3bh0OSpBUwfJA9jSLspiRs5bdqwVonf6I,663
|
|
46
|
+
enapter/standalone/mqtt_adapter.py,sha256=W5rlFfDAodK53EbvilgOCWF9agO3tFmTduJ_aqWLqpo,5356
|
|
47
|
+
enapter/standalone/run.py,sha256=pjdLLfU2CmQHToWDX_jhuEVqhGGGWfGQjsV-EtV7HBc,1365
|
|
48
|
+
enapter/standalone/ucm.py,sha256=2dPYLKlC1Alu2aL3O3-xVqKIp-bQb-ZlfUEiMklPe-E,829
|
|
49
|
+
enapter-0.12.1.dist-info/METADATA,sha256=AuUqAAOatB0p1IHqXNQmVOD_e7Vcr1jQzWANHVg8OyQ,2068
|
|
50
|
+
enapter-0.12.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
51
|
+
enapter-0.12.1.dist-info/top_level.txt,sha256=IeuS2-kf8S5mBht3xxULzMkYhXe08xGuailHeEaaD1A,8
|
|
52
|
+
enapter-0.12.1.dist-info/RECORD,,
|