Compare commits

...

3 Commits

Author SHA1 Message Date
6282f402db adjusted the codes completely 2025-06-11 13:33:57 +03:00
76f3d6369a test 2025-06-09 15:27:47 +03:00
14b7f4ab6b hot fix of occupancy 2025-06-09 15:24:18 +03:00
4 changed files with 384 additions and 170 deletions

View File

@ -13,81 +13,142 @@ WITH presence_logs AS (
AND p.cat_name = 'hps' AND p.cat_name = 'hps'
), ),
-- Intervals when device was in 'presence' (between prev_time and event_time when value='none') raw_absence_intervals AS (
presence_intervals AS (
SELECT SELECT
space_id, space_id,
device_id,
prev_time AS start_time, prev_time AS start_time,
event_time AS end_time event_time AS end_time
FROM presence_logs FROM presence_logs
WHERE value = 'none' WHERE prev_value = 'none' AND prev_time IS NOT NULL
AND prev_value = 'presence'
AND prev_time IS NOT NULL
), ),
-- Split intervals across days absence_intervals AS (
split_intervals AS ( SELECT
r.space_id,
r.device_id,
gs.day,
GREATEST(r.start_time, gs.day) AS start_time,
LEAST(r.end_time, gs.day + INTERVAL '1 day') AS end_time
FROM raw_absence_intervals r
CROSS JOIN LATERAL (
SELECT generate_series(
date_trunc('day', r.start_time),
date_trunc('day', r.end_time),
INTERVAL '1 day'
) AS day
) gs
WHERE GREATEST(r.start_time, gs.day) < LEAST(r.end_time, gs.day + INTERVAL '1 day')
),
device_counts AS (
SELECT space_id, day, COUNT(DISTINCT device_id) AS device_count
FROM absence_intervals
GROUP BY 1, 2
),
timeline AS (
SELECT
a.space_id,
a.day,
a.device_id,
a.start_time AS ts,
1 AS is_start
FROM absence_intervals a
UNION ALL
SELECT
a.space_id,
a.day,
a.device_id,
a.end_time AS ts,
0 AS is_start
FROM absence_intervals a
),
ordered_events AS (
SELECT SELECT
space_id, space_id,
generate_series( day,
date_trunc('day', start_time), ts,
date_trunc('day', end_time), is_start,
interval '1 day' device_id,
)::date AS event_date, SUM(CASE WHEN is_start = 1 THEN 1 ELSE -1 END)
GREATEST(start_time, date_trunc('day', start_time)) AS interval_start, OVER (PARTITION BY space_id, day, device_id ORDER BY ts) AS device_active
LEAST(end_time, date_trunc('day', end_time) + interval '1 day') AS interval_end FROM timeline
FROM presence_intervals
), ),
-- Mark and group overlapping intervals per space per day device_state_changes AS (
ordered_intervals AS (
SELECT SELECT
space_id, space_id,
event_date, day,
interval_start, ts,
interval_end, SUM(CASE WHEN is_start = 1 THEN 1 ELSE -1 END)
LAG(interval_end) OVER (PARTITION BY space_id, event_date ORDER BY interval_start) AS prev_end OVER (PARTITION BY space_id, day ORDER BY ts) AS net_active_devices
FROM split_intervals FROM (
SELECT DISTINCT space_id, day, ts, is_start
FROM timeline
) t
), ),
grouped_intervals AS ( absence_windows AS (
SELECT *, SELECT
SUM(CASE dc.space_id,
WHEN prev_end IS NULL OR interval_start > prev_end THEN 1 dc.day,
ELSE 0 ts AS start_time,
END) OVER (PARTITION BY space_id, event_date ORDER BY interval_start) AS grp LEAD(ts) OVER (PARTITION BY dc.space_id, dc.day ORDER BY ts) AS end_time
FROM ordered_intervals FROM device_state_changes dsc
JOIN device_counts dc ON dc.space_id = dsc.space_id AND dc.day = dsc.day
WHERE net_active_devices = 0
), ),
-- Merge overlapping intervals per group empty_periods AS (
merged_intervals AS (
SELECT SELECT
space_id, space_id,
event_date, day,
MIN(interval_start) AS merged_start, EXTRACT(EPOCH FROM (end_time - start_time)) AS unoccupied_seconds
MAX(interval_end) AS merged_end FROM absence_windows
FROM grouped_intervals WHERE end_time IS NOT NULL
GROUP BY space_id, event_date, grp
), ),
-- Sum durations of merged intervals unoccupied_summary AS (
summed_intervals AS (
SELECT SELECT
space_id, space_id,
event_date, day,
SUM(EXTRACT(EPOCH FROM (merged_end - merged_start))) AS raw_occupied_seconds SUM(unoccupied_seconds) AS total_unoccupied_seconds
FROM merged_intervals FROM empty_periods
GROUP BY space_id, event_date GROUP BY space_id, day
), ),
final_data AS ( -- Include device count even for days with 0 unoccupied time
all_days_with_devices AS (
SELECT
space_id,
DATE(event_time) AS day,
COUNT(DISTINCT device_id) AS device_count
FROM presence_logs
GROUP BY 1, 2
),
final_occupancy AS (
SELECT
d.space_id,
d.day,
d.device_count,
COALESCE(u.total_unoccupied_seconds, 0) AS unoccupied_seconds,
86400 - COALESCE(u.total_unoccupied_seconds, 0) AS occupied_seconds
FROM all_days_with_devices d
LEFT JOIN unoccupied_summary u
ON d.space_id = u.space_id AND d.day = u.day
)
, final_data as (
SELECT SELECT
space_id, space_id,
event_date, day,
LEAST(raw_occupied_seconds, 86400) AS occupied_seconds, device_count,
ROUND(LEAST(raw_occupied_seconds, 86400) / 86400.0 * 100, 2) AS occupancy_percentage occupied_seconds,
FROM summed_intervals ROUND(occupied_seconds / 86400.0 * 100, 2) AS occupancy_percentage
ORDER BY space_id, event_date) FROM final_occupancy
ORDER BY space_id, day)
INSERT INTO public."space-daily-occupancy-duration" ( INSERT INTO public."space-daily-occupancy-duration" (

View File

@ -10,7 +10,8 @@ presence_logs AS (
l.device_id, l.device_id,
l.event_time, l.event_time,
l.value, l.value,
LAG(l.event_time) OVER (PARTITION BY l.device_id ORDER BY l.event_time) AS prev_time LAG(l.event_time) OVER (PARTITION BY l.device_id ORDER BY l.event_time) AS prev_time,
LAG(l.value) OVER (PARTITION BY l.device_id ORDER BY l.event_time) AS prev_value
FROM device d FROM device d
JOIN "device-status-log" l ON d.uuid = l.device_id JOIN "device-status-log" l ON d.uuid = l.device_id
JOIN product p ON p.uuid = d.product_device_uuid JOIN product p ON p.uuid = d.product_device_uuid
@ -18,77 +19,147 @@ presence_logs AS (
AND p.cat_name = 'hps' AND p.cat_name = 'hps'
), ),
presence_intervals AS ( raw_absence_intervals AS (
SELECT SELECT
space_id, space_id,
device_id,
prev_time AS start_time, prev_time AS start_time,
event_time AS end_time event_time AS end_time
FROM presence_logs FROM presence_logs
WHERE value = 'none' AND prev_time IS NOT NULL WHERE prev_value = 'none' AND prev_time IS NOT NULL
), ),
split_intervals AS ( absence_intervals AS (
SELECT
r.space_id,
r.device_id,
gs.day,
GREATEST(r.start_time, gs.day) AS start_time,
LEAST(r.end_time, gs.day + INTERVAL '1 day') AS end_time
FROM raw_absence_intervals r
CROSS JOIN LATERAL (
SELECT generate_series(
date_trunc('day', r.start_time),
date_trunc('day', r.end_time),
INTERVAL '1 day'
) AS day
) gs
WHERE GREATEST(r.start_time, gs.day) < LEAST(r.end_time, gs.day + INTERVAL '1 day')
),
device_counts AS (
SELECT space_id, day, COUNT(DISTINCT device_id) AS device_count
FROM absence_intervals
GROUP BY 1, 2
),
timeline AS (
SELECT
a.space_id,
a.day,
a.device_id,
a.start_time AS ts,
1 AS is_start
FROM absence_intervals a
UNION ALL
SELECT
a.space_id,
a.day,
a.device_id,
a.end_time AS ts,
0 AS is_start
FROM absence_intervals a
),
ordered_events AS (
SELECT SELECT
space_id, space_id,
generate_series( day,
date_trunc('day', start_time), ts,
date_trunc('day', end_time), is_start,
interval '1 day' device_id,
)::date AS event_date, SUM(CASE WHEN is_start = 1 THEN 1 ELSE -1 END)
GREATEST(start_time, date_trunc('day', start_time)) AS interval_start, OVER (PARTITION BY space_id, day, device_id ORDER BY ts) AS device_active
LEAST(end_time, date_trunc('day', end_time) + INTERVAL '1 day') AS interval_end FROM timeline
FROM presence_intervals
), ),
ordered_intervals AS ( device_state_changes AS (
SELECT SELECT
space_id, space_id,
event_date, day,
interval_start, ts,
interval_end, SUM(CASE WHEN is_start = 1 THEN 1 ELSE -1 END)
LAG(interval_end) OVER (PARTITION BY space_id, event_date ORDER BY interval_start) AS prev_end OVER (PARTITION BY space_id, day ORDER BY ts) AS net_active_devices
FROM split_intervals FROM (
SELECT DISTINCT space_id, day, ts, is_start
FROM timeline
) t
), ),
grouped_intervals AS ( absence_windows AS (
SELECT *, SELECT
SUM(CASE dc.space_id,
WHEN prev_end IS NULL OR interval_start > prev_end THEN 1 dc.day,
ELSE 0 ts AS start_time,
END) OVER (PARTITION BY space_id, event_date ORDER BY interval_start) AS grp LEAD(ts) OVER (PARTITION BY dc.space_id, dc.day ORDER BY ts) AS end_time
FROM ordered_intervals FROM device_state_changes dsc
JOIN device_counts dc ON dc.space_id = dsc.space_id AND dc.day = dsc.day
WHERE net_active_devices = 0
), ),
merged_intervals AS ( empty_periods AS (
SELECT SELECT
space_id, space_id,
event_date, day,
MIN(interval_start) AS merged_start, EXTRACT(EPOCH FROM (end_time - start_time)) AS unoccupied_seconds
MAX(interval_end) AS merged_end FROM absence_windows
FROM grouped_intervals WHERE end_time IS NOT NULL
GROUP BY space_id, event_date, grp
), ),
summed_intervals AS ( unoccupied_summary AS (
SELECT SELECT
space_id, space_id,
event_date, day,
SUM(EXTRACT(EPOCH FROM (merged_end - merged_start))) AS raw_occupied_seconds SUM(unoccupied_seconds) AS total_unoccupied_seconds
FROM merged_intervals FROM empty_periods
GROUP BY space_id, event_date GROUP BY space_id, day
), ),
final_data AS ( -- Include device count even for days with 0 unoccupied time
all_days_with_devices AS (
SELECT SELECT
s.space_id, space_id,
s.event_date, DATE(event_time) AS day,
LEAST(raw_occupied_seconds, 86400) AS occupied_seconds, COUNT(DISTINCT device_id) AS device_count
ROUND(LEAST(raw_occupied_seconds, 86400) / 86400.0 * 100, 2) AS occupancy_percentage FROM presence_logs
FROM summed_intervals s GROUP BY 1, 2
JOIN params p ),
final_occupancy AS (
SELECT
d.space_id,
d.day,
d.device_count,
COALESCE(u.total_unoccupied_seconds, 0) AS unoccupied_seconds,
86400 - COALESCE(u.total_unoccupied_seconds, 0) AS occupied_seconds
FROM all_days_with_devices d
LEFT JOIN unoccupied_summary u
ON d.space_id = u.space_id AND d.day = u.day
),
final_data as(
SELECT
space_id,
day,
device_count,
occupied_seconds,
ROUND(occupied_seconds / 86400.0 * 100, 2) AS occupancy_percentage
FROM final_occupancy s
JOIN params p
ON p.space_id = s.space_id ON p.space_id = s.space_id
AND p.event_date = s.event_date AND p.event_date = s.event_date
) ORDER BY space_id, DAY)
INSERT INTO public."space-daily-occupancy-duration" ( INSERT INTO public."space-daily-occupancy-duration" (
space_uuid, space_uuid,

View File

@ -13,3 +13,7 @@ AND (
OR TO_CHAR(psdsd.event_date, 'YYYY') = TO_CHAR(P.event_year, 'YYYY') OR TO_CHAR(psdsd.event_date, 'YYYY') = TO_CHAR(P.event_year, 'YYYY')
) )
ORDER BY space_uuid, event_date ORDER BY space_uuid, event_date

View File

@ -13,78 +13,156 @@ WITH presence_logs AS (
AND p.cat_name = 'hps' AND p.cat_name = 'hps'
), ),
-- Intervals when device was in 'presence' (between prev_time and event_time when value='none') -- Intervals when device was in 'absence' (between prev_time and event_time when value='none')
presence_intervals AS ( raw_absence_intervals AS (
SELECT SELECT
space_id, space_id,
device_id,
prev_time AS start_time, prev_time AS start_time,
event_time AS end_time event_time AS end_time
FROM presence_logs FROM presence_logs
WHERE value = 'none' WHERE value <> 'none'
AND prev_value = 'presence' AND prev_value = 'none'
AND prev_time IS NOT NULL AND prev_time IS NOT NULL
), ),
-- Split intervals across days -- Split intervals that span multiple days into day-specific chunks
split_intervals AS ( absence_intervals AS (
SELECT SELECT
space_id, r.space_id,
generate_series( r.device_id,
date_trunc('day', start_time), GREATEST(r.start_time, gs.day::timestamp) AS start_time,
date_trunc('day', end_time), LEAST(r.end_time, (gs.day + INTERVAL '1 day' - INTERVAL '1 second')::timestamp) AS end_time
interval '1 day' FROM raw_absence_intervals r
)::date AS event_date, CROSS JOIN LATERAL (
GREATEST(start_time, date_trunc('day', start_time)) AS interval_start, SELECT generate_series(
LEAST(end_time, date_trunc('day', end_time) + interval '1 day') AS interval_end date_trunc('day', r.start_time),
FROM presence_intervals date_trunc('day', r.end_time),
INTERVAL '1 day'
) AS day
) gs
WHERE GREATEST(r.start_time, gs.day::timestamp) < LEAST(r.end_time, (gs.day + INTERVAL '1 day')::timestamp)
), ),
-- Mark and group overlapping intervals per space per day -- FIXED: Count devices based on presence_logs OR absence_intervals
ordered_intervals AS ( devices_per_day AS (
SELECT SELECT
space_id, space_id,
event_date, day,
interval_start, COUNT(DISTINCT device_id) AS device_count
interval_end, FROM (
LAG(interval_end) OVER (PARTITION BY space_id, event_date ORDER BY interval_start) AS prev_end -- Devices that logged events on that day
FROM split_intervals SELECT space_id, DATE(event_time) AS day, device_id
FROM presence_logs
UNION
-- Devices that had absence intervals on that day
SELECT space_id, DATE(start_time) AS day, device_id
FROM absence_intervals
) combined
GROUP BY space_id, day
), ),
grouped_intervals AS (
SELECT *,
SUM(CASE
WHEN prev_end IS NULL OR interval_start > prev_end THEN 1
ELSE 0
END) OVER (PARTITION BY space_id, event_date ORDER BY interval_start) AS grp
FROM ordered_intervals
),
-- Merge overlapping intervals per group -- For multi-device spaces, find all time periods when ALL devices were absent
merged_intervals AS ( multi_device_unoccupied AS (
WITH device_absence_per_day AS (
SELECT
a.space_id,
DATE(a.start_time) AS day,
a.device_id,
a.start_time,
a.end_time,
d.device_count
FROM absence_intervals a
JOIN devices_per_day d ON a.space_id = d.space_id AND DATE(a.start_time) = d.day
WHERE d.device_count > 1
),
-- Generate time slots for each day with multiple devices
time_ranges AS (
SELECT
space_id,
day,
day::timestamp AS range_start,
(day + INTERVAL '1 day')::timestamp AS range_end,
device_count
FROM devices_per_day
WHERE device_count > 1
),
-- Find all time periods when all devices were absent
all_devices_absent AS (
SELECT
t.space_id,
t.day,
t.range_start,
t.range_end,
t.device_count,
-- Find the latest start time of all devices' absence intervals
MAX(a.start_time) OVER (PARTITION BY t.space_id, t.day) AS max_start_time,
-- Find the earliest end time of all devices' absence intervals
MIN(a.end_time) OVER (PARTITION BY t.space_id, t.day) AS min_end_time
FROM time_ranges t
LEFT JOIN device_absence_per_day a ON
t.space_id = a.space_id AND
t.day = a.day
GROUP BY t.space_id, t.day, t.range_start, t.range_end, t.device_count, a.start_time, a.end_time
)
-- Calculate total unoccupied seconds when all devices were absent
SELECT SELECT
space_id, space_id,
event_date, day,
MIN(interval_start) AS merged_start, CASE
MAX(interval_end) AS merged_end WHEN max_start_time IS NULL OR min_end_time IS NULL THEN 0
FROM grouped_intervals WHEN max_start_time >= min_end_time THEN 0
GROUP BY space_id, event_date, grp ELSE EXTRACT(EPOCH FROM (LEAST(min_end_time, range_end) - GREATEST(max_start_time, range_start)))
END AS unoccupied_seconds
FROM all_devices_absent
GROUP BY space_id, day, range_start, range_end, device_count, max_start_time, min_end_time
HAVING COUNT(*) = device_count -- Only include periods when all devices were absent
)
,
-- Calculate unoccupied time for spaces with single device reporting
single_device_unoccupied AS (
SELECT
a.space_id,
DATE(a.start_time) AS day,
SUM(EXTRACT(EPOCH FROM (a.end_time - a.start_time))) AS unoccupied_seconds
FROM absence_intervals a
JOIN devices_per_day d ON a.space_id = d.space_id AND DATE(a.start_time) = d.day
WHERE d.device_count = 1
GROUP BY a.space_id, DATE(a.start_time)
)
,
-- Combine results from both single and multi-device cases
combined_unoccupied AS (
SELECT space_id, day, unoccupied_seconds FROM single_device_unoccupied
UNION ALL
SELECT space_id, day, unoccupied_seconds FROM multi_device_unoccupied
), ),
-- Sum durations of merged intervals -- Calculate total occupied time per space per day
summed_intervals AS ( daily_occupancy AS (
SELECT SELECT
space_id, space_id,
event_date, day,
SUM(EXTRACT(EPOCH FROM (merged_end - merged_start))) AS raw_occupied_seconds -- Total seconds in day (86400) minus unoccupied seconds
FROM merged_intervals 86400 - COALESCE(SUM(unoccupied_seconds), 0) AS occupied_seconds
GROUP BY space_id, event_date FROM combined_unoccupied
GROUP BY space_id, day
) )
-- Final output with capped seconds and percentage -- Final result
SELECT SELECT
space_id, space_id,
event_date, day,
LEAST(raw_occupied_seconds, 86400) AS occupied_seconds, occupied_seconds,
ROUND(LEAST(raw_occupied_seconds, 86400) / 86400.0 * 100, 2) AS occupancy_percentage -- Also include percentage for convenience
FROM summed_intervals ROUND((occupied_seconds / 86400.0) * 100, 2) AS occupancy_percentage
ORDER BY space_id, event_date; FROM daily_occupancy
ORDER BY space_id, day;