Skip to content

Commit bf59c14

Browse files
committed
add CV to parsing
1 parent 16ecc44 commit bf59c14

File tree

2 files changed

+111
-25
lines changed

2 files changed

+111
-25
lines changed

src/constants.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export const TERNOPIL_JSON_URL = "https://raw.githubusercontent.com/yaroslav2901
2727
export const ZAKARPATTIA_JSON_URL = "https://raw.githubusercontent.com/yaroslav2901/OE_OUTAGE_DATA/main/data/Zakarpattiaoblenerho.json"
2828
export const ZAPORIZHZHIA_JSON_URL = "https://raw.githubusercontent.com/yaroslav2901/OE_OUTAGE_DATA/main/data/Zaporizhzhiaoblenergo.json"
2929
export const ZHYTOMYR_JSON_URL = "https://raw.githubusercontent.com/yaroslav2901/OE_OUTAGE_DATA/main/data/Zhytomyroblenergo.json"
30+
export const CHERNIVTSI_URL = "https://oblenergo.cv.ua/shutdowns/"
3031
export const YASNO_KYIV_URL = "https://app.yasno.ua/api/blackout-service/public/shutdowns/regions/25/dsos/902/planned-outages"
3132
export const YASNO_DNIPRO_DNEM_URL = "https://app.yasno.ua/api/blackout-service/public/shutdowns/regions/3/dsos/301/planned-outages"
3233
export const YASNO_DNIPRO_CEK_URL = "https://app.yasno.ua/api/blackout-service/public/shutdowns/regions/3/dsos/303/planned-outages"

src/monitor.js

Lines changed: 110 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ import {
1919
ZHYTOMYR_JSON_URL,
2020
YASNO_KYIV_URL,
2121
YASNO_DNIPRO_DNEM_URL,
22-
YASNO_DNIPRO_CEK_URL
22+
YASNO_DNIPRO_CEK_URL,
23+
CHERNIVTSI_URL
2324
} from "./constants.js"
2425

2526
// --- КОНФІГУРАЦІЯ РЕГІОНІВ (ДТЕК - ОБЛАСТІ) ---
@@ -105,8 +106,8 @@ async function getDtekRegionInfo(browser, config) {
105106
// 🔥 ШУКАЄМО ТЕКСТ У МОДАЛКАХ (для Одеси та інших)
106107
const modals = document.querySelectorAll('.modal-content, .popup-content, [role="dialog"], .modal-body');
107108
modals.forEach(m => {
108-
// Беремо текст, якщо елемент існує і хоч трохи схожий на видимий
109-
if (m.innerText) fullText += " " + m.innerText;
109+
// Беремо текст, якщо елемент існує і хоч трохи схожий на видимий
110+
if (m.innerText) fullText += " " + m.innerText;
110111
});
111112

112113
const text = fullText.toLowerCase();
@@ -150,11 +151,11 @@ async function getDtekRegionInfo(browser, config) {
150151
// Спроба закрити модалку, щоб вона не блокувала отримання токенів (опціонально)
151152
try {
152153
await page.evaluate(() => {
153-
const closeBtn = document.querySelector('.modal .close, [data-dismiss="modal"], .btn-close');
154-
if (closeBtn) closeBtn.click();
154+
const closeBtn = document.querySelector('.modal .close, [data-dismiss="modal"], .btn-close');
155+
if (closeBtn) closeBtn.click();
155156
});
156157
await sleep(1000);
157-
} catch(e) {}
158+
} catch (e) { }
158159

159160
// Чекаємо на CSRF токен
160161
const csrfTokenTag = await page.waitForSelector('meta[name="csrf-token"]', { state: "attached", timeout: 15000 });
@@ -226,6 +227,75 @@ async function getYasnoData(url, label) {
226227
}
227228
}
228229

230+
// 5. ЧЕРНІВЦІ (Playwright)
231+
async function getChernivtsiData(browser) {
232+
const MAX_RETRIES = 3;
233+
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
234+
const context = await browser.newContext({
235+
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
236+
});
237+
const page = await context.newPage();
238+
try {
239+
console.log(`🌍 Visiting Chernivtsi (Attempt ${attempt})...`);
240+
await page.goto(CHERNIVTSI_URL, { waitUntil: 'domcontentloaded', timeout: 60000 });
241+
await sleep(5000);
242+
243+
const schedule = await page.evaluate(() => {
244+
const dateEl = document.querySelector('#gsv_t b');
245+
if (!dateEl) return null;
246+
247+
// Format: 30.01.2026
248+
const dateParts = dateEl.innerText.trim().split('.');
249+
if (dateParts.length !== 3) return null;
250+
const dateStr = `${dateParts[2]}-${dateParts[1]}-${dateParts[0]}`; // 2026-01-30
251+
252+
const rows = document.querySelectorAll('#gsv .scrollable div[id^="inf"]');
253+
const map = {};
254+
255+
rows.forEach(row => {
256+
const queueId = row.getAttribute('data-id');
257+
if (!queueId) return;
258+
259+
// Checking for the active container inside the row
260+
const cellContainer = row.querySelector('o.active');
261+
if (!cellContainer) return;
262+
263+
const cells = Array.from(cellContainer.children);
264+
if (cells.length < 48) return; // Expecting at least 48 slots
265+
266+
const dailySchedule = {};
267+
cells.forEach((cell, i) => {
268+
if (i >= 48) return;
269+
const hour = Math.floor(i / 2);
270+
const min = (i % 2 === 0) ? "00" : "30";
271+
const timeKey = `${String(hour).padStart(2, '0')}:${min}`;
272+
273+
const txt = cell.innerText.trim();
274+
// В = Відключено (2), МЗ = Можливо заживлено -> Відключено (2), З = Заживлено (1)
275+
let status = 1;
276+
if (txt === 'В' || txt === 'МЗ') status = 2;
277+
278+
dailySchedule[timeKey] = status;
279+
});
280+
281+
if (!map[queueId]) map[queueId] = {};
282+
map[queueId][dateStr] = dailySchedule;
283+
});
284+
return map;
285+
});
286+
287+
await context.close();
288+
return schedule;
289+
290+
} catch (e) {
291+
console.warn(`⚠️ Error scraping Chernivtsi: ${e.message}`);
292+
await context.close();
293+
if (attempt === MAX_RETRIES) return null;
294+
await sleep(3000);
295+
}
296+
}
297+
}
298+
229299
// --- ТРАНСФОРМАЦІЇ ---
230300

231301
// 🔥 ОНОВЛЕНА ЛОГІКА ДЛЯ ПОЛТАВИ ТА ІНШИХ JSON 🔥
@@ -262,44 +332,44 @@ function transformToSvitloFormat(dtekRaw) {
262332
let val30 = 1; // 1 = Є світло
263333

264334
switch (status) {
265-
case "yes":
266-
val00 = 1; val30 = 1;
335+
case "yes":
336+
val00 = 1; val30 = 1;
267337
break;
268-
269-
case "no":
338+
339+
case "no":
270340
val00 = 2; val30 = 2; // 2 = Немає світла
271341
break;
272-
342+
273343
// --- Точні відключення (без "m") - це точно НЕМАЄ ---
274344
case "first": // Немає 00-30
275-
val00 = 2; val30 = 1;
345+
val00 = 2; val30 = 1;
276346
break;
277-
347+
278348
case "second": // Немає 30-60
279-
val00 = 1; val30 = 2;
349+
val00 = 1; val30 = 2;
280350
break;
281351

282352
// --- Сірі зони (з "m") - вважаємо, що світло Є (1) ---
283-
284-
case "mfirst":
353+
354+
case "mfirst":
285355
// "Можливе 1-ша половина". Вважаємо як Є (1).
286356
// Навіть якщо до цього було "no", mfirst означає початок слота зі світлом.
287-
val00 = 1; val30 = 1;
357+
val00 = 1; val30 = 1;
288358
break;
289359

290360
case "msecond":
291361
// "Можливе 2-га половина".
292362
// Друга половина (30-60) - це сіра зона, тому вважаємо Є (1).
293-
val30 = 1;
294-
363+
val30 = 1;
364+
295365
// Перша половина (00-30) залежить від попередньої години:
296366
if (prevStatus === "no") {
297-
// Якщо минула година була "чорна", то перші 30 хв поточної -
298-
// це гарантоване продовження відключення.
299-
val00 = 2;
367+
// Якщо минула година була "чорна", то перші 30 хв поточної -
368+
// це гарантоване продовження відключення.
369+
val00 = 2;
300370
} else {
301-
// Інакше все ок, світло є.
302-
val00 = 1;
371+
// Інакше все ок, світло є.
372+
val00 = 1;
303373
}
304374
break;
305375

@@ -398,7 +468,7 @@ async function run() {
398468
name_ua: config.name_ua,
399469
name_ru: config.name_ru,
400470
name_en: config.name_en,
401-
schedule: cleanSchedule,
471+
schedule: cleanSchedule,
402472
emergency: rawInfo.emergency || false
403473
});
404474
} else {
@@ -584,6 +654,21 @@ async function run() {
584654
}
585655
}
586656

657+
// 7. ЧЕРНІВЦІ
658+
const chernivtsiSchedule = await getChernivtsiData(browser);
659+
if (chernivtsiSchedule && Object.keys(chernivtsiSchedule).length > 0) {
660+
console.log(`✅ Success Chernivtsi`);
661+
updateGlobalDates(chernivtsiSchedule, globalDates);
662+
processedRegions.push({
663+
cpu: "chernivetska-oblast",
664+
name_ua: "Чернівецька",
665+
name_ru: "Черновицкая",
666+
name_en: "Chernivtsi",
667+
schedule: chernivtsiSchedule,
668+
emergency: false
669+
});
670+
}
671+
587672
// ВІДПРАВКА
588673
if (processedRegions.length === 0) {
589674
console.error("❌ No data collected.");

0 commit comments

Comments
 (0)