Skip to content

Commit 379785f

Browse files
feat: Implement Generic IoT Data Simulator CLI
This commit introduces a command-line tool to simulate various types of IoT sensor data (temperature, humidity, GPS, boolean status, counter) and send it to the console or an HTTP POST endpoint. Features: - Configurable device profiles (ID and sensor types) via CLI strings or JSON. - Simulates temperature, humidity, GPS (with random walk), boolean status, and an incrementing counter. - Outputs data as JSON to console or via HTTP POST. - User-defined interval between messages and total message count. - Includes `iot_simulator_requirements.txt` (for `requests` library). - Comprehensive `README_iot_simulator.md` with setup, usage, and examples.
1 parent 1333fe7 commit 379785f

File tree

9 files changed

+660
-0
lines changed

9 files changed

+660
-0
lines changed

README_iot_simulator.md

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
# Generic IoT Data Simulator
2+
3+
This Python CLI tool simulates various types of IoT sensor data and sends it to configurable endpoints (console or HTTP POST). It's designed to help developers test IoT backends, data ingestion pipelines, dashboards, and other components without needing physical hardware.
4+
5+
## Features
6+
7+
* **Multiple Sensor Types:** Simulate data for:
8+
* Temperature (°C)
9+
* Humidity (%)
10+
* GPS Location (latitude, longitude - with random walk)
11+
* Boolean Status (e.g., on/off, open/closed)
12+
* Incrementing Counter
13+
* **Configurable Device Profiles:** Define multiple simulated devices, each with its own set of sensors. Device IDs can be specified or auto-generated.
14+
* **Flexible Output:**
15+
* **Console:** Print generated data as structured JSON to the standard output.
16+
* **HTTP POST:** Send data as a JSON payload to a specified HTTP(S) endpoint.
17+
* **Customizable Simulation:**
18+
* Control the interval between messages.
19+
* Set the total number of messages to send (or run indefinitely).
20+
* **Standardized Data Format:** Output includes `deviceId`, `timestamp` (ISO 8601 UTC), and sensor readings.
21+
* **Stateful Sensors:** GPS coordinates evolve with a random walk, and counters increment per device.
22+
23+
## Prerequisites
24+
25+
* Python 3.6+
26+
* `requests` library (for HTTP output).
27+
28+
## Installation
29+
30+
1. **Clone the repository or download the files.**
31+
Ensure you have `iot_sim_main.py`, the `iot_simulator` directory (containing `generators.py`, `publishers.py`, `__init__.py`), and the `iot_simulator_requirements.txt` file.
32+
33+
2. **Create a virtual environment (recommended):**
34+
```bash
35+
python3 -m venv venv
36+
source venv/bin/activate # On Windows: venv\Scripts\activate
37+
```
38+
39+
3. **Install dependencies:**
40+
Navigate to the directory containing `iot_simulator_requirements.txt` and run:
41+
```bash
42+
pip install -r iot_simulator_requirements.txt
43+
```
44+
45+
## Usage
46+
47+
The tool is run from the command line using `python3 iot_sim_main.py`.
48+
49+
### Command-Line Arguments
50+
51+
* `-p, --profiles PROFILES [PROFILES ...]`: **(Required)**
52+
* Defines one or more device profiles.
53+
* Each profile can be a string in the format `"device_id:sensor1,sensor2,..."`.
54+
* If `device_id:` is omitted (e.g., `"sensor1,sensor2"`), a unique device ID will be auto-generated.
55+
* Alternatively, you can provide a single argument which is a JSON string representing a list of profiles: `'[{"id":"dev1","sensors":["temp","hum"]}, {"id":"dev2","sensors":["gps"]}]'`
56+
* **Supported sensor types:** `temperature`, `humidity`, `gps`, `status`, `counter`.
57+
* `-i, --interval INTERVAL`: (Optional) Interval in seconds between sending message batches (default: 5.0 seconds).
58+
* `-n, --num_messages NUM_MESSAGES`: (Optional) Number of message batches to send. A batch includes data from all defined profiles. Set to `0` for an infinite simulation (stop with Ctrl+C) (default: 10).
59+
* `-o, --output {console,http}`: (Optional) Output target. Defaults to `console`.
60+
* `--http_url HTTP_URL`: (Optional) Target URL for HTTP POST output. **Required if `--output=http` is chosen.**
61+
* `-h, --help`: Show help message and exit.
62+
63+
### JSON Payload Format
64+
65+
The data is sent/printed as a JSON object with the following structure:
66+
```json
67+
{
68+
"deviceId": "your_device_id",
69+
"timestamp": "YYYY-MM-DDTHH:MM:SS.ffffffZ", // ISO 8601 UTC
70+
// Sensor fields appear here based on profile
71+
"temperature_celsius": 23.7,
72+
"humidity_percent": 45.8,
73+
"location": {
74+
"latitude": 34.052200,
75+
"longitude": -118.243700
76+
},
77+
"active_status": true,
78+
"event_count": 123
79+
}
80+
```
81+
*Note: Not all sensor fields will be present in every message; only those configured for the specific device profile.*
82+
83+
### Examples
84+
85+
1. **Simulate one device with temperature and humidity, output to console (10 messages, 5s interval):**
86+
```bash
87+
python3 iot_sim_main.py --profiles "device001:temperature,humidity"
88+
```
89+
90+
2. **Simulate two devices, 5 messages, 2s interval, output to console:**
91+
```bash
92+
python3 iot_sim_main.py \
93+
--profiles "thermostat1:temperature" "gps_tracker:gps,counter" \
94+
--num_messages 5 \
95+
--interval 2
96+
```
97+
98+
3. **Simulate one device (auto-generated ID) with all sensor types, run indefinitely, output to console every 10s:**
99+
```bash
100+
python3 iot_sim_main.py \
101+
--profiles "temperature,humidity,gps,status,counter" \
102+
--num_messages 0 \
103+
--interval 10
104+
```
105+
106+
4. **Simulate devices defined in a JSON string, output to an HTTP endpoint:**
107+
```bash
108+
python3 iot_sim_main.py \
109+
--profiles '[{"id":"factory_sensor_A","sensors":["temperature","counter"]},{"id":"asset_tracker_B","sensors":["gps"]}]' \
110+
--output http \
111+
--http_url "http://localhost:8080/api/data" \
112+
--interval 2 \
113+
--num_messages 100
114+
```
115+
*(Ensure you have an HTTP server listening at the specified `--http_url` if testing HTTP output.)*
116+
117+
## File Structure
118+
```
119+
.
120+
├── iot_simulator/
121+
│ ├── __init__.py # Makes 'iot_simulator' a Python package
122+
│ ├── generators.py # Logic for generating sensor data
123+
│ └── publishers.py # Logic for formatting and sending data (console, HTTP)
124+
├── iot_sim_main.py # CLI entry point and main simulation loop
125+
├── iot_simulator_requirements.txt # Python dependencies (requests)
126+
└── README_iot_simulator.md # This documentation file
127+
```
128+
129+
## Sensor Details
130+
131+
* **temperature:** Random float, default range -10.0 to 40.0 °C. Output key: `temperature_celsius`.
132+
* **humidity:** Random float, default range 20.0 to 80.0 %. Output key: `humidity_percent`.
133+
* **gps:** Simulates GPS coordinates (latitude, longitude) starting from a base point and performing a small random walk with each update. Output key: `location` (an object with `latitude` and `longitude`).
134+
* **status:** Random boolean (True/False). Output key: `active_status`.
135+
* **counter:** Integer that increments by 1 for each message from that specific device. Output key: `event_count`.
136+
137+
Sensor data generation (ranges, GPS step) is defined in `iot_simulator/generators.py` and can be customized there if needed.
138+
```

iot_sim_main.py

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import argparse
2+
import time
3+
import json # For parsing device_profiles if given as JSON string
4+
import uuid
5+
from iot_simulator.generators import generate_sensor_data, generate_timestamp, DEVICE_STATES
6+
from iot_simulator.publishers import format_payload, print_to_console, send_http_post
7+
8+
def parse_device_profiles(profiles_str_list):
9+
"""
10+
Parses device profile strings.
11+
Each string can be "device_id:sensor1,sensor2" or just "sensor1,sensor2" (auto-gen ID).
12+
Or, it can be a JSON string representing a list of more complex profiles.
13+
"""
14+
profiles = []
15+
if not profiles_str_list:
16+
return profiles
17+
18+
# Check if the input is a single string that might be JSON
19+
if len(profiles_str_list) == 1 and (profiles_str_list[0].startswith('[') or profiles_str_list[0].startswith('{')):
20+
try:
21+
parsed_json = json.loads(profiles_str_list[0])
22+
if isinstance(parsed_json, list):
23+
# Expecting list of {"id": "dev1", "sensors": ["temp", "hum"]}
24+
for p_item in parsed_json:
25+
if isinstance(p_item, dict) and "id" in p_item and "sensors" in p_item:
26+
profiles.append({"id": p_item["id"], "sensors": p_item["sensors"]})
27+
else:
28+
print(f"Warning: Invalid JSON profile item format: {p_item}. Skipping.")
29+
return profiles
30+
elif isinstance(parsed_json, dict): # Single profile as JSON object
31+
if "id" in parsed_json and "sensors" in parsed_json:
32+
profiles.append({"id": parsed_json["id"], "sensors": parsed_json["sensors"]})
33+
return profiles
34+
else:
35+
print(f"Warning: Invalid JSON profile object format: {parsed_json}. Skipping.")
36+
return [] # Or handle as error
37+
except json.JSONDecodeError as e:
38+
print(f"Warning: Could not parse profile string as JSON '{profiles_str_list[0]}': {e}. Proceeding with string parsing.")
39+
# Fall through to string parsing if JSON attempt fails for a single ambiguous string
40+
41+
# String parsing for "device_id:sensor1,sensor2" or "sensor1,sensor2"
42+
for profile_str in profiles_str_list:
43+
parts = profile_str.split(':', 1)
44+
device_id = ""
45+
sensors_str = ""
46+
47+
if len(parts) == 2:
48+
device_id = parts[0].strip()
49+
sensors_str = parts[1].strip()
50+
elif len(parts) == 1:
51+
sensors_str = parts[0].strip()
52+
# No device_id provided, will auto-generate
53+
else:
54+
print(f"Warning: Invalid profile string format '{profile_str}'. Skipping.")
55+
continue
56+
57+
if not device_id:
58+
device_id = f"sim-{uuid.uuid4().hex[:8]}"
59+
print(f"Auto-generated device ID: {device_id} for sensors: {sensors_str}")
60+
61+
sensor_types = [s.strip() for s in sensors_str.split(',') if s.strip()]
62+
if not sensor_types:
63+
print(f"Warning: No sensor types specified for device ID '{device_id}' in profile '{profile_str}'. Skipping.")
64+
continue
65+
66+
profiles.append({"id": device_id, "sensors": sensor_types})
67+
68+
return profiles
69+
70+
71+
def main_loop(profiles, interval_seconds, num_messages, output_target, http_url=None):
72+
"""
73+
Main simulation loop.
74+
"""
75+
if not profiles:
76+
print("No valid device profiles configured. Exiting.")
77+
return
78+
79+
print(f"\nStarting IoT simulation...")
80+
print(f"Device Profiles: {profiles}")
81+
print(f"Interval: {interval_seconds}s")
82+
print(f"Messages per run (per profile): {num_messages if num_messages > 0 else 'Infinite'}")
83+
print(f"Output Target: {output_target}")
84+
if output_target == 'http' and http_url:
85+
print(f"HTTP Target URL: {http_url}")
86+
87+
# Reset global device states for a fresh run if needed, or manage them per profile run.
88+
# For simplicity, DEVICE_STATES in generators.py is global.
89+
# If we want truly independent runs for counters/GPS per main_loop call, clear it here.
90+
# DEVICE_STATES.clear() # Uncomment if each run of main_loop should reset all device states
91+
92+
try:
93+
msg_count = 0
94+
while True:
95+
if num_messages > 0 and msg_count >= num_messages:
96+
print(f"\nReached target message count ({num_messages}). Simulation finished for this run.")
97+
break
98+
99+
current_ts = generate_timestamp()
100+
101+
for profile in profiles:
102+
device_id = profile["id"]
103+
sensor_types = profile["sensors"]
104+
105+
# This list will hold data from multiple sensors for this device for this timestamp
106+
all_sensor_readings_for_device = []
107+
108+
for sensor_type in sensor_types:
109+
# generate_sensor_data from generators.py manages state per device_id
110+
reading = generate_sensor_data(device_id, sensor_type)
111+
if reading:
112+
all_sensor_readings_for_device.append(reading)
113+
114+
if not all_sensor_readings_for_device:
115+
print(f"Warning: No sensor data generated for device {device_id} at {current_ts}. Skipping.")
116+
continue
117+
118+
# Format the payload with all readings for this device
119+
payload = format_payload(device_id, current_ts, all_sensor_readings_for_device)
120+
121+
if output_target == 'console':
122+
print_to_console(payload)
123+
elif output_target == 'http':
124+
if http_url:
125+
send_http_post(http_url, payload)
126+
else:
127+
print("Error: HTTP output specified but no URL provided. Skipping send.")
128+
129+
msg_count += 1
130+
if num_messages == 0 or msg_count < num_messages : # Only sleep if not the last message of a finite run
131+
print(f"--- Sent message batch #{msg_count}. Waiting {interval_seconds}s... ---")
132+
time.sleep(interval_seconds)
133+
134+
except KeyboardInterrupt:
135+
print("\nSimulation stopped by user (Ctrl+C).")
136+
except Exception as e:
137+
print(f"\nAn unexpected error occurred during simulation: {e}")
138+
finally:
139+
print("IoT Simulation ended.")
140+
141+
142+
def main():
143+
parser = argparse.ArgumentParser(description="Generic IoT Data Simulator.")
144+
145+
parser.add_argument(
146+
"-p", "--profiles",
147+
nargs='+',
148+
required=True,
149+
help='Device profiles. Each profile as "device_id:sensor1,sensor2,..." or "sensor1,sensor2" (ID auto-generated). '
150+
'Alternatively, a single argument which is a JSON string: \'[{"id":"dev1","sensors":["temp","hum"]}]\' '
151+
'Supported sensors: temperature, humidity, gps, status, counter.'
152+
)
153+
parser.add_argument(
154+
"-i", "--interval",
155+
type=float,
156+
default=5.0,
157+
help="Interval in seconds between sending messages (default: 5.0s)."
158+
)
159+
parser.add_argument(
160+
"-n", "--num_messages",
161+
type=int,
162+
default=10,
163+
help="Number of messages to send per simulation run (0 for infinite) (default: 10)."
164+
)
165+
parser.add_argument(
166+
"-o", "--output",
167+
choices=['console', 'http'],
168+
default='console',
169+
help="Output target: 'console' or 'http' (default: console)."
170+
)
171+
parser.add_argument(
172+
"--http_url",
173+
help="Target URL for HTTP POST output (required if --output=http)."
174+
)
175+
176+
args = parser.parse_args()
177+
178+
if args.output == 'http' and not args.http_url:
179+
parser.error("--http_url is required when --output is 'http'.")
180+
181+
parsed_profiles = parse_device_profiles(args.profiles)
182+
if not parsed_profiles:
183+
print("Error: No valid device profiles could be parsed. Please check your --profiles argument.")
184+
print("Examples: --profiles \"myDevice:temperature,humidity\" \"another:gps,counter\"")
185+
print(" or: --profiles '[{\"id\":\"dev1\",\"sensors\":[\"temperature\",\"status\"]}, {\"id\":\"dev2\",\"sensors\":[\"gps\"]}]'")
186+
return
187+
188+
main_loop(
189+
profiles=parsed_profiles,
190+
interval_seconds=args.interval,
191+
num_messages=args.num_messages,
192+
output_target=args.output,
193+
http_url=args.http_url
194+
)
195+
196+
if __name__ == "__main__":
197+
main()

iot_simulator/__init__.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# This file makes 'iot_simulator' a Python package.
2+
3+
from .generators import (
4+
generate_temperature,
5+
generate_humidity,
6+
generate_gps_coordinates,
7+
generate_boolean_status,
8+
generate_counter_value,
9+
generate_timestamp,
10+
generate_device_id,
11+
generate_sensor_data,
12+
DEVICE_STATES # Exposing for potential external state management if ever needed, or reset
13+
)
14+
15+
from .publishers import format_payload, print_to_console, send_http_post
548 Bytes
Binary file not shown.
7.16 KB
Binary file not shown.
5.94 KB
Binary file not shown.

0 commit comments

Comments
 (0)