espark-core 0.3.1__py3-none-any.whl → 0.4.0__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 espark-core might be problematic. Click here for more details.
- {espark_core-0.3.1.dist-info → espark_core-0.4.0.dist-info}/METADATA +2 -1
- {espark_core-0.3.1.dist-info → espark_core-0.4.0.dist-info}/RECORD +15 -12
- esparkcore/constants.py +2 -1
- esparkcore/data/models/__init__.py +1 -0
- esparkcore/data/models/device.py +2 -0
- esparkcore/data/models/version.py +6 -0
- esparkcore/data/repositories/__init__.py +1 -0
- esparkcore/data/repositories/version_repository.py +11 -0
- esparkcore/routers/__init__.py +1 -0
- esparkcore/routers/telemetry_router.py +2 -2
- esparkcore/routers/version_router.py +10 -0
- esparkcore/services/mqtt.py +30 -10
- {espark_core-0.3.1.dist-info → espark_core-0.4.0.dist-info}/WHEEL +0 -0
- {espark_core-0.3.1.dist-info → espark_core-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {espark_core-0.3.1.dist-info → espark_core-0.4.0.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: espark-core
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: The core module of the Espark ESP32-based IoT device management framework.
|
|
5
5
|
License: MIT
|
|
6
6
|
Requires-Python: >=3.8
|
|
@@ -9,6 +9,7 @@ License-File: LICENSE
|
|
|
9
9
|
Requires-Dist: aiomqtt
|
|
10
10
|
Requires-Dist: apscheduler
|
|
11
11
|
Requires-Dist: fastapi
|
|
12
|
+
Requires-Dist: packaging
|
|
12
13
|
Requires-Dist: sqlalchemy
|
|
13
14
|
Requires-Dist: sqlmodel
|
|
14
15
|
Requires-Dist: uvicorn[standard]
|
|
@@ -1,29 +1,32 @@
|
|
|
1
|
-
espark_core-0.
|
|
1
|
+
espark_core-0.4.0.dist-info/licenses/LICENSE,sha256=wkIXJHYIspOGVvn_ajKyLucpIyw9_pO3QExFcNTCY78,1065
|
|
2
2
|
esparkcore/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
-
esparkcore/constants.py,sha256=
|
|
3
|
+
esparkcore/constants.py,sha256=uI0bVa78CZa-ftlyTRMqWSBK275iDB1l8g1TmtJ1FGg,378
|
|
4
4
|
esparkcore/data/__init__.py,sha256=abMOPq6jC5qOQCl9j7iPDv8iyjzDF-oVd_gW0la5kL4,45
|
|
5
5
|
esparkcore/data/database.py,sha256=OaJSBDW6Qge-rsKkSIksJXOg3xF_y6LuBew--6uFbG4,532
|
|
6
|
-
esparkcore/data/models/__init__.py,sha256=
|
|
7
|
-
esparkcore/data/models/device.py,sha256=
|
|
6
|
+
esparkcore/data/models/__init__.py,sha256=9YBRwn7R_aovQjT-ZR64fBm7TPs0dsjoq3LzsUTaRtA,124
|
|
7
|
+
esparkcore/data/models/device.py,sha256=HQr941uabovQpyGZ1fOnsBNSU_LVmB7JQIZuDkk1uis,1086
|
|
8
8
|
esparkcore/data/models/outbox.py,sha256=r5bgjIbj9H-OsGhE-CWKGRf4Tp88qMvmxPYHNGOApY4,1309
|
|
9
9
|
esparkcore/data/models/telemetry.py,sha256=OfzGqGj2Y1sdK519sSOEXhfuErHoeEdpy5qmJMIc27A,652
|
|
10
|
-
esparkcore/data/
|
|
10
|
+
esparkcore/data/models/version.py,sha256=h2O_Owx-exx5tiQiiauCSELkUHDx-QZjiIGqFPybT4M,269
|
|
11
|
+
esparkcore/data/repositories/__init__.py,sha256=R4cFUr137IxhyQVwFaXRHikPkTBE_4nUAAFRchxcohA,248
|
|
11
12
|
esparkcore/data/repositories/base_repository.py,sha256=L5APTp8SczndbWqgJyEqKe-4NQoL_YYOapUN6i572cM,2097
|
|
12
13
|
esparkcore/data/repositories/device_repository.py,sha256=P9KK-Ta9MgMdrnct5hbGEoipPOPrrDQ4wINQnZZ6iZY,1131
|
|
13
14
|
esparkcore/data/repositories/outbox_repository.py,sha256=PdPyKq2Km4_7732O6HhA4BlV_zBjYF91GkMltNKUovg,1015
|
|
14
15
|
esparkcore/data/repositories/telemetry_repository.py,sha256=5UBBmY-ugcgP0Xji7cu58_nGt8MWKeHz9DHNVJxk0BY,1116
|
|
15
|
-
esparkcore/
|
|
16
|
+
esparkcore/data/repositories/version_repository.py,sha256=QZZX2BpFx1i0oeFJ6j0nZvlZH7cViKU8VERBE8AU3ZY,395
|
|
17
|
+
esparkcore/routers/__init__.py,sha256=K8-S0hugkQT5DB8i8O5YqeRsu9fGfLep6UQzCIpdgGg,131
|
|
16
18
|
esparkcore/routers/base_router.py,sha256=t-x8tYZVzBk9bD__9yaGYhm66glq_oM_lAHn0eplM2M,3902
|
|
17
19
|
esparkcore/routers/device_router.py,sha256=mIBAyTyxl9AF8Os_Sms6Bb0vEvWfljyElbTWEGLo0iU,1172
|
|
18
|
-
esparkcore/routers/telemetry_router.py,sha256=
|
|
20
|
+
esparkcore/routers/telemetry_router.py,sha256=sLO5-oN73IjdGC_ASNif5xmVZHjka_iLv0BKZ_deZP0,1981
|
|
21
|
+
esparkcore/routers/version_router.py,sha256=zoOteeK4Cs-OIk_k2v7vPwpR-OfLBwYhOUhNZjPYfHs,387
|
|
19
22
|
esparkcore/schedules/__init__.py,sha256=v5Wy3Cbt2AnudGMGXv_Cyaa10s6NAmP66rDjcMSvf0o,74
|
|
20
23
|
esparkcore/schedules/outbox.py,sha256=GGANvjkI7w1BdmmVRgKog498ASD_xRN0g8bG6yC_SN4,811
|
|
21
24
|
esparkcore/schedules/scheduler.py,sha256=aYQXImLZ4tW-hRavn3ObuV3lYjmOZD813YkeG1NsYfY,170
|
|
22
25
|
esparkcore/services/__init__.py,sha256=RIZOHBfkWn3HsApdA5rH9EZbbXGq0NfiGoOWDO_JX1k,30
|
|
23
|
-
esparkcore/services/mqtt.py,sha256=
|
|
26
|
+
esparkcore/services/mqtt.py,sha256=9MOlFIKtR42CuOfX-kEd-DT2od7mXtbD-0B8dNHqplg,6279
|
|
24
27
|
esparkcore/utils/__init__.py,sha256=rzuCJUAaQl-yPhM1vcWB_1zVXqtA71ChYP2aM_3UfKk,42
|
|
25
28
|
esparkcore/utils/logging.py,sha256=sRO8uOcPkFH-DwepqLOTS2qGaopXpMdVOjzZL6RpGs4,257
|
|
26
|
-
espark_core-0.
|
|
27
|
-
espark_core-0.
|
|
28
|
-
espark_core-0.
|
|
29
|
-
espark_core-0.
|
|
29
|
+
espark_core-0.4.0.dist-info/METADATA,sha256=Z2PvZcnFDZncpr1gk2A-Yabnx-4kI5zCuS3UgjhubGg,6161
|
|
30
|
+
espark_core-0.4.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
31
|
+
espark_core-0.4.0.dist-info/top_level.txt,sha256=hXVyhIPB4aGskFm5queWALxDPhcDkrN7_8vcjwA1iE4,11
|
|
32
|
+
espark_core-0.4.0.dist-info/RECORD,,
|
esparkcore/constants.py
CHANGED
|
@@ -2,8 +2,9 @@ ENV_DATABASE_URL : str = 'DATABASE_URL'
|
|
|
2
2
|
ENV_MQTT_HOST : str = 'MQTT_HOST'
|
|
3
3
|
ENV_MQTT_PORT : str = 'MQTT_PORT'
|
|
4
4
|
|
|
5
|
+
TOPIC_ACTION : str = 'espark/action'
|
|
5
6
|
TOPIC_DEVICE : str = 'espark/device'
|
|
6
7
|
TOPIC_REGISTRATION : str = 'espark/registration'
|
|
7
8
|
TOPIC_TELEMETRY : str = 'espark/telemetry'
|
|
8
|
-
|
|
9
|
+
TOPIC_OTP : str = 'espark/otp'
|
|
9
10
|
TOPIC_CRASH : str = 'espark/crash'
|
esparkcore/data/models/device.py
CHANGED
|
@@ -8,6 +8,8 @@ from sqlmodel import SQLModel, Field, JSON
|
|
|
8
8
|
class Device(SQLModel, table=True):
|
|
9
9
|
id : str = Field(primary_key=True, description='Unique identifier for the device')
|
|
10
10
|
display_name : Optional[str] = Field(default=None, description='Human-readable name of the device')
|
|
11
|
+
app_name : Optional[str] = Field(default=None, description='Name of the application running on the device')
|
|
12
|
+
app_version : Optional[str] = Field(default=None, description='Version of the application running on the device')
|
|
11
13
|
capabilities : Optional[str] = Field(default=None, description='Comma separated capabilities of the device')
|
|
12
14
|
parameters : Dict[str, str | int | bool] = Field(default_factory=dict, sa_column=Column(JSON), description='JSON string of capability-specific parameters')
|
|
13
15
|
last_seen : datetime = Field(index=True, description='Last time the device was seen online')
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from ..models import AppVersion
|
|
2
|
+
from .base_repository import AsyncRepository
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class AppVersionRepository(AsyncRepository[AppVersion]):
|
|
6
|
+
def __init__(self):
|
|
7
|
+
super().__init__(AppVersion)
|
|
8
|
+
|
|
9
|
+
async def get_by_app_name(self, session, app_name: str) -> AppVersion | None:
|
|
10
|
+
# pylint: disable=unexpected-keyword-arg
|
|
11
|
+
return await self.get(session, AppVersion.id == app_name)
|
esparkcore/routers/__init__.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from datetime import datetime, timedelta, timezone
|
|
2
|
-
from typing import
|
|
2
|
+
from typing import Sequence
|
|
3
3
|
|
|
4
4
|
from fastapi import Depends, Query, Response
|
|
5
5
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
@@ -28,7 +28,7 @@ class TelemetryRouter(BaseRouter):
|
|
|
28
28
|
capabilities = device.capabilities.split(',') if device.capabilities else []
|
|
29
29
|
for data_type in capabilities:
|
|
30
30
|
if not data_type.startswith('action_'):
|
|
31
|
-
telemetry = await
|
|
31
|
+
telemetry = await self.repo.get_latest_for_device(session, device.id, data_type)
|
|
32
32
|
if telemetry:
|
|
33
33
|
if telemetry.timestamp.tzinfo is None:
|
|
34
34
|
telemetry.timestamp = telemetry.timestamp.replace(tzinfo=timezone.utc)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from ..data.models import AppVersion
|
|
2
|
+
from ..data.repositories import AppVersionRepository
|
|
3
|
+
from .base_router import BaseRouter
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AppVersionRouter(BaseRouter):
|
|
7
|
+
def __init__(self, repo: AppVersionRepository = None) -> None:
|
|
8
|
+
self.repo : AppVersionRepository = repo or AppVersionRepository()
|
|
9
|
+
|
|
10
|
+
super().__init__(AppVersion, self.repo, '/api/v1/versions', ['version'])
|
esparkcore/services/mqtt.py
CHANGED
|
@@ -1,27 +1,29 @@
|
|
|
1
1
|
from asyncio import create_task, gather, Queue, sleep
|
|
2
2
|
from datetime import datetime, timezone
|
|
3
|
-
from json import JSONDecodeError, loads
|
|
3
|
+
from json import dumps, JSONDecodeError, loads
|
|
4
4
|
from os import getenv
|
|
5
5
|
from uuid import getnode
|
|
6
6
|
|
|
7
7
|
from aiomqtt import Client, MqttError
|
|
8
|
+
from packaging.version import Version
|
|
8
9
|
|
|
9
|
-
from ..constants import ENV_MQTT_HOST, ENV_MQTT_PORT, TOPIC_CRASH, TOPIC_REGISTRATION, TOPIC_TELEMETRY
|
|
10
|
+
from ..constants import ENV_MQTT_HOST, ENV_MQTT_PORT, TOPIC_CRASH, TOPIC_OTP, TOPIC_REGISTRATION, TOPIC_TELEMETRY
|
|
10
11
|
from ..data import async_session
|
|
11
|
-
from ..data.models import Device, Telemetry
|
|
12
|
-
from ..data.repositories import DeviceRepository, TelemetryRepository
|
|
12
|
+
from ..data.models import AppVersion, Device, Telemetry
|
|
13
|
+
from ..data.repositories import AppVersionRepository, DeviceRepository, TelemetryRepository
|
|
13
14
|
from ..utils import log_debug, log_error
|
|
14
15
|
|
|
15
16
|
MQTT_RETRY_DELAY: int = 5
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
class MQTTManager:
|
|
19
|
-
def __init__(self, device_repo: DeviceRepository = None, telemetry_repo: TelemetryRepository = None):
|
|
20
|
-
self.mqtt_host : str
|
|
21
|
-
self.mqtt_port : int
|
|
22
|
-
self.
|
|
23
|
-
self.
|
|
24
|
-
self.
|
|
20
|
+
def __init__(self, version_repo: AppVersionRepository = None, device_repo: DeviceRepository = None, telemetry_repo: TelemetryRepository = None):
|
|
21
|
+
self.mqtt_host : str = getenv(ENV_MQTT_HOST, 'localhost')
|
|
22
|
+
self.mqtt_port : int = int(getenv(ENV_MQTT_PORT, '1883'))
|
|
23
|
+
self.version_repo : AppVersionRepository = version_repo if version_repo else AppVersionRepository()
|
|
24
|
+
self.device_repo : DeviceRepository = device_repo if device_repo else DeviceRepository()
|
|
25
|
+
self.telemetry_repo : TelemetryRepository = telemetry_repo if telemetry_repo else TelemetryRepository()
|
|
26
|
+
self.queue : Queue = Queue()
|
|
25
27
|
|
|
26
28
|
async def _handle_registration(self, device_id: str, payload: dict) -> None:
|
|
27
29
|
try:
|
|
@@ -30,6 +32,8 @@ class MQTTManager:
|
|
|
30
32
|
async with async_session() as session:
|
|
31
33
|
device = await self.device_repo.get(session, Device.id == device_id)
|
|
32
34
|
if device:
|
|
35
|
+
device.app_name = payload['app_name']
|
|
36
|
+
device.app_version = payload['app_version']
|
|
33
37
|
device.capabilities = payload['capabilities']
|
|
34
38
|
device.last_seen = datetime.now(timezone.utc)
|
|
35
39
|
|
|
@@ -39,10 +43,26 @@ class MQTTManager:
|
|
|
39
43
|
|
|
40
44
|
device.id = device_id
|
|
41
45
|
device.display_name = None
|
|
46
|
+
device.app_name = payload['app_name']
|
|
47
|
+
device.app_version = payload['app_version']
|
|
42
48
|
device.capabilities = payload['capabilities']
|
|
43
49
|
device.last_seen = datetime.now(timezone.utc)
|
|
44
50
|
|
|
45
51
|
await self.device_repo.add(session, device)
|
|
52
|
+
|
|
53
|
+
latest_version = await self.version_repo.get(session, AppVersion.id == payload['app_name'])
|
|
54
|
+
current_version = Version(payload['app_version'])
|
|
55
|
+
|
|
56
|
+
if Version(latest_version.version) > current_version:
|
|
57
|
+
log_debug(f'Device {device_id} is running an outdated version ({current_version} < {latest_version.version})')
|
|
58
|
+
|
|
59
|
+
async with Client(self.mqtt_host, self.mqtt_port, identifier=f'espark-core-{hex(getnode())}') as client:
|
|
60
|
+
await client.publish(f'{TOPIC_OTP}/{device_id}', dumps({
|
|
61
|
+
'device_id' : device_id,
|
|
62
|
+
'app_name' : payload['app_name'],
|
|
63
|
+
'app_version' : latest_version.version,
|
|
64
|
+
'download_url' : None,
|
|
65
|
+
}), qos=1)
|
|
46
66
|
# pylint: disable=broad-exception-caught
|
|
47
67
|
except Exception as e:
|
|
48
68
|
log_error(e)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|