espark-core 0.3.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 (37) hide show
  1. espark_core-0.3.0/LICENSE +21 -0
  2. espark_core-0.3.0/MANIFEST.in +4 -0
  3. espark_core-0.3.0/PKG-INFO +137 -0
  4. espark_core-0.3.0/README.md +102 -0
  5. espark_core-0.3.0/espark_core.egg-info/PKG-INFO +137 -0
  6. espark_core-0.3.0/espark_core.egg-info/SOURCES.txt +61 -0
  7. espark_core-0.3.0/espark_core.egg-info/dependency_links.txt +1 -0
  8. espark_core-0.3.0/espark_core.egg-info/requires.txt +19 -0
  9. espark_core-0.3.0/espark_core.egg-info/top_level.txt +1 -0
  10. espark_core-0.3.0/esparkcore/__init__.py +0 -0
  11. espark_core-0.3.0/esparkcore/constants.py +9 -0
  12. espark_core-0.3.0/esparkcore/data/__init__.py +1 -0
  13. espark_core-0.3.0/esparkcore/data/database.py +15 -0
  14. espark_core-0.3.0/esparkcore/data/models/__init__.py +3 -0
  15. espark_core-0.3.0/esparkcore/data/models/device.py +13 -0
  16. espark_core-0.3.0/esparkcore/data/models/outbox.py +20 -0
  17. espark_core-0.3.0/esparkcore/data/models/telemetry.py +12 -0
  18. espark_core-0.3.0/esparkcore/data/repositories/__init__.py +4 -0
  19. espark_core-0.3.0/esparkcore/data/repositories/base_repository.py +70 -0
  20. espark_core-0.3.0/esparkcore/data/repositories/device_repository.py +26 -0
  21. espark_core-0.3.0/esparkcore/data/repositories/outbox_repository.py +22 -0
  22. espark_core-0.3.0/esparkcore/data/repositories/telemetry_repository.py +25 -0
  23. espark_core-0.3.0/esparkcore/routers/__init__.py +2 -0
  24. espark_core-0.3.0/esparkcore/routers/base_router.py +97 -0
  25. espark_core-0.3.0/esparkcore/routers/device_router.py +29 -0
  26. espark_core-0.3.0/esparkcore/routers/telemetry_router.py +45 -0
  27. espark_core-0.3.0/esparkcore/schedules/__init__.py +2 -0
  28. espark_core-0.3.0/esparkcore/schedules/outbox.py +24 -0
  29. espark_core-0.3.0/esparkcore/schedules/scheduler.py +8 -0
  30. espark_core-0.3.0/esparkcore/services/__init__.py +1 -0
  31. espark_core-0.3.0/esparkcore/services/mqtt.py +116 -0
  32. espark_core-0.3.0/esparkcore/utils/__init__.py +1 -0
  33. espark_core-0.3.0/esparkcore/utils/logging.py +11 -0
  34. espark_core-0.3.0/requirements.dev.txt +11 -0
  35. espark_core-0.3.0/requirements.txt +6 -0
  36. espark_core-0.3.0/setup.cfg +7 -0
  37. espark_core-0.3.0/setup.py +30 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Alan Tai
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,4 @@
1
+ include requirements.txt
2
+ include requirements.dev.txt
3
+ include ../README.md
4
+ include LICENSE
@@ -0,0 +1,137 @@
1
+ Metadata-Version: 2.4
2
+ Name: espark-core
3
+ Version: 0.3.0
4
+ Summary: The core module of the Espark ESP32-based IoT device management framework.
5
+ License: MIT
6
+ Requires-Python: >=3.8
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Requires-Dist: aiomqtt==2.4.0
10
+ Requires-Dist: apscheduler==3.11.1
11
+ Requires-Dist: fastapi==0.124.0
12
+ Requires-Dist: sqlalchemy==2.0.45
13
+ Requires-Dist: sqlmodel==0.0.27
14
+ Requires-Dist: uvicorn[standard]==0.38.0
15
+ Provides-Extra: dev
16
+ Requires-Dist: aiosqlite==0.21.0; extra == "dev"
17
+ Requires-Dist: autopep8==2.3.2; extra == "dev"
18
+ Requires-Dist: build==1.3.0; extra == "dev"
19
+ Requires-Dist: fastapi-cli[standard-no-fastapi-cloud-cli]==0.0.16; extra == "dev"
20
+ Requires-Dist: httpx==0.28.1; extra == "dev"
21
+ Requires-Dist: pycodestyle==2.14.0; extra == "dev"
22
+ Requires-Dist: pylint==4.0.4; extra == "dev"
23
+ Requires-Dist: pytest==9.0.2; extra == "dev"
24
+ Requires-Dist: pytest-asyncio==1.3.0; extra == "dev"
25
+ Requires-Dist: pytest-cov==7.0.0; extra == "dev"
26
+ Requires-Dist: twine==6.2.0; extra == "dev"
27
+ Dynamic: description
28
+ Dynamic: description-content-type
29
+ Dynamic: license
30
+ Dynamic: license-file
31
+ Dynamic: provides-extra
32
+ Dynamic: requires-dist
33
+ Dynamic: requires-python
34
+ Dynamic: summary
35
+
36
+ # Espark
37
+
38
+ Espark is a lightweight framework for building scalable and efficient ESP32-based IoT applications. It provides a modular architecture, easy-to-use APIs, and built-in support for common IoT protocols.
39
+
40
+ ## Project Goals
41
+
42
+ - Simplify the development of ESP32 applications.
43
+ - Provide a modular and extensible architecture.
44
+ - Support common IoT protocols like MQTT.
45
+ - Ensure efficient resource management for low-power devices.
46
+ - Provide a clean and easy-to-use API.
47
+ - Provide an user-friendly UI for configuration and monitoring.
48
+
49
+ ## Features
50
+
51
+ - **Device Provisioning**: Easy setup and configuration of ESP32 devices.
52
+ - **Telemetry Collection**: Built-in support for collecting and sending telemetry data.
53
+ - **Scalable Architecture**: Designed to handle a large number of devices efficiently.
54
+ - **Seamless Communication**: Support for MQTT protocol.
55
+
56
+ ## Hardware Requirements
57
+
58
+ - ESP32 Development Board
59
+ - USB Cable for programming and power
60
+ - Optional: Sensors and triggers for specific applications
61
+
62
+ ## Project Structure
63
+
64
+ ```
65
+ espark/
66
+ ├── espark-core/
67
+ │ ├── esparkcore/ # FastAPI backend framework
68
+ │ │ ├── data/ # Models, repositories
69
+ │ │ ├── routers/ # API endpoints
70
+ │ │ ├── schedules/ # Background tasks
71
+ │ │ ├── services/ # Business logic, MQTT handling
72
+ │ │ └── utils/ # Utility functions
73
+ │ └── Makefile
74
+ ├── espark-node/
75
+ │ ├── esparknode/ # MicroPython application framework
76
+ │ │ ├── actions/ # Action handlers
77
+ │ │ ├── data/ # Data storage
78
+ │ │ ├── libraries/ # External libraries
79
+ │ │ ├── networks/ # Network management
80
+ │ │ ├── sensors/ # Sensor interfaces
81
+ │ │ ├── triggers/ # Trigger interfaces
82
+ │ │ ├── utils/ # Utility functions
83
+ │ │ └── base_node.py # Main application file
84
+ │ └── Makefile
85
+ └── espark-react/ # React frontend application
86
+ ├── src/
87
+ │ ├── data/ # Data models and data providers
88
+ │ ├── i18n/ # Internationalization files
89
+ │ ├── pages/ # Application pages
90
+ │ ├── routes/ # Application routing
91
+ │ ├── utils/ # Utility functions
92
+ │ ├── App.tsx # Main application component
93
+ │ └── index.tsx # Application entry point
94
+ └── package.json
95
+ ```
96
+
97
+ ## Development Workflows
98
+
99
+ ### Setting up the backend
100
+
101
+ 1. Add espark-core as a dependency in your FastAPI project.
102
+ 2. Configure database connections and MQTT settings as environment variables.
103
+ 3. Implement additional data models, repositories, routers, and business logic if needed.
104
+ 4. Add the `DeviceRouter`, `TelemetryRouter`, and other additional routers to your FastAPI app.
105
+
106
+ ### Setting up the ESP32 application
107
+
108
+ 1. Clone the espark-node repository to your local machine.
109
+ 2. Copy `espark-core/Makefile.template` to `Makefile` and customize it for your device.
110
+ 3. Run `make upgrade` to copy the espark-core library to your device project.
111
+ 4. Implement device-specific actions, sensors, and triggers as needed.
112
+ 5. Run `make flash` to upload the firmware to your ESP32 device.
113
+ 6. Run `make deploy` to upload the application to the device.
114
+
115
+ ### Setting up the frontend
116
+
117
+ 1. Add espark-react as a dependency in your React project.
118
+ 2. Render `<EsparkApp />` in your main application file.
119
+
120
+ ### Configurations
121
+
122
+ - **espark-core**: Use environment variables, or `.env` file, for database and MQTT configurations.
123
+ - **espark-node**: Use `esparknode.configs` for device-specific configurations.
124
+ - **espark-react**: Customise `EsparkApp` props for application settings.
125
+
126
+ ## Examples and Patterns
127
+
128
+ - **Router Example**: `device_router.py` in `espark-core/esparkcore/routers/` demonstrates how to create API endpoints for device management.
129
+ - **Respository Example**: `device_repository.py` in `espark-core/esparkcore/data/repositories/` shows how to implement data access logic for devices.
130
+ - **Action Example**: `esp32_relay.py` in `espark-node/esparknode/actions/` illustrates how to define actions for ESP32 devices.
131
+ - **Sensor Example**: `sht20_sensor.py` in `espark-node/esparknode/sensors/` demonstrates how to read data from a SHT20 sensor.
132
+ - **Trigger Example**: `gpio_trigger.py` in `espark-node/esparknode/triggers/` shows how to create GPIO-based triggers for device actions.
133
+ - **List, Show, Edit Screens Example**: `DeviceList`, `DeviceShow`, and `DeviceEdit` components in `espark-react/src/pages/devices/` demonstrate how to create CRUD screens for device management.
134
+
135
+ ## Example Projects
136
+
137
+ - **Espartan**: A smart thermostat and open-door alert automation system using ESP32-C3 devices, leveraging espark for device management and telemetry, available at [https://github.com/ayltai/Espartan](https://github.com/ayltai/Espartan).
@@ -0,0 +1,102 @@
1
+ # Espark
2
+
3
+ Espark is a lightweight framework for building scalable and efficient ESP32-based IoT applications. It provides a modular architecture, easy-to-use APIs, and built-in support for common IoT protocols.
4
+
5
+ ## Project Goals
6
+
7
+ - Simplify the development of ESP32 applications.
8
+ - Provide a modular and extensible architecture.
9
+ - Support common IoT protocols like MQTT.
10
+ - Ensure efficient resource management for low-power devices.
11
+ - Provide a clean and easy-to-use API.
12
+ - Provide an user-friendly UI for configuration and monitoring.
13
+
14
+ ## Features
15
+
16
+ - **Device Provisioning**: Easy setup and configuration of ESP32 devices.
17
+ - **Telemetry Collection**: Built-in support for collecting and sending telemetry data.
18
+ - **Scalable Architecture**: Designed to handle a large number of devices efficiently.
19
+ - **Seamless Communication**: Support for MQTT protocol.
20
+
21
+ ## Hardware Requirements
22
+
23
+ - ESP32 Development Board
24
+ - USB Cable for programming and power
25
+ - Optional: Sensors and triggers for specific applications
26
+
27
+ ## Project Structure
28
+
29
+ ```
30
+ espark/
31
+ ├── espark-core/
32
+ │ ├── esparkcore/ # FastAPI backend framework
33
+ │ │ ├── data/ # Models, repositories
34
+ │ │ ├── routers/ # API endpoints
35
+ │ │ ├── schedules/ # Background tasks
36
+ │ │ ├── services/ # Business logic, MQTT handling
37
+ │ │ └── utils/ # Utility functions
38
+ │ └── Makefile
39
+ ├── espark-node/
40
+ │ ├── esparknode/ # MicroPython application framework
41
+ │ │ ├── actions/ # Action handlers
42
+ │ │ ├── data/ # Data storage
43
+ │ │ ├── libraries/ # External libraries
44
+ │ │ ├── networks/ # Network management
45
+ │ │ ├── sensors/ # Sensor interfaces
46
+ │ │ ├── triggers/ # Trigger interfaces
47
+ │ │ ├── utils/ # Utility functions
48
+ │ │ └── base_node.py # Main application file
49
+ │ └── Makefile
50
+ └── espark-react/ # React frontend application
51
+ ├── src/
52
+ │ ├── data/ # Data models and data providers
53
+ │ ├── i18n/ # Internationalization files
54
+ │ ├── pages/ # Application pages
55
+ │ ├── routes/ # Application routing
56
+ │ ├── utils/ # Utility functions
57
+ │ ├── App.tsx # Main application component
58
+ │ └── index.tsx # Application entry point
59
+ └── package.json
60
+ ```
61
+
62
+ ## Development Workflows
63
+
64
+ ### Setting up the backend
65
+
66
+ 1. Add espark-core as a dependency in your FastAPI project.
67
+ 2. Configure database connections and MQTT settings as environment variables.
68
+ 3. Implement additional data models, repositories, routers, and business logic if needed.
69
+ 4. Add the `DeviceRouter`, `TelemetryRouter`, and other additional routers to your FastAPI app.
70
+
71
+ ### Setting up the ESP32 application
72
+
73
+ 1. Clone the espark-node repository to your local machine.
74
+ 2. Copy `espark-core/Makefile.template` to `Makefile` and customize it for your device.
75
+ 3. Run `make upgrade` to copy the espark-core library to your device project.
76
+ 4. Implement device-specific actions, sensors, and triggers as needed.
77
+ 5. Run `make flash` to upload the firmware to your ESP32 device.
78
+ 6. Run `make deploy` to upload the application to the device.
79
+
80
+ ### Setting up the frontend
81
+
82
+ 1. Add espark-react as a dependency in your React project.
83
+ 2. Render `<EsparkApp />` in your main application file.
84
+
85
+ ### Configurations
86
+
87
+ - **espark-core**: Use environment variables, or `.env` file, for database and MQTT configurations.
88
+ - **espark-node**: Use `esparknode.configs` for device-specific configurations.
89
+ - **espark-react**: Customise `EsparkApp` props for application settings.
90
+
91
+ ## Examples and Patterns
92
+
93
+ - **Router Example**: `device_router.py` in `espark-core/esparkcore/routers/` demonstrates how to create API endpoints for device management.
94
+ - **Respository Example**: `device_repository.py` in `espark-core/esparkcore/data/repositories/` shows how to implement data access logic for devices.
95
+ - **Action Example**: `esp32_relay.py` in `espark-node/esparknode/actions/` illustrates how to define actions for ESP32 devices.
96
+ - **Sensor Example**: `sht20_sensor.py` in `espark-node/esparknode/sensors/` demonstrates how to read data from a SHT20 sensor.
97
+ - **Trigger Example**: `gpio_trigger.py` in `espark-node/esparknode/triggers/` shows how to create GPIO-based triggers for device actions.
98
+ - **List, Show, Edit Screens Example**: `DeviceList`, `DeviceShow`, and `DeviceEdit` components in `espark-react/src/pages/devices/` demonstrate how to create CRUD screens for device management.
99
+
100
+ ## Example Projects
101
+
102
+ - **Espartan**: A smart thermostat and open-door alert automation system using ESP32-C3 devices, leveraging espark for device management and telemetry, available at [https://github.com/ayltai/Espartan](https://github.com/ayltai/Espartan).
@@ -0,0 +1,137 @@
1
+ Metadata-Version: 2.4
2
+ Name: espark-core
3
+ Version: 0.3.0
4
+ Summary: The core module of the Espark ESP32-based IoT device management framework.
5
+ License: MIT
6
+ Requires-Python: >=3.8
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Requires-Dist: aiomqtt==2.4.0
10
+ Requires-Dist: apscheduler==3.11.1
11
+ Requires-Dist: fastapi==0.124.0
12
+ Requires-Dist: sqlalchemy==2.0.45
13
+ Requires-Dist: sqlmodel==0.0.27
14
+ Requires-Dist: uvicorn[standard]==0.38.0
15
+ Provides-Extra: dev
16
+ Requires-Dist: aiosqlite==0.21.0; extra == "dev"
17
+ Requires-Dist: autopep8==2.3.2; extra == "dev"
18
+ Requires-Dist: build==1.3.0; extra == "dev"
19
+ Requires-Dist: fastapi-cli[standard-no-fastapi-cloud-cli]==0.0.16; extra == "dev"
20
+ Requires-Dist: httpx==0.28.1; extra == "dev"
21
+ Requires-Dist: pycodestyle==2.14.0; extra == "dev"
22
+ Requires-Dist: pylint==4.0.4; extra == "dev"
23
+ Requires-Dist: pytest==9.0.2; extra == "dev"
24
+ Requires-Dist: pytest-asyncio==1.3.0; extra == "dev"
25
+ Requires-Dist: pytest-cov==7.0.0; extra == "dev"
26
+ Requires-Dist: twine==6.2.0; extra == "dev"
27
+ Dynamic: description
28
+ Dynamic: description-content-type
29
+ Dynamic: license
30
+ Dynamic: license-file
31
+ Dynamic: provides-extra
32
+ Dynamic: requires-dist
33
+ Dynamic: requires-python
34
+ Dynamic: summary
35
+
36
+ # Espark
37
+
38
+ Espark is a lightweight framework for building scalable and efficient ESP32-based IoT applications. It provides a modular architecture, easy-to-use APIs, and built-in support for common IoT protocols.
39
+
40
+ ## Project Goals
41
+
42
+ - Simplify the development of ESP32 applications.
43
+ - Provide a modular and extensible architecture.
44
+ - Support common IoT protocols like MQTT.
45
+ - Ensure efficient resource management for low-power devices.
46
+ - Provide a clean and easy-to-use API.
47
+ - Provide an user-friendly UI for configuration and monitoring.
48
+
49
+ ## Features
50
+
51
+ - **Device Provisioning**: Easy setup and configuration of ESP32 devices.
52
+ - **Telemetry Collection**: Built-in support for collecting and sending telemetry data.
53
+ - **Scalable Architecture**: Designed to handle a large number of devices efficiently.
54
+ - **Seamless Communication**: Support for MQTT protocol.
55
+
56
+ ## Hardware Requirements
57
+
58
+ - ESP32 Development Board
59
+ - USB Cable for programming and power
60
+ - Optional: Sensors and triggers for specific applications
61
+
62
+ ## Project Structure
63
+
64
+ ```
65
+ espark/
66
+ ├── espark-core/
67
+ │ ├── esparkcore/ # FastAPI backend framework
68
+ │ │ ├── data/ # Models, repositories
69
+ │ │ ├── routers/ # API endpoints
70
+ │ │ ├── schedules/ # Background tasks
71
+ │ │ ├── services/ # Business logic, MQTT handling
72
+ │ │ └── utils/ # Utility functions
73
+ │ └── Makefile
74
+ ├── espark-node/
75
+ │ ├── esparknode/ # MicroPython application framework
76
+ │ │ ├── actions/ # Action handlers
77
+ │ │ ├── data/ # Data storage
78
+ │ │ ├── libraries/ # External libraries
79
+ │ │ ├── networks/ # Network management
80
+ │ │ ├── sensors/ # Sensor interfaces
81
+ │ │ ├── triggers/ # Trigger interfaces
82
+ │ │ ├── utils/ # Utility functions
83
+ │ │ └── base_node.py # Main application file
84
+ │ └── Makefile
85
+ └── espark-react/ # React frontend application
86
+ ├── src/
87
+ │ ├── data/ # Data models and data providers
88
+ │ ├── i18n/ # Internationalization files
89
+ │ ├── pages/ # Application pages
90
+ │ ├── routes/ # Application routing
91
+ │ ├── utils/ # Utility functions
92
+ │ ├── App.tsx # Main application component
93
+ │ └── index.tsx # Application entry point
94
+ └── package.json
95
+ ```
96
+
97
+ ## Development Workflows
98
+
99
+ ### Setting up the backend
100
+
101
+ 1. Add espark-core as a dependency in your FastAPI project.
102
+ 2. Configure database connections and MQTT settings as environment variables.
103
+ 3. Implement additional data models, repositories, routers, and business logic if needed.
104
+ 4. Add the `DeviceRouter`, `TelemetryRouter`, and other additional routers to your FastAPI app.
105
+
106
+ ### Setting up the ESP32 application
107
+
108
+ 1. Clone the espark-node repository to your local machine.
109
+ 2. Copy `espark-core/Makefile.template` to `Makefile` and customize it for your device.
110
+ 3. Run `make upgrade` to copy the espark-core library to your device project.
111
+ 4. Implement device-specific actions, sensors, and triggers as needed.
112
+ 5. Run `make flash` to upload the firmware to your ESP32 device.
113
+ 6. Run `make deploy` to upload the application to the device.
114
+
115
+ ### Setting up the frontend
116
+
117
+ 1. Add espark-react as a dependency in your React project.
118
+ 2. Render `<EsparkApp />` in your main application file.
119
+
120
+ ### Configurations
121
+
122
+ - **espark-core**: Use environment variables, or `.env` file, for database and MQTT configurations.
123
+ - **espark-node**: Use `esparknode.configs` for device-specific configurations.
124
+ - **espark-react**: Customise `EsparkApp` props for application settings.
125
+
126
+ ## Examples and Patterns
127
+
128
+ - **Router Example**: `device_router.py` in `espark-core/esparkcore/routers/` demonstrates how to create API endpoints for device management.
129
+ - **Respository Example**: `device_repository.py` in `espark-core/esparkcore/data/repositories/` shows how to implement data access logic for devices.
130
+ - **Action Example**: `esp32_relay.py` in `espark-node/esparknode/actions/` illustrates how to define actions for ESP32 devices.
131
+ - **Sensor Example**: `sht20_sensor.py` in `espark-node/esparknode/sensors/` demonstrates how to read data from a SHT20 sensor.
132
+ - **Trigger Example**: `gpio_trigger.py` in `espark-node/esparknode/triggers/` shows how to create GPIO-based triggers for device actions.
133
+ - **List, Show, Edit Screens Example**: `DeviceList`, `DeviceShow`, and `DeviceEdit` components in `espark-react/src/pages/devices/` demonstrate how to create CRUD screens for device management.
134
+
135
+ ## Example Projects
136
+
137
+ - **Espartan**: A smart thermostat and open-door alert automation system using ESP32-C3 devices, leveraging espark for device management and telemetry, available at [https://github.com/ayltai/Espartan](https://github.com/ayltai/Espartan).
@@ -0,0 +1,61 @@
1
+ LICENSE
2
+ MANIFEST.in
3
+ README.md
4
+ requirements.dev.txt
5
+ requirements.txt
6
+ setup.cfg
7
+ setup.py
8
+ ../README.md
9
+ ./esparkcore/__init__.py
10
+ ./esparkcore/constants.py
11
+ ./esparkcore/data/__init__.py
12
+ ./esparkcore/data/database.py
13
+ ./esparkcore/data/models/__init__.py
14
+ ./esparkcore/data/models/device.py
15
+ ./esparkcore/data/models/outbox.py
16
+ ./esparkcore/data/models/telemetry.py
17
+ ./esparkcore/data/repositories/__init__.py
18
+ ./esparkcore/data/repositories/base_repository.py
19
+ ./esparkcore/data/repositories/device_repository.py
20
+ ./esparkcore/data/repositories/outbox_repository.py
21
+ ./esparkcore/data/repositories/telemetry_repository.py
22
+ ./esparkcore/routers/__init__.py
23
+ ./esparkcore/routers/base_router.py
24
+ ./esparkcore/routers/device_router.py
25
+ ./esparkcore/routers/telemetry_router.py
26
+ ./esparkcore/schedules/__init__.py
27
+ ./esparkcore/schedules/outbox.py
28
+ ./esparkcore/schedules/scheduler.py
29
+ ./esparkcore/services/__init__.py
30
+ ./esparkcore/services/mqtt.py
31
+ ./esparkcore/utils/__init__.py
32
+ ./esparkcore/utils/logging.py
33
+ espark_core.egg-info/PKG-INFO
34
+ espark_core.egg-info/SOURCES.txt
35
+ espark_core.egg-info/dependency_links.txt
36
+ espark_core.egg-info/requires.txt
37
+ espark_core.egg-info/top_level.txt
38
+ esparkcore/__init__.py
39
+ esparkcore/constants.py
40
+ esparkcore/data/__init__.py
41
+ esparkcore/data/database.py
42
+ esparkcore/data/models/__init__.py
43
+ esparkcore/data/models/device.py
44
+ esparkcore/data/models/outbox.py
45
+ esparkcore/data/models/telemetry.py
46
+ esparkcore/data/repositories/__init__.py
47
+ esparkcore/data/repositories/base_repository.py
48
+ esparkcore/data/repositories/device_repository.py
49
+ esparkcore/data/repositories/outbox_repository.py
50
+ esparkcore/data/repositories/telemetry_repository.py
51
+ esparkcore/routers/__init__.py
52
+ esparkcore/routers/base_router.py
53
+ esparkcore/routers/device_router.py
54
+ esparkcore/routers/telemetry_router.py
55
+ esparkcore/schedules/__init__.py
56
+ esparkcore/schedules/outbox.py
57
+ esparkcore/schedules/scheduler.py
58
+ esparkcore/services/__init__.py
59
+ esparkcore/services/mqtt.py
60
+ esparkcore/utils/__init__.py
61
+ esparkcore/utils/logging.py
@@ -0,0 +1,19 @@
1
+ aiomqtt==2.4.0
2
+ apscheduler==3.11.1
3
+ fastapi==0.124.0
4
+ sqlalchemy==2.0.45
5
+ sqlmodel==0.0.27
6
+ uvicorn[standard]==0.38.0
7
+
8
+ [dev]
9
+ aiosqlite==0.21.0
10
+ autopep8==2.3.2
11
+ build==1.3.0
12
+ fastapi-cli[standard-no-fastapi-cloud-cli]==0.0.16
13
+ httpx==0.28.1
14
+ pycodestyle==2.14.0
15
+ pylint==4.0.4
16
+ pytest==9.0.2
17
+ pytest-asyncio==1.3.0
18
+ pytest-cov==7.0.0
19
+ twine==6.2.0
@@ -0,0 +1 @@
1
+ esparkcore
File without changes
@@ -0,0 +1,9 @@
1
+ ENV_DATABASE_URL : str = 'DATABASE_URL'
2
+ ENV_MQTT_HOST : str = 'MQTT_HOST'
3
+ ENV_MQTT_PORT : str = 'MQTT_PORT'
4
+
5
+ TOPIC_DEVICE : str = 'espark/device'
6
+ TOPIC_REGISTRATION : str = 'espark/registration'
7
+ TOPIC_TELEMETRY : str = 'espark/telemetry'
8
+ TOPIC_ACTION : str = 'espark/action'
9
+ TOPIC_CRASH : str = 'espark/crash'
@@ -0,0 +1 @@
1
+ from .database import async_session, init_db
@@ -0,0 +1,15 @@
1
+ from os import getenv
2
+
3
+ from sqlalchemy.ext.asyncio import async_sessionmaker, AsyncSession, create_async_engine
4
+ from sqlmodel import SQLModel
5
+
6
+ from ..constants import ENV_DATABASE_URL
7
+
8
+ # pylint: disable=invalid-name
9
+ engine = create_async_engine(getenv(ENV_DATABASE_URL, 'sqlite+aiosqlite:///database.db'), echo=True)
10
+ async_session = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
11
+
12
+
13
+ async def init_db():
14
+ async with engine.begin() as conn:
15
+ await conn.run_sync(SQLModel.metadata.create_all)
@@ -0,0 +1,3 @@
1
+ from .device import Device
2
+ from .outbox import OutboxEvent
3
+ from .telemetry import Telemetry
@@ -0,0 +1,13 @@
1
+ from datetime import datetime
2
+ from typing import Dict, Optional
3
+
4
+ from sqlalchemy import Column
5
+ from sqlmodel import SQLModel, Field, JSON
6
+
7
+
8
+ class Device(SQLModel, table=True):
9
+ id : str = Field(primary_key=True, description='Unique identifier for the device')
10
+ display_name : Optional[str] = Field(default=None, description='Human-readable name of the device')
11
+ capabilities : Optional[str] = Field(default=None, description='Comma separated capabilities of the device')
12
+ parameters : Dict[str, str | int | bool] = Field(default_factory=dict, sa_column=Column(JSON), description='JSON string of capability-specific parameters')
13
+ last_seen : datetime = Field(index=True, description='Last time the device was seen online')
@@ -0,0 +1,20 @@
1
+ from datetime import datetime
2
+ from typing import Dict, Optional
3
+ from uuid import UUID, uuid4
4
+
5
+ from sqlalchemy import Column
6
+ from sqlmodel import SQLModel, Field, JSON, UniqueConstraint
7
+
8
+
9
+ class OutboxEvent(SQLModel, table=True):
10
+ __table_args__ = (
11
+ UniqueConstraint('device_id', 'event_type', 'is_processed', name='uq_device_event'),
12
+ )
13
+
14
+ id : UUID = Field(default_factory=uuid4, primary_key=True, description='Unique identifier for the outbox event')
15
+ device_id : str = Field(foreign_key='device.id', ondelete='CASCADE', index=True, description='Identifier of the device associated with the event')
16
+ event_type : str = Field(index=True, description='Type of the event')
17
+ payload : Dict[str, str] = Field(default_factory=dict, sa_column=Column(JSON), description='JSON string of event-specific payload data')
18
+ created_at : datetime = Field(default_factory=datetime.now, index=True, description='Timestamp when the event was created')
19
+ processed_at : Optional[datetime] = Field(default=None, index=True, description='Timestamp when the event was processed')
20
+ is_processed : bool = Field(default=False, index=True, description='Flag indicating whether the event has been processed')
@@ -0,0 +1,12 @@
1
+ from datetime import datetime
2
+ from typing import Optional
3
+
4
+ from sqlmodel import SQLModel, Field
5
+
6
+
7
+ class Telemetry(SQLModel, table=True):
8
+ id : Optional[int] = Field(primary_key=True, default=None)
9
+ device_id : str = Field(foreign_key='device.id', ondelete='CASCADE', description='Device that sent this data')
10
+ timestamp : datetime = Field(index=True, description='Timestamp of the data')
11
+ data_type : str = Field(index=True, description='Type of the data (e.g., motion, temperature)')
12
+ value : int = Field(description='Value of the data (e.g., temperature, human presence detected or not)')
@@ -0,0 +1,4 @@
1
+ from .base_repository import AsyncRepository
2
+ from .device_repository import DeviceRepository
3
+ from .outbox_repository import OutboxRepository
4
+ from .telemetry_repository import TelemetryRepository
@@ -0,0 +1,70 @@
1
+ from typing import Generic, Optional, Sequence, Type, TypeVar
2
+
3
+ from sqlalchemy import func, ColumnElement
4
+ from sqlalchemy.ext.asyncio import AsyncSession
5
+ from sqlmodel import select, SQLModel
6
+
7
+ T = TypeVar('T', bound=SQLModel)
8
+
9
+
10
+ class AsyncRepository(Generic[T]):
11
+ def __init__(self, model: Type[T]):
12
+ self.model = model
13
+
14
+ async def count(self, session: AsyncSession, *conditions: ColumnElement) -> int:
15
+ # pylint: disable=not-callable
16
+ query = select(func.count()).select_from(self.model)
17
+
18
+ if conditions:
19
+ query = query.where(*conditions)
20
+
21
+ result = await session.execute(query)
22
+ return result.scalar_one()
23
+
24
+ async def add(self, session: AsyncSession, entity: T) -> T:
25
+ session.add(entity)
26
+
27
+ await session.commit()
28
+ await session.refresh(entity)
29
+
30
+ return entity
31
+
32
+ async def delete(self, session: AsyncSession, entity: T) -> None:
33
+ await session.delete(entity)
34
+ await session.commit()
35
+
36
+ async def get(self, session: AsyncSession, *conditions: ColumnElement) -> Optional[T]:
37
+ query = select(self.model)
38
+
39
+ if conditions:
40
+ query = query.where(*conditions)
41
+
42
+ return (await session.execute(query)).scalars().first()
43
+
44
+ async def list(self, session: AsyncSession, *conditions: ColumnElement[bool], offset: Optional[int] = None, order_by=None, limit: Optional[int] = None) -> Sequence[T]:
45
+ query = select(self.model)
46
+
47
+ if conditions:
48
+ query = query.where(*conditions)
49
+
50
+ if order_by is not None:
51
+ query = query.order_by(order_by)
52
+
53
+ if offset is not None:
54
+ query = query.offset(offset)
55
+
56
+ if limit is not None:
57
+ query = query.limit(limit)
58
+
59
+ return (await session.execute(query)).scalars().all()
60
+
61
+ async def update(self, session: AsyncSession, entity: T, **kwargs) -> T:
62
+ for key, value in kwargs.items():
63
+ setattr(entity, key, value)
64
+
65
+ session.add(entity)
66
+
67
+ await session.commit()
68
+ await session.refresh(entity)
69
+
70
+ return entity
@@ -0,0 +1,26 @@
1
+ from datetime import datetime, timedelta, timezone
2
+ from typing import Optional, Sequence
3
+
4
+ from sqlalchemy.ext.asyncio import AsyncSession
5
+
6
+ from ..models import Device
7
+ from .base_repository import AsyncRepository
8
+
9
+ OFFLINE_THRESHOLD_MINUTES: int = 1440
10
+
11
+
12
+ class DeviceRepository(AsyncRepository[Device]):
13
+ def __init__(self):
14
+ super().__init__(Device)
15
+
16
+ async def get_by_id(self, session: AsyncSession, id: str) -> Optional[Device]:
17
+ # pylint: disable=unexpected-keyword-arg
18
+ return await self.get(session, Device.id == id)
19
+
20
+ async def list_by_capability(self, session: AsyncSession, capability: str) -> Sequence[Device]:
21
+ # pylint: disable=no-member,unexpected-keyword-arg
22
+ return await self.list(session, Device.capabilities.contains(capability))
23
+
24
+ async def list_offline(self, session: AsyncSession, offline_threshold_minutes: int = OFFLINE_THRESHOLD_MINUTES) -> Sequence[Device]:
25
+ # pylint: disable=no-member
26
+ return await self.list(session, Device.last_seen < datetime.now(timezone.utc) - timedelta(minutes=offline_threshold_minutes), order_by=Device.last_seen.asc())
@@ -0,0 +1,22 @@
1
+ from typing import Optional
2
+
3
+ from sqlmodel import and_
4
+
5
+ from ..models import OutboxEvent
6
+ from .base_repository import AsyncRepository
7
+
8
+
9
+ class OutboxRepository(AsyncRepository[OutboxEvent]):
10
+ def __init__(self):
11
+ super().__init__(OutboxEvent)
12
+
13
+ async def get_next(self, session, device_id: str, event_type: str) -> Optional[OutboxEvent]:
14
+ # pylint: disable=no-member,singleton-comparison
15
+ events = await self.list(session, and_(OutboxEvent.device_id == device_id, OutboxEvent.event_type == event_type, OutboxEvent.is_processed == False), order_by=OutboxEvent.created_at.desc())
16
+ return events[0] if events else None
17
+
18
+ async def delete_pending(self, session, device_id: str, event_type: str) -> None:
19
+ # pylint: disable=singleton-comparison
20
+ events = await self.list(session, and_(OutboxEvent.device_id == device_id, OutboxEvent.event_type == event_type, OutboxEvent.is_processed == False))
21
+ for event in events:
22
+ await self.delete(session, event)
@@ -0,0 +1,25 @@
1
+ from typing import Optional, Sequence
2
+
3
+ from sqlalchemy import ColumnElement
4
+ from sqlalchemy.ext.asyncio import AsyncSession
5
+ from sqlmodel import and_
6
+
7
+ from ..models import Telemetry
8
+ from .base_repository import AsyncRepository
9
+
10
+
11
+ class TelemetryRepository(AsyncRepository[Telemetry]):
12
+ def __init__(self):
13
+ super().__init__(Telemetry)
14
+
15
+ async def get_latest_for_device(self, session: AsyncSession, device_id: str, data_type: str) -> Optional[Telemetry]:
16
+ # pylint: disable=no-member
17
+ results = await self.list(session, and_(Telemetry.device_id == device_id, Telemetry.data_type == data_type), order_by=Telemetry.timestamp.desc(), limit=1)
18
+ return results[0] if results else None
19
+
20
+ async def list(self, session: AsyncSession, *conditions: ColumnElement[bool], offset: Optional[int] = None, order_by=None, limit: Optional[int] = None) -> Sequence[Telemetry]:
21
+ if order_by is None:
22
+ # pylint: disable=no-member
23
+ order_by = Telemetry.timestamp.desc()
24
+
25
+ return await super().list(session, *conditions, offset=offset, order_by=order_by, limit=limit)
@@ -0,0 +1,2 @@
1
+ from .device_router import DeviceRouter
2
+ from .telemetry_router import TelemetryRouter
@@ -0,0 +1,97 @@
1
+ from typing import Generic, List, Sequence, Type, TypeVar
2
+
3
+ from fastapi import APIRouter, Body, Depends, HTTPException, Query, status, Response
4
+ from sqlalchemy.ext.asyncio import AsyncSession
5
+ from sqlmodel import SQLModel
6
+
7
+ from ..data.repositories import AsyncRepository
8
+ from ..data import async_session
9
+
10
+ T = TypeVar('T', bound=SQLModel)
11
+
12
+
13
+ class BaseRouter(Generic[T]):
14
+ def __init__(self, model: Type[T], repo: AsyncRepository[T], prefix: str, tags: List[str]):
15
+ self.model = model
16
+ self.repo = repo
17
+ self.router = APIRouter(prefix=prefix, tags=tags)
18
+
19
+ self._setup_routes()
20
+
21
+ @staticmethod
22
+ async def _get_session():
23
+ async with async_session() as session:
24
+ yield session
25
+
26
+ async def _before_add(self, entity: T, session: AsyncSession) -> None:
27
+ pass
28
+
29
+ # pylint: disable=unused-argument
30
+ async def _after_add(self, entity: T, session: AsyncSession) -> None:
31
+ await session.commit()
32
+
33
+ async def _before_delete(self, entity: T, session: AsyncSession) -> None:
34
+ pass
35
+
36
+ # pylint: disable=unused-argument
37
+ async def _after_delete(self, entity: T, session: AsyncSession) -> None:
38
+ await session.commit()
39
+
40
+ # pylint: disable=unused-argument
41
+ async def _before_update(self, entity: T, data: dict, session: AsyncSession) -> None:
42
+ pass
43
+
44
+ # pylint: disable=unused-argument
45
+ async def _after_update(self, entity: T, session: AsyncSession) -> None:
46
+ await session.commit()
47
+
48
+ def _setup_routes(self) -> None:
49
+ @self.router.post('/', response_model=self.model, status_code=status.HTTP_201_CREATED)
50
+ async def add(data: dict = Body(...), session: AsyncSession = Depends(BaseRouter._get_session)) -> T:
51
+ entity = self.model(**data)
52
+
53
+ await self._before_add(entity, session)
54
+ result = await self.repo.add(session, entity)
55
+ await self._after_add(result, session)
56
+
57
+ return result
58
+
59
+ @self.router.delete('/{id}', status_code=status.HTTP_204_NO_CONTENT)
60
+ async def delete(id: str, session: AsyncSession = Depends(BaseRouter._get_session)) -> None:
61
+ entity = await self.repo.get(session, self.model.id == id)
62
+ if not entity:
63
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
64
+
65
+ await self._before_delete(entity, session)
66
+ await self.repo.delete(session, entity)
67
+ await self._after_delete(entity, session)
68
+
69
+ @self.router.get('/{id}', response_model=self.model)
70
+ async def get(id: str, session: AsyncSession = Depends(BaseRouter._get_session)) -> T:
71
+ entity = await self.repo.get(session, self.model.id == id)
72
+ if not entity:
73
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
74
+
75
+ return entity
76
+
77
+ @self.router.get('/', response_model=List[self.model])
78
+ async def list(response: Response, session: AsyncSession = Depends(BaseRouter._get_session), offset: int = Query(None, ge=0), limit: int = Query(None, ge=1, le=100)) -> Sequence[T]:
79
+ response.headers['X-Total-Count'] = str(await self.repo.count(session))
80
+
81
+ return await self.repo.list(session, offset=offset, limit=limit)
82
+
83
+ @self.router.put('/{id}', response_model=self.model)
84
+ async def update(id: str, data: dict = Body(...), session: AsyncSession = Depends(BaseRouter._get_session)) -> T:
85
+ entity = await self.repo.get(session, self.model.id == id)
86
+ if not entity:
87
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
88
+
89
+ await self._before_update(entity, data, session)
90
+
91
+ for key, value in data.items():
92
+ setattr(entity, key, value)
93
+
94
+ result = await self.repo.update(session, entity)
95
+ await self._after_update(result, session)
96
+
97
+ return result
@@ -0,0 +1,29 @@
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)
@@ -0,0 +1,45 @@
1
+ from datetime import datetime, timedelta, timezone
2
+ from typing import cast, Sequence
3
+
4
+ from fastapi import Depends, Query, Response
5
+ from sqlalchemy.ext.asyncio import AsyncSession
6
+
7
+ from ..data.models import Telemetry
8
+ from ..data.repositories import DeviceRepository, TelemetryRepository
9
+ from .base_router import BaseRouter
10
+
11
+
12
+ class TelemetryRouter(BaseRouter):
13
+ def __init__(self, repo: TelemetryRepository = None) -> None:
14
+ self.repo : TelemetryRepository = repo or TelemetryRepository()
15
+
16
+ super().__init__(Telemetry, self.repo, '/api/v1/telemetry', ['telemetry'])
17
+
18
+ def _setup_routes(self) -> None:
19
+ @self.router.get('/recent', response_model=Sequence[Telemetry])
20
+ async def list_recent(response: Response, session: AsyncSession = Depends(BaseRouter._get_session), offset: int = Query(..., min=0)) -> Sequence[Telemetry]:
21
+ from_date = datetime.now(timezone.utc) - timedelta(seconds=offset)
22
+ device_repo = DeviceRepository()
23
+ # pylint: disable=no-member
24
+ devices = await device_repo.list(session)
25
+ results = []
26
+
27
+ for device in devices:
28
+ capabilities = device.capabilities.split(',') if device.capabilities else []
29
+ for data_type in capabilities:
30
+ if not data_type.startswith('action_'):
31
+ telemetry = await cast(TelemetryRepository, self.repo).get_latest_for_device(session, device.id, data_type)
32
+ if telemetry:
33
+ if telemetry.timestamp.tzinfo is None:
34
+ telemetry.timestamp = telemetry.timestamp.replace(tzinfo=timezone.utc)
35
+
36
+ if telemetry.timestamp >= from_date:
37
+ results.append(telemetry)
38
+
39
+ response.headers['X-Total-Count'] = str(len(results))
40
+
41
+ results.sort(key=lambda result: result.timestamp, reverse=True)
42
+
43
+ return results
44
+
45
+ super()._setup_routes()
@@ -0,0 +1,2 @@
1
+ from .outbox import consume_outbox
2
+ from .scheduler import start_scheduler
@@ -0,0 +1,24 @@
1
+ from json import dumps
2
+ from os import getenv
3
+
4
+ from aiomqtt import Client
5
+
6
+ from ..constants import ENV_MQTT_HOST, ENV_MQTT_PORT, TOPIC_ACTION
7
+ from ..data import async_session
8
+ from ..data.repositories import OutboxRepository
9
+
10
+
11
+ async def consume_outbox(device_id: str, event_type: str) -> None:
12
+ outbox_repo = OutboxRepository()
13
+
14
+ async with async_session() as session:
15
+ event = await outbox_repo.get_next(session, device_id, event_type)
16
+ if not event:
17
+ return
18
+
19
+ await outbox_repo.delete_pending(session, device_id, event_type)
20
+
21
+ async with Client(getenv(ENV_MQTT_HOST, 'localhost'), int(getenv(ENV_MQTT_PORT, '1883'))) as client:
22
+ await client.publish(f'{TOPIC_ACTION}/{device_id}', dumps(event.payload), qos=1, retain=True)
23
+
24
+ await session.commit()
@@ -0,0 +1,8 @@
1
+ from apscheduler.schedulers.asyncio import AsyncIOScheduler
2
+
3
+
4
+ async def start_scheduler():
5
+ scheduler = AsyncIOScheduler()
6
+ scheduler.start()
7
+
8
+ return scheduler
@@ -0,0 +1 @@
1
+ from .mqtt import MQTTManager
@@ -0,0 +1,116 @@
1
+ from asyncio import create_task, gather, Queue, sleep
2
+ from datetime import datetime, timezone
3
+ from json import JSONDecodeError, loads
4
+ from os import getenv
5
+ from uuid import getnode
6
+
7
+ from aiomqtt import Client, MqttError
8
+
9
+ from ..constants import ENV_MQTT_HOST, ENV_MQTT_PORT, TOPIC_CRASH, TOPIC_REGISTRATION, TOPIC_TELEMETRY
10
+ from ..data import async_session
11
+ from ..data.models import Device, Telemetry
12
+ from ..data.repositories import DeviceRepository, TelemetryRepository
13
+ from ..utils import log_debug, log_error
14
+
15
+ MQTT_RETRY_DELAY: int = 5
16
+
17
+
18
+ class MQTTManager:
19
+ def __init__(self, device_repo: DeviceRepository = None, telemetry_repo: TelemetryRepository = None):
20
+ self.mqtt_host : str = getenv(ENV_MQTT_HOST, 'localhost')
21
+ self.mqtt_port : int = int(getenv(ENV_MQTT_PORT, '1883'))
22
+ self.device_repo : DeviceRepository = device_repo
23
+ self.telemetry_repo : TelemetryRepository = telemetry_repo
24
+ self.queue : Queue = Queue()
25
+
26
+ async def _handle_registration(self, device_id: str, payload: dict) -> None:
27
+ try:
28
+ log_debug(f'Registering device: {device_id}')
29
+
30
+ async with async_session() as session:
31
+ device = await self.device_repo.get(session, Device.id == device_id)
32
+ if device:
33
+ device.capabilities = payload['capabilities']
34
+ device.last_seen = datetime.now(timezone.utc)
35
+
36
+ await self.device_repo.update(session, device, last_seen=datetime.now(timezone.utc))
37
+ else:
38
+ device = Device()
39
+
40
+ device.id = device_id
41
+ device.display_name = None
42
+ device.capabilities = payload['capabilities']
43
+ device.last_seen = datetime.now(timezone.utc)
44
+
45
+ await self.device_repo.add(session, device)
46
+ # pylint: disable=broad-exception-caught
47
+ except Exception as e:
48
+ log_error(e)
49
+
50
+ async def _handle_telemetry(self, device_id: str, payload: dict) -> None:
51
+ try:
52
+ log_debug(f'Receiving telemetry from device: {device_id} - Payload: {payload}')
53
+
54
+ async with async_session() as session:
55
+ telemetry = Telemetry()
56
+
57
+ telemetry.device_id = device_id
58
+ telemetry.timestamp = datetime.now(timezone.utc)
59
+ telemetry.data_type = payload.get('data_type')
60
+ telemetry.value = payload.get('value')
61
+
62
+ await self.telemetry_repo.add(session, telemetry)
63
+ # pylint: disable=broad-exception-caught
64
+ except Exception as e:
65
+ log_error(e)
66
+
67
+ async def _process_queue(self) -> None:
68
+ while True:
69
+ topic, payload = await self.queue.get()
70
+
71
+ topic_parts: list[str] = str(topic).split('/')
72
+ if len(topic_parts) != 3:
73
+ log_error(Exception('Invalid topic format, skipping message'))
74
+ continue
75
+
76
+ app_type, message_type, device_id = topic_parts
77
+ if app_type != 'espark':
78
+ log_debug('Invalid app type, skipping message')
79
+ continue
80
+
81
+ try:
82
+ payload = loads(payload.decode())
83
+ except JSONDecodeError as e:
84
+ log_error(e)
85
+ continue
86
+
87
+ if message_type == TOPIC_REGISTRATION.split('/')[1]:
88
+ await self._handle_registration(device_id, payload)
89
+ elif message_type == TOPIC_TELEMETRY.split('/')[1]:
90
+ await self._handle_telemetry(device_id, payload)
91
+ elif message_type == TOPIC_CRASH.split('/')[1]:
92
+ log_debug(f'Received crash report from device {device_id}: {payload}')
93
+ else:
94
+ log_debug(f'Unknown message type "{message_type}", skipping message')
95
+
96
+ self.queue.task_done()
97
+
98
+ async def _process_messages(self) -> None:
99
+ while True:
100
+ try:
101
+ async with Client(self.mqtt_host, self.mqtt_port, identifier=f'espark-core-{hex(getnode())}') as client:
102
+ await client.subscribe(f'{TOPIC_REGISTRATION}/+')
103
+ await client.subscribe(f'{TOPIC_TELEMETRY}/+')
104
+ await client.subscribe(f'{TOPIC_CRASH}/+')
105
+
106
+ async for message in client.messages:
107
+ log_debug(f'Received MQTT message on topic {message.topic}')
108
+
109
+ self.queue.put_nowait((message.topic, message.payload))
110
+ except MqttError as e:
111
+ log_error(e)
112
+
113
+ await sleep(MQTT_RETRY_DELAY)
114
+
115
+ async def start(self) -> None:
116
+ await gather(create_task(self._process_queue()), create_task(self._process_messages()))
@@ -0,0 +1 @@
1
+ from .logging import log_debug, log_error
@@ -0,0 +1,11 @@
1
+ from datetime import datetime
2
+
3
+
4
+ def log_debug(message: str) -> None:
5
+ print(f'{datetime.now().isoformat()} [DEBUG] {message}')
6
+
7
+
8
+ def log_error(e: Exception) -> None:
9
+ print(f'{datetime.now().isoformat()} [ERROR] {type(e).__name__}: {e}')
10
+
11
+ raise e
@@ -0,0 +1,11 @@
1
+ aiosqlite==0.21.0
2
+ autopep8==2.3.2
3
+ build==1.3.0
4
+ fastapi-cli[standard-no-fastapi-cloud-cli]==0.0.16
5
+ httpx==0.28.1
6
+ pycodestyle==2.14.0
7
+ pylint==4.0.4
8
+ pytest==9.0.2
9
+ pytest-asyncio==1.3.0
10
+ pytest-cov==7.0.0
11
+ twine==6.2.0
@@ -0,0 +1,6 @@
1
+ aiomqtt==2.4.0
2
+ apscheduler==3.11.1
3
+ fastapi==0.124.0
4
+ sqlalchemy==2.0.45
5
+ sqlmodel==0.0.27
6
+ uvicorn[standard]==0.38.0
@@ -0,0 +1,7 @@
1
+ [pycodestyle]
2
+ ignore = E501,E203,E221,E712
3
+
4
+ [egg_info]
5
+ tag_build =
6
+ tag_date = 0
7
+
@@ -0,0 +1,30 @@
1
+ from pathlib import Path
2
+ from setuptools import find_packages, setup
3
+
4
+ def read_requirements(filename: str):
5
+ return [line.strip() for line in Path(filename).read_text().splitlines() if line.strip() and not line.startswith("#")]
6
+
7
+ root = Path(__file__).resolve().parent
8
+ production_requirements = (root / 'requirements.txt').read_text().splitlines()
9
+ extra_requirements = (root / 'requirements.dev.txt').read_text().splitlines()
10
+
11
+ setup(
12
+ name='espark-core',
13
+ version='0.3.0',
14
+ description='The core module of the Espark ESP32-based IoT device management framework.',
15
+ long_description=(root / 'README.md').read_text(),
16
+ long_description_content_type='text/markdown',
17
+ license='MIT',
18
+ license_files=[
19
+ 'LICENSE',
20
+ ],
21
+ python_requires='>=3.8',
22
+ packages=find_packages(where='.'),
23
+ package_dir={
24
+ '' : '.',
25
+ },
26
+ install_requires=production_requirements,
27
+ extras_require={
28
+ 'dev' : extra_requirements,
29
+ },
30
+ )