WITH -- 1. Получаем все проверки ICMP за период checks AS ( SELECT h.host AS host_name, COALESCE(h.name, h.host) AS visible_name, -- Тип оборудования из инвентаризации (поле type), если пусто - берем hardware COALESCE(hinv."name", 'не указан') AS equipment_type, -- Номер объекта ИТС РО (храним в поле notes инвентаризации) COALESCE(hinv.notes, '') AS object_number, -- Координаты: объединяем широту и долготу через пробел CASE WHEN hinv.location_lat IS NOT NULL AND hinv.location_lon IS NOT NULL AND hinv.location_lat != '' AND hinv.location_lon != '' THEN hinv.location_lat || ' ' || hinv.location_lon WHEN hinv.location_lat IS NOT NULL AND hinv.location_lat != '' THEN hinv.location_lat WHEN hinv.location_lon IS NOT NULL AND hinv.location_lon != '' THEN hinv.location_lon ELSE '' END AS coordinates, -- Время проверки с учетом часового пояса TO_TIMESTAMP(hx.clock) AT TIME ZONE 'Europe/Moscow' AS check_time, -- Статус: 1 = доступно, 0 = недоступно hx.value AS status FROM hosts h INNER JOIN items i ON i.hostid = h.hostid AND i.key_ = 'icmpping' AND i.status = 0 AND i.value_type = 3 -- unsigned numeric INNER JOIN history_uint hx ON hx.itemid = i.itemid LEFT JOIN host_inventory hinv ON hinv.hostid = h.hostid WHERE -- Только мониторимые хосты h.status = 0 -- Исключаем оборудование, начинающееся на IRZ (любой регистр) --AND UPPER(h.host) NOT LIKE 'IRZ%' -- Период: май 2026 года (измените под свои даты) --and hinv.notes = 'ОБЪЕКТ № 12' --and hinv."name" = 'Видеокамера Trassir TR-D6254IR15' and CAST(nullif(regexp_replace(hinv.notes, 'ОБЪЕКТ №\s*', '', 'g'),'') as INTEGER) between 1 and 85 --in (44,77,43,54,27,48,12,49,65,9,60,35,38,1,51,41,53,84,8) AND TO_TIMESTAMP(hx.clock) >= to_date(${start_dt}, 'yyyymmdd') - interval '1 day' AND TO_TIMESTAMP(hx.clock) <= to_date(${end_dt}, 'yyyymmdd') + interval '1 day' --and hinv.notes is null ), -- 2. Добавляем предыдущий статус для определения моментов смены checks_with_prev AS ( SELECT *, LAG(status) OVER (PARTITION BY host_name ORDER BY check_time) AS prev_status, coalesce(lead(check_time) OVER (PARTITION BY host_name ORDER BY check_time), date_trunc('day', check_time) + interval '1 day') AS next_check_time FROM checks ), -- 3. Определяем моменты смены статуса (начало нового интервала) status_changes AS ( SELECT host_name, visible_name, equipment_type, object_number, coordinates, check_time, status, prev_status, case when DATE(next_check_time) > DATE(check_time) then date_trunc('day', next_check_time) + interval '00:00:00' else next_check_time end as next_check_time, CASE WHEN status != prev_status THEN 1 ELSE 0 END AS is_start FROM checks_with_prev union all SELECT host_name, visible_name, equipment_type, object_number, coordinates, date_trunc('day', next_check_time) + interval '00:00:00' as check_time, status, prev_status, next_check_time, 1 as is_start FROM checks_with_prev where DATE(next_check_time) > DATE(check_time) ), -- 4. Нумеруем интервалы для каждого устройства intervals AS ( SELECT *, SUM(is_start) OVER (PARTITION BY host_name ORDER BY check_time) AS interval_id FROM status_changes WHERE status IS NOT null and date_trunc('day', check_time) >= to_date(${start_dt}, 'yyyymmdd') and date_trunc('day', check_time) <= to_date(${end_dt}, 'yyyymmdd') ), -- 5. Агрегируем каждый непрерывный интервал grouped_intervals AS ( SELECT host_name, visible_name, equipment_type, object_number, coordinates, MIN(date_trunc('second', check_time)) AS check_time, MAX(date_trunc('second', next_check_time)) AS next_check_time, MAX(status) AS status FROM intervals GROUP BY host_name, visible_name, equipment_type, object_number, coordinates, interval_id ), daily_stats AS ( select *, extract('hour' from available_hours)::int AS a_h, extract('minute' from available_hours)::int AS a_m, extract('second' from available_hours)::int AS a_s, extract('hour' from unavailable_hours)::int AS u_h, extract('minute' from unavailable_hours)::int AS u_m, extract('second' from unavailable_hours)::int AS u_s from ( SELECT host_name, visible_name, equipment_type, object_number, coordinates, DATE(check_time) AS check_date, sum(case when status = 1 then next_check_time - check_time else null end) AS available_hours, sum(case when status = 0 then next_check_time - check_time else null end) AS unavailable_hours, sum(next_check_time - check_time) AS all_hours FROM grouped_intervals GROUP BY host_name, visible_name, equipment_type, object_number, coordinates, DATE(check_time) ) f ), daily_graph AS ( SELECT visible_name, equipment_type, object_number, coordinates, max(case when object_number similar to '%№%' then regexp_replace(object_number, 'ОБЪЕКТ №\s*', '', 'g')::int else null end) as object_num, STRING_AGG( TO_CHAR(check_date, 'DD.MM') || ': Доступно ' || a_h || 'ч.' || a_m || 'м.' || a_s|| 'с.' || ', Недоступно ' || u_h || 'ч.' || u_m || 'м.' || u_s ||'с.', '; ' ORDER BY check_date ) AS daily_schedule FROM daily_stats GROUP BY visible_name, equipment_type, object_number, coordinates ), device_summary AS ( SELECT visible_name, equipment_type, object_number, coordinates, SUM(CASE WHEN (a_h + a_m/60 + a_s/360) > 12 THEN 1 ELSE 0 END) AS days_available_gt_12h, SUM(CASE WHEN (a_h + a_m/60 + a_s/360) <= 12 THEN 1 ELSE 0 END) AS days_unavailable_ge_12h FROM daily_stats GROUP BY visible_name, equipment_type, object_number, coordinates ) SELECT ROW_NUMBER() OVER (ORDER BY dg.object_num nulls last, dg.equipment_type) AS "№ п/п", dg.equipment_type AS "Тип оборудования", case when dg.object_num is null then 'на инвентаризации' else dg.object_number end AS "Номер_объекта_ИТС_РО", dg.coordinates AS "Координаты_расположения", dg.daily_schedule AS "Посуточный график функц оборуд", ds.days_available_gt_12h AS "Количество суток доступности", ds.days_unavailable_ge_12h AS "Количество суток недоступности" FROM daily_graph dg INNER JOIN device_summary ds ON ds.visible_name = dg.visible_name ORDER BY 1;