espark-core 0.4.8__tar.gz → 0.5.0__tar.gz

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 (52) hide show
  1. {espark_core-0.4.8/espark_core.egg-info → espark_core-0.5.0}/PKG-INFO +7 -7
  2. {espark_core-0.4.8 → espark_core-0.5.0/espark_core.egg-info}/PKG-INFO +7 -7
  3. {espark_core-0.4.8 → espark_core-0.5.0}/espark_core.egg-info/SOURCES.txt +12 -0
  4. {espark_core-0.4.8 → espark_core-0.5.0}/espark_core.egg-info/requires.txt +6 -6
  5. {espark_core-0.4.8 → espark_core-0.5.0}/esparkcore/constants.py +2 -1
  6. {espark_core-0.4.8 → espark_core-0.5.0}/esparkcore/data/models/__init__.py +2 -0
  7. {espark_core-0.4.8 → espark_core-0.5.0}/esparkcore/data/models/device.py +1 -1
  8. espark_core-0.5.0/esparkcore/data/models/notification.py +11 -0
  9. espark_core-0.5.0/esparkcore/data/models/trigger.py +13 -0
  10. {espark_core-0.4.8 → espark_core-0.5.0}/esparkcore/data/repositories/__init__.py +2 -0
  11. espark_core-0.5.0/esparkcore/data/repositories/notification_repository.py +7 -0
  12. {espark_core-0.4.8 → espark_core-0.5.0}/esparkcore/data/repositories/telemetry_repository.py +24 -1
  13. espark_core-0.5.0/esparkcore/data/repositories/trigger_repository.py +7 -0
  14. {espark_core-0.4.8 → espark_core-0.5.0}/esparkcore/notifications/notifier.py +1 -1
  15. espark_core-0.5.0/esparkcore/notifications/slack_notifier.py +17 -0
  16. {espark_core-0.4.8 → espark_core-0.5.0}/esparkcore/routers/__init__.py +2 -0
  17. espark_core-0.5.0/esparkcore/routers/device_router.py +57 -0
  18. espark_core-0.5.0/esparkcore/routers/notification_router.py +10 -0
  19. {espark_core-0.4.8 → espark_core-0.5.0}/esparkcore/routers/telemetry_router.py +10 -0
  20. espark_core-0.5.0/esparkcore/routers/trigger_router.py +10 -0
  21. {espark_core-0.4.8 → espark_core-0.5.0}/esparkcore/services/mqtt.py +48 -11
  22. {espark_core-0.4.8 → espark_core-0.5.0}/requirements.dev.txt +2 -2
  23. espark_core-0.5.0/requirements.txt +8 -0
  24. {espark_core-0.4.8 → espark_core-0.5.0}/setup.py +1 -1
  25. espark_core-0.4.8/esparkcore/notifications/slack_notifier.py +0 -16
  26. espark_core-0.4.8/esparkcore/routers/device_router.py +0 -29
  27. espark_core-0.4.8/requirements.txt +0 -8
  28. {espark_core-0.4.8 → espark_core-0.5.0}/LICENSE +0 -0
  29. {espark_core-0.4.8 → espark_core-0.5.0}/MANIFEST.in +0 -0
  30. {espark_core-0.4.8 → espark_core-0.5.0}/README.md +0 -0
  31. {espark_core-0.4.8 → espark_core-0.5.0}/espark_core.egg-info/dependency_links.txt +0 -0
  32. {espark_core-0.4.8 → espark_core-0.5.0}/espark_core.egg-info/top_level.txt +0 -0
  33. {espark_core-0.4.8 → espark_core-0.5.0}/esparkcore/__init__.py +0 -0
  34. {espark_core-0.4.8 → espark_core-0.5.0}/esparkcore/data/__init__.py +0 -0
  35. {espark_core-0.4.8 → espark_core-0.5.0}/esparkcore/data/database.py +0 -0
  36. {espark_core-0.4.8 → espark_core-0.5.0}/esparkcore/data/models/outbox.py +0 -0
  37. {espark_core-0.4.8 → espark_core-0.5.0}/esparkcore/data/models/telemetry.py +0 -0
  38. {espark_core-0.4.8 → espark_core-0.5.0}/esparkcore/data/models/version.py +0 -0
  39. {espark_core-0.4.8 → espark_core-0.5.0}/esparkcore/data/repositories/base_repository.py +0 -0
  40. {espark_core-0.4.8 → espark_core-0.5.0}/esparkcore/data/repositories/device_repository.py +0 -0
  41. {espark_core-0.4.8 → espark_core-0.5.0}/esparkcore/data/repositories/outbox_repository.py +0 -0
  42. {espark_core-0.4.8 → espark_core-0.5.0}/esparkcore/data/repositories/version_repository.py +0 -0
  43. {espark_core-0.4.8 → espark_core-0.5.0}/esparkcore/notifications/__init__.py +0 -0
  44. {espark_core-0.4.8 → espark_core-0.5.0}/esparkcore/routers/base_router.py +0 -0
  45. {espark_core-0.4.8 → espark_core-0.5.0}/esparkcore/routers/version_router.py +0 -0
  46. {espark_core-0.4.8 → espark_core-0.5.0}/esparkcore/schedules/__init__.py +0 -0
  47. {espark_core-0.4.8 → espark_core-0.5.0}/esparkcore/schedules/outbox.py +0 -0
  48. {espark_core-0.4.8 → espark_core-0.5.0}/esparkcore/schedules/scheduler.py +0 -0
  49. {espark_core-0.4.8 → espark_core-0.5.0}/esparkcore/services/__init__.py +0 -0
  50. {espark_core-0.4.8 → espark_core-0.5.0}/esparkcore/utils/__init__.py +0 -0
  51. {espark_core-0.4.8 → espark_core-0.5.0}/esparkcore/utils/logging.py +0 -0
  52. {espark_core-0.4.8 → espark_core-0.5.0}/setup.cfg +0 -0
@@ -1,24 +1,24 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: espark-core
3
- Version: 0.4.8
3
+ Version: 0.5.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
7
7
  Description-Content-Type: text/markdown
8
8
  License-File: LICENSE
9
9
  Requires-Dist: aiomqtt>=2.4.0
10
- Requires-Dist: apscheduler>=3.11.1
11
- Requires-Dist: fastapi>=0.124.2
10
+ Requires-Dist: apscheduler>=3.11.2
11
+ Requires-Dist: fastapi>=0.127.0
12
12
  Requires-Dist: packaging>=25.0
13
13
  Requires-Dist: slack-sdk==3.39.0
14
14
  Requires-Dist: sqlalchemy>=2.0.45
15
- Requires-Dist: sqlmodel>=0.0.27
16
- Requires-Dist: uvicorn[standard]>=0.38.0
15
+ Requires-Dist: sqlmodel>=0.0.29
16
+ Requires-Dist: uvicorn[standard]>=0.40.0
17
17
  Provides-Extra: dev
18
- Requires-Dist: aiosqlite==0.21.0; extra == "dev"
18
+ Requires-Dist: aiosqlite==0.22.1; extra == "dev"
19
19
  Requires-Dist: autopep8==2.3.2; extra == "dev"
20
20
  Requires-Dist: build==1.3.0; extra == "dev"
21
- Requires-Dist: fastapi-cli[standard-no-fastapi-cloud-cli]==0.0.16; extra == "dev"
21
+ Requires-Dist: fastapi-cli[standard-no-fastapi-cloud-cli]==0.0.20; extra == "dev"
22
22
  Requires-Dist: httpx==0.28.1; extra == "dev"
23
23
  Requires-Dist: pycodestyle==2.14.0; extra == "dev"
24
24
  Requires-Dist: pylint==4.0.4; extra == "dev"
@@ -1,24 +1,24 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: espark-core
3
- Version: 0.4.8
3
+ Version: 0.5.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
7
7
  Description-Content-Type: text/markdown
8
8
  License-File: LICENSE
9
9
  Requires-Dist: aiomqtt>=2.4.0
10
- Requires-Dist: apscheduler>=3.11.1
11
- Requires-Dist: fastapi>=0.124.2
10
+ Requires-Dist: apscheduler>=3.11.2
11
+ Requires-Dist: fastapi>=0.127.0
12
12
  Requires-Dist: packaging>=25.0
13
13
  Requires-Dist: slack-sdk==3.39.0
14
14
  Requires-Dist: sqlalchemy>=2.0.45
15
- Requires-Dist: sqlmodel>=0.0.27
16
- Requires-Dist: uvicorn[standard]>=0.38.0
15
+ Requires-Dist: sqlmodel>=0.0.29
16
+ Requires-Dist: uvicorn[standard]>=0.40.0
17
17
  Provides-Extra: dev
18
- Requires-Dist: aiosqlite==0.21.0; extra == "dev"
18
+ Requires-Dist: aiosqlite==0.22.1; extra == "dev"
19
19
  Requires-Dist: autopep8==2.3.2; extra == "dev"
20
20
  Requires-Dist: build==1.3.0; extra == "dev"
21
- Requires-Dist: fastapi-cli[standard-no-fastapi-cloud-cli]==0.0.16; extra == "dev"
21
+ Requires-Dist: fastapi-cli[standard-no-fastapi-cloud-cli]==0.0.20; extra == "dev"
22
22
  Requires-Dist: httpx==0.28.1; extra == "dev"
23
23
  Requires-Dist: pycodestyle==2.14.0; extra == "dev"
24
24
  Requires-Dist: pylint==4.0.4; extra == "dev"
@@ -12,14 +12,18 @@ setup.py
12
12
  ./esparkcore/data/database.py
13
13
  ./esparkcore/data/models/__init__.py
14
14
  ./esparkcore/data/models/device.py
15
+ ./esparkcore/data/models/notification.py
15
16
  ./esparkcore/data/models/outbox.py
16
17
  ./esparkcore/data/models/telemetry.py
18
+ ./esparkcore/data/models/trigger.py
17
19
  ./esparkcore/data/models/version.py
18
20
  ./esparkcore/data/repositories/__init__.py
19
21
  ./esparkcore/data/repositories/base_repository.py
20
22
  ./esparkcore/data/repositories/device_repository.py
23
+ ./esparkcore/data/repositories/notification_repository.py
21
24
  ./esparkcore/data/repositories/outbox_repository.py
22
25
  ./esparkcore/data/repositories/telemetry_repository.py
26
+ ./esparkcore/data/repositories/trigger_repository.py
23
27
  ./esparkcore/data/repositories/version_repository.py
24
28
  ./esparkcore/notifications/__init__.py
25
29
  ./esparkcore/notifications/notifier.py
@@ -27,7 +31,9 @@ setup.py
27
31
  ./esparkcore/routers/__init__.py
28
32
  ./esparkcore/routers/base_router.py
29
33
  ./esparkcore/routers/device_router.py
34
+ ./esparkcore/routers/notification_router.py
30
35
  ./esparkcore/routers/telemetry_router.py
36
+ ./esparkcore/routers/trigger_router.py
31
37
  ./esparkcore/routers/version_router.py
32
38
  ./esparkcore/schedules/__init__.py
33
39
  ./esparkcore/schedules/outbox.py
@@ -47,14 +53,18 @@ esparkcore/data/__init__.py
47
53
  esparkcore/data/database.py
48
54
  esparkcore/data/models/__init__.py
49
55
  esparkcore/data/models/device.py
56
+ esparkcore/data/models/notification.py
50
57
  esparkcore/data/models/outbox.py
51
58
  esparkcore/data/models/telemetry.py
59
+ esparkcore/data/models/trigger.py
52
60
  esparkcore/data/models/version.py
53
61
  esparkcore/data/repositories/__init__.py
54
62
  esparkcore/data/repositories/base_repository.py
55
63
  esparkcore/data/repositories/device_repository.py
64
+ esparkcore/data/repositories/notification_repository.py
56
65
  esparkcore/data/repositories/outbox_repository.py
57
66
  esparkcore/data/repositories/telemetry_repository.py
67
+ esparkcore/data/repositories/trigger_repository.py
58
68
  esparkcore/data/repositories/version_repository.py
59
69
  esparkcore/notifications/__init__.py
60
70
  esparkcore/notifications/notifier.py
@@ -62,7 +72,9 @@ esparkcore/notifications/slack_notifier.py
62
72
  esparkcore/routers/__init__.py
63
73
  esparkcore/routers/base_router.py
64
74
  esparkcore/routers/device_router.py
75
+ esparkcore/routers/notification_router.py
65
76
  esparkcore/routers/telemetry_router.py
77
+ esparkcore/routers/trigger_router.py
66
78
  esparkcore/routers/version_router.py
67
79
  esparkcore/schedules/__init__.py
68
80
  esparkcore/schedules/outbox.py
@@ -1,17 +1,17 @@
1
1
  aiomqtt>=2.4.0
2
- apscheduler>=3.11.1
3
- fastapi>=0.124.2
2
+ apscheduler>=3.11.2
3
+ fastapi>=0.127.0
4
4
  packaging>=25.0
5
5
  slack-sdk==3.39.0
6
6
  sqlalchemy>=2.0.45
7
- sqlmodel>=0.0.27
8
- uvicorn[standard]>=0.38.0
7
+ sqlmodel>=0.0.29
8
+ uvicorn[standard]>=0.40.0
9
9
 
10
10
  [dev]
11
- aiosqlite==0.21.0
11
+ aiosqlite==0.22.1
12
12
  autopep8==2.3.2
13
13
  build==1.3.0
14
- fastapi-cli[standard-no-fastapi-cloud-cli]==0.0.16
14
+ fastapi-cli[standard-no-fastapi-cloud-cli]==0.0.20
15
15
  httpx==0.28.1
16
16
  pycodestyle==2.14.0
17
17
  pylint==4.0.4
@@ -1,10 +1,11 @@
1
1
  ENV_DATABASE_URL : str = 'DATABASE_URL'
2
2
  ENV_MQTT_HOST : str = 'MQTT_HOST'
3
3
  ENV_MQTT_PORT : str = 'MQTT_PORT'
4
+ ENV_UPLOAD_PATH : str = 'UPLOAD_PATH'
4
5
 
5
6
  TOPIC_ACTION : str = 'espark/action'
6
7
  TOPIC_DEVICE : str = 'espark/device'
7
8
  TOPIC_REGISTRATION : str = 'espark/registration'
8
9
  TOPIC_TELEMETRY : str = 'espark/telemetry'
9
- TOPIC_OTP : str = 'espark/otp'
10
+ TOPIC_OTA : str = 'espark/ota'
10
11
  TOPIC_CRASH : str = 'espark/crash'
@@ -1,4 +1,6 @@
1
1
  from .device import Device
2
+ from .notification import Notification
2
3
  from .outbox import OutboxEvent
3
4
  from .telemetry import Telemetry
5
+ from .trigger import Trigger
4
6
  from .version import AppVersion
@@ -8,7 +8,7 @@ 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')
11
+ app_name : Optional[str] = Field(default=None, foreign_key='appversion.id', ondelete='CASCADE', description='Name of the application running on the device')
12
12
  app_version : Optional[str] = Field(default=None, description='Version of the application running on the device')
13
13
  capabilities : Optional[str] = Field(default=None, description='Comma separated capabilities of the device')
14
14
  parameters : Dict[str, str | int | bool] = Field(default_factory=dict, sa_column=Column(JSON), description='JSON string of capability-specific parameters')
@@ -0,0 +1,11 @@
1
+ from typing import Dict
2
+
3
+ from sqlalchemy import Column
4
+ from sqlmodel import SQLModel, Field, JSON
5
+
6
+
7
+ class Notification(SQLModel, table=True):
8
+ id : int = Field(primary_key=True, description='Unique identifier for the notification')
9
+ name : str = Field(index=True, unique=True, description='Name of the notification')
10
+ provider : str = Field(index=True, description='Notification provider (e.g., Slack, Twilio)')
11
+ config : Dict[str, str] = Field(default_factory=dict, sa_column=Column(JSON), description='JSON string of provider-specific configuration parameters')
@@ -0,0 +1,13 @@
1
+ from typing import Optional
2
+
3
+ from sqlmodel import Field, SQLModel
4
+
5
+
6
+ class Trigger(SQLModel, table=True):
7
+ id : str = Field(primary_key=True, description='Unique identifier for the trigger')
8
+ name : str = Field(index=True, unique=True, description='Name of the trigger')
9
+ device_id : Optional[str] = Field(index=True, default=None, description='Identifier of the associated device')
10
+ data_type : Optional[str] = Field(index=True, default=None, description='Type of telemetry data the trigger monitors (e.g., temperature, humidity)')
11
+ condition : str = Field(description='Condition to evaluate (e.g., ">", "<=")')
12
+ value : int = Field(description='Value to compare against for the trigger condition')
13
+ notification_ids : Optional[str] = Field(default=None, description='Comma-separated list of associated notification IDs to be sent when the trigger condition is met')
@@ -1,5 +1,7 @@
1
1
  from .base_repository import AsyncRepository
2
2
  from .device_repository import DeviceRepository
3
+ from .notification_repository import NotificationRepository
3
4
  from .outbox_repository import OutboxRepository
4
5
  from .telemetry_repository import TelemetryRepository
6
+ from .trigger_repository import TriggerRepository
5
7
  from .version_repository import AppVersionRepository
@@ -0,0 +1,7 @@
1
+ from ..models import Notification
2
+ from .base_repository import AsyncRepository
3
+
4
+
5
+ class NotificationRepository(AsyncRepository[Notification]):
6
+ def __init__(self):
7
+ super().__init__(Notification)
@@ -2,7 +2,7 @@ from typing import Optional, Sequence
2
2
 
3
3
  from sqlalchemy import ColumnElement
4
4
  from sqlalchemy.ext.asyncio import AsyncSession
5
- from sqlmodel import and_
5
+ from sqlmodel import and_, select
6
6
 
7
7
  from ..models import Telemetry
8
8
  from .base_repository import AsyncRepository
@@ -23,3 +23,26 @@ class TelemetryRepository(AsyncRepository[Telemetry]):
23
23
  order_by = Telemetry.timestamp.desc()
24
24
 
25
25
  return await super().list(session, *conditions, offset=offset, order_by=order_by, limit=limit)
26
+
27
+ async def search(self, session: AsyncSession, device_id: str = None, data_type: str = None, condition: str = None, value: int = None):
28
+ query = select(self.model)
29
+
30
+ if device_id is not None:
31
+ query = query.where(Telemetry.device_id == device_id)
32
+
33
+ if data_type is not None:
34
+ query = query.where(Telemetry.data_type == data_type)
35
+
36
+ if condition is not None and value is not None:
37
+ if condition == '<':
38
+ query = query.where(Telemetry.value < value)
39
+ elif condition == '<=':
40
+ query = query.where(Telemetry.value <= value)
41
+ elif condition == '==':
42
+ query = query.where(Telemetry.value == value)
43
+ elif condition == '>=':
44
+ query = query.where(Telemetry.value >= value)
45
+ elif condition == '>':
46
+ query = query.where(Telemetry.value > value)
47
+
48
+ return (await session.execute(query)).scalars().all()
@@ -0,0 +1,7 @@
1
+ from ..models import Trigger
2
+ from .base_repository import AsyncRepository
3
+
4
+
5
+ class TriggerRepository(AsyncRepository[Trigger]):
6
+ def __init__(self):
7
+ super().__init__(Trigger)
@@ -3,7 +3,7 @@ from abc import ABC, abstractmethod
3
3
 
4
4
  class BaseNotifier(ABC):
5
5
  @abstractmethod
6
- async def notify(self, device_id: str, event_type: str, value: int, **kwargs) -> None:
6
+ async def notify(self, device_id: str, event_type: str, value: int) -> None:
7
7
  """
8
8
  Send a notification for an event.
9
9
 
@@ -0,0 +1,17 @@
1
+ from slack_sdk import WebClient
2
+
3
+ from ..utils import log_debug
4
+ from .notifier import BaseNotifier
5
+
6
+
7
+ class SlackNotifier(BaseNotifier):
8
+ def __init__(self, slack_token: str, slack_channel: str) -> None:
9
+ self.slack_token = slack_token
10
+ self.slack_channel = slack_channel
11
+
12
+ async def notify(self, device_id: str, event_type: str, value: int) -> None:
13
+ client = WebClient(token=self.slack_token)
14
+
15
+ log_debug(f'Posting {event_type} event to Slack for device {device_id}: {value}')
16
+
17
+ client.chat_postMessage(channel=self.slack_channel, text=f'Device {device_id} reported {event_type} with value {value}.')
@@ -1,3 +1,5 @@
1
1
  from .device_router import DeviceRouter
2
+ from .notification_router import NotificationRouter
2
3
  from .telemetry_router import TelemetryRouter
4
+ from .trigger_router import TriggerRouter
3
5
  from .version_router import AppVersionRouter
@@ -0,0 +1,57 @@
1
+ from json import dumps
2
+ from os import getenv
3
+
4
+ from aiomqtt import Client
5
+ from fastapi import Depends, Query, Response
6
+ from sqlalchemy.ext.asyncio import AsyncSession
7
+
8
+ from ..constants import ENV_MQTT_HOST, ENV_MQTT_PORT, TOPIC_DEVICE
9
+ from ..data.models import Device
10
+ from ..data.repositories import DeviceRepository, TelemetryRepository
11
+ from ..utils import log_debug
12
+ from .base_router import BaseRouter
13
+
14
+
15
+ class DeviceRouter(BaseRouter):
16
+ def __init__(self, repo: DeviceRepository = None) -> None:
17
+ self.repo : DeviceRepository = repo or DeviceRepository()
18
+
19
+ super().__init__(Device, self.repo, '/api/v1/devices', ['device'])
20
+
21
+ async def _publish_update(self, entity: Device) -> None:
22
+ log_debug(f'Publishing parameters update for device {entity.id}: {entity.parameters}')
23
+
24
+ async with Client(getenv(ENV_MQTT_HOST, 'localhost'), int(getenv(ENV_MQTT_PORT, '1883'))) as client:
25
+ await client.publish(f'{TOPIC_DEVICE}/{entity.id}', payload=dumps(entity.parameters) if entity.parameters else None, qos=1, retain=True)
26
+
27
+ async def _after_update(self, entity: Device, session: AsyncSession) -> None:
28
+ await super()._after_update(entity, session)
29
+
30
+ await self._publish_update(entity)
31
+
32
+ def _setup_routes(self) -> None:
33
+ @self.router.get('/all')
34
+ async def list_all(response: Response, session: AsyncSession = Depends(BaseRouter._get_session), offset: int = Query(None, ge=0), limit: int = Query(None, ge=1, le=100)):
35
+ response.headers['X-Total-Count'] = str(await self.repo.count(session))
36
+
37
+ devices = await self.repo.list(session, offset=offset, limit=limit)
38
+ devices_with_telemetry = []
39
+ telemetry_repo = TelemetryRepository()
40
+
41
+ for device in devices:
42
+ battery = await telemetry_repo.get_latest_for_device(session, device.id, 'battery')
43
+
44
+ devices_with_telemetry.append({
45
+ 'id' : device.id,
46
+ 'display_name' : device.display_name,
47
+ 'app_name' : device.app_name,
48
+ 'app_version' : device.app_version,
49
+ 'capabilities' : device.capabilities,
50
+ 'parameters' : device.parameters,
51
+ 'last_seen' : device.last_seen,
52
+ 'battery' : battery.value if battery else None,
53
+ })
54
+
55
+ return devices_with_telemetry
56
+
57
+ super()._setup_routes()
@@ -0,0 +1,10 @@
1
+ from ..data.models import Notification
2
+ from ..data.repositories import NotificationRepository
3
+ from .base_router import BaseRouter
4
+
5
+
6
+ class NotificationRouter(BaseRouter):
7
+ def __init__(self, repo: NotificationRepository = None) -> None:
8
+ self.repo : NotificationRepository = repo or NotificationRepository()
9
+
10
+ super().__init__(Notification, self.repo, '/api/v1/notifications', ['notification'])
@@ -3,6 +3,7 @@ from typing import Sequence
3
3
 
4
4
  from fastapi import Depends, Query, Response
5
5
  from sqlalchemy.ext.asyncio import AsyncSession
6
+ from sqlmodel import and_
6
7
 
7
8
  from ..data.models import Telemetry
8
9
  from ..data.repositories import DeviceRepository, TelemetryRepository
@@ -16,6 +17,15 @@ class TelemetryRouter(BaseRouter):
16
17
  super().__init__(Telemetry, self.repo, '/api/v1/telemetry', ['telemetry'])
17
18
 
18
19
  def _setup_routes(self) -> None:
20
+ @self.router.get('/history', response_model=Sequence[Telemetry])
21
+ async def list_history(response: Response, session: AsyncSession = Depends(BaseRouter._get_session), device_id: int = Query(..., ge=1), offset: int = Query(..., min=0)) -> Sequence[Telemetry]:
22
+ from_date = datetime.now(timezone.utc) - timedelta(seconds=offset)
23
+ results = await self.repo.list(session, and_(Telemetry.device_id == device_id, Telemetry.timestamp >= from_date))
24
+
25
+ response.headers['X-Total-Count'] = str(len(results))
26
+
27
+ return results
28
+
19
29
  @self.router.get('/recent', response_model=Sequence[Telemetry])
20
30
  async def list_recent(response: Response, session: AsyncSession = Depends(BaseRouter._get_session), offset: int = Query(..., min=0)) -> Sequence[Telemetry]:
21
31
  from_date = datetime.now(timezone.utc) - timedelta(seconds=offset)
@@ -0,0 +1,10 @@
1
+ from ..data.models import Trigger
2
+ from ..data.repositories import TriggerRepository
3
+ from .base_router import BaseRouter
4
+
5
+
6
+ class TriggerRouter(BaseRouter):
7
+ def __init__(self, repo: TriggerRepository = None) -> None:
8
+ self.repo : TriggerRepository = repo or TriggerRepository()
9
+
10
+ super().__init__(Trigger, self.repo, '/api/v1/triggers', ['trigger'])
@@ -7,23 +7,26 @@ from uuid import getnode
7
7
  from aiomqtt import Client, MqttError
8
8
  from packaging.version import Version
9
9
 
10
- from ..constants import ENV_MQTT_HOST, ENV_MQTT_PORT, TOPIC_CRASH, TOPIC_OTP, TOPIC_REGISTRATION, TOPIC_TELEMETRY
10
+ from ..constants import ENV_MQTT_HOST, ENV_MQTT_PORT, TOPIC_CRASH, TOPIC_OTA, TOPIC_REGISTRATION, TOPIC_TELEMETRY
11
11
  from ..data import async_session
12
12
  from ..data.models import AppVersion, Device, Telemetry
13
- from ..data.repositories import AppVersionRepository, DeviceRepository, TelemetryRepository
13
+ from ..data.repositories import AppVersionRepository, DeviceRepository, NotificationRepository, TelemetryRepository, TriggerRepository
14
+ from ..notifications import SlackNotifier
14
15
  from ..utils import log_debug, log_error
15
16
 
16
17
  MQTT_RETRY_DELAY: int = 5
17
18
 
18
19
 
19
20
  class MQTTManager:
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()
21
+ def __init__(self, version_repo: AppVersionRepository = None, device_repo: DeviceRepository = None, notification_repo: NotificationRepository = None, telemetry_repo: TelemetryRepository = None, trigger_repo: TriggerRepository = None) -> None:
22
+ self.mqtt_host : str = getenv(ENV_MQTT_HOST, 'localhost')
23
+ self.mqtt_port : int = int(getenv(ENV_MQTT_PORT, '1883'))
24
+ self.version_repo : AppVersionRepository = version_repo if version_repo else AppVersionRepository()
25
+ self.device_repo : DeviceRepository = device_repo if device_repo else DeviceRepository()
26
+ self.notification_repo : NotificationRepository = notification_repo if notification_repo else NotificationRepository()
27
+ self.telemetry_repo : TelemetryRepository = telemetry_repo if telemetry_repo else TelemetryRepository()
28
+ self.trigger_repo : TriggerRepository = trigger_repo if trigger_repo else TriggerRepository()
29
+ self.queue : Queue = Queue()
27
30
 
28
31
  async def _handle_registration(self, device_id: str, payload: dict) -> None:
29
32
  try:
@@ -61,11 +64,11 @@ class MQTTManager:
61
64
  log_debug(f'Device {device_id} is running an outdated version ({current_version} < {latest_version.version})')
62
65
 
63
66
  async with Client(self.mqtt_host, self.mqtt_port, identifier=f'espark-core-{hex(getnode())}') as client:
64
- await client.publish(f'{TOPIC_OTP}/{device_id}', dumps({
67
+ await client.publish(f'{TOPIC_OTA}/{device_id}', dumps({
65
68
  'device_id' : device_id,
66
69
  'app_name' : payload['app_name'],
67
70
  'app_version' : latest_version.version,
68
- 'download_url' : None,
71
+ 'download_url' : f'/downloads/{payload["app_name"]}/{latest_version.version}',
69
72
  }), qos=1)
70
73
  # pylint: disable=broad-exception-caught
71
74
  except Exception as e:
@@ -88,6 +91,40 @@ class MQTTManager:
88
91
  except Exception as e:
89
92
  log_error(e)
90
93
 
94
+ await self._handle_triggers(device_id, payload.get('data_type'), payload.get('value'))
95
+
96
+ async def _handle_triggers(self, device_id: str, data_type: str, value: int) -> None:
97
+ async with async_session() as session:
98
+ triggers = await self.trigger_repo.list(session)
99
+ for trigger in triggers:
100
+ matched_device_id = trigger.device_id is None or trigger.device_id == device_id
101
+ matched_data_type = trigger.data_type is None or trigger.data_type == data_type
102
+ if matched_device_id and matched_data_type:
103
+ condition_met = False
104
+ if trigger.condition is None:
105
+ condition_met = True
106
+ elif trigger.condition == '==' and value == trigger.value:
107
+ condition_met = True
108
+ elif trigger.condition == '>' and value > trigger.value:
109
+ condition_met = True
110
+ elif trigger.condition == '>=' and value >= trigger.value:
111
+ condition_met = True
112
+ elif trigger.condition == '<' and value < trigger.value:
113
+ condition_met = True
114
+ elif trigger.condition == '<=' and value <= trigger.value:
115
+ condition_met = True
116
+
117
+ if condition_met:
118
+ log_debug(f'Trigger {trigger.name} activated for device {device_id} with data type {data_type} and value {value}')
119
+
120
+ notifications = await self.notification_repo.list(session)
121
+ for notification in notifications:
122
+ if trigger.notification_ids.index(str(notification.id)) != -1:
123
+ log_debug(f'Sending notification {notification.name} for trigger {trigger.name}')
124
+
125
+ if notification.provider == 'Slack':
126
+ await SlackNotifier(notification.config['slack_token'], notification.config['slack_channel']).notify(device_id, data_type, value)
127
+
91
128
  async def _process_queue(self) -> None:
92
129
  while True:
93
130
  topic, payload = await self.queue.get()
@@ -1,7 +1,7 @@
1
- aiosqlite==0.21.0
1
+ aiosqlite==0.22.1
2
2
  autopep8==2.3.2
3
3
  build==1.3.0
4
- fastapi-cli[standard-no-fastapi-cloud-cli]==0.0.16
4
+ fastapi-cli[standard-no-fastapi-cloud-cli]==0.0.20
5
5
  httpx==0.28.1
6
6
  pycodestyle==2.14.0
7
7
  pylint==4.0.4
@@ -0,0 +1,8 @@
1
+ aiomqtt>=2.4.0
2
+ apscheduler>=3.11.2
3
+ fastapi>=0.127.0
4
+ packaging>=25.0
5
+ slack-sdk==3.39.0
6
+ sqlalchemy>=2.0.45
7
+ sqlmodel>=0.0.29
8
+ uvicorn[standard]>=0.40.0
@@ -10,7 +10,7 @@ extra_requirements = (root / 'requirements.dev.txt').read_text().splitlines
10
10
 
11
11
  setup(
12
12
  name='espark-core',
13
- version='0.4.8',
13
+ version='0.5.0',
14
14
  description='The core module of the Espark ESP32-based IoT device management framework.',
15
15
  long_description=(root / 'README.md').read_text(),
16
16
  long_description_content_type='text/markdown',
@@ -1,16 +0,0 @@
1
- from slack_sdk import WebClient
2
-
3
- from ..utils import log_debug
4
- from .notifier import BaseNotifier
5
-
6
-
7
- class SlackNotifier(BaseNotifier):
8
- async def notify(self, device_id: str, event_type: str, value: int, **kwargs) -> None:
9
- slack_token = kwargs.get('slack_token')
10
- slack_channel = kwargs.get('slack_channel')
11
-
12
- client = WebClient(token=slack_token)
13
-
14
- log_debug(f'Posting {event_type} event to Slack for device {device_id}: {value}')
15
-
16
- client.chat_postMessage(channel=slack_channel, text=f'Device {device_id} reported {event_type} with value {value}.')
@@ -1,29 +0,0 @@
1
- from json import dumps
2
- from os import getenv
3
-
4
- from aiomqtt import Client
5
- from sqlalchemy.ext.asyncio import AsyncSession
6
-
7
- from ..constants import ENV_MQTT_HOST, ENV_MQTT_PORT, TOPIC_DEVICE
8
- from ..data.models import Device
9
- from ..data.repositories import DeviceRepository
10
- from ..utils import log_debug
11
- from .base_router import BaseRouter
12
-
13
-
14
- class DeviceRouter(BaseRouter):
15
- def __init__(self, repo: DeviceRepository = None) -> None:
16
- self.repo : DeviceRepository = repo or DeviceRepository()
17
-
18
- super().__init__(Device, self.repo, '/api/v1/devices', ['device'])
19
-
20
- async def _publish_update(self, entity: Device) -> None:
21
- log_debug(f'Publishing parameters update for device {entity.id}: {entity.parameters}')
22
-
23
- async with Client(getenv(ENV_MQTT_HOST, 'localhost'), int(getenv(ENV_MQTT_PORT, '1883'))) as client:
24
- await client.publish(f'{TOPIC_DEVICE}/{entity.id}', payload=dumps(entity.parameters) if entity.parameters else None, qos=1, retain=True)
25
-
26
- async def _after_update(self, entity: Device, session: AsyncSession) -> None:
27
- await super()._after_update(entity, session)
28
-
29
- await self._publish_update(entity)
@@ -1,8 +0,0 @@
1
- aiomqtt>=2.4.0
2
- apscheduler>=3.11.1
3
- fastapi>=0.124.2
4
- packaging>=25.0
5
- slack-sdk==3.39.0
6
- sqlalchemy>=2.0.45
7
- sqlmodel>=0.0.27
8
- uvicorn[standard]>=0.38.0
File without changes
File without changes
File without changes
File without changes