enapter 0.10.2__py3-none-any.whl → 0.11.3__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 enapter might be problematic. Click here for more details.
- enapter/__init__.py +1 -1
- enapter/async_/generator.py +5 -2
- enapter/async_/routine.py +9 -8
- enapter/log/json_formatter.py +15 -3
- enapter/mdns/resolver.py +6 -6
- enapter/mqtt/__init__.py +3 -2
- enapter/mqtt/api/command.py +12 -6
- enapter/mqtt/api/device_channel.py +28 -18
- enapter/mqtt/client.py +46 -77
- enapter/mqtt/config.py +46 -25
- enapter/vucm/app.py +13 -3
- enapter/vucm/config.py +27 -16
- enapter/vucm/device.py +13 -9
- enapter/vucm/logger.py +7 -7
- enapter/vucm/ucm.py +4 -4
- {enapter-0.10.2.dist-info → enapter-0.11.3.dist-info}/METADATA +9 -12
- enapter-0.11.3.dist-info/RECORD +38 -0
- tests/integration/test_mqtt.py +1 -1
- enapter-0.10.2.dist-info/RECORD +0 -38
- {enapter-0.10.2.dist-info → enapter-0.11.3.dist-info}/WHEEL +0 -0
- {enapter-0.10.2.dist-info → enapter-0.11.3.dist-info}/top_level.txt +0 -0
enapter/__init__.py
CHANGED
enapter/async_/generator.py
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import contextlib
|
|
2
2
|
import functools
|
|
3
|
+
from typing import AsyncContextManager, AsyncGenerator, Callable
|
|
3
4
|
|
|
4
5
|
|
|
5
|
-
def generator(
|
|
6
|
+
def generator(
|
|
7
|
+
func: Callable[..., AsyncGenerator],
|
|
8
|
+
) -> Callable[..., AsyncContextManager[AsyncGenerator]]:
|
|
6
9
|
@functools.wraps(func)
|
|
7
10
|
@contextlib.asynccontextmanager
|
|
8
|
-
async def wrapper(*args, **kwargs):
|
|
11
|
+
async def wrapper(*args, **kwargs) -> AsyncGenerator[AsyncGenerator, None]:
|
|
9
12
|
gen = func(*args, **kwargs)
|
|
10
13
|
try:
|
|
11
14
|
yield gen
|
enapter/async_/routine.py
CHANGED
|
@@ -5,20 +5,20 @@ import contextlib
|
|
|
5
5
|
|
|
6
6
|
class Routine(abc.ABC):
|
|
7
7
|
@abc.abstractmethod
|
|
8
|
-
async def _run(self):
|
|
8
|
+
async def _run(self) -> None:
|
|
9
9
|
raise NotImplementedError # pragma: no cover
|
|
10
10
|
|
|
11
11
|
async def __aenter__(self):
|
|
12
12
|
await self.start()
|
|
13
13
|
return self
|
|
14
14
|
|
|
15
|
-
async def __aexit__(self, *_):
|
|
15
|
+
async def __aexit__(self, *_) -> None:
|
|
16
16
|
await self.stop()
|
|
17
17
|
|
|
18
|
-
def task(self):
|
|
18
|
+
def task(self) -> asyncio.Task:
|
|
19
19
|
return self._task
|
|
20
20
|
|
|
21
|
-
async def start(self, cancel_parent_task_on_exception=True):
|
|
21
|
+
async def start(self, cancel_parent_task_on_exception: bool = True) -> None:
|
|
22
22
|
self._started = asyncio.Event()
|
|
23
23
|
self._stack = contextlib.AsyncExitStack()
|
|
24
24
|
|
|
@@ -43,26 +43,27 @@ class Routine(abc.ABC):
|
|
|
43
43
|
if self._task in done:
|
|
44
44
|
self._task.result()
|
|
45
45
|
|
|
46
|
-
async def stop(self):
|
|
46
|
+
async def stop(self) -> None:
|
|
47
47
|
self.cancel()
|
|
48
48
|
await self.join()
|
|
49
49
|
|
|
50
|
-
def cancel(self):
|
|
50
|
+
def cancel(self) -> None:
|
|
51
51
|
self._task.cancel()
|
|
52
52
|
|
|
53
|
-
async def join(self):
|
|
53
|
+
async def join(self) -> None:
|
|
54
54
|
if self._task.done():
|
|
55
55
|
self._task.result()
|
|
56
56
|
else:
|
|
57
57
|
await self._task
|
|
58
58
|
|
|
59
|
-
async def __run(self):
|
|
59
|
+
async def __run(self) -> None:
|
|
60
60
|
try:
|
|
61
61
|
await self._run()
|
|
62
62
|
except asyncio.CancelledError:
|
|
63
63
|
pass
|
|
64
64
|
except:
|
|
65
65
|
if self._started.is_set() and self._cancel_parent_task_on_exception:
|
|
66
|
+
assert self._parent_task is not None
|
|
66
67
|
self._parent_task.cancel()
|
|
67
68
|
raise
|
|
68
69
|
finally:
|
enapter/log/json_formatter.py
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
import datetime
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Any, Dict
|
|
2
4
|
|
|
3
|
-
import json_log_formatter
|
|
5
|
+
import json_log_formatter # type: ignore
|
|
4
6
|
|
|
5
7
|
|
|
6
8
|
class JSONFormatter(json_log_formatter.JSONFormatter):
|
|
7
|
-
def json_record(
|
|
9
|
+
def json_record(
|
|
10
|
+
self,
|
|
11
|
+
message: str,
|
|
12
|
+
extra: Dict[str, Any],
|
|
13
|
+
record: logging.LogRecord,
|
|
14
|
+
) -> Dict[str, Any]:
|
|
15
|
+
try:
|
|
16
|
+
del extra["taskName"]
|
|
17
|
+
except KeyError:
|
|
18
|
+
pass
|
|
19
|
+
|
|
8
20
|
json_record = {
|
|
9
21
|
"time": datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
|
10
22
|
"level": record.levelname[:4],
|
|
@@ -22,5 +34,5 @@ class JSONFormatter(json_log_formatter.JSONFormatter):
|
|
|
22
34
|
|
|
23
35
|
return json_record
|
|
24
36
|
|
|
25
|
-
def mutate_json_record(self, json_record):
|
|
37
|
+
def mutate_json_record(self, json_record: Dict[str, Any]) -> Dict[str, Any]:
|
|
26
38
|
return json_record
|
enapter/mdns/resolver.py
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
|
|
3
|
-
import dns.asyncresolver
|
|
3
|
+
import dns.asyncresolver # type: ignore
|
|
4
4
|
|
|
5
5
|
LOGGER = logging.getLogger(__name__)
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class Resolver:
|
|
9
|
-
def __init__(self):
|
|
9
|
+
def __init__(self) -> None:
|
|
10
10
|
self._logger = LOGGER
|
|
11
11
|
self._dns_resolver = self._new_dns_resolver()
|
|
12
12
|
self._mdns_resolver = self._new_mdns_resolver()
|
|
13
13
|
|
|
14
|
-
async def resolve(self, host):
|
|
14
|
+
async def resolve(self, host: str) -> str:
|
|
15
15
|
# TODO: Resolve concurrently.
|
|
16
16
|
try:
|
|
17
17
|
ip = await self._resolve(self._dns_resolver, host)
|
|
@@ -25,17 +25,17 @@ class Resolver:
|
|
|
25
25
|
self._logger.info("%r resolved using mDNS: %r", host, ip)
|
|
26
26
|
return ip
|
|
27
27
|
|
|
28
|
-
async def _resolve(self, resolver, host):
|
|
28
|
+
async def _resolve(self, resolver: dns.asyncresolver.Resolver, host: str) -> str:
|
|
29
29
|
answer = await resolver.resolve(host, "A")
|
|
30
30
|
if not answer:
|
|
31
31
|
raise ValueError(f"empty answer received: {host}")
|
|
32
32
|
|
|
33
33
|
return answer[0].address
|
|
34
34
|
|
|
35
|
-
def _new_dns_resolver(self):
|
|
35
|
+
def _new_dns_resolver(self) -> dns.asyncresolver.Resolver:
|
|
36
36
|
return dns.asyncresolver.Resolver(configure=True)
|
|
37
37
|
|
|
38
|
-
def _new_mdns_resolver(self):
|
|
38
|
+
def _new_mdns_resolver(self) -> dns.asyncresolver.Resolver:
|
|
39
39
|
r = dns.asyncresolver.Resolver(configure=False)
|
|
40
40
|
r.nameservers = ["224.0.0.251"]
|
|
41
41
|
r.port = 5353
|
enapter/mqtt/__init__.py
CHANGED
enapter/mqtt/api/command.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import enum
|
|
2
2
|
import json
|
|
3
|
+
from typing import Any, Dict, Optional, Union
|
|
3
4
|
|
|
4
5
|
|
|
5
6
|
class CommandState(enum.Enum):
|
|
@@ -9,11 +10,11 @@ class CommandState(enum.Enum):
|
|
|
9
10
|
|
|
10
11
|
class CommandRequest:
|
|
11
12
|
@classmethod
|
|
12
|
-
def unmarshal_json(cls, data):
|
|
13
|
+
def unmarshal_json(cls, data: Union[str, bytes]) -> "CommandRequest":
|
|
13
14
|
req = json.loads(data)
|
|
14
15
|
return cls(id_=req["id"], name=req["name"], args=req.get("arguments"))
|
|
15
16
|
|
|
16
|
-
def __init__(self, id_, name, args=None):
|
|
17
|
+
def __init__(self, id_: str, name: str, args: Optional[Dict[str, Any]] = None):
|
|
17
18
|
self.id = id_
|
|
18
19
|
self.name = name
|
|
19
20
|
|
|
@@ -21,12 +22,17 @@ class CommandRequest:
|
|
|
21
22
|
args = {}
|
|
22
23
|
self.args = args
|
|
23
24
|
|
|
24
|
-
def new_response(self, *args, **kwargs):
|
|
25
|
+
def new_response(self, *args, **kwargs) -> "CommandResponse":
|
|
25
26
|
return CommandResponse(self.id, *args, **kwargs)
|
|
26
27
|
|
|
27
28
|
|
|
28
29
|
class CommandResponse:
|
|
29
|
-
def __init__(
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
id_: str,
|
|
33
|
+
state: Union[str, CommandState],
|
|
34
|
+
payload: Optional[Union[Dict[str, Any], str]] = None,
|
|
35
|
+
) -> None:
|
|
30
36
|
self.id = id_
|
|
31
37
|
|
|
32
38
|
if not isinstance(state, CommandState):
|
|
@@ -37,8 +43,8 @@ class CommandResponse:
|
|
|
37
43
|
payload = {"message": payload}
|
|
38
44
|
self.payload = payload
|
|
39
45
|
|
|
40
|
-
def json(self):
|
|
41
|
-
json_object = {"id": self.id, "state": self.state.value}
|
|
46
|
+
def json(self) -> Dict[str, Any]:
|
|
47
|
+
json_object: Dict[str, Any] = {"id": self.id, "state": self.state.value}
|
|
42
48
|
if self.payload is not None:
|
|
43
49
|
json_object["payload"] = self.payload
|
|
44
50
|
|
|
@@ -1,72 +1,82 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import logging
|
|
3
3
|
import time
|
|
4
|
+
from typing import Any, AsyncContextManager, AsyncGenerator, Dict
|
|
5
|
+
|
|
6
|
+
import aiomqtt # type: ignore
|
|
4
7
|
|
|
5
8
|
import enapter
|
|
6
9
|
|
|
7
|
-
from
|
|
10
|
+
from ..client import Client
|
|
11
|
+
from .command import CommandRequest, CommandResponse
|
|
12
|
+
from .log_severity import LogSeverity
|
|
8
13
|
|
|
9
14
|
LOGGER = logging.getLogger(__name__)
|
|
10
15
|
|
|
11
16
|
|
|
12
17
|
class DeviceChannel:
|
|
13
|
-
def __init__(self, client, hardware_id, channel_id):
|
|
18
|
+
def __init__(self, client: Client, hardware_id: str, channel_id: str) -> None:
|
|
14
19
|
self._client = client
|
|
15
20
|
self._logger = self._new_logger(hardware_id, channel_id)
|
|
16
21
|
self._hardware_id = hardware_id
|
|
17
22
|
self._channel_id = channel_id
|
|
18
23
|
|
|
19
24
|
@property
|
|
20
|
-
def hardware_id(self):
|
|
25
|
+
def hardware_id(self) -> str:
|
|
21
26
|
return self._hardware_id
|
|
22
27
|
|
|
23
28
|
@property
|
|
24
|
-
def channel_id(self):
|
|
29
|
+
def channel_id(self) -> str:
|
|
25
30
|
return self._channel_id
|
|
26
31
|
|
|
27
32
|
@staticmethod
|
|
28
|
-
def _new_logger(hardware_id, channel_id):
|
|
33
|
+
def _new_logger(hardware_id, channel_id) -> logging.LoggerAdapter:
|
|
29
34
|
extra = {"hardware_id": hardware_id, "channel_id": channel_id}
|
|
30
35
|
return logging.LoggerAdapter(LOGGER, extra=extra)
|
|
31
36
|
|
|
32
37
|
@enapter.async_.generator
|
|
33
|
-
async def subscribe_to_command_requests(
|
|
38
|
+
async def subscribe_to_command_requests(
|
|
39
|
+
self,
|
|
40
|
+
) -> AsyncGenerator[CommandRequest, None]:
|
|
34
41
|
async with self._subscribe("v1/command/requests") as messages:
|
|
35
42
|
async for msg in messages:
|
|
43
|
+
assert isinstance(msg.payload, str) or isinstance(msg.payload, bytes)
|
|
36
44
|
yield CommandRequest.unmarshal_json(msg.payload)
|
|
37
45
|
|
|
38
|
-
async def publish_command_response(self, resp):
|
|
46
|
+
async def publish_command_response(self, resp: CommandResponse) -> None:
|
|
39
47
|
await self._publish_json("v1/command/responses", resp.json())
|
|
40
48
|
|
|
41
|
-
async def publish_telemetry(self, telemetry, **kwargs):
|
|
49
|
+
async def publish_telemetry(self, telemetry: Dict[str, Any], **kwargs) -> None:
|
|
42
50
|
await self._publish_json("v1/telemetry", telemetry, **kwargs)
|
|
43
51
|
|
|
44
|
-
async def publish_properties(self, properties, **kwargs):
|
|
52
|
+
async def publish_properties(self, properties: Dict[str, Any], **kwargs) -> None:
|
|
45
53
|
await self._publish_json("v1/register", properties, **kwargs)
|
|
46
54
|
|
|
47
|
-
async def publish_logs(
|
|
55
|
+
async def publish_logs(
|
|
56
|
+
self, msg: str, severity: LogSeverity, persist: bool = False, **kwargs
|
|
57
|
+
) -> None:
|
|
48
58
|
logs = {
|
|
49
59
|
"message": msg,
|
|
50
60
|
"severity": severity.value,
|
|
61
|
+
"persist": persist,
|
|
51
62
|
}
|
|
52
|
-
if persist:
|
|
53
|
-
logs["persist"] = True
|
|
54
|
-
|
|
55
63
|
await self._publish_json("v3/logs", logs, **kwargs)
|
|
56
64
|
|
|
57
|
-
def _subscribe(
|
|
65
|
+
def _subscribe(
|
|
66
|
+
self, path: str
|
|
67
|
+
) -> AsyncContextManager[AsyncGenerator[aiomqtt.Message, None]]:
|
|
58
68
|
topic = f"v1/to/{self._hardware_id}/{self._channel_id}/{path}"
|
|
59
69
|
return self._client.subscribe(topic)
|
|
60
70
|
|
|
61
|
-
async def _publish_json(
|
|
71
|
+
async def _publish_json(
|
|
72
|
+
self, path: str, json_object: Dict[str, Any], **kwargs
|
|
73
|
+
) -> None:
|
|
62
74
|
if "timestamp" not in json_object:
|
|
63
75
|
json_object["timestamp"] = int(time.time())
|
|
64
|
-
|
|
65
76
|
payload = json.dumps(json_object)
|
|
66
|
-
|
|
67
77
|
await self._publish(path, payload, **kwargs)
|
|
68
78
|
|
|
69
|
-
async def _publish(self, path, payload, **kwargs):
|
|
79
|
+
async def _publish(self, path: str, payload: str, **kwargs) -> None:
|
|
70
80
|
topic = f"v1/from/{self._hardware_id}/{self._channel_id}/{path}"
|
|
71
81
|
try:
|
|
72
82
|
await self._client.publish(topic, payload, **kwargs)
|
enapter/mqtt/client.py
CHANGED
|
@@ -1,145 +1,114 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
import collections
|
|
3
2
|
import contextlib
|
|
4
3
|
import logging
|
|
5
4
|
import ssl
|
|
6
5
|
import tempfile
|
|
6
|
+
from typing import AsyncGenerator, Optional
|
|
7
7
|
|
|
8
|
-
import aiomqtt
|
|
8
|
+
import aiomqtt # type: ignore
|
|
9
9
|
|
|
10
10
|
import enapter
|
|
11
11
|
|
|
12
|
+
from .config import Config
|
|
13
|
+
|
|
12
14
|
LOGGER = logging.getLogger(__name__)
|
|
13
15
|
|
|
14
16
|
|
|
15
17
|
class Client(enapter.async_.Routine):
|
|
16
|
-
def __init__(self, config):
|
|
18
|
+
def __init__(self, config: Config) -> None:
|
|
17
19
|
self._logger = self._new_logger(config)
|
|
18
20
|
self._config = config
|
|
19
21
|
self._mdns_resolver = enapter.mdns.Resolver()
|
|
20
22
|
self._tls_context = self._new_tls_context(config)
|
|
21
|
-
self.
|
|
22
|
-
self.
|
|
23
|
-
self._subscribers = collections.defaultdict(int)
|
|
23
|
+
self._publisher: Optional[aiomqtt.Client] = None
|
|
24
|
+
self._publisher_connected = asyncio.Event()
|
|
24
25
|
|
|
25
26
|
@staticmethod
|
|
26
|
-
def _new_logger(config):
|
|
27
|
+
def _new_logger(config: Config) -> logging.LoggerAdapter:
|
|
27
28
|
extra = {"host": config.host, "port": config.port}
|
|
28
29
|
return logging.LoggerAdapter(LOGGER, extra=extra)
|
|
29
30
|
|
|
30
|
-
def config(self):
|
|
31
|
+
def config(self) -> Config:
|
|
31
32
|
return self._config
|
|
32
33
|
|
|
33
|
-
async def publish(self, *args, **kwargs):
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
async def publish(self, *args, **kwargs) -> None:
|
|
35
|
+
await self._publisher_connected.wait()
|
|
36
|
+
assert self._publisher is not None
|
|
37
|
+
await self._publisher.publish(*args, **kwargs)
|
|
36
38
|
|
|
37
39
|
@enapter.async_.generator
|
|
38
|
-
async def subscribe(self,
|
|
40
|
+
async def subscribe(self, *topics: str) -> AsyncGenerator[aiomqtt.Message, None]:
|
|
39
41
|
while True:
|
|
40
|
-
client = await self._wait_client()
|
|
41
|
-
|
|
42
42
|
try:
|
|
43
|
-
async with
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
43
|
+
async with self._connect() as subscriber:
|
|
44
|
+
for topic in topics:
|
|
45
|
+
await subscriber.subscribe(topic)
|
|
46
|
+
self._logger.info("subscriber [%s] connected", ",".join(topics))
|
|
47
|
+
async for msg in subscriber.messages:
|
|
48
|
+
yield msg
|
|
49
49
|
except aiomqtt.MqttError as e:
|
|
50
50
|
self._logger.error(e)
|
|
51
51
|
retry_interval = 5
|
|
52
52
|
await asyncio.sleep(retry_interval)
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
async def _subscribe(self, client, topic):
|
|
56
|
-
first_subscriber = not self._subscribers[topic]
|
|
57
|
-
self._subscribers[topic] += 1
|
|
58
|
-
try:
|
|
59
|
-
if first_subscriber:
|
|
60
|
-
await client.subscribe(topic)
|
|
61
|
-
yield
|
|
62
|
-
finally:
|
|
63
|
-
self._subscribers[topic] -= 1
|
|
64
|
-
assert not self._subscribers[topic] < 0
|
|
65
|
-
last_unsubscriber = not self._subscribers[topic]
|
|
66
|
-
if last_unsubscriber:
|
|
67
|
-
del self._subscribers[topic]
|
|
68
|
-
await client.unsubscribe(topic)
|
|
69
|
-
|
|
70
|
-
async def _wait_client(self):
|
|
71
|
-
await self._client_ready.wait()
|
|
72
|
-
assert self._client_ready.is_set()
|
|
73
|
-
return self._client
|
|
74
|
-
|
|
75
|
-
async def _run(self):
|
|
54
|
+
async def _run(self) -> None:
|
|
76
55
|
self._logger.info("starting")
|
|
77
|
-
|
|
78
56
|
self._started.set()
|
|
79
|
-
|
|
80
57
|
while True:
|
|
81
58
|
try:
|
|
82
|
-
async with self._connect() as
|
|
83
|
-
self.
|
|
84
|
-
self.
|
|
85
|
-
self.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
async with client.messages() as messages:
|
|
89
|
-
async for msg in messages:
|
|
90
|
-
pass
|
|
59
|
+
async with self._connect() as publisher:
|
|
60
|
+
self._logger.info("publisher connected")
|
|
61
|
+
self._publisher = publisher
|
|
62
|
+
self._publisher_connected.set()
|
|
63
|
+
async for msg in publisher.messages:
|
|
64
|
+
pass
|
|
91
65
|
except aiomqtt.MqttError as e:
|
|
92
66
|
self._logger.error(e)
|
|
93
67
|
retry_interval = 5
|
|
94
68
|
await asyncio.sleep(retry_interval)
|
|
95
69
|
finally:
|
|
96
|
-
self.
|
|
97
|
-
self.
|
|
98
|
-
self._logger.info("
|
|
70
|
+
self._publisher_connected.clear()
|
|
71
|
+
self._publisher = None
|
|
72
|
+
self._logger.info("publisher disconnected")
|
|
99
73
|
|
|
100
74
|
@contextlib.asynccontextmanager
|
|
101
|
-
async def _connect(self):
|
|
75
|
+
async def _connect(self) -> AsyncGenerator[aiomqtt.Client, None]:
|
|
102
76
|
host = await self._maybe_resolve_mdns(self._config.host)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
) as client:
|
|
113
|
-
yield client
|
|
114
|
-
except asyncio.CancelledError:
|
|
115
|
-
# FIXME: A cancelled `aiomqtt.Client.connect` leaks resources.
|
|
116
|
-
raise
|
|
77
|
+
async with aiomqtt.Client(
|
|
78
|
+
hostname=host,
|
|
79
|
+
port=self._config.port,
|
|
80
|
+
username=self._config.user,
|
|
81
|
+
password=self._config.password,
|
|
82
|
+
logger=LOGGER,
|
|
83
|
+
tls_context=self._tls_context,
|
|
84
|
+
) as client:
|
|
85
|
+
yield client
|
|
117
86
|
|
|
118
87
|
@staticmethod
|
|
119
|
-
def _new_tls_context(config):
|
|
120
|
-
if
|
|
88
|
+
def _new_tls_context(config: Config) -> Optional[ssl.SSLContext]:
|
|
89
|
+
if config.tls is None:
|
|
121
90
|
return None
|
|
122
91
|
|
|
123
92
|
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
|
124
93
|
|
|
125
94
|
ctx.verify_mode = ssl.CERT_REQUIRED
|
|
126
95
|
ctx.check_hostname = False
|
|
127
|
-
ctx.load_verify_locations(None, None, config.
|
|
96
|
+
ctx.load_verify_locations(None, None, config.tls.ca_cert)
|
|
128
97
|
|
|
129
98
|
with contextlib.ExitStack() as stack:
|
|
130
99
|
certfile = stack.enter_context(tempfile.NamedTemporaryFile())
|
|
131
|
-
certfile.write(config.
|
|
100
|
+
certfile.write(config.tls.cert.encode())
|
|
132
101
|
certfile.flush()
|
|
133
102
|
|
|
134
103
|
keyfile = stack.enter_context(tempfile.NamedTemporaryFile())
|
|
135
|
-
keyfile.write(config.
|
|
104
|
+
keyfile.write(config.tls.secret_key.encode())
|
|
136
105
|
keyfile.flush()
|
|
137
106
|
|
|
138
107
|
ctx.load_cert_chain(certfile.name, keyfile=keyfile.name)
|
|
139
108
|
|
|
140
109
|
return ctx
|
|
141
110
|
|
|
142
|
-
async def _maybe_resolve_mdns(self, host):
|
|
111
|
+
async def _maybe_resolve_mdns(self, host: str) -> str:
|
|
143
112
|
if not host.endswith(".local"):
|
|
144
113
|
return host
|
|
145
114
|
|
enapter/mqtt/config.py
CHANGED
|
@@ -1,48 +1,69 @@
|
|
|
1
1
|
import os
|
|
2
|
+
from typing import MutableMapping, Optional
|
|
2
3
|
|
|
3
4
|
|
|
4
|
-
class
|
|
5
|
+
class TLSConfig:
|
|
6
|
+
|
|
5
7
|
@classmethod
|
|
6
|
-
def from_env(
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
def from_env(
|
|
9
|
+
cls, prefix: str = "ENAPTER_", env: MutableMapping[str, str] = os.environ
|
|
10
|
+
) -> Optional["TLSConfig"]:
|
|
11
|
+
secret_key = env.get(prefix + "MQTT_TLS_SECRET_KEY")
|
|
12
|
+
cert = env.get(prefix + "MQTT_TLS_CERT")
|
|
13
|
+
ca_cert = env.get(prefix + "MQTT_TLS_CA_CERT")
|
|
14
|
+
|
|
15
|
+
nothing_defined = {secret_key, cert, ca_cert} == {None}
|
|
16
|
+
if nothing_defined:
|
|
17
|
+
return None
|
|
18
|
+
|
|
19
|
+
if secret_key is None:
|
|
20
|
+
raise KeyError(prefix + "MQTT_TLS_SECRET_KEY")
|
|
21
|
+
if cert is None:
|
|
22
|
+
raise KeyError(prefix + "MQTT_TLS_CERT")
|
|
23
|
+
if ca_cert is None:
|
|
24
|
+
raise KeyError(prefix + "MQTT_TLS_CA_CERT")
|
|
25
|
+
|
|
26
|
+
def pem(value: str) -> str:
|
|
10
27
|
return value.replace("\\n", "\n")
|
|
11
28
|
|
|
29
|
+
return cls(secret_key=pem(secret_key), cert=pem(cert), ca_cert=pem(ca_cert))
|
|
30
|
+
|
|
31
|
+
def __init__(self, secret_key: str, cert: str, ca_cert: str) -> None:
|
|
32
|
+
self.secret_key = secret_key
|
|
33
|
+
self.cert = cert
|
|
34
|
+
self.ca_cert = ca_cert
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Config:
|
|
38
|
+
@classmethod
|
|
39
|
+
def from_env(
|
|
40
|
+
cls, prefix: str = "ENAPTER_", env: MutableMapping[str, str] = os.environ
|
|
41
|
+
) -> "Config":
|
|
12
42
|
return cls(
|
|
13
43
|
host=env[prefix + "MQTT_HOST"],
|
|
14
44
|
port=int(env[prefix + "MQTT_PORT"]),
|
|
15
45
|
user=env.get(prefix + "MQTT_USER", default=None),
|
|
16
46
|
password=env.get(prefix + "MQTT_PASSWORD", default=None),
|
|
17
|
-
|
|
18
|
-
tls_cert=pem(env.get(prefix + "MQTT_TLS_CERT", default=None)),
|
|
19
|
-
tls_ca_cert=pem(env.get(prefix + "MQTT_TLS_CA_CERT", default=None)),
|
|
47
|
+
tls=TLSConfig.from_env(prefix=prefix, env=env),
|
|
20
48
|
)
|
|
21
49
|
|
|
22
50
|
def __init__(
|
|
23
51
|
self,
|
|
24
|
-
host,
|
|
25
|
-
port,
|
|
26
|
-
user=None,
|
|
27
|
-
password=None,
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
tls_ca_cert=None,
|
|
31
|
-
):
|
|
52
|
+
host: str,
|
|
53
|
+
port: int,
|
|
54
|
+
user: Optional[str] = None,
|
|
55
|
+
password: Optional[str] = None,
|
|
56
|
+
tls: Optional[TLSConfig] = None,
|
|
57
|
+
) -> None:
|
|
32
58
|
self.host = host
|
|
33
59
|
self.port = port
|
|
34
60
|
self.user = user
|
|
35
61
|
self.password = password
|
|
62
|
+
self.tls = tls
|
|
36
63
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
self.tls_ca_cert = tls_ca_cert
|
|
40
|
-
|
|
41
|
-
self.tls_enabled = {tls_secret_key, tls_cert, tls_ca_cert} != {None}
|
|
42
|
-
|
|
43
|
-
def __repr__(self):
|
|
44
|
-
return "mqtt.Config(host=%r, port=%r, tls_enabled=%r)" % (
|
|
64
|
+
def __repr__(self) -> str:
|
|
65
|
+
return "mqtt.Config(host=%r, port=%r, tls=%r)" % (
|
|
45
66
|
self.host,
|
|
46
67
|
self.port,
|
|
47
|
-
self.
|
|
68
|
+
self.tls is not None,
|
|
48
69
|
)
|
enapter/vucm/app.py
CHANGED
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
from typing import Optional, Protocol
|
|
2
3
|
|
|
3
4
|
import enapter
|
|
4
5
|
|
|
5
6
|
from .config import Config
|
|
7
|
+
from .device import Device
|
|
6
8
|
from .ucm import UCM
|
|
7
9
|
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
class DeviceFactory(Protocol):
|
|
12
|
+
|
|
13
|
+
def __call__(self, channel: enapter.mqtt.api.DeviceChannel, **kwargs) -> Device:
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def run(
|
|
18
|
+
device_factory: DeviceFactory, config_prefix: Optional[str] = None
|
|
19
|
+
) -> None:
|
|
10
20
|
enapter.log.configure(level=enapter.log.LEVEL or "info")
|
|
11
21
|
|
|
12
22
|
config = Config.from_env(prefix=config_prefix)
|
|
@@ -16,11 +26,11 @@ async def run(device_factory, config_prefix=None):
|
|
|
16
26
|
|
|
17
27
|
|
|
18
28
|
class App(enapter.async_.Routine):
|
|
19
|
-
def __init__(self, config, device_factory):
|
|
29
|
+
def __init__(self, config: Config, device_factory: DeviceFactory) -> None:
|
|
20
30
|
self._config = config
|
|
21
31
|
self._device_factory = device_factory
|
|
22
32
|
|
|
23
|
-
async def _run(self):
|
|
33
|
+
async def _run(self) -> None:
|
|
24
34
|
tasks = set()
|
|
25
35
|
|
|
26
36
|
mqtt_client = await self._stack.enter_async_context(
|
enapter/vucm/config.py
CHANGED
|
@@ -1,61 +1,72 @@
|
|
|
1
1
|
import base64
|
|
2
2
|
import json
|
|
3
3
|
import os
|
|
4
|
+
from typing import MutableMapping, Optional
|
|
4
5
|
|
|
5
6
|
import enapter
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
class Config:
|
|
9
10
|
@classmethod
|
|
10
|
-
def from_env(
|
|
11
|
+
def from_env(
|
|
12
|
+
cls, prefix: Optional[str] = None, env: MutableMapping[str, str] = os.environ
|
|
13
|
+
) -> "Config":
|
|
11
14
|
if prefix is None:
|
|
12
15
|
prefix = "ENAPTER_VUCM_"
|
|
13
16
|
try:
|
|
14
|
-
blob =
|
|
17
|
+
blob = env[prefix + "BLOB"]
|
|
15
18
|
except KeyError:
|
|
16
19
|
pass
|
|
17
20
|
else:
|
|
18
21
|
config = cls.from_blob(blob)
|
|
19
22
|
try:
|
|
20
|
-
config.channel_id =
|
|
23
|
+
config.channel_id = env[prefix + "CHANNEL_ID"]
|
|
21
24
|
except KeyError:
|
|
22
25
|
pass
|
|
23
26
|
return config
|
|
24
27
|
|
|
25
|
-
hardware_id =
|
|
26
|
-
channel_id =
|
|
28
|
+
hardware_id = env[prefix + "HARDWARE_ID"]
|
|
29
|
+
channel_id = env[prefix + "CHANNEL_ID"]
|
|
27
30
|
|
|
28
|
-
|
|
31
|
+
mqtt = enapter.mqtt.Config.from_env(prefix=prefix, env=env)
|
|
29
32
|
|
|
30
|
-
start_ucm =
|
|
33
|
+
start_ucm = env.get(prefix + "START_UCM", "1") != "0"
|
|
31
34
|
|
|
32
35
|
return cls(
|
|
33
36
|
hardware_id=hardware_id,
|
|
34
37
|
channel_id=channel_id,
|
|
35
|
-
|
|
38
|
+
mqtt=mqtt,
|
|
36
39
|
start_ucm=start_ucm,
|
|
37
40
|
)
|
|
38
41
|
|
|
39
42
|
@classmethod
|
|
40
|
-
def from_blob(cls, blob):
|
|
43
|
+
def from_blob(cls, blob: str) -> "Config":
|
|
41
44
|
payload = json.loads(base64.b64decode(blob))
|
|
42
45
|
|
|
43
|
-
|
|
46
|
+
mqtt = enapter.mqtt.Config(
|
|
44
47
|
host=payload["mqtt_host"],
|
|
45
48
|
port=int(payload["mqtt_port"]),
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
+
tls=enapter.mqtt.TLSConfig(
|
|
50
|
+
ca_cert=payload["mqtt_ca"],
|
|
51
|
+
cert=payload["mqtt_cert"],
|
|
52
|
+
secret_key=payload["mqtt_private_key"],
|
|
53
|
+
),
|
|
49
54
|
)
|
|
50
55
|
|
|
51
56
|
return cls(
|
|
52
57
|
hardware_id=payload["ucm_id"],
|
|
53
58
|
channel_id=payload["channel_id"],
|
|
54
|
-
|
|
59
|
+
mqtt=mqtt,
|
|
55
60
|
)
|
|
56
61
|
|
|
57
|
-
def __init__(
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
hardware_id: str,
|
|
65
|
+
channel_id: str,
|
|
66
|
+
mqtt: enapter.mqtt.Config,
|
|
67
|
+
start_ucm: bool = True,
|
|
68
|
+
) -> None:
|
|
58
69
|
self.hardware_id = hardware_id
|
|
59
70
|
self.channel_id = channel_id
|
|
60
|
-
self.mqtt =
|
|
71
|
+
self.mqtt = mqtt
|
|
61
72
|
self.start_ucm = start_ucm
|
enapter/vucm/device.py
CHANGED
|
@@ -2,7 +2,7 @@ import asyncio
|
|
|
2
2
|
import concurrent
|
|
3
3
|
import functools
|
|
4
4
|
import traceback
|
|
5
|
-
from typing import Any, Callable, Coroutine, Optional, Set
|
|
5
|
+
from typing import Any, Callable, Coroutine, Dict, Optional, Set, Tuple
|
|
6
6
|
|
|
7
7
|
import enapter
|
|
8
8
|
|
|
@@ -34,7 +34,9 @@ def is_device_command(func: DeviceCommandFunc) -> bool:
|
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
class Device(enapter.async_.Routine):
|
|
37
|
-
def __init__(
|
|
37
|
+
def __init__(
|
|
38
|
+
self, channel: enapter.mqtt.api.DeviceChannel, thread_pool_workers: int = 1
|
|
39
|
+
) -> None:
|
|
38
40
|
self.__channel = channel
|
|
39
41
|
|
|
40
42
|
self.__tasks = {}
|
|
@@ -57,7 +59,7 @@ class Device(enapter.async_.Routine):
|
|
|
57
59
|
self.alerts: Set[str] = set()
|
|
58
60
|
|
|
59
61
|
async def send_telemetry(
|
|
60
|
-
self, telemetry: Optional[enapter.types.JSON] = None
|
|
62
|
+
self, telemetry: Optional[Dict[str, enapter.types.JSON]] = None
|
|
61
63
|
) -> None:
|
|
62
64
|
if telemetry is None:
|
|
63
65
|
telemetry = {}
|
|
@@ -69,7 +71,7 @@ class Device(enapter.async_.Routine):
|
|
|
69
71
|
await self.__channel.publish_telemetry(telemetry)
|
|
70
72
|
|
|
71
73
|
async def send_properties(
|
|
72
|
-
self, properties: Optional[enapter.types.JSON] = None
|
|
74
|
+
self, properties: Optional[Dict[str, enapter.types.JSON]] = None
|
|
73
75
|
) -> None:
|
|
74
76
|
if properties is None:
|
|
75
77
|
properties = {}
|
|
@@ -78,13 +80,13 @@ class Device(enapter.async_.Routine):
|
|
|
78
80
|
|
|
79
81
|
await self.__channel.publish_properties(properties)
|
|
80
82
|
|
|
81
|
-
async def run_in_thread(self, func, *args, **kwargs):
|
|
83
|
+
async def run_in_thread(self, func, *args, **kwargs) -> Any:
|
|
82
84
|
loop = asyncio.get_running_loop()
|
|
83
85
|
return await loop.run_in_executor(
|
|
84
86
|
self.__thread_pool_executor, functools.partial(func, *args, **kwargs)
|
|
85
87
|
)
|
|
86
88
|
|
|
87
|
-
async def _run(self):
|
|
89
|
+
async def _run(self) -> None:
|
|
88
90
|
self._stack.enter_context(self.__thread_pool_executor)
|
|
89
91
|
|
|
90
92
|
tasks = set()
|
|
@@ -110,7 +112,7 @@ class Device(enapter.async_.Routine):
|
|
|
110
112
|
task.cancel()
|
|
111
113
|
self._stack.push_async_callback(self.__wait_task, task)
|
|
112
114
|
|
|
113
|
-
async def __wait_task(self, task):
|
|
115
|
+
async def __wait_task(self, task) -> None:
|
|
114
116
|
try:
|
|
115
117
|
await task
|
|
116
118
|
except asyncio.CancelledError:
|
|
@@ -122,14 +124,16 @@ class Device(enapter.async_.Routine):
|
|
|
122
124
|
pass
|
|
123
125
|
raise
|
|
124
126
|
|
|
125
|
-
async def __process_command_requests(self):
|
|
127
|
+
async def __process_command_requests(self) -> None:
|
|
126
128
|
async with self.__channel.subscribe_to_command_requests() as reqs:
|
|
127
129
|
async for req in reqs:
|
|
128
130
|
state, payload = await self.__execute_command(req)
|
|
129
131
|
resp = req.new_response(state, payload)
|
|
130
132
|
await self.__channel.publish_command_response(resp)
|
|
131
133
|
|
|
132
|
-
async def __execute_command(
|
|
134
|
+
async def __execute_command(
|
|
135
|
+
self, req
|
|
136
|
+
) -> Tuple[enapter.mqtt.api.CommandState, enapter.types.JSON]:
|
|
133
137
|
try:
|
|
134
138
|
cmd = self.__commands[req.name]
|
|
135
139
|
except KeyError:
|
enapter/vucm/logger.py
CHANGED
|
@@ -6,32 +6,32 @@ LOGGER = logging.getLogger(__name__)
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class Logger:
|
|
9
|
-
def __init__(self, channel):
|
|
9
|
+
def __init__(self, channel) -> None:
|
|
10
10
|
self._channel = channel
|
|
11
11
|
self._logger = self._new_logger(channel.hardware_id, channel.channel_id)
|
|
12
12
|
|
|
13
13
|
@staticmethod
|
|
14
|
-
def _new_logger(hardware_id, channel_id):
|
|
14
|
+
def _new_logger(hardware_id, channel_id) -> logging.LoggerAdapter:
|
|
15
15
|
extra = {"hardware_id": hardware_id, "channel_id": channel_id}
|
|
16
16
|
return logging.LoggerAdapter(LOGGER, extra=extra)
|
|
17
17
|
|
|
18
|
-
async def debug(self, msg: str, persist: bool = False):
|
|
18
|
+
async def debug(self, msg: str, persist: bool = False) -> None:
|
|
19
19
|
self._logger.debug(msg)
|
|
20
20
|
await self.log(
|
|
21
21
|
msg, severity=enapter.mqtt.api.LogSeverity.DEBUG, persist=persist
|
|
22
22
|
)
|
|
23
23
|
|
|
24
|
-
async def info(self, msg: str, persist: bool = False):
|
|
24
|
+
async def info(self, msg: str, persist: bool = False) -> None:
|
|
25
25
|
self._logger.info(msg)
|
|
26
26
|
await self.log(msg, severity=enapter.mqtt.api.LogSeverity.INFO, persist=persist)
|
|
27
27
|
|
|
28
|
-
async def warning(self, msg: str, persist: bool = False):
|
|
28
|
+
async def warning(self, msg: str, persist: bool = False) -> None:
|
|
29
29
|
self._logger.warning(msg)
|
|
30
30
|
await self.log(
|
|
31
31
|
msg, severity=enapter.mqtt.api.LogSeverity.WARNING, persist=persist
|
|
32
32
|
)
|
|
33
33
|
|
|
34
|
-
async def error(self, msg: str, persist: bool = False):
|
|
34
|
+
async def error(self, msg: str, persist: bool = False) -> None:
|
|
35
35
|
self._logger.error(msg)
|
|
36
36
|
await self.log(
|
|
37
37
|
msg, severity=enapter.mqtt.api.LogSeverity.ERROR, persist=persist
|
|
@@ -39,7 +39,7 @@ class Logger:
|
|
|
39
39
|
|
|
40
40
|
async def log(
|
|
41
41
|
self, msg: str, severity: enapter.mqtt.api.LogSeverity, persist: bool = False
|
|
42
|
-
):
|
|
42
|
+
) -> None:
|
|
43
43
|
await self._channel.publish_logs(msg=msg, severity=severity, persist=persist)
|
|
44
44
|
|
|
45
45
|
__call__ = log
|
enapter/vucm/ucm.py
CHANGED
|
@@ -6,7 +6,7 @@ from .device import Device, device_command, device_task
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class UCM(Device):
|
|
9
|
-
def __init__(self, mqtt_client, hardware_id):
|
|
9
|
+
def __init__(self, mqtt_client, hardware_id) -> None:
|
|
10
10
|
super().__init__(
|
|
11
11
|
channel=enapter.mqtt.api.DeviceChannel(
|
|
12
12
|
client=mqtt_client, hardware_id=hardware_id, channel_id="ucm"
|
|
@@ -14,12 +14,12 @@ class UCM(Device):
|
|
|
14
14
|
)
|
|
15
15
|
|
|
16
16
|
@device_command
|
|
17
|
-
async def reboot(self):
|
|
17
|
+
async def reboot(self) -> None:
|
|
18
18
|
await asyncio.sleep(0)
|
|
19
19
|
raise NotImplementedError
|
|
20
20
|
|
|
21
21
|
@device_command
|
|
22
|
-
async def upload_lua_script(self, url, sha1, payload=None):
|
|
22
|
+
async def upload_lua_script(self, url, sha1, payload=None) -> None:
|
|
23
23
|
await asyncio.sleep(0)
|
|
24
24
|
raise NotImplementedError
|
|
25
25
|
|
|
@@ -30,7 +30,7 @@ class UCM(Device):
|
|
|
30
30
|
await asyncio.sleep(1)
|
|
31
31
|
|
|
32
32
|
@device_task
|
|
33
|
-
async def properties_publisher(self):
|
|
33
|
+
async def properties_publisher(self) -> None:
|
|
34
34
|
while True:
|
|
35
35
|
await self.send_properties({"virtual": True, "lua_api_ver": 1})
|
|
36
36
|
await asyncio.sleep(10)
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: enapter
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.3
|
|
4
4
|
Summary: Enapter Python SDK
|
|
5
5
|
Home-page: https://github.com/Enapter/python-sdk
|
|
6
6
|
Author: Roman Novatorov
|
|
7
7
|
Author-email: rnovatorov@enapter.com
|
|
8
8
|
Description-Content-Type: text/markdown
|
|
9
|
-
Requires-Dist: aiomqtt==
|
|
10
|
-
Requires-Dist: dnspython==2.
|
|
11
|
-
Requires-Dist: json-log-formatter==
|
|
9
|
+
Requires-Dist: aiomqtt==2.4.*
|
|
10
|
+
Requires-Dist: dnspython==2.8.*
|
|
11
|
+
Requires-Dist: json-log-formatter==1.1.*
|
|
12
12
|
Dynamic: author
|
|
13
13
|
Dynamic: author-email
|
|
14
14
|
Dynamic: description
|
|
@@ -25,20 +25,17 @@ Dynamic: summary
|
|
|
25
25
|
|
|
26
26
|
Enapter software development kit for Python.
|
|
27
27
|
|
|
28
|
-
:warning: **This project is work in progress. The API is not stable and may change at any time.** :warning:
|
|
29
|
-
|
|
30
28
|
## Installation
|
|
31
29
|
|
|
32
|
-
|
|
30
|
+
This project uses [semantic versioning](https://semver.org/).
|
|
33
31
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
```
|
|
32
|
+
The API is still under development and may change at any time. It is
|
|
33
|
+
recommended to pin the version during installation.
|
|
37
34
|
|
|
38
|
-
Latest
|
|
35
|
+
Latest from PyPI:
|
|
39
36
|
|
|
40
37
|
```bash
|
|
41
|
-
pip install
|
|
38
|
+
pip install enapter==0.11.3'
|
|
42
39
|
```
|
|
43
40
|
|
|
44
41
|
## Usage
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
enapter/__init__.py,sha256=awKW7jrI1c27sJl9KJ8WSFwREQ5me8hitA7HmBuhnJk,183
|
|
2
|
+
enapter/types.py,sha256=J_rFCW79cloh2FF_49Oab5kaEGdZohymkCJU7vfko-8,113
|
|
3
|
+
enapter/async_/__init__.py,sha256=JuiRI2bN2AgB-HLfAUoSsZpEziwFRftNNEp59Evnd0M,109
|
|
4
|
+
enapter/async_/generator.py,sha256=BQPPtPCYdsZIjgVy0UlC81DqId3yIDrpEyrmJ2cHdc8,497
|
|
5
|
+
enapter/async_/routine.py,sha256=zWAWWAEjwFoL8vmknC5d1oU7i2dr-HLUn-hbXEZuqHY,1903
|
|
6
|
+
enapter/log/__init__.py,sha256=n1sWMDKJSs_ebZzjbTrVdfg-oi0V1tvliTxgIV-msJ0,600
|
|
7
|
+
enapter/log/json_formatter.py,sha256=mNB9ORZ6To-Xv429dPyHiGrZcg9WadgYQN5ourib8Qw,1004
|
|
8
|
+
enapter/mdns/__init__.py,sha256=uwsg8wJ0Lcsr9oOEW1PkEV3jVgWzgA7RG2ur_MRLtM0,55
|
|
9
|
+
enapter/mdns/resolver.py,sha256=SyxA8LtoTjpKlr77HAnLZb7BH-6CPuFIxCjbsT0P1OY,1433
|
|
10
|
+
enapter/mqtt/__init__.py,sha256=TXdhxYwkzH-sqc84gO5tR1Jple29jCByW48C-Uyl-dU,154
|
|
11
|
+
enapter/mqtt/client.py,sha256=D_rZ_MEOPg2hGBTLRMOnDQulDrA1pfsba6k4abxHMCA,4212
|
|
12
|
+
enapter/mqtt/config.py,sha256=m3CoZf9EYqmjHWxefTH_dgDWNY2FGZfgjHXFw6RN9yc,2077
|
|
13
|
+
enapter/mqtt/api/__init__.py,sha256=M1g2891bSLCnDbZOuLElEPPlN6pJk0J1w_1Fi8x5xJU,267
|
|
14
|
+
enapter/mqtt/api/command.py,sha256=YEnKh2Uzv45GkQqmAhP9BOeyAiHDO1iLClZ_vyCkV9g,1388
|
|
15
|
+
enapter/mqtt/api/device_channel.py,sha256=h02nU8FhnSQ1H-BqDpsT93n79StIMFo8_58whBk8ku4,2995
|
|
16
|
+
enapter/mqtt/api/log_severity.py,sha256=ZmHXMc8d1e2ZnsXDWwl3S3noAEJjILYab_qjqk374Qw,126
|
|
17
|
+
enapter/vucm/__init__.py,sha256=40GAkJvpCl7oMuWDU09zpjP5rM4V-oKRbh_R1uhx4aE,247
|
|
18
|
+
enapter/vucm/app.py,sha256=53Z7x85GrRatEpvD7FZWy37ZchZ5TOAdlHMsd6DRFVw,1706
|
|
19
|
+
enapter/vucm/config.py,sha256=Cgx_-zW556opsNnSBhrLMHClvG5CwP1zva45dgCQyu4,1903
|
|
20
|
+
enapter/vucm/device.py,sha256=8_kuQ1PFYMrj-kZg2WLuDztcdf_dddlTdJM2BTtGupw,4427
|
|
21
|
+
enapter/vucm/logger.py,sha256=mClOnuNCR72W51BVH9uGuBsy109Ev4tBZxTTVHqmgAE,1518
|
|
22
|
+
enapter/vucm/ucm.py,sha256=Bc_sCo6EVjUprshf0AnNqKeShAmwdWOi5uZ4v_g_elc,989
|
|
23
|
+
tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
24
|
+
tests/conftest.py,sha256=qEZwCEK8F6hJZ5sSHK6hUmUYKaWfY77UuNOHntfNS3c,397
|
|
25
|
+
tests/fake_data_generator.py,sha256=lpVgazRRXAP07SeiTeY3Fe1LzDdM44lS50QTlwBdMYg,587
|
|
26
|
+
tests/integration/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
27
|
+
tests/integration/conftest.py,sha256=TpbUrExImSsLp77FIp1O-6wJrxgTmyxCgmmATIDEmL4,1617
|
|
28
|
+
tests/integration/test_mqtt.py,sha256=FDoRDZlmDM9j0ji04SG8410sR1egeSRMKv4ovdrRiaQ,3885
|
|
29
|
+
tests/unit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
30
|
+
tests/unit/test_async.py,sha256=KwhwKBSeSb7Qyaf50Ca2LGB7gm3m5j5wgGgWnvYY98k,4208
|
|
31
|
+
tests/unit/test_log.py,sha256=Q-ZelqGfladBCaw-BQrwRrxbxMK1VZxgY7HBsBb1GHw,1875
|
|
32
|
+
tests/unit/test_vucm.py,sha256=a3euiqV9etsfrWFDUtHCNqKU3irx-GEZMczBcmddhek,3626
|
|
33
|
+
tests/unit/test_mqtt/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
34
|
+
tests/unit/test_mqtt/test_api.py,sha256=ObKCHB-KDOYQLFrdzjTmLfjdWXXX0oanGKpX49P0qMI,2670
|
|
35
|
+
enapter-0.11.3.dist-info/METADATA,sha256=vDupirPofGAFHYW-rpzjtY-di0ftkfu6L_1KbJ-RW54,3321
|
|
36
|
+
enapter-0.11.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
37
|
+
enapter-0.11.3.dist-info/top_level.txt,sha256=DsMzVradd7z3A0fm7zmn9oh08ijO41RtzglrnPlx54w,14
|
|
38
|
+
enapter-0.11.3.dist-info/RECORD,,
|
tests/integration/test_mqtt.py
CHANGED
|
@@ -92,6 +92,6 @@ class HeartbitSender(enapter.async_.Routine):
|
|
|
92
92
|
payload = str(int(time.time()))
|
|
93
93
|
try:
|
|
94
94
|
await self.enapter_mqtt_client.publish(self.topic, payload)
|
|
95
|
-
except aiomqtt.
|
|
95
|
+
except aiomqtt.MqttError as e:
|
|
96
96
|
print(f"failed to publish heartbit: {e}")
|
|
97
97
|
await asyncio.sleep(self.interval)
|
enapter-0.10.2.dist-info/RECORD
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
enapter/__init__.py,sha256=rr5vMbAJ4AeIadqpC-r68wCLU0tZrMT-081OMjG_LyE,183
|
|
2
|
-
enapter/types.py,sha256=J_rFCW79cloh2FF_49Oab5kaEGdZohymkCJU7vfko-8,113
|
|
3
|
-
enapter/async_/__init__.py,sha256=JuiRI2bN2AgB-HLfAUoSsZpEziwFRftNNEp59Evnd0M,109
|
|
4
|
-
enapter/async_/generator.py,sha256=qBhnt36Gl2166sJFnZHsREbZu8l43M4DfxybUMIv6W4,300
|
|
5
|
-
enapter/async_/routine.py,sha256=A5fG4XnCEQT0Qa_JNh1N43Fv5lnLaCoGF4xt6pOAdNs,1770
|
|
6
|
-
enapter/log/__init__.py,sha256=n1sWMDKJSs_ebZzjbTrVdfg-oi0V1tvliTxgIV-msJ0,600
|
|
7
|
-
enapter/log/json_formatter.py,sha256=R5-nNEtFKdkv_SAi0gkGOaRrzsbxBjUmgZzzuJk2z7A,723
|
|
8
|
-
enapter/mdns/__init__.py,sha256=uwsg8wJ0Lcsr9oOEW1PkEV3jVgWzgA7RG2ur_MRLtM0,55
|
|
9
|
-
enapter/mdns/resolver.py,sha256=FFQuZiaKOaNtfSgI2YOaSdG-BuZwOKmYVy06Sc735Zo,1297
|
|
10
|
-
enapter/mqtt/__init__.py,sha256=aKJklTmR8OEnwnQXN1MtrvJimC9EfxcTOqhhBsPcb84,126
|
|
11
|
-
enapter/mqtt/client.py,sha256=2oExtmLOdrfo_s_wQuE7ECNK1kuFSoyhaFOkg3AP44w,4874
|
|
12
|
-
enapter/mqtt/config.py,sha256=Bng9A271vXMg1cNZ1A0lWXVKQUzun8fQHLqqt8AMXNw,1399
|
|
13
|
-
enapter/mqtt/api/__init__.py,sha256=M1g2891bSLCnDbZOuLElEPPlN6pJk0J1w_1Fi8x5xJU,267
|
|
14
|
-
enapter/mqtt/api/command.py,sha256=ozhDTjRrdCWv_bzPTjVFpL8tx7nhirm3JtQaD45wTdo,1092
|
|
15
|
-
enapter/mqtt/api/device_channel.py,sha256=fma_R59kxMoH6S4DwDBcSqgTsr8KNJkEov-c3poNsD0,2371
|
|
16
|
-
enapter/mqtt/api/log_severity.py,sha256=ZmHXMc8d1e2ZnsXDWwl3S3noAEJjILYab_qjqk374Qw,126
|
|
17
|
-
enapter/vucm/__init__.py,sha256=40GAkJvpCl7oMuWDU09zpjP5rM4V-oKRbh_R1uhx4aE,247
|
|
18
|
-
enapter/vucm/app.py,sha256=zHdEEUnKrVHNvVGmw7Jrv5MFkVnbg5yiiped86jxbSU,1424
|
|
19
|
-
enapter/vucm/config.py,sha256=Lj-YLQvoZ9ivXVBF0oKRvdupLRJ2l55EEzoxZ4HWWRE,1713
|
|
20
|
-
enapter/vucm/device.py,sha256=sxoXEzGKtalmJqxX1ZKL6hhg9gUx31TlEGVKenRmLOM,4241
|
|
21
|
-
enapter/vucm/logger.py,sha256=a_ZtuIinATbEtiP5avV8JGAKFKG0-RZhF6SozaF-_PU,1445
|
|
22
|
-
enapter/vucm/ucm.py,sha256=r1q1rSYlT7GWZ24JLmTbzkpMDUUHcvCNp9t5xHTEyzY,957
|
|
23
|
-
tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
24
|
-
tests/conftest.py,sha256=qEZwCEK8F6hJZ5sSHK6hUmUYKaWfY77UuNOHntfNS3c,397
|
|
25
|
-
tests/fake_data_generator.py,sha256=lpVgazRRXAP07SeiTeY3Fe1LzDdM44lS50QTlwBdMYg,587
|
|
26
|
-
tests/integration/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
27
|
-
tests/integration/conftest.py,sha256=TpbUrExImSsLp77FIp1O-6wJrxgTmyxCgmmATIDEmL4,1617
|
|
28
|
-
tests/integration/test_mqtt.py,sha256=Hm165Sxysrc5ZatJjiMKITv0-78N0NbGiTI5QrJ_aSU,3891
|
|
29
|
-
tests/unit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
30
|
-
tests/unit/test_async.py,sha256=KwhwKBSeSb7Qyaf50Ca2LGB7gm3m5j5wgGgWnvYY98k,4208
|
|
31
|
-
tests/unit/test_log.py,sha256=Q-ZelqGfladBCaw-BQrwRrxbxMK1VZxgY7HBsBb1GHw,1875
|
|
32
|
-
tests/unit/test_vucm.py,sha256=a3euiqV9etsfrWFDUtHCNqKU3irx-GEZMczBcmddhek,3626
|
|
33
|
-
tests/unit/test_mqtt/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
34
|
-
tests/unit/test_mqtt/test_api.py,sha256=ObKCHB-KDOYQLFrdzjTmLfjdWXXX0oanGKpX49P0qMI,2670
|
|
35
|
-
enapter-0.10.2.dist-info/METADATA,sha256=IiVXJso-oWzcLXHW11ofsOERnL3BGVQBwDY9Mt-heaY,3335
|
|
36
|
-
enapter-0.10.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
37
|
-
enapter-0.10.2.dist-info/top_level.txt,sha256=DsMzVradd7z3A0fm7zmn9oh08ijO41RtzglrnPlx54w,14
|
|
38
|
-
enapter-0.10.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|