DmitrMakeev's picture
Update app.py
7bd2372 verified
raw
history blame
28.5 kB
from flask import Flask, request, jsonify, redirect, url_for, render_template, send_from_directory, send_file, render_template_string
import os
import sqlite3
from datetime import datetime
import pytz
import io
import base64
from dotenv import load_dotenv
import requests
import json
import logging
import uuid
from io import BytesIO
import numpy as np
from tabulate import tabulate
from werkzeug.utils import secure_filename
import globs
from api_logic import api
from urllib.parse import urlencode # Добавлен правильный импорт
load_dotenv()
# Инициализация базы данных
def init_db(db_name):
conn = sqlite3.connect(db_name)
cursor = conn.cursor()
# Таблица с системными данными (твоя старая таблица)
cursor.execute('''
CREATE TABLE IF NOT EXISTS system_data (
id INTEGER PRIMARY KEY AUTOINCREMENT,
date_time TEXT,
dey TEXT,
wek TEXT,
v_hid TEXT,
v_min TEXT,
ph TEXT,
ec TEXT,
tS TEXT,
tA TEXT,
hDm TEXT,
sVen TEXT,
onA TEXT,
onB TEXT,
onC TEXT,
nPh TEXT,
nEC TEXT,
nTa TEXT,
nLon TEXT,
nLoff TEXT
)
''')
# **Новая таблица для пользователей бота**
cursor.execute('''
CREATE TABLE IF NOT EXISTS bot_users (
id INTEGER PRIMARY KEY AUTOINCREMENT, -- Уникальный ID
chat_id INTEGER UNIQUE, -- Telegram ID пользователя
created_at TEXT -- Время добавления (ISO формат)
)
''')
conn.commit()
conn.close()
# Глобальные переменные
api_key_sys = os.getenv('api_key') # Берём значение API-ключа
btg_key = os.getenv('btg_key') # Берём значение
btg_id = os.getenv('btg_id') # Берём значение
btg_on = os.getenv('btg_on') # Берём значение
globs.dey = 0
globs.wek = 0
globs.v_hid = 0
globs.v_min = 0
globs.ph = 0
globs.ec = 0
globs.tS = 0
globs.tA = 0
globs.hDm = 0
globs.sVen = 0
globs.onA = 0
globs.onB = 0
globs.onC = 0
globs.ph_eep = 0
globs.ph_on_eep = 0
globs.ec_eep = 0
globs.ec_A_eep = 0
globs.ec_B_eep = 0
globs.ec_C_eep = 0
globs.l_ON_h_eep = 0
globs.l_ON_m_eep = 0
globs.l_OFF_h_eep = 0
globs.l_OFF_m_eep = 0
globs.t_Voz_eep = 0
# Создаем экземпляр Flask-приложения
app = Flask(__name__, template_folder="./")
app.config['DEBUG'] = True
UPLOAD_FOLDER = 'uploads'
ALLOWED_EXTENSIONS = {'jpg', 'jpeg', 'png', 'gif'}
# Создаем папку для загрузок если ее нет
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
# Глобальная переменная для хранения последнего изображения в памяти
latest_image = {"data": None, "filename": None}
# Настроим логирование
logging.basicConfig(level=logging.DEBUG)
# Функция сохранения в базу данных системы автоматизации гидропоники
def save_data_to_db(db_name, data):
try:
conn = sqlite3.connect(db_name)
cursor = conn.cursor()
# ✅ Устанавливаем московское время (UTC+3)
moscow_tz = pytz.timezone("Europe/Moscow")
current_time = datetime.now(moscow_tz).strftime('%Y-%m-%d %H:%M:%S')
# Вставляем данные в таблицу
cursor.execute('''
INSERT INTO system_data (
date_time, dey, wek, v_hid, v_min, ph, ec, tS, tA, hDm, sVen, onA, onB, onC, nPh, nEC, nTa, nLon, nLoff
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (
current_time, # ✅ Дата и время по Москве
data['dey'], data['wek'], data['v_hid'], data['v_min'], data['ph'], data['ec'],
data['tS'], data['tA'], data['hDm'], data['sVen'], data['onA'], data['onB'],
data['onC'], data['nPh'], data['nEC'], data['nTa'], data['nLon'], data['nLoff']
))
conn.commit()
conn.close()
except Exception as e:
return jsonify({'status': 'error', 'message': str(e)}), 500
# Маршрут сохранения в базу
@app.route('/sav_db_api', methods=['GET'])
def sav_db_api():
# Инициализируем базу данных
init_db('system_data.db')
# Получаем данные из запроса
data = {
'dey': request.args.get('dey'),
'wek': request.args.get('wek'),
'v_hid': request.args.get('v_hid'),
'v_min': request.args.get('v_min'),
'ph': request.args.get('ph'),
'ec': request.args.get('ec'),
'tS': request.args.get('tS'),
'tA': request.args.get('tA'),
'hDm': request.args.get('hDm'),
'sVen': request.args.get('sVen'),
'onA': request.args.get('onA'),
'onB': request.args.get('onB'),
'onC': request.args.get('onC'),
'nPh': request.args.get('nPh'),
'nEC': request.args.get('nEC'),
'nTa': request.args.get('nTa'),
'nLon': request.args.get('nLon'),
'nLoff': request.args.get('nLoff')
}
# Проверяем, что все необходимые параметры переданы
required_params = ['dey', 'wek', 'v_hid', 'v_min', 'ph', 'ec', 'tS', 'tA', 'hDm', 'sVen', 'onA', 'onB', 'onC', 'nPh', 'nEC', 'nTa', 'nLon', 'nLoff']
for param in required_params:
if data[param] is None:
return jsonify({'status': 'error', 'message': f'Отсутствует параметр: {param}'}), 400
# Сохраняем данные в базу
save_data_to_db('system_data.db', data)
# Возвращаем ответ
return jsonify({'status': 'success', 'message': 'Save OK'})
# Проверка входа на страницы
@app.route('/page_key', methods=['GET'])
def check_api_key():
api_sys_param = request.args.get('api_sys') # Получаем параметр из запроса
if api_sys_param == api_key_sys:
return jsonify({"status": "ok"}), 200 # ✅ Совпадает — отправляем "ok"
else:
return jsonify({"status": "error", "message": "Invalid API key"}), 403 # ❌ Ошибка 403
# Тестовый запрос с установки
@app.route('/test_server', methods=['GET'])
def test_server():
api_key_param = request.args.get('api_sys') # Получаем параметр из запроса
err_ser = 1 if api_key_param == api_key_sys else 0 # Проверяем совпадение ключей
return jsonify(err_ser=err_ser)
@app.route('/test_server_str', methods=['GET'])
def test_server_str():
api_key_param = request.args.get('api_sys')
err_ser = "1" if api_key_param == api_key_sys else "0"
return err_ser # Возвращаем строку "1" или "0"
# Тестовый запрос с установки
@app.route('/btg_teleg', methods=['GET'])
def btg_teleg():
api_key_param = request.args.get('api_sys') # Получаем параметр из запроса
return jsonify(btg_key_ser=btg_key,btg_id_ser=btg_id,btg_on_ser=btg_on)
# Маршрут для вывода всех данных из таблицы
@app.route('/get_all_data', methods=['GET'])
def get_all_data():
try:
conn = sqlite3.connect('system_data.db')
cursor = conn.cursor()
# Выполняем запрос для получения всех данных из таблицы
cursor.execute('SELECT * FROM system_data')
rows = cursor.fetchall()
# Получаем названия столбцов
column_names = [description[0] for description in cursor.description]
# Преобразуем данные в формат JSON
data = []
for row in rows:
data.append(dict(zip(column_names, row)))
conn.close()
# Возвращаем данные в формате JSON
return jsonify(data)
except Exception as e:
return jsonify({'status': 'error', 'message': str(e)}), 500
# Удаление базы
@app.route('/delite_db', methods=['GET'])
def delete_db():
try:
conn = sqlite3.connect("system_data.db") # Используем вашу БД
cursor = conn.cursor()
# ✅ Удаляем все записи из таблицы
cursor.execute("DELETE FROM system_data")
# ✅ Сбрасываем автоинкрементный счётчик ID (для SQLite)
cursor.execute("DELETE FROM sqlite_sequence WHERE name='system_data'")
conn.commit()
conn.close()
return jsonify({'status': 'ok', 'message': 'База данных успешно очищена'})
except Exception as e:
return jsonify({'status': 'error', 'message': str(e)}), 500
@app.route('/plot_week', methods=['GET'])
def plot_week():
try:
# Получаем номер недели из параметров запроса
week_number = request.args.get('week', default=1, type=int)
week_number = max(1, min(30, week_number)) # Ограничиваем диапазон 1-30
# Подключаемся к базе данных
conn = sqlite3.connect('system_data.db')
cursor = conn.cursor()
# Проверяем существование таблицы
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='system_data'")
table_exists = cursor.fetchone()
if not table_exists:
conn.close()
return render_template('plot_week.html', data=None, week_number=week_number, table_exists=False)
# Запрашиваем данные за выбранную неделю
cursor.execute('''
SELECT date_time, dey, ph, ec, tS, tA, hDm, sVen, onA, onB, onC, v_hid, v_min
FROM system_data
WHERE wek = ?
ORDER BY date_time
''', (str(week_number),)) # Приводим week_number к строке, так как wek имеет тип TEXT
rows = cursor.fetchall()
conn.close()
# Если данных нет
if not rows:
return render_template('plot_week.html', data=None, week_number=week_number, table_exists=True)
# Формируем данные для JSON
data = {
'week': week_number,
'dates': [row[0] for row in rows],
'days_of_week': [int(row[1]) if row[1] else 0 for row in rows], # Преобразуем dey в int
'ph': [float(row[2]) if row[2] else 0.0 for row in rows], # pH
'ec': [float(row[3]) if row[3] else 0.0 for row in rows], # EC
'tS': [float(row[4]) if row[4] else 0.0 for row in rows], # Температура раствора
'tA': [float(row[5]) if row[5] else 0.0 for row in rows], # Температура воздуха
'hDm': [float(row[6]) if row[6] else 0.0 for row in rows], # Влажность воздуха
'sVen': [float(row[7]) if row[7] else 0.0 for row in rows], # Обороты вентилятора
'onA': [float(row[8]) if row[8] else 0.0 for row in rows], # Насос A
'onB': [float(row[9]) if row[9] else 0.0 for row in rows], # Насос B
'onC': [float(row[10]) if row[10] else 0.0 for row in rows], # Насос C
'sus': [f"{row[11]}:{row[12]}" if row[11] and row[12] else "0:0" for row in rows] # Объединяем v_hid и v_min
}
# Отправляем данные в HTML-шаблон
return render_template('plot_week.html', data=data, week_number=week_number, table_exists=True)
except Exception as e:
# Логируем ошибку в консоль для отладки
print(f"Ошибка: {str(e)}")
return render_template('plot_week.html', data=None, week_number=week_number, table_exists=True, message=f"Ошибка: {str(e)}")
@app.route("/")
def index():
return flask.render_template('index.html')
@app.route('/online', methods=['GET'])
def online():
return render_template('online.html')
@app.route('/table', methods=['GET'])
def table():
return render_template('table.html')
@app.route('/online_api', methods=['GET'])
def online_api():
# Устанавливаем московское время (UTC+3)
moscow_tz = pytz.timezone("Europe/Moscow")
current_time = datetime.now(moscow_tz)
# Форматируем дату и время отдельно
date = current_time.strftime('%Y-%m-%d') # Например, "2025-03-23"
time = current_time.strftime('%H:%M:%S') # Например, "14:35:42"
return jsonify(
dey=globs.dey,
wek=globs.wek,
v_hid=globs.v_hid,
v_min=globs.v_min,
ph=globs.ph,
ec=globs.ec,
tS=globs.tS,
tA=globs.tA,
hDm=globs.hDm,
sVen=globs.sVen,
rFul=globs.rFul,
rLi=globs.rLi,
rWat=globs.rWat,
rRas=globs.rRas,
rPH=globs.rPH,
rEC=globs.rEC,
rSl=globs.rSl,
rLe=globs.rLe,
alW=globs.alW,
ec_A_eep=globs.ec_A_eep,
ec_B_eep=globs.ec_B_eep,
ec_C_eep=globs.ec_C_eep,
date=date, # Добавляем дату
time=time # Добавляем время
)
@app.route('/settings', methods=['GET'])
def settings():
return render_template('settings.html')
@app.route('/settings_api', methods=['GET'])
def settings_api():
return jsonify(ph_eep=globs.ph_eep,
ph_on_eep=globs.ph_on_eep,
ec_eep=globs.ec_eep,
ec_A_eep=globs.ec_A_eep,
ec_B_eep=globs.ec_B_eep,
ec_C_eep=globs.ec_C_eep,
l_ON_h_eep=globs.l_ON_h_eep,
l_ON_m_eep=globs.l_ON_m_eep,
l_OFF_h_eep=globs.l_OFF_h_eep,
l_OFF_m_eep=globs.l_OFF_m_eep,
t_Voz_eep=globs.t_Voz_eep,
set_st=globs.set_status
)
@app.route('/pH_set', methods=['GET'])
def set_pH_value():
ph_value = request.args.get('value')
globs.ph_set = ph_value
globs.eep_set = 1
return "pH value set successfully"
@app.route('/ph_on_set', methods=['GET'])
def ph_on_value():
ph_on_value = request.args.get('value')
globs.ph_on_set = ph_on_value
globs.eep_set = 2
return "EC value set successfully"
@app.route('/EC_set', methods=['GET'])
def set_EC_value():
ec_value = request.args.get('value')
globs.ec_set = ec_value
globs.eep_set = 3
return "EC value set successfully"
@app.route('/ec_A_set', methods=['GET'])
def ec_A_setValue():
ec_A_setValue = request.args.get('value')
globs.ec_A_set = ec_A_setValue
globs.eep_set = 4
return "EC value set successfully"
@app.route('/ec_B_set', methods=['GET'])
def ec_B_setValue():
ec_B_setValue = request.args.get('value')
globs.ec_B_set = ec_B_setValue
globs.eep_set = 5
return "EC value set successfully"
@app.route('/ec_C_set', methods=['GET'])
def ec_C_setValue():
ec_C_setValue = request.args.get('value')
globs.ec_C_set = ec_C_setValue
globs.eep_set = 6
return "EC value set successfully"
@app.route('/l_ON_set', methods=['GET'])
def l_ON_set():
globs.l_ON_h_set = request.args.get('l_ON_h_set')
globs.l_ON_m_set = request.args.get('l_ON_m_set')
globs.eep_set = 7
return "EC value set successfully"
@app.route('/l_OFF_set', methods=['GET'])
def l_OFF_set():
globs.l_OFF_h_set = request.args.get('l_OFF_h_set')
globs.l_OFF_m_set = request.args.get('l_OFF_m_set')
globs.eep_set = 8
return "EC value set successfully"
@app.route('/t_Voz_eep_set', methods=['GET'])
def t_Voz_eep_set():
t_Voz_eep_set = request.args.get('value')
globs.t_Voz_set = t_Voz_eep_set
globs.eep_set = 9
return "EC value set successfully"
@app.route('/but_start', methods=['GET'])
def but_start():
globs.eep_set = 10
return jsonify(value_set="start")
@app.route('/but_stop', methods=['GET'])
def but_stop():
globs.eep_set = 11
return jsonify(value_set="stop")
@app.route('/but_res', methods=['GET'])
def but_res():
globs.eep_set = 12
return jsonify(value_set="res")
@app.route('/but_sliv', methods=['GET'])
def but_sliv():
globs.eep_set = 13
return jsonify(value_set="sliv")
@app.route("/api", methods=['GET'])
def handle_api():
response = api()
return response
@app.route("/save_db", methods=['GET'])
def handle_save_db():
response = save_db()
return response
@app.route('/set_res')
def set_res():
globs.eep_set = 0
return jsonify(value_set="reset")
def allowed_file(filename):
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
@app.route('/upload', methods=['POST'])
def upload_file():
if 'file' not in request.files:
return jsonify({"error": "No file part"}), 400
file = request.files['file']
if file.filename == '':
return jsonify({"error": "No selected file"}), 400
if not allowed_file(file.filename):
return jsonify({"error": "Invalid file type"}), 400
# Генерируем имя для файла с использованием времени
timestamp = datetime.now().strftime('%Y.%m.%d_%H:%M:%S_')
filename = timestamp + file.filename
save_path = os.path.join(UPLOAD_FOLDER, filename)
# Открываем файл для записи и собираем его части
try:
with open(save_path, 'wb') as f:
while chunk := file.read(1024): # Чтение и запись данных частями
f.write(chunk)
return jsonify({
"message": "File uploaded successfully",
"filename": filename,
"path": save_path
}), 200
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route('/uploads/<filename>', methods=['GET'])
def uploaded_file(filename):
return send_from_directory(UPLOAD_FOLDER, filename)
# 🧠 2. Маршрут: сохраняет файл только в память (BytesIO)
# Маршрут для загрузки файла в память
# Загрузка изображения в память (в виде байтов)
@app.route('/upload_memory', methods=['POST'])
def upload_file_to_memory():
if 'file' not in request.files:
return jsonify({"error": "No file part"}), 400
file = request.files['file']
if file.filename == '':
return jsonify({"error": "No selected file"}), 400
if not file.filename.lower().endswith(('.jpg', '.jpeg', '.png')):
return jsonify({"error": "Invalid file type"}), 400
timestamp = datetime.now().strftime('%Y.%m.%d_%H:%M:%S_')
filename = timestamp + file.filename
try:
# Читаем весь файл в байты
file_bytes = file.read()
# Сохраняем в переменную
latest_image["data"] = file_bytes
latest_image["filename"] = filename
return jsonify({
"message": "Image uploaded successfully",
"filename": filename
}), 200
except Exception as e:
return jsonify({"error": str(e)}), 500
# Получение последнего изображения
@app.route('/last_image', methods=['GET'])
def get_last_image():
if not latest_image["data"]:
return jsonify({"error": "No image available"}), 404
# Возвращаем через новый BytesIO каждый раз
return send_file(
BytesIO(latest_image["data"]),
mimetype='image/jpeg',
download_name=latest_image["filename"]
)
@app.route('/view_image', methods=['GET'])
def view_image():
return render_template('show_image.html')
from tabulate import tabulate
# Константы для расчёта
TOTAL_NITROGEN = 125 # ppm
NO3_RATIO = 8.25 # Доля нитратного азота
NH4_RATIO = 1.00 # Доля аммонийного азота
VOLUME_LITERS = 10 # Объём раствора
# Базовый профиль (7 элементов)
BASE_PROFILE = {
'P': 31.000,
'K': 210.694,
'Mg': 24.000,
'Ca': 84.000,
'S': 137.397,
'N (NO3-)': 0,
'N (NH4+)': 0
}
# База удобрений (6 удобрений)
FERTILIZERS = {
"Кальциевая селитра": {"N (NO3-)": 0.118, "Ca": 0.169},
"Калий азотнокислый": {"N (NO3-)": 0.138, "K": 0.387},
"Калий сернокислый": {"K": 0.448, "S": 0.184},
"Аммоний азотнокислый": {"N (NO3-)": 0.175, "N (NH4+)": 0.175},
"Сульфат магния": {"Mg": 0.098, "S": 0.13},
"Калий фосфорнокислый": {"P": 0.228, "K": 0.287}
}
class NutrientCalculator:
def __init__(self, volume_liters=1.0):
self.volume = volume_liters
self.results = {}
self.profile = BASE_PROFILE.copy()
self.total_ppm = sum(BASE_PROFILE.values()) + TOTAL_NITROGEN
self.fertilizers = FERTILIZERS
# Целевые значения азота
self.target_no3 = TOTAL_NITROGEN * (NO3_RATIO / (NO3_RATIO + NH4_RATIO))
self.target_nh4 = TOTAL_NITROGEN * (NH4_RATIO / (NO3_RATIO + NH4_RATIO))
def calculate(self):
# 1. Вносим сульфат магния (покрываем Mg)
self._apply_fertilizer(
"Сульфат магния",
self.profile['Mg'],
"Mg",
{"S": "внесет S"}
)
# 2. Вносим кальциевую селитру (покрываем Ca)
self._apply_fertilizer(
"Кальциевая селитра",
self.profile['Ca'],
"Ca",
{"N (NO3-)": "внесет NO3"}
)
# 3. Вносим калий фосфорнокислый (покрываем P)
self._apply_fertilizer(
"Калий фосфорнокислый",
self.profile['P'],
"P",
{"K": "внесет K"}
)
# 4. Вносим аммоний азотнокислый (покрываем NH4)
nh4_needed = self.target_nh4
if nh4_needed > 0:
self._apply_fertilizer(
"Аммоний азотнокислый",
nh4_needed,
"N (NH4+)",
{"N (NO3-)": "внесет NO3"}
)
# 5. Вносим калий сернокислый (покрываем K и S)
self._apply_potassium_sulfate()
# 6. Вносим калий азотнокислый (добираем K)
self._apply_potassium_nitrate()
# Пересчитываем фактическое содержание азота
self._recalculate_nitrogen()
return self.results
def _apply_fertilizer(self, name, need, main_element, additions):
if need <= 0:
return
content = self.fertilizers[name][main_element]
grams = (need * self.volume) / (content * 1000)
added = {}
for element, label in additions.items():
added_amount = grams * self.fertilizers[name][element] * 1000 / self.volume
added[label] = round(added_amount, 1)
self.profile[element] -= added_amount
self.profile[main_element] = 0
self.results[name] = {
'граммы': round(grams, 3),
'миллиграммы': int(grams * 1000),
**added
}
def _apply_potassium_sulfate(self):
k_need = self.profile['K']
s_need = self.profile['S']
if k_need <= 0 or s_need <= 0:
return
k_content = self.fertilizers["Калий сернокислый"]["K"]
s_content = self.fertilizers["Калий сернокислый"]["S"]
# Рассчитываем максимально возможное внесение
grams = min(
(k_need * self.volume) / (k_content * 1000),
(s_need * self.volume) / (s_content * 1000)
)
added_k = grams * k_content * 1000 / self.volume
added_s = grams * s_content * 1000 / self.volume
self.profile['K'] -= added_k
self.profile['S'] -= added_s
self.results["Калий сернокислый"] = {
'граммы': round(grams, 3),
'миллиграммы': int(grams * 1000),
'внесет K': round(added_k, 1),
'внесет S': round(added_s, 1)
}
def _apply_potassium_nitrate(self):
k_need = self.profile['K']
if k_need <= 0:
return
k_content = self.fertilizers["Калий азотнокислый"]["K"]
grams = (k_need * self.volume) / (k_content * 1000)
added_n = grams * self.fertilizers["Калий азотнокислый"]["N (NO3-)"] * 1000 / self.volume
self.profile['K'] = 0
self.results["Калий азотнокислый"] = {
'граммы': round(grams, 3),
'миллиграммы': int(grams * 1000),
'внесет NO3': round(added_n, 1)
}
def _recalculate_nitrogen(self):
# Считаем фактически внесенный азот
no3 = 0
nh4 = 0
for fert in self.results.values():
if 'внесет NO3' in fert:
no3 += fert['внесет NO3']
if 'внесет NH4' in fert:
nh4 += fert['внесет NH4']
self.profile['N (NO3-)'] = no3
self.profile['N (NH4+)'] = nh4
def calculate_ec(self):
return round(self.total_ppm / 700, 2)
def print_report(self):
print("\n" + "="*60)
print("ПРОФИЛЬ ПИТАТЕЛЬНОГО РАСТВОРА:")
print("="*60)
table = [[el, round(val, 1)] for el, val in self.profile.items()]
print(tabulate(table, headers=["Элемент", "ppm"]))
print("\n" + "="*60)
print(f"РАСЧЕТ ДЛЯ {self.volume} ЛИТРОВ РАСТВОРА")
print("="*60)
print(f"Общая концентрация: {round(self.total_ppm, 1)} ppm")
print(f"EC: {self.calculate_ec()} mS/cm")
print("\nРЕКОМЕНДУЕМЫЕ УДОБРЕНИЯ (6 штук):")
fert_table = []
for fert, data in self.results.items():
details = [f"+{k}: {v} ppm" for k, v in data.items() if k.startswith('внесет')]
fert_table.append([
fert,
f"{data['граммы']} г",
f"{data['миллиграммы']} мг",
"\n".join(details)
])
print(tabulate(fert_table, headers=["Удобрение", "Граммы", "Миллиграммы", "Добавит"]))
print("\nОСТАТОЧНЫЙ ДЕФИЦИТ:")
deficit = {k: round(v, 1) for k, v in self.profile.items() if v > 0.1}
if deficit:
for el, val in deficit.items():
print(f" {el}: {val} ppm")
else:
print(" Все элементы покрыты полностью")
if __name__ == "__main__":
calculator = NutrientCalculator(volume_liters=10)
calculator.calculate()
calculator.print_report()
if __name__ == '__main__':
app.run(host='0.0.0.0', port=int(os.environ.get('PORT', 7860)))