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.
Files changed (64) hide show
  1. enapter/__init__.py +4 -4
  2. enapter/async_/__init__.py +1 -4
  3. enapter/async_/generator.py +5 -2
  4. enapter/async_/routine.py +21 -53
  5. enapter/http/__init__.py +3 -0
  6. enapter/http/api/__init__.py +5 -0
  7. enapter/http/api/client.py +31 -0
  8. enapter/http/api/config.py +23 -0
  9. enapter/http/api/devices/__init__.py +21 -0
  10. enapter/http/api/devices/authorized_role.py +11 -0
  11. enapter/http/api/devices/client.py +25 -0
  12. enapter/http/api/devices/communication_config.py +45 -0
  13. enapter/http/api/devices/device.py +32 -0
  14. enapter/http/api/devices/device_type.py +10 -0
  15. enapter/http/api/devices/mqtt_credentials.py +13 -0
  16. enapter/http/api/devices/mqtt_protocol.py +7 -0
  17. enapter/http/api/devices/mqtts_credentials.py +18 -0
  18. enapter/http/api/devices/time_sync_protocol.py +6 -0
  19. enapter/log/json_formatter.py +14 -4
  20. enapter/mdns/resolver.py +14 -11
  21. enapter/mqtt/__init__.py +5 -12
  22. enapter/mqtt/api/__init__.py +6 -0
  23. enapter/mqtt/api/client.py +62 -0
  24. enapter/mqtt/api/config.py +77 -0
  25. enapter/mqtt/api/device/__init__.py +21 -0
  26. enapter/mqtt/api/device/channel.py +56 -0
  27. enapter/mqtt/api/device/command_request.py +38 -0
  28. enapter/mqtt/api/device/command_response.py +28 -0
  29. enapter/mqtt/api/device/command_state.py +8 -0
  30. enapter/mqtt/api/device/log.py +31 -0
  31. enapter/mqtt/api/device/log_severity.py +9 -0
  32. enapter/mqtt/api/device/message.py +24 -0
  33. enapter/mqtt/api/device/properties.py +24 -0
  34. enapter/mqtt/api/device/telemetry.py +28 -0
  35. enapter/mqtt/client.py +88 -119
  36. enapter/mqtt/errors.py +3 -0
  37. enapter/mqtt/message.py +3 -0
  38. enapter/standalone/__init__.py +25 -0
  39. enapter/standalone/config.py +218 -0
  40. enapter/standalone/device.py +59 -0
  41. enapter/standalone/device_protocol.py +33 -0
  42. enapter/standalone/logger.py +21 -0
  43. enapter/standalone/mqtt_adapter.py +134 -0
  44. enapter/standalone/run.py +39 -0
  45. enapter/standalone/ucm.py +28 -0
  46. enapter-0.12.1.dist-info/METADATA +74 -0
  47. enapter-0.12.1.dist-info/RECORD +52 -0
  48. {enapter-0.6.3.dist-info → enapter-0.12.1.dist-info}/WHEEL +1 -1
  49. {enapter-0.6.3.dist-info → enapter-0.12.1.dist-info}/top_level.txt +0 -1
  50. enapter/mqtt/command.py +0 -45
  51. enapter/mqtt/config.py +0 -48
  52. enapter/mqtt/device_channel.py +0 -83
  53. enapter/types.py +0 -3
  54. enapter/vucm/__init__.py +0 -12
  55. enapter/vucm/app.py +0 -48
  56. enapter/vucm/config.py +0 -59
  57. enapter/vucm/device.py +0 -92
  58. enapter/vucm/logger.py +0 -39
  59. enapter/vucm/ucm.py +0 -30
  60. enapter-0.6.3.dist-info/METADATA +0 -111
  61. enapter-0.6.3.dist-info/RECORD +0 -27
  62. tests/test_async.py +0 -152
  63. tests/test_log.py +0 -69
  64. /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
+ [![CI](https://github.com/Enapter/python-sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/Enapter/python-sdk/actions/workflows/ci.yml)
24
+ [![PyPI version](https://img.shields.io/pypi/v/enapter.svg)](https://pypi.org/project/enapter)
25
+ [![Code Style](https://img.shields.io/badge/code%20style-black-000000.svg)](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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.37.1)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5