Skip to content

Commit

Permalink
Cliente MQTT + Simulador mejorado (#7)
Browse files Browse the repository at this point in the history
* changed protocol from AMQP to MQTT in rabbitmq client

* add unused device_id

* change int to float in temperature & light. light sent with lux unit. add id_device. add time_stamp with arg tz

* add executable to send simple package

* add main as cli to run executables more easily

* fix lint

* fix requeriment & dockerfile

* remove unused requeriment

* fix lint

* fix unique id_device in simulator

* add light with LUX unity to send !

* add executable to send deviated package

* rename command of simulator

* add README

* update .env.dist

* add -pn lower case
  • Loading branch information
fjpacheco authored Jun 21, 2024
1 parent 1da48ef commit a157659
Show file tree
Hide file tree
Showing 12 changed files with 679 additions and 144 deletions.
11 changes: 8 additions & 3 deletions .env.dist
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
RABBITMQ_HOST=
QUEUE_NAME=
LOGGING_LEVEL=
WEATHER_API_KEY=
WEATHER_API_URL=
WEATHER_API_URL=

# RabbitMQ - MQTT broker details
MQTT_HOST=
MQTT_PORT=
MQTT_USERNAME=
MQTT_PASSWORD=
MQTT_TOPIC=
9 changes: 7 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY src src
COPY ./src .

CMD ["python", "src/main.py"]
COPY .env .env

CMD ["python", "main.py", "run-simulator"]


# docker build -t simulator . && docker run simulator && docker rmi simulator
92 changes: 91 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,22 @@
# Sensor simulator
Indoor plant sensor simulator for early testing.


## Environment variables

Configure the service and the connection to the OpenWeatherMap API:
* **LOGGING_LEVEL** (Default: "INFO"): Controls the level of logging output.
* **WEATHER_API_KEY**: API key to access the OpenWeatherMap API.
* **WEATHER_API_URL**: URL to access the OpenWeatherMap API.

Configure the connection to your RabbitMQ server:
* **MQTT_HOST**
* **MQTT_PORT**
* **MQTT_USERNAME**
* **MQTT_PASSWORD**
* **MQTT_TOPIC**


## How it works

The simulator creates a data packet every fixed period. This packet contains simulated parameters data like:
Expand All @@ -21,7 +37,9 @@ This parameters are fetched from the [OpenWeatherMap API](https://openweathermap
## Commands
It would be nice to accept commands from TUI to simulate deviations fixes. Something like `<parameter> <increase/decrease> <amount>`

## Usage Instructions
## Usage Instructions to Simulator of Packages

### Docker
The repository includes a **Makefile** that encapsulates various commands used frequently in the project as targets. The targets are executed by invoking:

* **make \<target\>**:
Expand All @@ -34,3 +52,75 @@ Available targets are:
* **docker-image**: Builds the images to be used. This target is used by **docker-compose-up**, so it can be used to test new changes in the images before starting the project.

Important Note: This service assumes a running instance of RabbitMQ and connects to it. Therefore, to run this service, it is necessary to first have the **measurements** service running. Please make sure to also check [measurements repository](https://github.com/Hanagotchi/measurements).

### CLI (command-line interface) with Virtual Environment

To run the simulator using the CLI, you need to install the dependencies. Its highly recommended to use a virtual environment. You can create a virtual environment and install the dependencies by running the following commands:

```bash
cd simulator
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
```

After installing the dependencies, you can run the simulator by executing the following command:

```bash
cd src
python3 main.py simulator
```

By default, the simulator assigns a random device ID to the sensor. If you want to specify a device ID, you can pass it as an argument:

```
cd src
python3 main.py simulator --id-device <string>
```

## Usage Instructions to Send Custom Package

### Docker

Sorry, this is not available yet. . .

### CLI (command-line interface) with Virtual Environment

#### Sending a Simple Package with custom parameters

To send a custom package, you can run the following command:

```bash
cd src
python3 main.py simple-package --id-device <string> --temperature <float> --light <float> --humidity <int> --watering <int>
```

or reduce the typing by using the short version of the arguments:

```bash
cd src
python3 main.py simple-package -i <string> -t <float> -l <float> -h <int> -w <int>
```

By executing the command above, the simulator will send a package with the specified parameters.


#### Sending a deviated package

To send a deviated package, you can run the following command:

```bash
cd src
python3 main.py deviated-package --id-device <string> --plant-name <string> --deviated-temperature <higher|lower|ideal> --deviated-light <higher|lower|ideal> --deviated-humidity <higher|lower|ideal> --deviated-watering <higher|lower|ideal>
```

or reduce the typing by using the short version of the arguments:

```bash
cd src
python3 main.py deviated-package -i <string> -pn <string> -dt <higher|lower|ideal> -dl <higher|lower|ideal> -dh <higher|lower|ideal> -dw <higher|lower|ideal>
```

By executing the command above, the simulator will send a package with the specified deviations.

The parameters `deviated-temperature`, `deviated-light`, `deviated-humidity`, and `deviated-watering` can be set to `higher`, `lower`, or `ideal`. The simulator will generate a package with the specified deviations based on the plant's ideal values [indicated in the dataset](src/resources/plants_dataset.csv) and rules defined in measurements service.
8 changes: 6 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
pika
requests
python-dotenv
python-dotenv
paho-mqtt
cachetools==3.1.
typer[all]
pandas
typing-extensions
140 changes: 95 additions & 45 deletions src/common/middleware.py
Original file line number Diff line number Diff line change
@@ -1,52 +1,102 @@
import pika
import os
import time
import logging
import paho.mqtt.client as mqtt

FIRST_RECONNECT_DELAY = 1
RECONNECT_RATE = 2
MAX_RECONNECT_COUNT = 12
MAX_RECONNECT_DELAY = 60
logger = logging.getLogger("simulator")

class Middleware:

class Middleware:
def __init__(self):
rabbitmq_host = os.environ.get("RABBITMQ_HOST", "localhost")
self._connection = pika.BlockingConnection(
pika.ConnectionParameters(host=rabbitmq_host)
)
self._channel = self._connection.channel()
self._exit = False
self._remake = False

def create_queue(self, queue_name):
self._channel.queue_declare(queue=queue_name)

def _setup_message_consumption(self, queue_name, user_function):
self._channel.basic_consume(queue=queue_name,
on_message_callback=lambda channel,
method, properties, body:
(user_function(body),
channel.basic_ack
(delivery_tag=method.delivery_tag),
self._verify_connection_end()))
self._channel.start_consuming()

def _verify_connection_end(self):
if self._exit:
self._channel.close()
if self._remake:
self._exit = False
self._channel = self._connection.channel()

def finish(self, open_new_channel=False):
self._exit = True
self._remake = open_new_channel

# Work queue methods
def listen_on(self, queue_name, user_function):
self.create_queue(queue_name)
self._channel.basic_qos(prefetch_count=30)
self._setup_message_consumption(queue_name, user_function)

def send_message(self, queue_name, message):
self._channel.basic_publish(exchange='',
routing_key=queue_name,
body=message)
self._client = mqtt.Client()
self._client.username_pw_set(os.environ.get("MQTT_USERNAME"),
os.environ.get("MQTT_PASSWORD"))
self._topic = None

def _on_connect(self, client, userdata, flags, rc):
if rc == 0 and self._client.is_connected():
logger.info(f"Connected successfully with code {rc}: " +
f"{mqtt.connack_string(rc)}")
if self._topic is not None:
self._client.subscribe(self._topic)
logger.info(f"Subscribed to '{self._topic}' topic")
else:
logger.info(f"Connect failed with code {rc}: " +
f"{mqtt.connack_string(rc)}")

def _on_disconnect(self, client, userdata, rc):
logger.info(f"Disconnected with result code {rc}: " +
f"{mqtt.error_string(rc)}")
if rc == 0:
logger.info(f"Disconnect successfully with code {rc}: " +
f"{mqtt.error_string(rc)}")
return
reconnect_count, reconnect_delay = 0, FIRST_RECONNECT_DELAY
while reconnect_count < MAX_RECONNECT_COUNT:
logger.info("Reconnecting in %d seconds...", reconnect_delay)
time.sleep(reconnect_delay)

try:
client.reconnect()
logger.info("Reconnected successfully!")
return
except Exception as err:
logger.error("%s. Reconnect failed. Retrying...", err)

reconnect_delay *= RECONNECT_RATE
reconnect_delay = min(reconnect_delay, MAX_RECONNECT_DELAY)
reconnect_count += 1
logger.info("Reconnect failed after %s attempts. Exiting...",
reconnect_count)

def _on_publish(self, client, userdata, mid):
logger.info(f"Message publishd! Message id: {mid}")

def listen_on(self, user_function, topic):
self._topic = topic
self._client.on_message = user_function

def send_message(self, topic, msg, qos=2):
result = self._client.publish(topic, msg, qos=qos)
result.wait_for_publish()
status = result[0]
if status == 0:
logger.info(f"Succesfully sent message '{msg}' to topic {topic} " +
f"with qos {qos} and message id {result[1]}. " +
"Result code: " +
f"{status}: {mqtt.error_string(status)}")
else:
logger.info(f"Failed to send message '{msg}' to topic {topic}. " +
f"with qos {qos} and message id {result[1]}. " +
"Result code: " +
f"{status}: {mqtt.error_string(status)}")

def __del__(self):
self._connection.close()
self.finish()

def connect(self):
self._client.on_connect = self._on_connect
self._client.on_disconnect = self._on_disconnect
self._client.on_publish = self._on_publish
self._client.connect(os.environ.get("MQTT_HOST"),
int(os.environ.get("MQTT_PORT")))
logger.info("Connecting to " +
f"{os.environ.get('MQTT_HOST')}:" +
f"{os.environ.get('MQTT_PORT')}")

def run(self):
self._client.loop_start()
while True:
self._client.loop()
if self._client.is_connected():
logger.info("Connected to broker. Listening for messages or " +
"ready to send.")
break

def finish(self):
self._client.loop_stop()
self._client.disconnect()
1 change: 0 additions & 1 deletion src/config.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
{
"packet_period": 1,
"device_id": "97bebd21-a9c8-41a3-9a88-969cd3dd7bba",
"deviations": {
"temperature": 3,
"humidity": 5,
Expand Down
Loading

0 comments on commit a157659

Please sign in to comment.