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,21 @@
|
|
|
1
|
+
from .channel import Channel
|
|
2
|
+
from .command_request import CommandRequest
|
|
3
|
+
from .command_response import CommandResponse
|
|
4
|
+
from .command_state import CommandState
|
|
5
|
+
from .log import Log
|
|
6
|
+
from .log_severity import LogSeverity
|
|
7
|
+
from .message import Message
|
|
8
|
+
from .properties import Properties
|
|
9
|
+
from .telemetry import Telemetry
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"CommandRequest",
|
|
13
|
+
"CommandResponse",
|
|
14
|
+
"Channel",
|
|
15
|
+
"CommandState",
|
|
16
|
+
"Log",
|
|
17
|
+
"LogSeverity",
|
|
18
|
+
"Message",
|
|
19
|
+
"Properties",
|
|
20
|
+
"Telemetry",
|
|
21
|
+
]
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from typing import AsyncContextManager, AsyncGenerator
|
|
2
|
+
|
|
3
|
+
from enapter import async_, mqtt
|
|
4
|
+
|
|
5
|
+
from .command_request import CommandRequest
|
|
6
|
+
from .command_response import CommandResponse
|
|
7
|
+
from .log import Log
|
|
8
|
+
from .properties import Properties
|
|
9
|
+
from .telemetry import Telemetry
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Channel:
|
|
13
|
+
|
|
14
|
+
def __init__(self, client: mqtt.Client, hardware_id: str, channel_id: str) -> None:
|
|
15
|
+
self._client = client
|
|
16
|
+
self._hardware_id = hardware_id
|
|
17
|
+
self._channel_id = channel_id
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def hardware_id(self) -> str:
|
|
21
|
+
return self._hardware_id
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def channel_id(self) -> str:
|
|
25
|
+
return self._channel_id
|
|
26
|
+
|
|
27
|
+
@async_.generator
|
|
28
|
+
async def subscribe_to_command_requests(
|
|
29
|
+
self,
|
|
30
|
+
) -> AsyncGenerator[CommandRequest, None]:
|
|
31
|
+
async with self._subscribe("v1/command/requests") as messages:
|
|
32
|
+
async for msg in messages:
|
|
33
|
+
assert isinstance(msg.payload, str) or isinstance(msg.payload, bytes)
|
|
34
|
+
yield CommandRequest.from_json(msg.payload)
|
|
35
|
+
|
|
36
|
+
async def publish_command_response(self, response: CommandResponse) -> None:
|
|
37
|
+
await self._publish("v1/command/responses", response.to_json())
|
|
38
|
+
|
|
39
|
+
async def publish_telemetry(self, telemetry: Telemetry, **kwargs) -> None:
|
|
40
|
+
await self._publish("v1/telemetry", telemetry.to_json(), **kwargs)
|
|
41
|
+
|
|
42
|
+
async def publish_properties(self, properties: Properties, **kwargs) -> None:
|
|
43
|
+
await self._publish("v1/register", properties.to_json(), **kwargs)
|
|
44
|
+
|
|
45
|
+
async def publish_log(self, log: Log, **kwargs) -> None:
|
|
46
|
+
await self._publish("v3/logs", log.to_json(), **kwargs)
|
|
47
|
+
|
|
48
|
+
def _subscribe(
|
|
49
|
+
self, path: str
|
|
50
|
+
) -> AsyncContextManager[AsyncGenerator[mqtt.Message, None]]:
|
|
51
|
+
topic = f"v1/to/{self._hardware_id}/{self._channel_id}/{path}"
|
|
52
|
+
return self._client.subscribe(topic)
|
|
53
|
+
|
|
54
|
+
async def _publish(self, path: str, payload: str, **kwargs) -> None:
|
|
55
|
+
topic = f"v1/from/{self._hardware_id}/{self._channel_id}/{path}"
|
|
56
|
+
await self._client.publish(topic, payload, **kwargs)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
from typing import Any, Self
|
|
3
|
+
|
|
4
|
+
from .command_response import CommandResponse
|
|
5
|
+
from .command_state import CommandState
|
|
6
|
+
from .message import Message
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclasses.dataclass
|
|
10
|
+
class CommandRequest(Message):
|
|
11
|
+
|
|
12
|
+
id: str
|
|
13
|
+
name: str
|
|
14
|
+
arguments: dict[str, Any]
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def from_dto(cls, dto: dict[str, Any]) -> Self:
|
|
18
|
+
return cls(
|
|
19
|
+
id=dto["id"],
|
|
20
|
+
name=dto["name"],
|
|
21
|
+
arguments=dto.get("arguments", {}),
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
def to_dto(self) -> dict[str, Any]:
|
|
25
|
+
return {
|
|
26
|
+
"id": self.id,
|
|
27
|
+
"name": self.name,
|
|
28
|
+
"arguments": self.arguments,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
def new_response(
|
|
32
|
+
self, state: CommandState, payload: dict[str, Any]
|
|
33
|
+
) -> CommandResponse:
|
|
34
|
+
return CommandResponse(
|
|
35
|
+
id=self.id,
|
|
36
|
+
state=state,
|
|
37
|
+
payload=payload,
|
|
38
|
+
)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
from typing import Any, Self
|
|
3
|
+
|
|
4
|
+
from .command_state import CommandState
|
|
5
|
+
from .message import Message
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclasses.dataclass
|
|
9
|
+
class CommandResponse(Message):
|
|
10
|
+
|
|
11
|
+
id: str
|
|
12
|
+
state: CommandState
|
|
13
|
+
payload: dict[str, Any]
|
|
14
|
+
|
|
15
|
+
@classmethod
|
|
16
|
+
def from_dto(cls, dto: dict[str, Any]) -> Self:
|
|
17
|
+
return cls(
|
|
18
|
+
id=dto["id"],
|
|
19
|
+
state=CommandState(dto["state"]),
|
|
20
|
+
payload=dto.get("payload", {}),
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
def to_dto(self) -> dict[str, Any]:
|
|
24
|
+
return {
|
|
25
|
+
"id": self.id,
|
|
26
|
+
"state": self.state.value,
|
|
27
|
+
"payload": self.payload,
|
|
28
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
from typing import Any, Self
|
|
3
|
+
|
|
4
|
+
from .log_severity import LogSeverity
|
|
5
|
+
from .message import Message
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclasses.dataclass
|
|
9
|
+
class Log(Message):
|
|
10
|
+
|
|
11
|
+
timestamp: int
|
|
12
|
+
message: str
|
|
13
|
+
severity: LogSeverity
|
|
14
|
+
persist: bool
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def from_dto(cls, dto: dict[str, Any]) -> Self:
|
|
18
|
+
return cls(
|
|
19
|
+
timestamp=dto["timestamp"],
|
|
20
|
+
message=dto["message"],
|
|
21
|
+
severity=LogSeverity(dto["severity"]),
|
|
22
|
+
persist=dto["persist"],
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
def to_dto(self) -> dict[str, Any]:
|
|
26
|
+
return {
|
|
27
|
+
"timestamp": self.timestamp,
|
|
28
|
+
"message": self.message,
|
|
29
|
+
"severity": self.severity.value,
|
|
30
|
+
"persist": self.persist,
|
|
31
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import json
|
|
3
|
+
from typing import Any, Self
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Message(abc.ABC):
|
|
7
|
+
|
|
8
|
+
@classmethod
|
|
9
|
+
@abc.abstractmethod
|
|
10
|
+
def from_dto(cls, dto: dict[str, Any]) -> Self:
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
@abc.abstractmethod
|
|
14
|
+
def to_dto(self) -> dict[str, Any]:
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
@classmethod
|
|
18
|
+
def from_json(cls, data: str | bytes) -> Self:
|
|
19
|
+
dto = json.loads(data)
|
|
20
|
+
return cls.from_dto(dto)
|
|
21
|
+
|
|
22
|
+
def to_json(self) -> str:
|
|
23
|
+
dto = self.to_dto()
|
|
24
|
+
return json.dumps(dto)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
from typing import Any, Self
|
|
3
|
+
|
|
4
|
+
from .message import Message
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclasses.dataclass
|
|
8
|
+
class Properties(Message):
|
|
9
|
+
|
|
10
|
+
timestamp: int
|
|
11
|
+
values: dict[str, Any] = dataclasses.field(default_factory=dict)
|
|
12
|
+
|
|
13
|
+
def __post_init__(self) -> None:
|
|
14
|
+
if "timestamp" in self.values:
|
|
15
|
+
raise ValueError("`timestamp` is reserved")
|
|
16
|
+
|
|
17
|
+
@classmethod
|
|
18
|
+
def from_dto(cls, dto: dict[str, Any]) -> Self:
|
|
19
|
+
timestamp = dto["timestamp"]
|
|
20
|
+
values = {k: v for k, v in dto.items() if k != "timestamp"}
|
|
21
|
+
return cls(timestamp=timestamp, values=values)
|
|
22
|
+
|
|
23
|
+
def to_dto(self) -> dict[str, Any]:
|
|
24
|
+
return {"timestamp": self.timestamp, **self.values}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
from typing import Any, Self
|
|
3
|
+
|
|
4
|
+
from .message import Message
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclasses.dataclass
|
|
8
|
+
class Telemetry(Message):
|
|
9
|
+
|
|
10
|
+
timestamp: int
|
|
11
|
+
alerts: list[str] | None = None
|
|
12
|
+
values: dict[str, Any] = dataclasses.field(default_factory=dict)
|
|
13
|
+
|
|
14
|
+
def __post_init__(self) -> None:
|
|
15
|
+
if "timestamp" in self.values:
|
|
16
|
+
raise ValueError("`timestamp` is reserved")
|
|
17
|
+
if "alerts" in self.values:
|
|
18
|
+
raise ValueError("`alerts` is reserved")
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def from_dto(cls, dto: dict[str, Any]) -> Self:
|
|
22
|
+
dto = dto.copy()
|
|
23
|
+
timestamp = dto.pop("timestamp")
|
|
24
|
+
alerts = dto.pop("alerts", None)
|
|
25
|
+
return cls(timestamp=timestamp, alerts=alerts, values=dto)
|
|
26
|
+
|
|
27
|
+
def to_dto(self) -> dict[str, Any]:
|
|
28
|
+
return {"timestamp": self.timestamp, "alerts": self.alerts, **self.values}
|
enapter/mqtt/client.py
CHANGED
|
@@ -2,157 +2,126 @@ import asyncio
|
|
|
2
2
|
import contextlib
|
|
3
3
|
import logging
|
|
4
4
|
import ssl
|
|
5
|
-
import
|
|
5
|
+
from typing import AsyncGenerator
|
|
6
6
|
|
|
7
|
-
import
|
|
7
|
+
import aiomqtt # type: ignore
|
|
8
8
|
|
|
9
|
-
from
|
|
10
|
-
|
|
9
|
+
from enapter import async_, mdns
|
|
10
|
+
|
|
11
|
+
from .message import Message
|
|
11
12
|
|
|
12
13
|
LOGGER = logging.getLogger(__name__)
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
class Client(async_.Routine):
|
|
16
|
-
def __init__(self, config):
|
|
17
|
-
self._logger = self._new_logger(config)
|
|
18
|
-
self._config = config
|
|
19
|
-
self._mdns_resolver = mdns.Resolver()
|
|
20
|
-
self._tls_context = self._new_tls_context(config)
|
|
21
|
-
self._lock = asyncio.Lock()
|
|
22
|
-
self._client = None
|
|
23
|
-
self._client_ready = asyncio.Event()
|
|
24
|
-
|
|
25
|
-
@staticmethod
|
|
26
|
-
def _new_logger(config):
|
|
27
|
-
extra = {"host": config.host, "port": config.port}
|
|
28
|
-
return logging.LoggerAdapter(LOGGER, extra=extra)
|
|
29
|
-
|
|
30
|
-
def config(self):
|
|
31
|
-
return self._config
|
|
32
|
-
|
|
33
|
-
def device_channel(self, hardware_id, channel_id):
|
|
34
|
-
return DeviceChannel(
|
|
35
|
-
client=self, hardware_id=hardware_id, channel_id=channel_id
|
|
36
|
-
)
|
|
37
17
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
hostname: str,
|
|
21
|
+
port: int = 1883,
|
|
22
|
+
*,
|
|
23
|
+
username: str | None = None,
|
|
24
|
+
password: str | None = None,
|
|
25
|
+
tls_context: ssl.SSLContext | None = None,
|
|
26
|
+
task_group: asyncio.TaskGroup | None = None
|
|
27
|
+
) -> None:
|
|
28
|
+
super().__init__(task_group=task_group)
|
|
29
|
+
self._logger = logging.LoggerAdapter(
|
|
30
|
+
LOGGER, extra={"hostname": hostname, "port": port}
|
|
31
|
+
)
|
|
32
|
+
self._hostname = hostname
|
|
33
|
+
self._port = port
|
|
34
|
+
self._username = username
|
|
35
|
+
self._password = password
|
|
36
|
+
self._tls_context = tls_context
|
|
37
|
+
self._mdns_resolver = mdns.Resolver()
|
|
38
|
+
self._publisher: aiomqtt.Client | None = None
|
|
39
|
+
self._publisher_connected = asyncio.Event()
|
|
47
40
|
|
|
48
|
-
|
|
41
|
+
async def publish(self, *args, **kwargs) -> None:
|
|
42
|
+
await self._publisher_connected.wait()
|
|
43
|
+
assert self._publisher is not None
|
|
44
|
+
await self._publisher.publish(*args, **kwargs)
|
|
49
45
|
|
|
50
46
|
@async_.generator
|
|
51
|
-
async def subscribe(self,
|
|
47
|
+
async def subscribe(self, *topics: str) -> AsyncGenerator[Message, None]:
|
|
52
48
|
while True:
|
|
53
|
-
client = None
|
|
54
|
-
|
|
55
|
-
while True:
|
|
56
|
-
await self._client_ready.wait()
|
|
57
|
-
async with self._lock:
|
|
58
|
-
if self._client_ready.is_set():
|
|
59
|
-
client = self._client
|
|
60
|
-
break
|
|
61
|
-
|
|
62
49
|
try:
|
|
63
|
-
async with
|
|
64
|
-
|
|
65
|
-
|
|
50
|
+
async with self._connect() as subscriber:
|
|
51
|
+
for topic in topics:
|
|
52
|
+
await subscriber.subscribe(topic)
|
|
53
|
+
self._logger.info("subscriber [%s] connected", ",".join(topics))
|
|
54
|
+
async for msg in subscriber.messages:
|
|
66
55
|
yield msg
|
|
67
|
-
|
|
68
|
-
except asyncio_mqtt.MqttError as e:
|
|
56
|
+
except aiomqtt.MqttError as e:
|
|
69
57
|
self._logger.error(e)
|
|
70
58
|
retry_interval = 5
|
|
71
59
|
await asyncio.sleep(retry_interval)
|
|
72
60
|
|
|
73
|
-
async def _run(self):
|
|
61
|
+
async def _run(self) -> None:
|
|
74
62
|
self._logger.info("starting")
|
|
75
|
-
|
|
76
|
-
self._started.set()
|
|
77
|
-
|
|
78
63
|
while True:
|
|
79
64
|
try:
|
|
80
|
-
async with self._connect() as
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
async for message in messages:
|
|
88
|
-
self._logger.warn(
|
|
89
|
-
"received unfiltered message: %s", message.topic
|
|
90
|
-
)
|
|
91
|
-
except asyncio_mqtt.MqttError as e:
|
|
65
|
+
async with self._connect() as publisher:
|
|
66
|
+
self._logger.info("publisher connected")
|
|
67
|
+
self._publisher = publisher
|
|
68
|
+
self._publisher_connected.set()
|
|
69
|
+
async for msg in publisher.messages:
|
|
70
|
+
pass
|
|
71
|
+
except aiomqtt.MqttError as e:
|
|
92
72
|
self._logger.error(e)
|
|
93
73
|
retry_interval = 5
|
|
94
74
|
await asyncio.sleep(retry_interval)
|
|
95
75
|
finally:
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
self._client = None
|
|
99
|
-
self._logger.info("client not ready")
|
|
76
|
+
self._publisher_connected.clear()
|
|
77
|
+
self._publisher = None
|
|
100
78
|
|
|
101
79
|
@contextlib.asynccontextmanager
|
|
102
|
-
async def _connect(self):
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
raise
|
|
118
|
-
|
|
119
|
-
@staticmethod
|
|
120
|
-
def _new_tls_context(config):
|
|
121
|
-
if not config.tls_enabled:
|
|
122
|
-
return None
|
|
123
|
-
|
|
124
|
-
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
|
125
|
-
|
|
126
|
-
ctx.verify_mode = ssl.CERT_REQUIRED
|
|
127
|
-
ctx.check_hostname = False
|
|
128
|
-
ctx.load_verify_locations(None, None, config.tls_ca_cert)
|
|
129
|
-
|
|
130
|
-
with contextlib.ExitStack() as stack:
|
|
131
|
-
certfile = stack.enter_context(tempfile.NamedTemporaryFile())
|
|
132
|
-
certfile.write(config.tls_cert.encode())
|
|
133
|
-
certfile.flush()
|
|
134
|
-
|
|
135
|
-
keyfile = stack.enter_context(tempfile.NamedTemporaryFile())
|
|
136
|
-
keyfile.write(config.tls_secret_key.encode())
|
|
137
|
-
keyfile.flush()
|
|
138
|
-
|
|
139
|
-
ctx.load_cert_chain(certfile.name, keyfile=keyfile.name)
|
|
140
|
-
|
|
141
|
-
return ctx
|
|
142
|
-
|
|
143
|
-
async def _maybe_resolve_mdns(self, host):
|
|
144
|
-
if not host.endswith(".local"):
|
|
145
|
-
return host
|
|
80
|
+
async def _connect(self) -> AsyncGenerator[aiomqtt.Client, None]:
|
|
81
|
+
hostname = await self._maybe_resolve_hostname()
|
|
82
|
+
async with _new_aiomqtt_client(
|
|
83
|
+
hostname=hostname,
|
|
84
|
+
port=self._port,
|
|
85
|
+
username=self._username,
|
|
86
|
+
password=self._password,
|
|
87
|
+
logger=LOGGER,
|
|
88
|
+
tls_context=self._tls_context,
|
|
89
|
+
) as client:
|
|
90
|
+
yield client
|
|
91
|
+
|
|
92
|
+
async def _maybe_resolve_hostname(self) -> str:
|
|
93
|
+
if not self._hostname.endswith(".local"):
|
|
94
|
+
return self._hostname
|
|
146
95
|
|
|
147
96
|
while True:
|
|
148
97
|
try:
|
|
149
|
-
|
|
98
|
+
return await self._mdns_resolver.resolve(self._hostname)
|
|
150
99
|
except Exception as e:
|
|
151
|
-
self._logger.error("failed to resolve mDNS host
|
|
100
|
+
self._logger.error("failed to resolve mDNS host: %s", e)
|
|
152
101
|
retry_interval = 5
|
|
153
102
|
await asyncio.sleep(retry_interval)
|
|
154
|
-
continue
|
|
155
103
|
|
|
156
|
-
self._logger.info("mDNS host resolved: %s", ip)
|
|
157
104
|
|
|
158
|
-
|
|
105
|
+
@contextlib.asynccontextmanager
|
|
106
|
+
async def _new_aiomqtt_client(*args, **kwargs) -> AsyncGenerator[aiomqtt.Client, None]:
|
|
107
|
+
"""
|
|
108
|
+
Creates `aiomqtt.Client` shielding `__aenter__` from cancellation.
|
|
109
|
+
|
|
110
|
+
See:
|
|
111
|
+
- https://github.com/empicano/aiomqtt/issues/377
|
|
112
|
+
"""
|
|
113
|
+
client = aiomqtt.Client(*args, **kwargs)
|
|
114
|
+
setup_task = asyncio.create_task(client.__aenter__())
|
|
115
|
+
try:
|
|
116
|
+
await asyncio.shield(setup_task)
|
|
117
|
+
except asyncio.CancelledError as e:
|
|
118
|
+
await setup_task
|
|
119
|
+
await client.__aexit__(type(e), e, e.__traceback__)
|
|
120
|
+
raise
|
|
121
|
+
try:
|
|
122
|
+
yield client
|
|
123
|
+
except BaseException as e:
|
|
124
|
+
await client.__aexit__(type(e), e, e.__traceback__)
|
|
125
|
+
raise
|
|
126
|
+
else:
|
|
127
|
+
await client.__aexit__(None, None, None)
|
enapter/mqtt/errors.py
ADDED
enapter/mqtt/message.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from .config import Config
|
|
2
|
+
from .device import Device
|
|
3
|
+
from .device_protocol import (
|
|
4
|
+
CommandArgs,
|
|
5
|
+
CommandResult,
|
|
6
|
+
DeviceProtocol,
|
|
7
|
+
Log,
|
|
8
|
+
Properties,
|
|
9
|
+
Telemetry,
|
|
10
|
+
)
|
|
11
|
+
from .logger import Logger
|
|
12
|
+
from .run import run
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"CommandArgs",
|
|
16
|
+
"CommandResult",
|
|
17
|
+
"Config",
|
|
18
|
+
"Device",
|
|
19
|
+
"DeviceProtocol",
|
|
20
|
+
"Log",
|
|
21
|
+
"Logger",
|
|
22
|
+
"Properties",
|
|
23
|
+
"Telemetry",
|
|
24
|
+
"run",
|
|
25
|
+
]
|