diff --git a/.gitignore b/.gitignore new file mode 100755 index 00000000..157c537e --- /dev/null +++ b/.gitignore @@ -0,0 +1,141 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +bin/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +include/ +lib/ +lib64/ +lib64 +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST +pyvenv.cfg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +.env + +#swag related files: ignore all except NGINX conf +swag/ +grafana/ +mosquitto/ +wireguard/ +mariadb/ diff --git a/README.md b/README.md index b5710abe..d037f849 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,4 @@ -# docker +# Ejercicio Telegram bot + +Vellbach, Lucas Alejandro diff --git a/clienteMqtt/Dockerfile b/clienteMqtt/Dockerfile new file mode 100755 index 00000000..8353d235 --- /dev/null +++ b/clienteMqtt/Dockerfile @@ -0,0 +1,13 @@ +FROM python:3.11-slim + +WORKDIR /app + +ENV TZ="America/Argentina/Buenos_Aires" + +COPY ./requirements.txt /app/requirements.txt + +RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt + +COPY . /app + +CMD ["python", "/app/clienteMqtt.py"] \ No newline at end of file diff --git a/clienteMqtt/clienteMqtt.py b/clienteMqtt/clienteMqtt.py new file mode 100755 index 00000000..17fd23de --- /dev/null +++ b/clienteMqtt/clienteMqtt.py @@ -0,0 +1,47 @@ +import asyncio, ssl, certifi, logging, os, aiomysql, json, traceback +import aiomqtt + +logging.basicConfig(format='%(asctime)s - cliente mqtt - %(levelname)s:%(message)s', level=logging.INFO, datefmt='%d/%m/%Y %H:%M:%S %z') + +async def main(): + + tls_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + tls_context.verify_mode = ssl.CERT_REQUIRED + tls_context.check_hostname = True + tls_context.load_default_certs() + + async with aiomqtt.Client( + os.environ["SERVIDOR"], + username=os.environ["MQTT_USR"], + password=os.environ["MQTT_PASS"], + port=int(os.environ["PUERTO_MQTTS"]), + tls_context=tls_context, + ) as client: + await client.subscribe(os.environ['TOPICO']) + async for message in client.messages: + logging.info(str(message.topic) + ": " + message.payload.decode("utf-8")) + dispositivo=str(message.topic).split('/')[-1] + datos=json.loads(message.payload.decode('utf8')) + sql = "INSERT INTO `mediciones` (`sensor_id`, `temperatura`, `humedad`) VALUES (%s, %s, %s)" + try: + conn = await aiomysql.connect(host=os.environ["MARIADB_SERVER"], port=3306, + user=os.environ["MARIADB_USER"], + password=os.environ["MARIADB_USER_PASS"], + db=os.environ["MARIADB_DB"]) + except Exception as e: + logging.error(traceback.format_exc()) + + cur = await conn.cursor() + + async with conn.cursor() as cur: + try: + await cur.execute(sql, (dispositivo, datos['temperatura'], datos['humedad'])) + await conn.commit() + await cur.close() + await conn.ensure_closed() + except Exception as e: + logging.error(traceback.format_exc()) + # Logs the error appropriately. + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/clienteMqtt/requirements.txt b/clienteMqtt/requirements.txt new file mode 100755 index 00000000..9c15641c --- /dev/null +++ b/clienteMqtt/requirements.txt @@ -0,0 +1,4 @@ +aiomqtt +certifi +environs +aiomysql diff --git a/compose.yaml b/compose.yaml new file mode 100755 index 00000000..78d07243 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,120 @@ +services: + mariadb: + image: mariadb + container_name: mariadb + environment: + - PUID=1000 + - PGID=1000 + - MARIADB_ROOT_PASSWORD=${MARIADB_ROOT_PASSWORD} + - TZ=America/Argentina/Buenos_Aires + volumes: + - ./mariadb:/config + ports: + - 3306:3306 + restart: unless-stopped + phpmyadmin: + image: phpmyadmin + container_name: phpmyadmin + restart: always + environment: + - PMA_HOST=mariadb + - PMA_ABSOLUTE_URI=https://${DOMINIO}:${PUERTO}/phpmyadmin/ + ports: + - 8080:80 + depends_on: + - mariadb + clientemqtt: + image: clientemqtt + container_name: clientemqtt + environment: + - TZ=America/Argentina/Buenos_Aires + - SERVIDOR=${SERVIDOR} #Se reemplazará por la env var SERVIDOR defineda en .env + - TOPICO=${TOPICO} + - MARIADB_SERVER=${MARIADB_SERVER} + - MARIADB_USER=${MARIADB_USER} + - MARIADB_USER_PASS=${MARIADB_USER_PASS} + - MARIADB_DB=${MARIADB_DB} + - MQTT_USR=${MQTT_USR} + - MQTT_PASS=${MQTT_PASS} + - PUERTO_MQTTS=${PUERTO_MQTTS} + restart: unless-stopped + depends_on: + - mariadb + mosquitto: + image: eclipse-mosquitto + container_name: mosquitto + user: "1000:1000" + ports: + - 1883:1883 + - ${PUERTO_MQTTS}:8883 + restart: unless-stopped + volumes: + - ./mosquitto/config/mosquitto.conf:/mosquitto/config/mosquitto.conf + - ./mosquitto/config:/mosquitto/config + - ./swag/etc/letsencrypt:/var/tmp + - ./mosquitto/data:/mosquitto/data + - ./mosquitto/log:/mosquitto/log + grafana: + image: grafana/grafana-oss + container_name: grafana + user: "1000" + volumes: + - ./grafana:/var/lib/grafana + ports: + - 3000:3000 + depends_on: + - mariadb + environment: + GF_SERVER_PROTOCOL: http + GF_SERVER_ROOT_URL: https://${DOMINIO}:${PUERTO}/grafana/ + # GF_SERVER_SERVE_FROM_SUB_PATH: "true" + GF_SERVER_DOMAIN: https://${DOMINIO} + GF_ANALYTICS_REPORTING_ENABLED: "false" + restart: unless-stopped + portainer: + image: portainer/portainer-ce + container_name: portainer + ports: + - 9443:9443 + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - portainer_data:/data + restart: unless-stopped + swag: + image: lscr.io/linuxserver/swag:latest + container_name: swag + cap_add: + - NET_ADMIN + environment: + - PUID=1000 + - PGID=1000 + - TZ=America/Argentina/Buenos_Aires + - URL=${DOMINIO} + - VALIDATION=dns + - DNSPLUGIN=duckdns + - SUBDOMAINS= + volumes: + - ./swag:/config + ports: + - ${PUERTO}:443/tcp + - 80:80 + restart: unless-stopped + telegrambot: + image: telegrambot + container_name: telegrambot + environment: + - TZ=America/Argentina/Buenos_Aires + - TB_TOKEN=${TB_TOKEN} + - MARIADB_SERVER=${MARIADB_SERVER} + - MARIADB_USER=${MARIADB_USER} + - MARIADB_USER_PASS=${MARIADB_USER_PASS} + - MARIADB_DB=${MARIADB_DB} + - DOMINIO=${DOMINIO} + - MQTT_USR=${MQTT_USR} + - MQTT_PASS=${MQTT_PASS} + - PUERTO_MQTTS=${PUERTO_MQTTS} + restart: unless-stopped + depends_on: + - mariadb +volumes: + portainer_data: \ No newline at end of file diff --git a/sensores_remotos.sql b/sensores_remotos.sql new file mode 100755 index 00000000..52db7bf3 --- /dev/null +++ b/sensores_remotos.sql @@ -0,0 +1,36 @@ +-- +-- Base de datos: `sensores_remotos` +-- +CREATE DATABASE IF NOT EXISTS `sensores_remotos` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci; +USE `sensores_remotos`; + +-- -------------------------------------------------------- + +-- +-- Estructura de tabla para la tabla `mediciones` +-- + +CREATE TABLE `mediciones` ( + `id` int(11) NOT NULL, + `sensor_id` char(12) NOT NULL, + `timestamp` timestamp NOT NULL DEFAULT current_timestamp(), + `temperatura` decimal(3,1) NOT NULL, + `humedad` decimal(3,1) NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +-- +-- Indices de la tabla `mediciones` +-- +ALTER TABLE `mediciones` + ADD PRIMARY KEY (`id`), + ADD KEY `timestamp` (`timestamp`); + +-- +-- AUTO_INCREMENT de la tabla `mediciones` +-- +ALTER TABLE `mediciones` + MODIFY `id` int(11) NOT NULL AUTO_INCREMENT; +COMMIT; + +CREATE USER 'mediciones'@'%' IDENTIFIED BY 'passworddeiot';GRANT USAGE ON *.* TO 'mediciones'@'%' REQUIRE NONE WITH MAX_QUERIES_PER_HOUR 0 MAX_CONNECTIONS_PER_HOUR 0 MAX_UPDATES_PER_HOUR 0 MAX_USER_CONNECTIONS 0; +GRANT SELECT, INSERT ON `sensores\_remotos`.* TO 'mediciones'@'%'; \ No newline at end of file diff --git a/telegrambot/Dockerfile b/telegrambot/Dockerfile new file mode 100755 index 00000000..53aaa6c7 --- /dev/null +++ b/telegrambot/Dockerfile @@ -0,0 +1,13 @@ +FROM python:3.11-slim + +WORKDIR /app + +ENV TZ="America/Argentina/Buenos_Aires" + +COPY ./requirements.txt /app/requirements.txt + +RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt + +COPY . /app + +CMD ["python", "/app/telegrambot.py"] \ No newline at end of file diff --git a/telegrambot/requirements.txt b/telegrambot/requirements.txt new file mode 100755 index 00000000..e3494dd8 --- /dev/null +++ b/telegrambot/requirements.txt @@ -0,0 +1,4 @@ +python-telegram-bot +aiomysql +aiomqtt +matplotlib diff --git a/telegrambot/telegrambot.py b/telegrambot/telegrambot.py new file mode 100755 index 00000000..c7567e14 --- /dev/null +++ b/telegrambot/telegrambot.py @@ -0,0 +1,182 @@ +from telegram import Update, ReplyKeyboardMarkup +from telegram.ext import Application, CommandHandler, ContextTypes, MessageHandler, filters +import logging, os, asyncio, aiomysql, traceback, locale, aiomqtt, ssl, certifi, json +import matplotlib.pyplot as plt +from io import BytesIO + +token=os.environ["TB_TOKEN"] + +logging.basicConfig(format='%(asctime)s - TelegramBot - %(levelname)s - %(message)s', level=logging.INFO) + +async def start(update: Update, context: ContextTypes.DEFAULT_TYPE): + logging.info("se conectó: " + str(update.message.from_user.id)) + if update.message.from_user.first_name: + nombre=update.message.from_user.first_name + else: + nombre="" + if update.message.from_user.last_name: + apellido=update.message.from_user.last_name + else: + apellido="" + kb = [["Destello"],["Modo"],["Relé"]] + await context.bot.send_message(update.message.chat.id, text="Bienvenido al Bot "+ nombre + " " + apellido,reply_markup=ReplyKeyboardMarkup(kb)) + +async def acercade(update: Update, context): + await context.bot.send_message(update.message.chat.id, text="Este bot fue creado para el curso de IoT FIO") + +async def kill(update: Update, context): + logging.info(context.args) + if context.args and context.args[0] == '@e': + await context.bot.send_animation(update.message.chat.id, "CgACAgEAAxkBAAOPZkuctzsWZVlDSNoP9PavSZmH5poAAmUCAALrx0lEVKaX7K-68Ns1BA") + await asyncio.sleep(6) + await context.bot.send_message(update.message.chat.id, text="¡¡¡Ahora estan todos muertos!!!") + else: + await context.bot.send_message(update.message.chat.id, text="☠️ ¡¡¡Esto es muy peligroso!!! ☠️") + +async def setpoint(update: Update, context): + logging.info(context.args) + + if not context.args: + await context.bot.send_message(chat_id=update.message.chat.id, text="Por favor ingresa un valor numérico.") + return + try: + setpoint_value = float(context.args[0]) + await context.bot.send_message(chat_id=update.message.chat.id, text=f"Cambiando el setpoint a: {setpoint_value}") + + payload = json.dumps({"setpoint": setpoint_value}) + await publicar(context, "setpoint", payload) + + except ValueError: + await context.bot.send_message(chat_id=update.message.chat.id, text="El valor ingresado no es un número válido.") + +async def periodo(update: Update, context): + logging.info(context.args) + + if not context.args: + await context.bot.send_message(chat_id=update.message.chat.id, text="Por favor ingresa un valor numérico.") + return + try: + periodo_value = float(context.args[0]) + await context.bot.send_message(chat_id=update.message.chat.id, text=f"Cambiando el periodo a: {periodo_value}") + + payload = json.dumps({"periodo": periodo_value}) + await publicar(context, "periodo", payload) + + except ValueError: + await context.bot.send_message(chat_id=update.message.chat.id, text="El valor ingresado no es un número válido.") + +async def publicar(context: ContextTypes.DEFAULT_TYPE, topico: str, payload: str): + client = context.application.bot_data.get("mqtt_client") + if not client: + logging.error("MQTT client no disponible en el contexto.") + return + try: + await client.publish(topico, payload) + logging.info(f"Publicado en MQTT: {topico} -> {payload}") + except Exception as e: + logging.error(f"Error al publicar en MQTT: {e}") + traceback.print_exc() + + + +async def DMR(update: Update, context: ContextTypes.DEFAULT_TYPE): + text = update.message.text.lower() + acciones = { + "destello": "Encendiendo el LED", + "modo": "Cambiando el modo", + "relé": "Activando el relé" + } + + await publicar(context, text, text) + await context.bot.send_message(update.message.chat.id, text=acciones[text]) + +async def medicion(update: Update, context): + logging.info(update.message.text) + sql = f"SELECT timestamp, {update.message.text} FROM mediciones ORDER BY timestamp DESC LIMIT 1" + conn = await aiomysql.connect(host=os.environ["MARIADB_SERVER"], port=3306, + user=os.environ["MARIADB_USER"], + password=os.environ["MARIADB_USER_PASS"], + db=os.environ["MARIADB_DB"]) + async with conn.cursor() as cur: + await cur.execute(sql) + r = await cur.fetchone() + if update.message.text == 'temperatura': + unidad = 'ºC' + else: + unidad = '%' + await context.bot.send_message(update.message.chat.id, + text="La última {} es de {} {},\nregistrada a las {:%H:%M:%S %d/%m/%Y}" + .format(update.message.text, str(r[1]).replace('.',','), unidad, r[0])) + logging.info("La última {} es de {} {}, medida a las {:%H:%M:%S %d/%m/%Y}".format(update.message.text, r[1], unidad, r[0])) + conn.close() + +async def graficos(update: Update, context): + logging.info(update.message.text) + sql = f"SELECT timestamp, {update.message.text.split()[1]} FROM mediciones where id mod 2 = 0 AND timestamp >= NOW() - INTERVAL 1 DAY AND sensor_id LIKE 'sensor_1' ORDER BY timestamp" + conn = await aiomysql.connect(host=os.environ["MARIADB_SERVER"], port=3306, + user=os.environ["MARIADB_USER"], + password=os.environ["MARIADB_USER_PASS"], + db=os.environ["MARIADB_DB"]) + async with conn.cursor() as cur: + await cur.execute(sql) + filas = await cur.fetchall() + + fig, ax = plt.subplots(figsize=(7, 4)) + fecha,var=zip(*filas) + ax.plot(fecha,var) + ax.grid(True, which='both') + ax.set_title(update.message.text, fontsize=14, verticalalignment='bottom') + ax.set_xlabel('fecha') + ax.set_ylabel('unidad') + + buffer = BytesIO() + fig.tight_layout() + fig.savefig(buffer, format='png') + plt.close() + buffer.seek(0) + await context.bot.send_photo(chat_id=update.effective_chat.id, photo=buffer) + buffer.close() + conn.close() + +async def main(): + + tls_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + tls_context.verify_mode = ssl.CERT_REQUIRED + tls_context.check_hostname = True + tls_context.load_default_certs() + + + async with aiomqtt.Client( + os.environ["DOMINIO"], + username=os.environ["MQTT_USR"], + password=os.environ["MQTT_PASS"], + port=int(os.environ["PUERTO_MQTTS"]), + tls_context=tls_context, + ) as client: + + application = Application.builder().token(token).build() + + application.add_handler(CommandHandler('start', start)) + application.add_handler(CommandHandler('about', acercade)) + application.add_handler(CommandHandler('setpoint', setpoint)) + application.add_handler(CommandHandler('periodo', periodo)) + application.add_handler(MessageHandler(filters.Regex("^(Destello|Modo|Relé)$"), DMR)) + + application.bot_data["mqtt_client"] = client + + # Inicializar la aplicación Telegram + async with application: # Calls initialize and shutdown + await application.start() + await application.updater.start_polling() + # Start other asyncio frameworks here + # Add some logic that keeps the event loop running until you want to shutdown + while True: + try: + await asyncio.sleep(1) + except Exception: + # Stop the other asyncio frameworks here + await application.updater.stop() + await application.stop() + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file