Ed5 commited on
Commit
3749910
·
verified ·
1 Parent(s): e3010e5

Upload 2 files

Browse files
pages/2_⚙️_Hardware.py ADDED
@@ -0,0 +1,712 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import pandas as pd
3
+ import math
4
+ import json
5
+ import os
6
+ from io import BytesIO
7
+
8
+ st.set_page_config(page_title="Расчет потребления", layout="wide")
9
+
10
+ DB_FILE = "equipment_db.json"
11
+
12
+
13
+ # ==================== ФУНКЦИИ БАЗЫ ДАННЫХ ====================
14
+ def load_db():
15
+ if os.path.exists(DB_FILE):
16
+ try:
17
+ with open(DB_FILE, "r", encoding="utf-8") as f:
18
+ return json.load(f)
19
+ except:
20
+ return []
21
+ return []
22
+
23
+
24
+ def save_db(data):
25
+ with open(DB_FILE, "w", encoding="utf-8") as f:
26
+ json.dump(data, f, ensure_ascii=False, indent=4)
27
+
28
+
29
+ # ==================== БОКОВАЯ ПАНЕЛЬ ====================
30
+ with st.sidebar:
31
+ st.header("📂 База данных")
32
+ uploaded_file = st.file_uploader("Импорт Excel", type=["xlsx", "xls"])
33
+ if uploaded_file:
34
+ try:
35
+ df_import = pd.read_excel(uploaded_file, header=None)
36
+ df_import.columns = [f"Колонка {i}" for i in range(len(df_import.columns))]
37
+ col_target = st.selectbox("В какой колонке данные?", options=df_import.columns)
38
+ if st.button("📥 Разбить текст и загрузить", type="primary"):
39
+ current_db = load_db()
40
+ existing_map = {item["article"]: item["power"] for item in current_db}
41
+ new_items = []
42
+ count_ok = 0
43
+ for index, row in df_import.iterrows():
44
+ raw_text = str(row[col_target]).strip()
45
+ if not raw_text or raw_text.lower() == "nan": continue
46
+ parts = raw_text.rsplit(' ', 1)
47
+ if len(parts) == 2:
48
+ name = parts[0].strip()
49
+ article = parts[1].strip()
50
+ else:
51
+ name = raw_text
52
+ article = "-"
53
+ power = existing_map.get(article, 0.0)
54
+ new_items.append({"name": name, "article": article, "power": power})
55
+ count_ok += 1
56
+ final_db_map = {item["article"]: item for item in current_db}
57
+ for item in new_items: final_db_map[item["article"]] = item
58
+ save_db(list(final_db_map.values()))
59
+ st.success(f"Готово! Обработано строк: {count_ok}.")
60
+ except Exception as e:
61
+ st.error(f"Ошибка: {e}")
62
+
63
+ # ==================== ОСНОВНОЙ КОД ====================
64
+ raw_cabinet = st.session_state.get("cabinet_final", "")
65
+ cabinet_label = raw_cabinet if raw_cabinet else "Общий_шкаф"
66
+ st.title(f"⚡ Расчет потребления: {cabinet_label}")
67
+
68
+ # --- ИНИЦИАЛИЗАЦИЯ И РАСЧЕТ МОДУЛЕЙ ---
69
+ if "manual_signals_counts" in st.session_state:
70
+ manual = st.session_state['manual_signals_counts']
71
+ init_rs, init_eth = manual.get("RS-485", 0), manual.get("ETH", 0)
72
+ init_ti, init_ts = manual.get("AI (TI)", 0), manual.get("DI (TS)", 0)
73
+ init_tu, init_tr = manual.get("DO (TU)", 0), manual.get("TR (AO)", 0)
74
+ init_reserve = st.session_state.get("reserve_val", 20)
75
+ st.toast("✅ Загружены данные с учетом ручной коррекции!", icon="✏️")
76
+ elif "res_list" in st.session_state and st.session_state.res_list:
77
+ init_rs = sum(r.counts.RS485 for r in st.session_state.res_list)
78
+ init_eth = sum(r.counts.ETH for r in st.session_state.res_list)
79
+ init_ti = sum(r.counts.TI for r in st.session_state.res_list)
80
+ init_ts = sum(r.counts.TS for r in st.session_state.res_list)
81
+ init_tu = sum(r.counts.TU for r in st.session_state.res_list)
82
+ init_tr = sum(r.counts.AO for r in st.session_state.res_list)
83
+ init_reserve = st.session_state.get("reserve_val", 20)
84
+ else:
85
+ init_rs, init_eth, init_ti, init_ts, init_tu, init_tr = 12, 2, 42, 102, 51, 0
86
+ init_reserve = 20
87
+
88
+ with st.expander("🛠 Настройки модулей (Канальность)", expanded=False):
89
+ c1, c2 = st.columns(2)
90
+ ti_ch = c1.number_input("Каналов ТИ", value=32)
91
+ ts_ch = c2.number_input("Каналов ТС", value=32)
92
+ tu_ch = c1.number_input("Каналов ТУ", value=32)
93
+ tr_ch = c2.number_input("Каналов TR", value=8)
94
+ rs_ch = c1.number_input("Каналов RS", value=8)
95
+
96
+ st.subheader("1. Сигналы")
97
+ col_l, col_r = st.columns([3, 1])
98
+ with col_r:
99
+ st.write("###")
100
+ reserve_pct = st.number_input("Резерв %", value=init_reserve, step=5)
101
+ with col_l:
102
+ signals_data = [
103
+ {"Тип": "RS-485", "Факт": init_rs}, {"Тип": "ETH", "Факт": init_eth},
104
+ {"Тип": "TI (AI)", "Факт": init_ti}, {"Тип": "TS (DI)", "Факт": init_ts},
105
+ {"Тип": "TU (DO)", "Факт": init_tu}, {"Тип": "TR (AO)", "Факт": init_tr},
106
+ ]
107
+ for item in signals_data:
108
+ item["Итого (с резервом)"] = math.ceil(item["Факт"] * (1 + reserve_pct / 100))
109
+ edited_signals = st.data_editor(pd.DataFrame(signals_data), hide_index=True, key="sig_edit")
110
+
111
+ final_signals = {row["Тип"]: row["Итого (с резервом)"] for _, row in edited_signals.iterrows()}
112
+ q_ti = math.ceil(final_signals.get("TI (AI)", 0) / ti_ch) if ti_ch else 0
113
+ q_ts = math.ceil(final_signals.get("TS (DI)", 0) / ts_ch) if ts_ch else 0
114
+ q_tu = math.ceil(final_signals.get("TU (DO)", 0) / tu_ch) if tu_ch else 0
115
+ q_tr = math.ceil(final_signals.get("TR (AO)", 0) / tr_ch) if tr_ch else 0
116
+ q_rs = math.ceil(final_signals.get("RS-485", 0) / rs_ch) if rs_ch else 0
117
+ total_io = q_ti + q_ts + q_tu + q_tr + q_rs
118
+ designation_io = f"A12...A{11 + total_io}" if total_io > 0 else ""
119
+
120
+ # ==================== ТАБЛИЦА ОБОРУДОВАНИЯ ====================
121
+ st.subheader("2. Спецификация оборудования")
122
+
123
+ current_project_id = st.session_state.get("last_filename", "")
124
+ saved_project_id = st.session_state.get("equip_rows_source_id", "")
125
+
126
+ if current_project_id != saved_project_id:
127
+ if "equip_rows" in st.session_state:
128
+ del st.session_state["equip_rows"]
129
+ st.session_state["equip_rows_source_id"] = current_project_id
130
+ st.toast("🔄 Таблица оборудования сброшена для нового проекта!", icon="🆕")
131
+
132
+ # --- ИНИЦИАЛИЗАЦИЯ ---
133
+ if "equip_rows" not in st.session_state or len(st.session_state.equip_rows) == 0:
134
+ st.session_state.equip_rows = [
135
+ {"Потребитель": "ПЛК (Master)", "Артикул": "ПР (Р02)", "P_шт": 3.0, "Q_внутр": 1, "Q_внешн": 0, "Q_спец": 0,
136
+ "Q_220": 0, "Коэф.": 1.0, "Это БП?": False, "ups": False, "input2": False},
137
+ {"Потребитель": "Блок питания осн.", "Артикул": "БП (Р01)", "P_шт": 2.0, "Q_внутр": 1, "Q_внешн": 0,
138
+ "Q_спец": 0, "Q_220": 0, "Коэф.": 1.0, "Это БП?": True, "ups": False, "input2": False},
139
+ {"Потребитель": "Модули ввода-вывода", "Артикул": designation_io, "P_шт": 3.0, "Q_внутр": total_io,
140
+ "Q_внешн": 0, "Q_спец": 0, "Q_220": 0, "Коэф.": 1.0, "Это БП?": False, "ups": False, "input2": False},
141
+ {"Потребитель": "Вентилятор", "Артикул": "NLV-2600", "P_шт": 50.0, "Q_внутр": 0, "Q_внешн": 0, "Q_спец": 0,
142
+ "Q_220": 1, "Коэф.": 1.0, "Это БП?": False, "ups": False, "input2": False},
143
+ {"Потребитель": "Светильник", "Артикул": "ДБО 3001", "P_шт": 4.0, "Q_внутр": 0, "Q_внешн": 0, "Q_спец": 0,
144
+ "Q_220": 1, "Коэф.": 1.0, "Это БП?": False, "ups": False, "input2": False},
145
+ {"Потребитель": "Розетка пользователя", "Артикул": "18012DEK", "P_шт": 0.0, "Q_внутр": 0, "Q_внешн": 0,
146
+ "Q_спец": 0, "Q_220": 1, "Коэф.": 1.0, "Это БП?": False, "ups": False, "input2": False},
147
+ ]
148
+
149
+ # === НАСТРОЙКА ТАБЛИЦЫ ===
150
+ has_input2_flag = st.session_state.get("has_input2", False)
151
+
152
+ for r in st.session_state.equip_rows:
153
+ if "input2" not in r: r["input2"] = False
154
+ if "Q_спец" not in r: r["Q_спец"] = 0
155
+
156
+ df_to_edit = pd.DataFrame(st.session_state.equip_rows)
157
+
158
+ try:
159
+ df_to_edit = df_to_edit.astype({
160
+ "P_шт": "float", "Q_внутр": "int", "Q_внешн": "int", "Q_спец": "int", "Q_220": "int",
161
+ "Коэф.": "float", "Это БП?": "bool", "ups": "bool", "input2": "bool"
162
+ })
163
+ except Exception:
164
+ pass
165
+
166
+ my_column_config = {
167
+ "Потребитель": st.column_config.TextColumn(required=True, width="medium"),
168
+ "P_шт": st.column_config.NumberColumn("P_1шт (Вт)", format="%.2f", width="small"),
169
+ "Q_внутр": st.column_config.NumberColumn("Q (Внутр)", min_value=0, step=1, help="Нагрузка 24В (Логика)"),
170
+ "Q_внешн": st.column_config.NumberColumn("Q (Внешн)", min_value=0, step=1, help="Нагрузка 24В (Датчики)"),
171
+ "Q_спец": st.column_config.NumberColumn("Q (Доп.)", min_value=0, step=1, help="Нагрузка 12В"),
172
+ "Q_220": st.column_config.NumberColumn("Q (220В)", min_value=0, step=1),
173
+ "Коэф.": st.column_config.NumberColumn("К спр.", step=0.05, format="%.2f", width="small"),
174
+ "Это БП?": st.column_config.CheckboxColumn("Это БП?", help="Источник Питания"),
175
+ "ups": st.column_config.CheckboxColumn("На ИБП?", help="Питается от ИБП"),
176
+ }
177
+ my_column_order = ["Потребитель", "Артикул", "P_шт", "Q_внутр", "Q_внешн", "Q_спец", "Q_220", "Коэф.", "Это БП?", "ups"]
178
+
179
+ if has_input2_flag:
180
+ my_column_config["input2"] = st.column_config.CheckboxColumn("На ��вод 2?", help="Питается от резерва")
181
+ my_column_order.append("input2")
182
+
183
+ st.info(
184
+ "ℹ️ Потребление Блоков Питания (DC) добавляется к ОБОИМ вводам (резервирование). Потребители 220В распределяются по галочке 'На Ввод 2'.")
185
+ edited_equip = st.data_editor(
186
+ df_to_edit,
187
+ num_rows="dynamic",
188
+ use_container_width=True,
189
+ column_config=my_column_config,
190
+ column_order=my_column_order,
191
+ key="equip_editor"
192
+ )
193
+
194
+ st.session_state.equip_rows = edited_equip.to_dict('records')
195
+
196
+ col_save, _ = st.columns([1, 4])
197
+ with col_save:
198
+ if st.button("💾 Сохранить устройство в db"):
199
+ current_db = load_db()
200
+ db_map = {(str(i.get("article", "")), str(i.get("name", ""))): i for i in current_db}
201
+ upd = 0
202
+ for _, row in edited_equip.iterrows():
203
+ name = str(row.get("Потребитель", "")).strip()
204
+ art = str(row.get("Артикул", "")).strip()
205
+ if not name: continue
206
+ try:
207
+ p_val = float(row.get("P_шт", 0))
208
+ except:
209
+ p_val = 0.0
210
+
211
+ key = (art, name)
212
+ new_item = {"name": name, "article": art, "power": p_val}
213
+
214
+ if key not in db_map or db_map[key]["power"] != p_val:
215
+ db_map[key] = new_item
216
+ upd += 1
217
+
218
+ if upd > 0:
219
+ save_db(list(db_map.values()))
220
+ st.toast(f"✅ Успешно! Обновлено: {upd} записей.")
221
+ else:
222
+ st.toast("Нет изменений для сохранения.")
223
+
224
+ # Меню добавления
225
+ with st.container():
226
+ db_data = load_db()
227
+ db_options = {f"{i['name']} ({i['article']}) — {i['power']}Вт": i for i in db_data}
228
+ c_a1, c_a2, c_a3 = st.columns([3, 1, 1])
229
+ sel_key = c_a1.selectbox("Поиск по базе:", [""] + list(db_options.keys()))
230
+ add_qty = c_a2.number_input("Кол-во", 1, value=1)
231
+ if c_a3.button("➕ Добавить") and sel_key:
232
+ item = db_options[sel_key]
233
+ nm_low = item["name"].lower()
234
+ is_psu = any(x in nm_low for x in ["ип ", "бп ", "питания"])
235
+ q_in, q_ext, q_sp, q_220 = 0, 0, 0, 0
236
+ if "вентилятор" in nm_low or "свет" in nm_low or "220" in nm_low or "розетка" in nm_low:
237
+ q_220 = add_qty
238
+ elif "датчик" in nm_low:
239
+ q_ext = add_qty
240
+ elif "12в" in nm_low or "tk16" in item.get("article", "").lower():
241
+ q_sp = add_qty
242
+ else:
243
+ q_in = add_qty
244
+
245
+ new_row = {
246
+ "Потребитель": item["name"], "Артикул": item["article"], "P_шт": float(item["power"]),
247
+ "Q_внутр": int(q_in), "Q_внешн": int(q_ext), "Q_спец": int(q_sp), "Q_220": int(q_220),
248
+ "Коэф.": 1.0, "Это БП?": is_psu, "ups": False, "input2": False
249
+ }
250
+ st.session_state.equip_rows.append(new_row)
251
+ st.rerun()
252
+
253
+ st.divider()
254
+
255
+
256
+ # ==================== ЛОГИКА РАСЧЕТА ====================
257
+ def safe_float(val):
258
+ try:
259
+ return float(val) if val is not None else 0.0
260
+ except:
261
+ return 0.0
262
+
263
+
264
+ col_eff, _ = st.columns([1, 3])
265
+ with col_eff:
266
+ efficiency = st.number_input("КПД Источников Питания (AC/DC)", min_value=0.5, max_value=1.0, value=0.92, step=0.01)
267
+
268
+ # Списки для суммирования
269
+ loads = {"Internal": 0.0, "External": 0.0, "Special": 0.0, "Mains": 0.0}
270
+ ups_groups_map = {"Internal": False, "External": False, "Special": False}
271
+ direct_ups_load_220 = 0.0
272
+ non_critical_220 = 0.0
273
+
274
+ mains_220_in1 = 0.0
275
+ mains_220_in2 = 0.0
276
+
277
+ psu_export_list = []
278
+ custom_220_list = []
279
+
280
+ # --- 1. ПЕРВЫЙ ПРОХОД: Считаем нагрузки (DC) и распределяем 220В ---
281
+ dc_loads_total = {"Internal": 0.0, "External": 0.0, "Special": 0.0}
282
+
283
+ for _, row in edited_equip.iterrows():
284
+ is_psu = row.get("Это БП?", False)
285
+ if is_psu: continue
286
+
287
+ p_unit = safe_float(row.get("P_шт"))
288
+ k_coef = safe_float(row.get("Коэф.")) or 1.0
289
+
290
+ # --- DC Нагрузка ---
291
+ val_in = p_unit * safe_float(row.get("Q_внутр")) * k_coef
292
+ val_ex = p_unit * safe_float(row.get("Q_внешн")) * k_coef
293
+ val_sp = p_unit * safe_float(row.get("Q_спец")) * k_coef
294
+
295
+ dc_loads_total["Internal"] += val_in
296
+ dc_loads_total["External"] += val_ex
297
+ dc_loads_total["Special"] += val_sp
298
+
299
+ loads["Internal"] += val_in
300
+ loads["External"] += val_ex
301
+ loads["Special"] += val_sp
302
+
303
+ # --- AC Нагрузка ---
304
+ q_220 = safe_float(row.get("Q_220"))
305
+ if q_220 > 0:
306
+ p_ac = p_unit * q_220 * k_coef
307
+ loads["Mains"] += p_ac
308
+
309
+ is_in2 = row.get("input2", False)
310
+ if is_in2:
311
+ mains_220_in2 += p_ac
312
+ else:
313
+ mains_220_in1 += p_ac
314
+
315
+ if row.get("ups", False):
316
+ direct_ups_load_220 += p_ac
317
+ else:
318
+ non_critical_220 += p_ac
319
+
320
+ custom_220_list.append({
321
+ "name": str(row.get("Потребитель", "")),
322
+ "power": p_ac,
323
+ "on_ups": row.get("ups", False),
324
+ "input_id": 2 if is_in2 else 1
325
+ })
326
+
327
+ # --- 2. РАСЧЕТ ИТОГОВОГО ПОТРЕБЛЕНИЯ ---
328
+ total_dc_watts = dc_loads_total["Internal"] + dc_loads_total["External"] + dc_loads_total["Special"]
329
+ psu_ac_consumption_total = total_dc_watts / efficiency if efficiency > 0 else 0
330
+
331
+ watts_input_1 = mains_220_in1 + psu_ac_consumption_total
332
+ watts_input_2 = mains_220_in2 + psu_ac_consumption_total if has_input2_flag else 0
333
+
334
+ # --- 3. ВТОРОЙ ПРОХОД: Формируем список БП ---
335
+ for _, row in edited_equip.iterrows():
336
+ if not row.get("Это БП?", False): continue
337
+
338
+ original_name = str(row.get("Потребитель", ""))
339
+ article_name = str(row.get("Артикул", ""))
340
+ is_in2 = row.get("input2", False)
341
+ on_ups = row.get("ups", False)
342
+
343
+ has_in = safe_float(row.get("Q_внутр")) > 0
344
+ has_ex = safe_float(row.get("Q_внешн")) > 0
345
+ has_sp = safe_float(row.get("Q_спец")) > 0
346
+
347
+ # ФОРМИРУЕМ ИМЯ
348
+ if has_sp:
349
+ export_name = "Питание терминального контроллера"
350
+ elif has_in and has_ex:
351
+ export_name = "Питание внутренних и внешних цепей шкафа"
352
+ elif has_ex:
353
+ export_name = "Питание внешних потребителей"
354
+ elif has_in:
355
+ export_name = "Питание внутренних цепей шкафа"
356
+ else:
357
+ # Если БП без нагрузки - добавляем артикул к имени для ясности
358
+ if article_name and article_name != "-":
359
+ export_name = f"{original_name} ({article_name})"
360
+ else:
361
+ export_name = original_name
362
+
363
+ current_psu_dc_load = 0.0
364
+ if has_in: current_psu_dc_load += dc_loads_total["Internal"]
365
+ if has_ex: current_psu_dc_load += dc_loads_total["External"]
366
+ if has_sp: current_psu_dc_load += dc_loads_total["Special"]
367
+
368
+ if on_ups:
369
+ if has_in: ups_groups_map["Internal"] = True
370
+ if has_ex: ups_groups_map["External"] = True
371
+ if has_sp: ups_groups_map["Special"] = True
372
+
373
+ volts = 12 if has_sp else 24
374
+
375
+ psu_export_list.append({
376
+ "name": export_name,
377
+ "dc_power": current_psu_dc_load,
378
+ "voltage_dc": volts,
379
+ "ac_power": 0,
380
+ "on_ups": on_ups,
381
+ "input_id": 2 if is_in2 else 1
382
+ })
383
+
384
+ # --- ВЫВОД РЕЗУЛЬТАТОВ ---
385
+ st.subheader("3. Расчет нагрузки по группам (DC)")
386
+ c1, c2, c3, c4 = st.columns(4)
387
+ c1.metric("Внутр (24В)", f"{loads['Internal']:.1f} Вт", delta="На ИБП" if ups_groups_map["Internal"] else "Мимо ИБП")
388
+ c2.metric("Внешн (24В)", f"{loads['External']:.1f} Вт", delta="На ИБП" if ups_groups_map["External"] else "Мимо ИБП")
389
+ c3.metric("Спец (12В)", f"{loads['Special']:.1f} Вт", delta="На ИБП" if ups_groups_map["Special"] else "Мимо ИБП")
390
+ c4.metric("Сеть (220В)", f"{loads['Mains']:.1f} Вт", f"UPS: {direct_ups_load_220:.1f} | Grid: {non_critical_220:.1f}")
391
+
392
+ # === БЛОК: Скачивание спецификации + ИТОГИ ===
393
+
394
+ st.write("###") # Отступ
395
+ if st.session_state.equip_rows:
396
+ # 1. Готовим данные таблицы
397
+ df_spec = pd.DataFrame(st.session_state.equip_rows).copy()
398
+
399
+ # Считаем общее кол-во и мощность
400
+ df_spec["Всего_шт"] = (df_spec.get("Q_внутр", 0) + df_spec.get("Q_внешн", 0) +
401
+ df_spec.get("Q_спец", 0) + df_spec.get("Q_220", 0))
402
+ df_spec["P_общ (Вт)"] = df_spec["P_шт"] * df_spec["Всего_шт"] * df_spec["Коэф."]
403
+
404
+ # --- ЗАМЕНА TRUE/FALSE НА ГАЛОЧКИ ---
405
+ bool_cols = ["Это БП?", "ups", "input2"]
406
+ for col in bool_cols:
407
+ if col in df_spec.columns:
408
+ df_spec[col] = df_spec[col].map({True: "✔", False: ""})
409
+
410
+ # Настройка названий
411
+ cols_export = {
412
+ "Потребитель": "Наименование",
413
+ "Артикул": "Артикул",
414
+ "P_шт": "P_ед (Вт)",
415
+ "Всего_шт": "Кол-во",
416
+ "P_общ (Вт)": "P_итог (Вт)",
417
+ "Q_внутр": "Внутр (24В)",
418
+ "Q_внешн": "Внешн (24В)",
419
+ "Q_спец": "Спец",
420
+ "Q_220": "220В",
421
+ "Это БП?": "Блок Питания",
422
+ "ups": "На ИБП",
423
+ "input2": "На Ввод 2"
424
+ }
425
+
426
+ available_cols = [c for c in cols_export.keys() if c in df_spec.columns]
427
+ df_final = df_spec[available_cols].rename(columns=cols_export)
428
+
429
+ # 2. Подготовка данных для ИТОГОВ
430
+ # Используем переменные loads и efficiency, которые уже посчитаны выше в коде
431
+ val_in = loads['Internal']
432
+ val_ex = loads['External']
433
+ val_psu_in = val_in / efficiency if efficiency > 0 else 0
434
+ val_psu_ex = val_ex / efficiency if efficiency > 0 else 0
435
+
436
+ # Общая мощность (220В + вся DC через КПД)
437
+ val_total_cab = loads['Mains'] + (loads['Internal'] + loads['External'] + loads['Special']) / efficiency
438
+
439
+ summary_data = [
440
+ ["--- ИТОГИ ---", ""],
441
+ ["Нагрузка Внутр (24В)", round(val_in, 2)],
442
+ ["Нагрузка Внешн (24В)", round(val_ex, 2)],
443
+ [f"Потребление БП Внутр (КПД {efficiency})", round(val_psu_in, 2)],
444
+ [f"Потребление БП Внешн (КПД {efficiency})", round(val_psu_ex, 2)],
445
+ ["ОБЩИЙ ВВОД 220В", round(val_total_cab, 2)]
446
+ ]
447
+
448
+ # 3. Создаем Excel
449
+ bio_spec = BytesIO()
450
+ with pd.ExcelWriter(bio_spec, engine='openpyxl') as writer:
451
+ # Записываем основную таблицу
452
+ df_final.to_excel(writer, index=False, sheet_name="Спецификация")
453
+
454
+ ws = writer.sheets["Спецификация"]
455
+ from openpyxl.styles import Alignment, Font
456
+
457
+ # Настройка ширины
458
+ ws.column_dimensions['A'].width = 40
459
+ ws.column_dimensions['B'].width = 20
460
+
461
+ # Центрируем галочки
462
+ for row in ws.iter_rows(min_row=2, max_row=len(df_final) + 1):
463
+ for cell in row:
464
+ if cell.column > 2:
465
+ cell.alignment = Alignment(horizontal='center')
466
+
467
+ # --- ДОПИСЫВАЕМ ИТОГИ ВНИЗУ ---
468
+ start_row = len(df_final) + 3 # Отступаем 2 строки вниз
469
+
470
+ for i, (label, value) in enumerate(summary_data):
471
+ current_row = start_row + i
472
+
473
+ # Пишем название в колонку A
474
+ cell_name = ws.cell(row=current_row, column=1, value=label)
475
+ cell_name.font = Font(bold=True)
476
+
477
+ # Пишем значение в колонку E (P_итог) - чтобы было наглядно
478
+ # Колонка E это 5-я по счету
479
+ ws.cell(row=current_row, column=5, value=value)
480
+
481
+ # 4. Кнопка
482
+ st.download_button(
483
+ label="📥 Скачать спецификацию оборудования (.xlsx)",
484
+ data=bio_spec.getvalue(),
485
+ file_name=f"Spec_{cabinet_label}.xlsx",
486
+ mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
487
+ )
488
+ # ===========================================
489
+ st.markdown("---")
490
+
491
+ # ==================== БЛОК РАСЧЕТА ИБП ====================
492
+ # 1. Инициализация переменных (чтобы не было ошибки NameError)
493
+ ups_dc_demand = 0.0
494
+ ups_charge_power = 0.0
495
+ ups_input_normal = 0.0
496
+ ups_input_max = 0.0
497
+
498
+
499
+
500
+ # 2. Сбор нагрузки по группам
501
+ if ups_groups_map["Internal"]: ups_dc_demand += loads["Internal"]
502
+ if ups_groups_map["External"]: ups_dc_demand += loads["External"]
503
+ if ups_groups_map["Special"]: ups_dc_demand += loads["Special"]
504
+
505
+ ups_load_from_psus = ups_dc_demand / efficiency if efficiency > 0 else 0
506
+ ups_inverter_load = direct_ups_load_220 + ups_load_from_psus
507
+
508
+ # 3. ЕСЛИ ЕСТЬ НАГРУЗКА -> РАСЧЕТ
509
+ ups_vars = {
510
+ "t_req": 0, "u_sys": 0, "eff": 0, "k_temp": 0, "k_depth": 0,
511
+ "c_req": 0, "c_act": 0, "bat_qty": 0, "bat_nom": 0,
512
+ "t_fact_str": "", "p_norm": 0, "p_charge": 0, "p_max": 0
513
+ }
514
+
515
+ if ups_inverter_load > 0:
516
+ st.subheader("4. Подробный расчет ИБП и АКБ")
517
+ st.markdown("#### А. Определение нагрузки на инвертор")
518
+ st.latex(r"P_{Inv} = P_{220(UPS)} + \frac{P_{DC(UPS)}}{\eta_{PSU}}")
519
+ st.info(f"⚡ **Расчетная нагрузка на инвертор:** {ups_inverter_load:.2f} Вт")
520
+
521
+ # Ввод параметров
522
+ c_u1, c_u2, c_u3 = st.columns(3)
523
+ t_autonomy_req = c_u1.number_input("Время автономии (мин)", value=60, step=10)
524
+ u_bat_sys = c_u2.selectbox("Напряжение АКБ (U_sys)", [12, 24, 36, 48, 72, 96], index=2)
525
+ k_inv_eff = c_u3.number_input("КПД ИБП (Inv)", 0.8, 1.0, 0.90, step=0.01)
526
+
527
+ with st.expander("Коэф. АКБ", expanded=False):
528
+ c_k1, c_k2 = st.columns(2)
529
+ k_temp = c_k1.number_input("K_temp", 1.0, 1.5, 1.2)
530
+ k_depth = c_k2.number_input("K_depth", 0.5, 1.0, 0.7)
531
+
532
+ # 1. Расчет требуемой емкости
533
+ hours_req = t_autonomy_req / 60
534
+ st.markdown("**1. Рас��ет требуемой емкости:**")
535
+ st.latex(r"C_{req} = \frac{P_{Inv} \times T_{hours} \times K_{temp}}{U_{sys} \times \eta_{Inv} \times K_{depth}}")
536
+ capacity_needed = (ups_inverter_load * hours_req * k_temp) / (u_bat_sys * k_inv_eff * k_depth)
537
+ st.write(f"📉 Требуемая емкость: **{capacity_needed:.2f} Ач**")
538
+
539
+ col_b1, col_b2 = st.columns(2)
540
+ with col_b1:
541
+ st.write("**2. Выбор батарей:**")
542
+ bat_nominal = st.selectbox("Номинал АКБ (Ач)", [4.5, 5, 7, 9, 12, 17, 26, 40, 55, 100], index=3)
543
+ series_qty = int(u_bat_sys / 12)
544
+ parallel_qty = math.ceil(capacity_needed / bat_nominal)
545
+ total_bat_qty = series_qty * parallel_qty
546
+ actual_capacity_system = parallel_qty * bat_nominal
547
+
548
+ st.success(
549
+ f"🔋 Конфигурация: {total_bat_qty} шт. ({series_qty} послед. x {parallel_qty} паралл.) = {actual_capacity_system} Ач")
550
+
551
+ if st.button("➕ Добавить ИБП и АКБ в таблицу"):
552
+ st.session_state.equip_rows.append(
553
+ {"Потребитель": f"ИБП (Вх. макс)", "Артикул": "UPS", "P_шт": 0.0, "Q_внутр": 0, "Q_внешн": 0,
554
+ "Q_спец": 0, "Q_220": 1, "Коэф.": 1.0, "Это БП?": False, "ups": False, "input2": False})
555
+ st.session_state.equip_rows.append(
556
+ {"Потребитель": f"АКБ 12В {bat_nominal}Ач", "Артикул": "BAT", "P_шт": 0.0, "Q_внутр": 0, "Q_внешн": 0,
557
+ "Q_спец": 0, "Q_220": total_bat_qty, "Коэф.": 1.0, "Это БП?": False, "ups": False, "input2": False})
558
+ st.rerun()
559
+
560
+ with col_b2:
561
+ st.write("**3. Энергобаланс:**")
562
+ ups_input_normal = ups_inverter_load / k_inv_eff
563
+ charge_current = actual_capacity_system * 0.15
564
+ ups_charge_power = (u_bat_sys * charge_current) / 0.85
565
+ ups_input_max = ups_input_normal + ups_charge_power
566
+
567
+ st.latex(r"P_{Charge} = \frac{U_{sys} \times C_{act} \times 0.15}{0.85}")
568
+ st.write(f"Мощность заряда: **{ups_charge_power:.2f} Вт**")
569
+
570
+ # Расчет фактического времени (для Excel)
571
+ t_fact_hours = (actual_capacity_system * u_bat_sys * k_inv_eff * k_depth) / (ups_inverter_load * k_temp)
572
+ h_disp = int(t_fact_hours)
573
+ m_disp = int((t_fact_hours - h_disp) * 60)
574
+ t_fact_str = f"{h_disp}ч {m_disp}мин"
575
+ st.caption(f"Фактическое время автономии: {t_fact_str}")
576
+
577
+ # Сохраняем данные для Excel
578
+ ups_vars = {
579
+ "t_req": t_autonomy_req, "u_sys": u_bat_sys, "eff": k_inv_eff,
580
+ "k_temp": k_temp, "k_depth": k_depth,
581
+ "c_req": capacity_needed, "c_act": actual_capacity_system,
582
+ "bat_qty": total_bat_qty, "bat_nom": bat_nominal,
583
+ "t_fact_str": t_fact_str,
584
+ "p_norm": ups_input_normal, "p_charge": ups_charge_power, "p_max": ups_input_max
585
+ }
586
+ else:
587
+ st.info("Нет нагрузки на ИБП.")
588
+
589
+ st.divider()
590
+ st.subheader("5. Итоговое потребление (Вводные автоматы)")
591
+
592
+ final_cabinet_max = watts_input_1 + ups_charge_power
593
+
594
+ c_fin1, c_fin2 = st.columns(2)
595
+ with c_fin1:
596
+ st.write(f"### Ввод 1 (Основной)")
597
+ st.latex(r"P_{In1} = \sum P_{220(In1)} + P_{PSU(Total)}")
598
+ st.write(f"= {mains_220_in1:.2f} + {psu_ac_consumption_total:.2f}")
599
+ st.success(f"Рабочий ток: **{watts_input_1:.2f} Вт**")
600
+ if ups_charge_power > 0:
601
+ st.warning(f"Макс. (с зарядом АКБ): **{final_cabinet_max:.2f} Вт**")
602
+
603
+ with c_fin2:
604
+ if has_input2_flag:
605
+ st.write(f"### Ввод 2 (Резерв)")
606
+ st.latex(r"P_{In2} = \sum P_{220(In2)} + P_{PSU(Total)}")
607
+ st.write(f"= {mains_220_in2:.2f} + {psu_ac_consumption_total:.2f}")
608
+ st.success(f"Рабочий ток: **{watts_input_2:.2f} Вт**")
609
+ else:
610
+ st.caption("Ввод 2 не используется")
611
+
612
+ # Сохранение данных для файла 3
613
+ st.session_state['summary_data'] = {
614
+ "total_input_1": watts_input_1,
615
+ "max_input_1": final_cabinet_max,
616
+ "total_input_2": watts_input_2,
617
+ "has_input2": has_input2_flag,
618
+ "psu_list": psu_export_list,
619
+ "custom_220_list": custom_220_list,
620
+ "cabinet_name": cabinet_label,
621
+ "ups_charge_power": ups_charge_power,
622
+ "ups_input_normal": ups_input_normal, # Передаем потребление ИБП в норм. режиме
623
+ "ups_input_max": ups_input_max # Передаем потребление ИБП в макс. режиме
624
+ }
625
+ st.success("✅ Данные сохранены для экспорта.")
626
+
627
+ # ==================== ЭКСПОРТ В EXCEL (ПОДРОБНЫЙ) ====================
628
+ st.markdown("---")
629
+ st.subheader("7. Выгрузка отчета")
630
+
631
+ if st.button("📥 Скачать полный расчет (.xlsx)"):
632
+ report_data = []
633
+
634
+
635
+ # Функция-помощник для добавления строк
636
+ def add_row(param, val, unit, comment=""):
637
+ report_data.append({
638
+ "Параметр": param,
639
+ "Значение": val,
640
+ "Ед.изм": unit,
641
+ "Формула / Комментарий": comment
642
+ })
643
+
644
+
645
+ # === БЛОК 1: ИСХОДНЫЕ ДАННЫЕ ===
646
+ add_row("ИСХОДНЫЕ ДАННЫЕ", "", "", "")
647
+ add_row("Нагрузка 24В (Внутр)", round(loads["Internal"], 2), "Вт", "Логика, модули")
648
+ add_row("Нагрузка 24В (Внешн)", round(loads["External"], 2), "Вт", "Датчики")
649
+ add_row("Нагрузка 220В (Прямая)", round(loads["Mains"], 2), "Вт", "Вент, розетки")
650
+ add_row("КПД Блоков питания", efficiency, "о.е.", "")
651
+ add_row("", "", "", "") # Пустая строка
652
+
653
+ # === БЛОК 2: РАСЧЕТ ИБП ===
654
+ if ups_inverter_load > 0:
655
+ add_row("РАСЧЕТ ИБП И АКБ", "", "", "")
656
+ add_row("Нагрузка на инвертор (P_inv)", round(ups_inverter_load, 2), "Вт", "P_220(ups) + P_24(ups)/Efficiency")
657
+ add_row("Время автономии (T_req)", ups_vars["t_req"], "мин", "Задание")
658
+ add_row("Напряжение АКБ (U_sys)", ups_vars["u_sys"], "В", "")
659
+ add_row("КПД Инвертора (Eff)", ups_vars["eff"], "о.е.", "")
660
+ add_row("Коэф. глубины разряда (K_depth)", ups_vars["k_depth"], "о.е.", "")
661
+ add_row("Коэф. запаса (K_temp)", ups_vars["k_temp"], "о.е.", "")
662
+
663
+ add_row("Требуемая емкость (C_req)", round(ups_vars["c_req"], 2), "Ач",
664
+ "(P_inv * T_h * K_temp) / (U_sys * Eff * K_depth)")
665
+ add_row("Выбранная емкость (C_act)", ups_vars["c_act"], "Ач",
666
+ f"{ups_vars['bat_qty']} шт по {ups_vars['bat_nom']} Ач")
667
+ add_row("Фактическое время (T_fact)", ups_vars["t_fact_str"], "",
668
+ "(C_act * U_sys * Eff * K_depth) / (P_inv * K_temp)")
669
+
670
+ add_row("", "", "", "") # Пустая строка
671
+
672
+ add_row("ЭНЕРГОБАЛАНС", "", "", "")
673
+ add_row("Потребление ИБП (Норма)", round(ups_vars["p_norm"], 2), "Вт", "P_inv / Eff_inv")
674
+ add_row("Мощность заряда (P_charge)", round(ups_vars["p_charge"], 2), "Вт", "(U_sys * C_act * 0.15) / 0.85")
675
+ add_row("Потребление ИБП (Макс)", round(ups_vars["p_max"], 2), "Вт", "P_norm + P_charge")
676
+ add_row("", "", "", "") # Пустая строка
677
+
678
+ # === БЛОК 3: ИТОГИ ===
679
+ add_row("ИТОГИ ПО ШКАФУ", "", "", "")
680
+
681
+ # Ввод 1
682
+ add_row("Ввод 1 (Рабочий ток)", round(watts_input_1, 2), "Вт", "Вся нагрузка + потери КПД")
683
+ if ups_charge_power > 0:
684
+ add_row("Ввод 1 (Для автомата)", round(final_cabinet_max, 2), "Вт", "С учетом заряда АКБ")
685
+
686
+ # Ввод 2 (если есть)
687
+ if has_input2_flag:
688
+ add_row("Ввод 2 (Резерв)", round(watts_input_2, 2), "Вт", "Резервирование нагрузки")
689
+
690
+ # Генерация Excel
691
+ df_rep = pd.DataFrame(report_data)
692
+ bio_rep = BytesIO()
693
+ with pd.ExcelWriter(bio_rep, engine='openpyxl') as writer:
694
+ df_rep.to_excel(writer, index=False, sheet_name="Calculation")
695
+
696
+ # Наводим красоту
697
+ ws = writer.sheets["Calculation"]
698
+ from openpyxl.styles import Font, Border, Side
699
+
700
+ # Ширина колонок
701
+ ws.column_dimensions['A'].width = 35
702
+ ws.column_dimensions['B'].width = 15
703
+ ws.column_dimensions['C'].width = 10
704
+ ws.column_dimensions['D'].width = 60
705
+
706
+ # Жирный шрифт для заголовков разделов-
707
+ for row in ws.iter_rows():
708
+ cell_param = row[0]
709
+ if cell_param.value in ["ИСХОДНЫЕ ДАННЫЕ", "РАСЧЕТ ИБП И АКБ", "ЭНЕРГОБАЛАНС", "ИТОГИ ПО ШКАФУ"]:
710
+ cell_param.font = Font(bold=True)
711
+
712
+ st.download_button("Скачать файл (.xlsx)", data=bio_rep.getvalue(), file_name=f"Calculation_{cabinet_label}.xlsx")
pages/3_📝_SingleLine.py ADDED
@@ -0,0 +1,199 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import pandas as pd
3
+ import os
4
+ from io import BytesIO
5
+
6
+ st.set_page_config(page_title="Генерация таблицы Eplan", layout="wide")
7
+ st.title("📑 Генерация данных для Eplan")
8
+
9
+ # ==================== 1. ЗАГРУЗКА ДАННЫХ ====================
10
+ if "summary_data" not in st.session_state:
11
+ st.warning("⚠️ Нет данных для расчета! Сначала выполните расчет на странице 'Hardware' и нажмите 'Сохранить'.")
12
+ st.stop()
13
+
14
+ data = st.session_state['summary_data']
15
+ cabinet_name = data.get("cabinet_name", "Шкаф")
16
+ st.info(f"Активный проект: **{cabinet_name}**")
17
+
18
+ # ==================== 2. НАСТРОЙКИ МАКРОСОВ ====================
19
+ with st.expander("⚙️ Настройки макросов", expanded=False):
20
+ base_macro_path = st.text_input("Базовая папка:", value=r"C:\Eplan_DB\Data\Макросы\Однолинейные схемы")
21
+ col1, col2 = st.columns(2)
22
+ with col1:
23
+ f_in = st.text_input("Ввод (XT)", "XT.ema")
24
+ f_qs = st.text_input("Автомат (QS)", "QF.ema")
25
+ with col2:
26
+ f_psu = st.text_input("Блок питания (PSU)", "Fus_24.ems")
27
+ f_light = st.text_input("Свет/Вент", "Lamp_fan.ems")
28
+ f_generic = st.text_input("Устройства", "Gen_Device.ems")
29
+
30
+
31
+ def get_path(filename):
32
+ return os.path.join(base_macro_path, filename) if filename else ""
33
+
34
+
35
+ u_ac = 230
36
+
37
+ # ==================== 3. ПОДГОТОВКА ДАННЫХ ====================
38
+ p_charge = data.get('ups_charge_power', 0)
39
+ val_in1_norm = data.get('total_input_1', 0)
40
+ val_in1_max = data.get('max_input_1', 0)
41
+ val_in2 = data.get('total_input_2', 0)
42
+
43
+ # ДАННЫЕ ПО ИБП ИЗ ФАЙЛА 2
44
+ ups_val_norm = data.get('ups_input_normal', 0)
45
+ ups_val_max = data.get('ups_input_max', 0)
46
+
47
+ has_in2 = data.get('has_input2', False)
48
+
49
+ psu_items = data.get('psu_list', [])
50
+ custom_items = data.get('custom_220_list', [])
51
+
52
+
53
+ # Функции форматирования
54
+ def fmt_p(val_normal, val_max=None, placeholder=False):
55
+ if not val_normal or val_normal <= 0:
56
+ return "ХХ Вт" if placeholder else ""
57
+
58
+ base = f"{int(val_normal)} Вт"
59
+ if val_max and (val_max - val_normal) > 5:
60
+ return f"{base}\n{int(val_max)} Вт (при заряде АКБ)"
61
+ return base
62
+
63
+
64
+ def fmt_iu(watts, volts, suffix, watts_max=None, placeholder=False):
65
+ if not watts or watts <= 0:
66
+ return "ХХ-ХХ" if placeholder else ""
67
+
68
+ amp = watts / volts
69
+ base = f"{amp:.2f}A / {volts}В {suffix}"
70
+ if watts_max and (watts_max - watts) > 5:
71
+ amp_max = watts_max / volts
72
+ return f"{base}\n{amp_max:.2f} А (макс)"
73
+ return base
74
+
75
+
76
+ # ==================== 4. СБОРКА ТАБЛИЦЫ ====================
77
+ structure = []
78
+
79
+ # 1. ВВОД 1
80
+ structure.append({
81
+ "Установка": "Вводные клеммы", "Устройство": "Ввод питания 230В (Ввод 1)",
82
+ "I-расч./U-ном.": fmt_iu(val_in1_norm, u_ac, "AC", val_in1_max),
83
+ "P установочное": fmt_p(val_in1_norm, val_in1_max),
84
+ "Макросы": get_path(f_in)
85
+ })
86
+
87
+ # 1.1 ВВОД 2 (Если есть)
88
+ if has_in2:
89
+ structure.append({
90
+ "Установка": "Вводные клеммы", "Устройство": "Ввод питания 230В (Ввод 2)",
91
+ "I-расч./U-ном.": fmt_iu(val_in2, u_ac, "AC"),
92
+ "P установочное": fmt_p(val_in2),
93
+ "Макросы": get_path(f_in)
94
+ })
95
+
96
+ structure.append({"Установка": "Вводные автоматы", "Устройство": "", "I-расч./U-ном.": "", "P установочное": "",
97
+ "Макросы": get_path(f_qs)})
98
+ structure.append({"Установка": "Распред.Автоматы шкафа", "Устройство": "", "I-расч./U-ном.": "", "P установочное": "",
99
+ "Макросы": get_path(f_qs)})
100
+
101
+ # 2. Блоки Питания
102
+ for psu in psu_items:
103
+ name = psu['name']
104
+ dc_p = psu.get('dc_power', 0)
105
+ volts = psu['voltage_dc']
106
+ i_dc = dc_p / volts if volts else 0
107
+
108
+ structure.append({
109
+ "Установка": "Оборудование шкафа",
110
+ "Устройство": name,
111
+ "I-расч./U-ном.": f"{i_dc:.2f}A / {volts}В DC",
112
+ "P установочное": fmt_p(dc_p),
113
+ "Макросы": get_path(f_psu)
114
+ })
115
+
116
+ # 3. Устройства 220В
117
+ for item in custom_items:
118
+ name = item['name']
119
+ p_val = item['power']
120
+
121
+ # ПРОВЕРКА НА ИБП
122
+ is_ups_device = any(k in name.lower() for k in ["ибп", "ups", "sr1101"])
123
+
124
+ # Если это ИБП, подставляем расчетные данные из Файла 2
125
+ if is_ups_device:
126
+ p_show = ups_val_norm
127
+ p_max_show = ups_val_max
128
+ use_place = False
129
+ else:
130
+ p_show = p_val
131
+ p_max_show = None
132
+ # Если мощность 0 (как у розетки), включаем плейсхолдер
133
+ use_place = True if p_val == 0 else False
134
+
135
+ if any(x in name.lower() for x in ["свет", "вент", "розет"]):
136
+ macro = get_path(f_light)
137
+ else:
138
+ macro = get_path(f_generic)
139
+
140
+ structure.append({
141
+ "Установка": "Оборудование шкафа",
142
+ "Устройство": name,
143
+ "I-расч./U-ном.": fmt_iu(p_show, u_ac, "AC", p_max_show, placeholder=use_place),
144
+ "P установочное": fmt_p(p_show, p_max_show, placeholder=use_place),
145
+ "Макросы": macro
146
+ })
147
+
148
+ # Пустые строки
149
+ structure.append(
150
+ {"Установка": "Р установочное", "Устройство": "", "I-расч./U-ном.": "", "P установочное": "", "Макросы": ""})
151
+ structure.append(
152
+ {"Установка": "I-расч./U-ном.", "Устройство": "", "I-расч./U-ном.": "", "P установочное": "", "Макросы": ""})
153
+ structure.append(
154
+ {"Установка": "Устройство", "Устройство": "", "I-расч./U-ном.": "", "P установочное": "", "Макросы": ""})
155
+
156
+ # Создание DataFrame
157
+ df_initial = pd.DataFrame(structure)
158
+ if df_initial.empty:
159
+ df_initial = pd.DataFrame(columns=["Установка", "Устройство", "I-расч./U-ном.", "P установочное", "Макросы"])
160
+ else:
161
+ df_initial = df_initial[["Установка", "Устройство", "I-расч./U-ном.", "P установочное", "Макросы"]]
162
+
163
+ # ==================== 5. ВЫВОД- ====================
164
+ st.subheader("Предварительный просмотр (Можно добавлять строки 👇)")
165
+
166
+ edited_df = st.data_editor(
167
+ df_initial,
168
+ use_container_width=True,
169
+ hide_index=True,
170
+ num_rows="dynamic",
171
+ column_config={
172
+ "Установка": st.column_config.TextColumn("Установка", width="medium"),
173
+ "Устройство": st.column_config.TextColumn("Устройство", width="large"),
174
+ "Макросы": st.column_config.TextColumn("Макросы", width="large"),
175
+ }
176
+ )
177
+
178
+ st.divider()
179
+ if st.button("📥 Скачать Excel (.xlsx)", type="primary"):
180
+ bio = BytesIO()
181
+ with pd.ExcelWriter(bio, engine='openpyxl') as writer:
182
+ edited_df.to_excel(writer, index=False, sheet_name="Generate_Data")
183
+
184
+ ws = writer.sheets["Generate_Data"]
185
+ from openpyxl.styles import Alignment, Font
186
+
187
+ ws.column_dimensions['A'].width = 25
188
+ ws.column_dimensions['B'].width = 40
189
+ ws.column_dimensions['C'].width = 30
190
+ ws.column_dimensions['D'].width = 30
191
+ ws.column_dimensions['E'].width = 60
192
+
193
+ for row in ws.iter_rows(min_row=1):
194
+ for cell in row:
195
+ cell.alignment = Alignment(wrap_text=True, vertical='center', horizontal='left')
196
+ if cell.row == 1:
197
+ cell.font = Font(bold=True)
198
+
199
+ st.download_button("Скачать файл", data=bio.getvalue(), file_name=f"Eplan_{cabinet_name}.xlsx")