From 2fee8c055ef085990f6c3d9a6c126c7fe0f037a5 Mon Sep 17 00:00:00 2001 From: khuss Date: Sun, 1 Jun 2025 16:03:07 -0400 Subject: [PATCH 01/17] device model for aqi updated with hourly to daily logic getting max, min, avergae and percentage of categorical values for each aqi bracket --- .../fact_daily_device_aqi_score.sql | 242 +++++++++++++----- 1 file changed, 171 insertions(+), 71 deletions(-) diff --git a/libs/common/src/sql/queries/fact_daily_device_aqi_score/fact_daily_device_aqi_score.sql b/libs/common/src/sql/queries/fact_daily_device_aqi_score/fact_daily_device_aqi_score.sql index 4e5ad1d..6b919d0 100644 --- a/libs/common/src/sql/queries/fact_daily_device_aqi_score/fact_daily_device_aqi_score.sql +++ b/libs/common/src/sql/queries/fact_daily_device_aqi_score/fact_daily_device_aqi_score.sql @@ -51,7 +51,7 @@ BEGIN END; $$ LANGUAGE plpgsql; --- Function to convert Tuya AQI level (e.g., level_0, level_1) to numeric value +-- Function to convert Tuya AQI level (e.g., level_1, level_2) to numeric value CREATE OR REPLACE FUNCTION level_to_numeric(level_text TEXT) RETURNS NUMERIC AS $$ BEGIN @@ -62,101 +62,201 @@ EXCEPTION WHEN others THEN END; $$ LANGUAGE plpgsql; - -- CTE for device + status log + space WITH device_space AS ( SELECT device.uuid AS device_id, - device.created_at, device.space_device_uuid AS space_id, - "device-status-log".event_id, - "device-status-log".event_time::date, + "device-status-log".event_time::timestamp AS event_time, "device-status-log".code, - "device-status-log".value, - "device-status-log".log + "device-status-log".value FROM device LEFT JOIN "device-status-log" ON device.uuid = "device-status-log".device_id LEFT JOIN product ON product.uuid = device.product_device_uuid - WHERE product.cat_name = 'hjjcy' + WHERE product.cat_name = 'hjjcy' ), --- Aggregate air sensor data per device per day -air_data AS ( +-- Getting the hourly pollutants max min avg for each device +average_pollutants AS ( SELECT - event_time AS date, + date_trunc('hour', event_time) AS event_hour, device_id, space_id, - -- VOC - MIN(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_min, - MAX(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_max, + -- AVG READINGS AVG(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_avg, - - -- PM1 - MIN(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_min, - MAX(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_max, AVG(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_avg, - - -- PM2.5 - MIN(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_min, - MAX(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_max, AVG(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_avg, - - -- PM10 - MIN(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_min, - MAX(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_max, AVG(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_avg, - - -- CH2O - MIN(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_min, - MAX(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_max, AVG(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_avg, - - -- Humidity - MIN(CASE WHEN code = 'humidity_value' THEN value::numeric END) AS humidity_low, - MAX(CASE WHEN code = 'humidity_value' THEN value::numeric END) AS humidity_high, - AVG(CASE WHEN code = 'humidity_value' THEN value::numeric END) AS humidity_avg, - - -- Temperature - MIN(CASE WHEN code = 'temp_current' THEN value::numeric END) AS temp_low, - MAX(CASE WHEN code = 'temp_current' THEN value::numeric END) AS temp_high, - AVG(CASE WHEN code = 'temp_current' THEN value::numeric END) AS temp_avg, - - -- CO2 - MIN(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_min, - MAX(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_max, AVG(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_avg, - - -- AQI - AVG(CASE WHEN code = 'air_quality_index' then level_to_numeric(value) END) as air_quality_index - + + -- MIN READINGS + MIN(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_min, + MIN(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_min, + MIN(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_min, + MIN(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_min, + MIN(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_min, + MIN(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_min, + + -- MAX READINGS + MAX(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_max, + MAX(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_max, + MAX(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_max, + MAX(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_max, + MAX(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_max, + MAX(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_max FROM device_space - GROUP BY date, device_id, space_id + GROUP BY device_id, space_id, event_hour +), + + +-- Fill NULLs due to missing log values +filled_pollutants AS ( + SELECT + *, + COALESCE(pm25_avg, LAG(pm25_avg) OVER (PARTITION BY device_id ORDER BY event_hour)) AS pm25_filled_avg, + COALESCE(pm10_avg, LAG(pm10_avg) OVER (PARTITION BY device_id ORDER BY event_hour)) AS pm10_filled_avg, + COALESCE(voc_avg, LAG(voc_avg) OVER (PARTITION BY device_id ORDER BY event_hour)) AS voc_filled_avg, + COALESCE(co2_avg, LAG(co2_avg) OVER (PARTITION BY device_id ORDER BY event_hour)) AS co2_filled_avg, + COALESCE(ch2o_avg, LAG(ch2o_avg) OVER (PARTITION BY device_id ORDER BY event_hour)) AS ch2o_filled_avg, + + COALESCE(pm25_min, LAG(pm25_min) OVER (PARTITION BY device_id ORDER BY event_hour)) AS pm25_min_filled, + COALESCE(pm10_min, LAG(pm10_min) OVER (PARTITION BY device_id ORDER BY event_hour)) AS pm10_min_filled, + + COALESCE(pm25_max, LAG(pm25_max) OVER (PARTITION BY device_id ORDER BY event_hour)) AS pm25_max_filled, + COALESCE(pm10_max, LAG(pm10_max) OVER (PARTITION BY device_id ORDER BY event_hour)) AS pm10_max_filled + + FROM average_pollutants +), +-- Calculate max, min, avg hourly AQI for each device +hourly_results AS ( + SELECT + device_id, + space_id, + event_hour, + pm25_filled_avg, + pm10_filled_avg, + pm25_max_filled, + pm10_max_filled, + pm25_min_filled, + pm10_min_filled, + + GREATEST( + calculate_aqi('pm25', pm25_filled_avg), + calculate_aqi('pm10', pm10_filled_avg) + ) AS hourly_avg_aqi, + + GREATEST( + calculate_aqi('pm25', pm25_max_filled), + calculate_aqi('pm10', pm10_max_filled) + ) AS hourly_max_aqi, + + GREATEST( + calculate_aqi('pm25', pm25_min_filled), + calculate_aqi('pm10', pm10_min_filled) + ) AS hourly_min_aqi, + + CASE + WHEN GREATEST( + calculate_aqi('pm25', pm25_filled_avg), + calculate_aqi('pm10', pm10_filled_avg) + ) <= 50 THEN 'Good' + WHEN GREATEST( + calculate_aqi('pm25', pm25_filled_avg), + calculate_aqi('pm10', pm10_filled_avg) + ) <= 100 THEN 'Moderate' + WHEN GREATEST( + calculate_aqi('pm25', pm25_filled_avg), + calculate_aqi('pm10', pm10_filled_avg) + ) <= 150 THEN 'Unhealthy for Sensitive Groups' + WHEN GREATEST( + calculate_aqi('pm25', pm25_filled_avg), + calculate_aqi('pm10', pm10_filled_avg) + ) <= 200 THEN 'Unhealthy' + WHEN GREATEST( + calculate_aqi('pm25', pm25_filled_avg), + calculate_aqi('pm10', pm10_filled_avg) + ) <= 300 THEN 'Very Unhealthy' + ELSE 'Hazardous' + END AS aqi_category + + FROM filled_pollutants + ORDER BY device_id, event_hour +), + +-- counting how many categories/hours are there in total (in case device was disconnected could be less than 24) +daily_category_counts AS ( + SELECT + space_id, + device_id, + date_trunc('day', event_hour) AS event_day, + aqi_category, + COUNT(*) AS category_count + FROM hourly_results + GROUP BY device_id, space_id, event_day, aqi_category +), + +-- Aggregate Total Counts per Day +daily_totals AS ( + select + device_id, + space_id, + event_day, + SUM(category_count) AS total_count + FROM daily_category_counts + GROUP BY device_id, space_id, event_day +), +-- Calculate the daily of the daily min max avg AQI values +daily_averages AS ( + select + device_id, + space_id, + date_trunc('day', event_hour) AS event_day, + ROUND(AVG(hourly_avg_aqi)::numeric, 2) AS daily_avg_aqi, + ROUND(AVG(hourly_max_aqi)::numeric, 2) AS daily_max_aqi, + ROUND(AVG(hourly_min_aqi)::numeric, 2) AS daily_min_aqi + FROM hourly_results + GROUP BY device_id, space_id, event_day +), + +-- Pivot Categories into Columns +daily_percentages AS ( + select + dt.device_id, + dt.space_id, + dt.event_day, + ROUND(COALESCE(SUM(CASE WHEN dcc.aqi_category = 'Good' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.aqi_category = 'Moderate' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.aqi_category = 'Unhealthy for Sensitive Groups' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.aqi_category = 'Unhealthy' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.aqi_category = 'Very Unhealthy' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.aqi_category = 'Hazardous' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_percentage + FROM daily_totals dt + LEFT JOIN daily_category_counts dcc + ON dt.device_id = dcc.device_id AND dt.event_day = dcc.event_day + GROUP BY dt.device_id, dt.space_id, dt.event_day, dt.total_count ) --- Final select with AQI calculation -SELECT - date, - device_id, - space_id, - voc_min, voc_max, voc_avg, - pm1_min, pm1_max, pm1_avg, - pm25_min, pm25_max, pm25_avg, - pm10_min, pm10_max, pm10_avg, - ch2o_min, ch2o_max, ch2o_avg, - humidity_low, humidity_high, humidity_avg, - temp_low, temp_high, temp_avg, - co2_min, co2_max, co2_avg, - GREATEST( - calculate_aqi('pm25', pm25_avg), - calculate_aqi('pm10', pm10_avg), - calculate_aqi('voc_value', voc_avg), - calculate_aqi('co2_value', co2_avg), - calculate_aqi('ch2o_value', ch2o_avg) - ) AS overall_AQI, - air_quality_index as avg_device_index -FROM air_data; +-- Final Output +SELECT + p.device_id, + p.space_id, + p.event_day, + p.good_percentage, + p.moderate_percentage, + p.unhealthy_sensitive_percentage, + p.unhealthy_percentage, + p.very_unhealthy_percentage, + p.hazardous_percentage, + a.daily_avg_aqi, + a.daily_max_aqi, + a.daily_min_aqi +FROM daily_percentages p +LEFT JOIN daily_averages a + ON p.device_id = a.device_id AND p.event_day = a.event_day +ORDER BY p.space_id, p.event_day; From 5b0135ba805d8e313b481cc70ce104db7c62a927 Mon Sep 17 00:00:00 2001 From: khuss Date: Sun, 1 Jun 2025 16:09:17 -0400 Subject: [PATCH 02/17] AQI space model updated with new hourly to daily logic for calculations and categorization of aqi brackets --- .../fact_daily_space_aqi_score.sql | 234 +++++++++++++++--- 1 file changed, 197 insertions(+), 37 deletions(-) diff --git a/libs/common/src/sql/queries/fact_daily_space_aqi_score/fact_daily_space_aqi_score.sql b/libs/common/src/sql/queries/fact_daily_space_aqi_score/fact_daily_space_aqi_score.sql index 3ffe42a..ccee7f4 100644 --- a/libs/common/src/sql/queries/fact_daily_space_aqi_score/fact_daily_space_aqi_score.sql +++ b/libs/common/src/sql/queries/fact_daily_space_aqi_score/fact_daily_space_aqi_score.sql @@ -1,9 +1,11 @@ - WITH device_space AS ( +-- CTE for device + status log + space + +WITH device_space AS ( SELECT device.uuid AS device_id, device.created_at, device.space_device_uuid AS space_id, - "device-status-log".event_time::date, + "device-status-log".event_time::timestamp AS event_time, "device-status-log".code, "device-status-log".value, "device-status-log".log @@ -12,43 +14,201 @@ ON device.uuid = "device-status-log".device_id LEFT JOIN product ON product.uuid = device.product_device_uuid - WHERE product.cat_name = 'hjjcy' + WHERE product.cat_name = 'hjjcy' ), -average_pollutants as( -SELECT -event_time, -space_id, -AVG(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_avg, -AVG(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_avg, -AVG(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_avg, -AVG(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_avg, -AVG(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_avg, -AVG(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_avg, -AVG(CASE WHEN code = 'air_quality_index' then level_to_numeric(value) END) as air_quality_index +-- Getting the hourly pollutants max min avg for each space +average_pollutants AS ( + SELECT + date_trunc('hour', event_time) AS event_hour, + space_id, + + -- AVG READINGS + AVG(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_avg, + AVG(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_avg, + AVG(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_avg, + AVG(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_avg, + AVG(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_avg, + AVG(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_avg, + AVG(CASE WHEN code = 'air_quality_index' THEN level_to_numeric(value) END) AS air_quality_index_avg, + + -- MIN READINGS + MIN(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_min, + MIN(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_min, + MIN(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_min, + MIN(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_min, + MIN(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_min, + MIN(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_min, + MIN(CASE WHEN code = 'air_quality_index' THEN level_to_numeric(value) END) AS air_quality_index_min, + + -- MAX READINGS + MAX(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_max, + MAX(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_max, + MAX(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_max, + MAX(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_max, + MAX(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_max, + MAX(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_max, + MAX(CASE WHEN code = 'air_quality_index' THEN level_to_numeric(value) END) AS air_quality_index_max -FROM device_space ---WHERE code IN ('pm25_value', 'pm10') -GROUP BY space_id, event_time + + FROM device_space + GROUP BY space_id, event_hour +), + +-- Fill NULLs due to missing log values +filled_pollutants AS ( + SELECT + *, + -- Forward-fill nulls using LAG() over partitioned data + COALESCE(pm25_avg, LAG(pm25_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm25_filled_avg, + COALESCE(pm10_avg, LAG(pm10_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm10_filled_avg, + COALESCE(voc_avg, LAG(voc_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS voc_filled_avg, + COALESCE(co2_avg, LAG(co2_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS co2_filled_avg, + COALESCE(ch2o_avg, LAG(ch2o_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS ch2o_filled_avg, + COALESCE(air_quality_index_avg, LAG(air_quality_index_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS aqi_filled_avg, + + COALESCE(pm25_min, LAG(pm25_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm25_min_filled, + COALESCE(pm10_min, LAG(pm10_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm10_min_filled, + COALESCE(voc_min, LAG(voc_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS voc_min_filled, + COALESCE(co2_min, LAG(co2_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS co2_min_filled, + COALESCE(ch2o_min, LAG(ch2o_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS ch2o_min_filled, + COALESCE(air_quality_index_min, LAG(air_quality_index_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS aqi_min_filled, + + COALESCE(pm25_max, LAG(pm25_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm25_max_filled, + COALESCE(pm10_max, LAG(pm10_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm10_max_filled, + COALESCE(voc_max, LAG(voc_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS voc_max_filled, + COALESCE(co2_max, LAG(co2_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS co2_max_filled, + COALESCE(ch2o_max, LAG(ch2o_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS ch2o_max_filled, + COALESCE(air_quality_index_max, LAG(air_quality_index_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS aqi_max_filled + + + FROM average_pollutants +), + +-- Calculate max, min, avg hourly AQI for each space +hourly_results as ( + SELECT + space_id, + event_hour, + --voc_filled, + pm25_filled_avg, + pm10_filled_avg, + -- co2_filled, + --ch2o_filled, + --aqi_filled, + pm25_max_filled, + pm10_max_filled, + pm25_min_filled, + pm10_min_filled, + air_quality_index_avg, + GREATEST( + calculate_aqi('pm25', pm25_filled_avg), + calculate_aqi('pm10', pm10_filled_avg) + --calculate_aqi('voc_value', voc_filled_avg), + --calculate_aqi('co2_value', co2_filled_avg), + --calculate_aqi('ch2o_value', ch2o_filled_avg) + -- Add more AQI sources as needed + ) AS hourly_avg_aqi, + GREATEST( + calculate_aqi('pm25', pm25_max_filled), + calculate_aqi('pm10', pm10_max_filled) + ) AS hourly_max_aqi, + GREATEST( + calculate_aqi('pm25', pm25_min_filled), + calculate_aqi('pm10', pm10_min_filled) + ) AS hourly_min_aqi, + case + when GREATEST( + calculate_aqi('pm25', pm25_filled_avg), + calculate_aqi('pm10', pm10_filled_avg) + ) <= 50 then 'Good' + when GREATEST( + calculate_aqi('pm25', pm25_filled_avg), + calculate_aqi('pm10', pm10_filled_avg) + ) <= 100 then 'Moderate' + when GREATEST( + calculate_aqi('pm25', pm25_filled_avg), + calculate_aqi('pm10', pm10_filled_avg) + ) <= 150 then 'Unhealthy for Sensitive Groups' + when GREATEST( + calculate_aqi('pm25', pm25_filled_avg), + calculate_aqi('pm10', pm10_filled_avg) + ) <= 200 then 'Unhealthy' + when GREATEST( + calculate_aqi('pm25', pm25_filled_avg), + calculate_aqi('pm10', pm10_filled_avg) + ) <= 300 then 'Very Unhealthy' + else 'Hazardous' + end as aqi_category + + FROM filled_pollutants + ORDER BY space_id, event_hour +), + +-- counting how many categories/hours are there in total (in case device(s) was disconnected could be less than 24) +daily_category_counts AS ( + SELECT + space_id, + date_trunc('day', event_hour) AS event_day, + aqi_category, + COUNT(*) AS category_count + FROM hourly_results + GROUP BY space_id, event_day, aqi_category +), + +-- Aggregate Total Counts per Day +daily_totals AS ( + SELECT + space_id, + event_day, + SUM(category_count) AS total_count + FROM daily_category_counts + GROUP BY space_id, event_day +), + +-- Calculate the daily of the daily min max avg AQI values +daily_averages AS ( + SELECT + space_id, + date_trunc('day', event_hour) AS event_day, + ROUND(AVG(hourly_avg_aqi)::numeric, 2) AS daily_avg_aqi, + ROUND(AVG(hourly_max_aqi)::numeric, 2) AS daily_max_aqi, + ROUND(AVG(hourly_min_aqi)::numeric, 2) AS daily_min_aqi + FROM hourly_results + GROUP BY space_id, event_day +), + +-- Pivot Categories into Columns +daily_percentages AS ( + SELECT + dt.space_id, + dt.event_day, + ROUND(COALESCE(SUM(CASE WHEN dcc.aqi_category = 'Good' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.aqi_category = 'Moderate' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.aqi_category = 'Unhealthy for Sensitive Groups' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.aqi_category = 'Unhealthy' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.aqi_category = 'Very Unhealthy' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.aqi_category = 'Hazardous' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_percentage + FROM daily_totals dt + LEFT JOIN daily_category_counts dcc + ON dt.space_id = dcc.space_id AND dt.event_day = dcc.event_day + GROUP BY dt.space_id, dt.event_day, dt.total_count ) -SELECT - event_time::date as date, - space_id, - pm1_avg, - pm25_avg, - pm10_avg, - voc_avg, - ch2o_avg, - co2_avg, - --calculate_aqi('pm25', pm25_avg) AS aqi_pm25, - --calculate_aqi('pm10', pm10_avg) AS aqi_pm10, - GREATEST( - calculate_aqi('pm25', pm25_avg), - calculate_aqi('pm10', pm10_avg), - calculate_aqi('voc_value', voc_avg), - calculate_aqi('co2_value', co2_avg), - calculate_aqi('ch2o_value', ch2o_avg) - ) AS overall_AQI, - air_quality_index as avg_space_device_aqi -FROM average_pollutants; \ No newline at end of file +-- Final Output +SELECT + p.space_id, + p.event_day, + p.good_percentage, + p.moderate_percentage, + p.unhealthy_sensitive_percentage, + p.unhealthy_percentage, + p.very_unhealthy_percentage, + p.hazardous_percentage, + a.daily_avg_aqi, + a.daily_max_aqi, + a.daily_min_aqi +FROM daily_percentages p +LEFT JOIN daily_averages a + ON p.space_id = a.space_id AND p.event_day = a.event_day +ORDER BY p.space_id, p.event_day; \ No newline at end of file From 35ce13a67f10e5c37c43ac6aca42bdd35a327d03 Mon Sep 17 00:00:00 2001 From: ZaydSkaff Date: Tue, 3 Jun 2025 09:47:24 +0300 Subject: [PATCH 03/17] fix: return proper error on login API (#386) --- libs/common/src/auth/services/auth.service.ts | 29 ++++++++++--------- .../role-type/entities/role.type.entity.ts | 1 + 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/libs/common/src/auth/services/auth.service.ts b/libs/common/src/auth/services/auth.service.ts index 41ef028..db191a5 100644 --- a/libs/common/src/auth/services/auth.service.ts +++ b/libs/common/src/auth/services/auth.service.ts @@ -1,18 +1,18 @@ +import { PlatformType } from '@app/common/constants/platform-type.enum'; +import { RoleType } from '@app/common/constants/role.type.enum'; import { BadRequestException, Injectable, UnauthorizedException, } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { JwtService } from '@nestjs/jwt'; import * as argon2 from 'argon2'; -import { HelperHashService } from '../../helper/services'; -import { UserRepository } from '../../../../common/src/modules/user/repositories'; -import { UserSessionRepository } from '../../../../common/src/modules/session/repositories/session.repository'; -import { UserSessionEntity } from '../../../../common/src/modules/session/entities'; -import { ConfigService } from '@nestjs/config'; import { OAuth2Client } from 'google-auth-library'; -import { PlatformType } from '@app/common/constants/platform-type.enum'; -import { RoleType } from '@app/common/constants/role.type.enum'; +import { UserSessionEntity } from '../../../../common/src/modules/session/entities'; +import { UserSessionRepository } from '../../../../common/src/modules/session/repositories/session.repository'; +import { UserRepository } from '../../../../common/src/modules/user/repositories'; +import { HelperHashService } from '../../helper/services'; @Injectable() export class AuthService { @@ -40,16 +40,17 @@ export class AuthService { }, relations: ['roleType', 'project'], }); - if ( - platform === PlatformType.WEB && - (user.roleType.type === RoleType.SPACE_OWNER || - user.roleType.type === RoleType.SPACE_MEMBER) - ) { - throw new UnauthorizedException('Access denied for web platform'); - } if (!user) { throw new BadRequestException('Invalid credentials'); } + if ( + platform === PlatformType.WEB && + [RoleType.SPACE_OWNER, RoleType.SPACE_MEMBER].includes( + user.roleType.type as RoleType, + ) + ) { + throw new UnauthorizedException('Access denied for web platform'); + } if (!user.isUserVerified) { throw new BadRequestException('User is not verified'); diff --git a/libs/common/src/modules/role-type/entities/role.type.entity.ts b/libs/common/src/modules/role-type/entities/role.type.entity.ts index b7289a3..79d5acf 100644 --- a/libs/common/src/modules/role-type/entities/role.type.entity.ts +++ b/libs/common/src/modules/role-type/entities/role.type.entity.ts @@ -12,6 +12,7 @@ export class RoleTypeEntity extends AbstractEntity { nullable: false, enum: Object.values(RoleType), }) + // why is this ts-type string not enum? type: string; @OneToMany(() => UserEntity, (inviteUser) => inviteUser.roleType, { nullable: true, From c39129f75b4cf0296a96f65cc9c10489d9feee17 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 3 Jun 2025 02:38:44 -0600 Subject: [PATCH 04/17] Add Weather module with controller, service, and DTO for fetching weather details --- libs/common/src/constants/controller-route.ts | 9 +++++ libs/common/src/util/calculate.aqi.ts | 18 +++++++++ src/app.module.ts | 2 + src/config/index.ts | 3 +- src/config/weather.open.config.ts | 8 ++++ src/weather/controllers/index.ts | 1 + src/weather/controllers/weather.controller.ts | 28 +++++++++++++ src/weather/dto/get.weather.dto.ts | 21 ++++++++++ src/weather/services/index.ts | 1 + src/weather/services/weather.service.ts | 39 +++++++++++++++++++ src/weather/weather.module.ts | 12 ++++++ 11 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 libs/common/src/util/calculate.aqi.ts create mode 100644 src/config/weather.open.config.ts create mode 100644 src/weather/controllers/index.ts create mode 100644 src/weather/controllers/weather.controller.ts create mode 100644 src/weather/dto/get.weather.dto.ts create mode 100644 src/weather/services/index.ts create mode 100644 src/weather/services/weather.service.ts create mode 100644 src/weather/weather.module.ts diff --git a/libs/common/src/constants/controller-route.ts b/libs/common/src/constants/controller-route.ts index 7afbaa5..064deba 100644 --- a/libs/common/src/constants/controller-route.ts +++ b/libs/common/src/constants/controller-route.ts @@ -465,7 +465,16 @@ export class ControllerRoute { 'This endpoint retrieves the terms and conditions for the application.'; }; }; + static WEATHER = class { + public static readonly ROUTE = 'weather'; + static ACTIONS = class { + public static readonly FETCH_WEATHER_DETAILS_SUMMARY = + 'Fetch Weather Details'; + public static readonly FETCH_WEATHER_DETAILS_DESCRIPTION = + 'This endpoint retrieves the current weather details for a specified location like temperature, humidity, etc.'; + }; + }; static PRIVACY_POLICY = class { public static readonly ROUTE = 'policy'; diff --git a/libs/common/src/util/calculate.aqi.ts b/libs/common/src/util/calculate.aqi.ts new file mode 100644 index 0000000..7911ad6 --- /dev/null +++ b/libs/common/src/util/calculate.aqi.ts @@ -0,0 +1,18 @@ +export function calculateAQI(pm2_5: number): number { + const breakpoints = [ + { pmLow: 0.0, pmHigh: 12.0, aqiLow: 0, aqiHigh: 50 }, + { pmLow: 12.1, pmHigh: 35.4, aqiLow: 51, aqiHigh: 100 }, + { pmLow: 35.5, pmHigh: 55.4, aqiLow: 101, aqiHigh: 150 }, + { pmLow: 55.5, pmHigh: 150.4, aqiLow: 151, aqiHigh: 200 }, + { pmLow: 150.5, pmHigh: 250.4, aqiLow: 201, aqiHigh: 300 }, + { pmLow: 250.5, pmHigh: 500.4, aqiLow: 301, aqiHigh: 500 }, + ]; + + const bp = breakpoints.find((b) => pm2_5 >= b.pmLow && pm2_5 <= b.pmHigh); + if (!bp) return pm2_5 > 500.4 ? 500 : 0; // Handle out-of-range values + + return Math.round( + ((bp.aqiHigh - bp.aqiLow) / (bp.pmHigh - bp.pmLow)) * (pm2_5 - bp.pmLow) + + bp.aqiLow, + ); +} diff --git a/src/app.module.ts b/src/app.module.ts index e674880..bde273d 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -38,6 +38,7 @@ import { HealthModule } from './health/health.module'; import { winstonLoggerOptions } from '../libs/common/src/logger/services/winston.logger'; import { OccupancyModule } from './occupancy/occupancy.module'; +import { WeatherModule } from './weather/weather.module'; @Module({ imports: [ ConfigModule.forRoot({ @@ -79,6 +80,7 @@ import { OccupancyModule } from './occupancy/occupancy.module'; PowerClampModule, HealthModule, OccupancyModule, + WeatherModule, ], providers: [ { diff --git a/src/config/index.ts b/src/config/index.ts index d7d0014..b1d9833 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -1,4 +1,5 @@ import AuthConfig from './auth.config'; import AppConfig from './app.config'; import JwtConfig from './jwt.config'; -export default [AuthConfig, AppConfig, JwtConfig]; +import WeatherOpenConfig from './weather.open.config'; +export default [AuthConfig, AppConfig, JwtConfig, WeatherOpenConfig]; diff --git a/src/config/weather.open.config.ts b/src/config/weather.open.config.ts new file mode 100644 index 0000000..2182490 --- /dev/null +++ b/src/config/weather.open.config.ts @@ -0,0 +1,8 @@ +import { registerAs } from '@nestjs/config'; + +export default registerAs( + 'openweather-config', + (): Record => ({ + OPEN_WEATHER_MAP_API_KEY: process.env.OPEN_WEATHER_MAP_API_KEY, + }), +); diff --git a/src/weather/controllers/index.ts b/src/weather/controllers/index.ts new file mode 100644 index 0000000..8157369 --- /dev/null +++ b/src/weather/controllers/index.ts @@ -0,0 +1 @@ +export * from './weather.controller'; diff --git a/src/weather/controllers/weather.controller.ts b/src/weather/controllers/weather.controller.ts new file mode 100644 index 0000000..280e09d --- /dev/null +++ b/src/weather/controllers/weather.controller.ts @@ -0,0 +1,28 @@ +import { Controller, Get, Query } from '@nestjs/common'; +import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { EnableDisableStatusEnum } from '@app/common/constants/days.enum'; +import { ControllerRoute } from '@app/common/constants/controller-route'; // Assuming this is where the routes are defined +import { WeatherService } from '../services'; +import { BaseResponseDto } from '@app/common/dto/base.response.dto'; +import { GetWeatherDetailsDto } from '../dto/get.weather.dto'; + +@ApiTags('Weather Module') +@Controller({ + version: EnableDisableStatusEnum.ENABLED, + path: ControllerRoute.WEATHER.ROUTE, // use the static route constant +}) +export class WeatherController { + constructor(private readonly weatherService: WeatherService) {} + + @Get() + @ApiOperation({ + summary: ControllerRoute.WEATHER.ACTIONS.FETCH_WEATHER_DETAILS_SUMMARY, + description: + ControllerRoute.WEATHER.ACTIONS.FETCH_WEATHER_DETAILS_DESCRIPTION, + }) + async fetchWeatherDetails( + @Query() query: GetWeatherDetailsDto, + ): Promise { + return await this.weatherService.fetchWeatherDetails(query); + } +} diff --git a/src/weather/dto/get.weather.dto.ts b/src/weather/dto/get.weather.dto.ts new file mode 100644 index 0000000..86aefdd --- /dev/null +++ b/src/weather/dto/get.weather.dto.ts @@ -0,0 +1,21 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsNumber } from 'class-validator'; + +export class GetWeatherDetailsDto { + @ApiProperty({ + description: 'Latitude coordinate', + example: 35.6895, + }) + @IsNumber() + @Type(() => Number) + lat: number; + + @ApiProperty({ + description: 'Longitude coordinate', + example: 139.6917, + }) + @IsNumber() + @Type(() => Number) + lon: number; +} diff --git a/src/weather/services/index.ts b/src/weather/services/index.ts new file mode 100644 index 0000000..9b2cb64 --- /dev/null +++ b/src/weather/services/index.ts @@ -0,0 +1 @@ +export * from './weather.service'; diff --git a/src/weather/services/weather.service.ts b/src/weather/services/weather.service.ts new file mode 100644 index 0000000..11e1677 --- /dev/null +++ b/src/weather/services/weather.service.ts @@ -0,0 +1,39 @@ +import { HttpStatus, Injectable } from '@nestjs/common'; +import { HttpService } from '@nestjs/axios'; +import { ConfigService } from '@nestjs/config'; +import { firstValueFrom } from 'rxjs'; +import { GetWeatherDetailsDto } from '../dto/get.weather.dto'; +import { calculateAQI } from '@app/common/util/calculate.aqi'; +import { BaseResponseDto } from '@app/common/dto/base.response.dto'; +import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; + +@Injectable() +export class WeatherService { + constructor( + private readonly configService: ConfigService, + private readonly httpService: HttpService, + ) {} + + async fetchWeatherDetails( + query: GetWeatherDetailsDto, + ): Promise { + const { lat, lon } = query; + const weatherApiKey = this.configService.get( + 'OPEN_WEATHER_MAP_API_KEY', + ); + const url = `http://api.weatherapi.com/v1/current.json?key=${weatherApiKey}&q=${lat},${lon}&aqi=yes`; + + const response = await firstValueFrom(this.httpService.get(url)); + const pm2_5 = response.data.current.air_quality.pm2_5; // Raw PM2.5 (µg/m³) + + return new SuccessResponseDto({ + message: `Weather details fetched successfully`, + data: { + aqi: calculateAQI(pm2_5), // Converted AQI (0-500) + temperature: response.data.current.temp_c, + humidity: response.data.current.humidity, + }, + statusCode: HttpStatus.OK, + }); + } +} diff --git a/src/weather/weather.module.ts b/src/weather/weather.module.ts new file mode 100644 index 0000000..eb65b5b --- /dev/null +++ b/src/weather/weather.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { HttpModule } from '@nestjs/axios'; // <-- Import this! +import { WeatherController } from './controllers'; +import { WeatherService } from './services'; + +@Module({ + imports: [ConfigModule, HttpModule], + controllers: [WeatherController], + providers: [WeatherService], +}) +export class WeatherModule {} From ab3efedc35cbae2bd61117e5be7a88b1e9055805 Mon Sep 17 00:00:00 2001 From: khuss Date: Tue, 3 Jun 2025 20:52:27 -0400 Subject: [PATCH 05/17] device model updated to include the fixes and final columns --- .../fact_daily_device_aqi_score.sql | 370 +++++++++++------- 1 file changed, 235 insertions(+), 135 deletions(-) diff --git a/libs/common/src/sql/queries/fact_daily_device_aqi_score/fact_daily_device_aqi_score.sql b/libs/common/src/sql/queries/fact_daily_device_aqi_score/fact_daily_device_aqi_score.sql index 6b919d0..53e68ee 100644 --- a/libs/common/src/sql/queries/fact_daily_device_aqi_score/fact_daily_device_aqi_score.sql +++ b/libs/common/src/sql/queries/fact_daily_device_aqi_score/fact_daily_device_aqi_score.sql @@ -26,43 +26,60 @@ BEGIN ('pm10', 255, 354, 151, 200), -- VOC - ('voc_value', 0, 200, 0, 50), - ('voc_value', 201, 400, 51, 100), - ('voc_value', 401, 600, 101, 150), - ('voc_value', 601, 1000, 151, 200), + ('voc', 0, 200, 0, 50), + ('voc', 201, 400, 51, 100), + ('voc', 401, 600, 101, 150), + ('voc', 601, 1000, 151, 200), -- CH2O - ('ch2o_value', 0, 2, 0, 50), - ('ch2o_value', 2.1, 4, 51, 100), - ('ch2o_value', 4.1, 6, 101, 150), + ('ch2o', 0, 2, 0, 50), + ('ch2o', 2.1, 4, 51, 100), + ('ch2o', 4.1, 6, 101, 150), -- CO2 - ('co2_value', 350, 1000, 0, 50), - ('co2_value', 1001, 1250, 51, 100), - ('co2_value', 1251, 1500, 101, 150), - ('co2_value', 1501, 2000, 151, 200) + ('co2', 350, 1000, 0, 50), + ('co2', 1001, 1250, 51, 100), + ('co2', 1251, 1500, 101, 150), + ('co2', 1501, 2000, 151, 200) ) AS v(pollutant, c_low, c_high, i_low, i_high) WHERE v.pollutant = LOWER(p_pollutant) AND concentration BETWEEN v.c_low AND v.c_high LIMIT 1; - -- Linear interpolation RETURN ROUND(((i_high - i_low) * (concentration - c_low) / (c_high - c_low)) + i_low); END; $$ LANGUAGE plpgsql; --- Function to convert Tuya AQI level (e.g., level_1, level_2) to numeric value + +-- Function to classify AQI +CREATE OR REPLACE FUNCTION classify_aqi(aqi NUMERIC) +RETURNS TEXT AS $$ +BEGIN + RETURN CASE + WHEN aqi BETWEEN 0 AND 50 THEN 'Good' + WHEN aqi BETWEEN 51 AND 100 THEN 'Moderate' + WHEN aqi BETWEEN 101 AND 150 THEN 'Unhealthy for Sensitive Groups' + WHEN aqi BETWEEN 151 AND 200 THEN 'Unhealthy' + WHEN aqi BETWEEN 201 AND 300 THEN 'Very Unhealthy' + WHEN aqi >= 301 THEN 'Hazardous' + ELSE NULL + END; +END; +$$ LANGUAGE plpgsql; + + +-- Function to convert AQI level string to number CREATE OR REPLACE FUNCTION level_to_numeric(level_text TEXT) RETURNS NUMERIC AS $$ BEGIN - -- Extract the number from the string, default to NULL if not found RETURN CAST(regexp_replace(level_text, '[^0-9]', '', 'g') AS NUMERIC); EXCEPTION WHEN others THEN RETURN NULL; END; $$ LANGUAGE plpgsql; --- CTE for device + status log + space + +-- Query Pipeline Starts Here WITH device_space AS ( SELECT device.uuid AS device_id, @@ -78,149 +95,160 @@ WITH device_space AS ( WHERE product.cat_name = 'hjjcy' ), --- Getting the hourly pollutants max min avg for each device average_pollutants AS ( SELECT + event_time::date AS event_date, date_trunc('hour', event_time) AS event_hour, device_id, space_id, - -- AVG READINGS - AVG(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_avg, - AVG(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_avg, - AVG(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_avg, - AVG(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_avg, - AVG(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_avg, - AVG(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_avg, - - -- MIN READINGS - MIN(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_min, + -- PM1 MIN(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_min, - MIN(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_min, - MIN(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_min, - MIN(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_min, - MIN(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_min, - - -- MAX READINGS - MAX(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_max, + AVG(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_avg, MAX(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_max, + + -- PM25 + MIN(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_min, + AVG(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_avg, MAX(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_max, + + -- PM10 + MIN(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_min, + AVG(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_avg, MAX(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_max, + + -- VOC + MIN(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_min, + AVG(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_avg, + MAX(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_max, + + -- CH2O + MIN(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_min, + AVG(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_avg, MAX(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_max, + + -- CO2 + MIN(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_min, + AVG(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_avg, MAX(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_max FROM device_space - GROUP BY device_id, space_id, event_hour + GROUP BY device_id, space_id, event_hour, event_date ), - --- Fill NULLs due to missing log values filled_pollutants AS ( SELECT *, - COALESCE(pm25_avg, LAG(pm25_avg) OVER (PARTITION BY device_id ORDER BY event_hour)) AS pm25_filled_avg, - COALESCE(pm10_avg, LAG(pm10_avg) OVER (PARTITION BY device_id ORDER BY event_hour)) AS pm10_filled_avg, - COALESCE(voc_avg, LAG(voc_avg) OVER (PARTITION BY device_id ORDER BY event_hour)) AS voc_filled_avg, - COALESCE(co2_avg, LAG(co2_avg) OVER (PARTITION BY device_id ORDER BY event_hour)) AS co2_filled_avg, - COALESCE(ch2o_avg, LAG(ch2o_avg) OVER (PARTITION BY device_id ORDER BY event_hour)) AS ch2o_filled_avg, + -- AVG + COALESCE(pm25_avg, LAG(pm25_avg) OVER (PARTITION BY device_id ORDER BY event_hour)) AS pm25_avg_f, + COALESCE(pm10_avg, LAG(pm10_avg) OVER (PARTITION BY device_id ORDER BY event_hour)) AS pm10_avg_f, + COALESCE(voc_avg, LAG(voc_avg) OVER (PARTITION BY device_id ORDER BY event_hour)) AS voc_avg_f, + COALESCE(co2_avg, LAG(co2_avg) OVER (PARTITION BY device_id ORDER BY event_hour)) AS co2_avg_f, + COALESCE(ch2o_avg, LAG(ch2o_avg) OVER (PARTITION BY device_id ORDER BY event_hour)) AS ch2o_avg_f, - COALESCE(pm25_min, LAG(pm25_min) OVER (PARTITION BY device_id ORDER BY event_hour)) AS pm25_min_filled, - COALESCE(pm10_min, LAG(pm10_min) OVER (PARTITION BY device_id ORDER BY event_hour)) AS pm10_min_filled, - - COALESCE(pm25_max, LAG(pm25_max) OVER (PARTITION BY device_id ORDER BY event_hour)) AS pm25_max_filled, - COALESCE(pm10_max, LAG(pm10_max) OVER (PARTITION BY device_id ORDER BY event_hour)) AS pm10_max_filled + -- MIN + COALESCE(pm25_min, LAG(pm25_min) OVER (PARTITION BY device_id ORDER BY event_hour)) AS pm25_min_f, + COALESCE(pm10_min, LAG(pm10_min) OVER (PARTITION BY device_id ORDER BY event_hour)) AS pm10_min_f, + COALESCE(voc_min, LAG(voc_min) OVER (PARTITION BY device_id ORDER BY event_hour)) AS voc_min_f, + COALESCE(co2_min, LAG(co2_min) OVER (PARTITION BY device_id ORDER BY event_hour)) AS co2_min_f, + COALESCE(ch2o_min, LAG(ch2o_min) OVER (PARTITION BY device_id ORDER BY event_hour)) AS ch2o_min_f, + -- MAX + COALESCE(pm25_max, LAG(pm25_max) OVER (PARTITION BY device_id ORDER BY event_hour)) AS pm25_max_f, + COALESCE(pm10_max, LAG(pm10_max) OVER (PARTITION BY device_id ORDER BY event_hour)) AS pm10_max_f, + COALESCE(voc_max, LAG(voc_max) OVER (PARTITION BY device_id ORDER BY event_hour)) AS voc_max_f, + COALESCE(co2_max, LAG(co2_max) OVER (PARTITION BY device_id ORDER BY event_hour)) AS co2_max_f, + COALESCE(ch2o_max, LAG(ch2o_max) OVER (PARTITION BY device_id ORDER BY event_hour)) AS ch2o_max_f FROM average_pollutants ), --- Calculate max, min, avg hourly AQI for each device + hourly_results AS ( SELECT device_id, space_id, + event_date, event_hour, - pm25_filled_avg, - pm10_filled_avg, - pm25_max_filled, - pm10_max_filled, - pm25_min_filled, - pm10_min_filled, + pm1_min, pm1_avg, pm1_max, + pm25_min_f, pm25_avg_f, pm25_max_f, + pm10_min_f, pm10_avg_f, pm10_max_f, + voc_min_f, voc_avg_f, voc_max_f, + co2_min_f, co2_avg_f, co2_max_f, + ch2o_min_f, ch2o_avg_f, ch2o_max_f, GREATEST( - calculate_aqi('pm25', pm25_filled_avg), - calculate_aqi('pm10', pm10_filled_avg) + calculate_aqi('pm25', pm25_min_f), + calculate_aqi('pm10', pm10_min_f) + ) AS hourly_min_aqi, + + GREATEST( + calculate_aqi('pm25', pm25_avg_f), + calculate_aqi('pm10', pm10_avg_f) ) AS hourly_avg_aqi, GREATEST( - calculate_aqi('pm25', pm25_max_filled), - calculate_aqi('pm10', pm10_max_filled) + calculate_aqi('pm25', pm25_max_f), + calculate_aqi('pm10', pm10_max_f) ) AS hourly_max_aqi, - GREATEST( - calculate_aqi('pm25', pm25_min_filled), - calculate_aqi('pm10', pm10_min_filled) - ) AS hourly_min_aqi, - - CASE - WHEN GREATEST( - calculate_aqi('pm25', pm25_filled_avg), - calculate_aqi('pm10', pm10_filled_avg) - ) <= 50 THEN 'Good' - WHEN GREATEST( - calculate_aqi('pm25', pm25_filled_avg), - calculate_aqi('pm10', pm10_filled_avg) - ) <= 100 THEN 'Moderate' - WHEN GREATEST( - calculate_aqi('pm25', pm25_filled_avg), - calculate_aqi('pm10', pm10_filled_avg) - ) <= 150 THEN 'Unhealthy for Sensitive Groups' - WHEN GREATEST( - calculate_aqi('pm25', pm25_filled_avg), - calculate_aqi('pm10', pm10_filled_avg) - ) <= 200 THEN 'Unhealthy' - WHEN GREATEST( - calculate_aqi('pm25', pm25_filled_avg), - calculate_aqi('pm10', pm10_filled_avg) - ) <= 300 THEN 'Very Unhealthy' - ELSE 'Hazardous' - END AS aqi_category + classify_aqi(GREATEST( + calculate_aqi('pm25', pm25_avg_f), + calculate_aqi('pm10', pm10_avg_f) + )) AS aqi_category, + + classify_aqi(calculate_aqi('pm25',pm25_avg_f)) as pm25_category, + classify_aqi(calculate_aqi('pm10',pm10_avg_f)) as pm10_category, + classify_aqi(calculate_aqi('voc',voc_avg_f)) as voc_category, + classify_aqi(calculate_aqi('co2',co2_avg_f)) as co2_category, + classify_aqi(calculate_aqi('ch2o',ch2o_avg_f)) as ch2o_category FROM filled_pollutants - ORDER BY device_id, event_hour ), --- counting how many categories/hours are there in total (in case device was disconnected could be less than 24) daily_category_counts AS ( - SELECT - space_id, - device_id, - date_trunc('day', event_hour) AS event_day, - aqi_category, - COUNT(*) AS category_count + SELECT device_id, space_id, event_date, aqi_category AS category, 'aqi' AS pollutant, COUNT(*) AS category_count FROM hourly_results - GROUP BY device_id, space_id, event_day, aqi_category -), + GROUP BY device_id, space_id, event_date, aqi_category + + UNION ALL + + SELECT device_id, space_id, event_date, pm25_category AS category, 'pm25' AS pollutant, COUNT(*) AS category_count + FROM hourly_results + GROUP BY device_id, space_id, event_date, pm25_category + + UNION ALL + + SELECT device_id, space_id, event_date, pm10_category AS category, 'pm10' AS pollutant, COUNT(*) AS category_count + FROM hourly_results + GROUP BY device_id, space_id, event_date, pm10_category + + UNION ALL + + SELECT device_id, space_id, event_date, voc_category AS category, 'voc' AS pollutant, COUNT(*) AS category_count + FROM hourly_results + GROUP BY device_id, space_id, event_date, voc_category + + UNION ALL + + SELECT device_id, space_id, event_date, co2_category AS category, 'co2' AS pollutant, COUNT(*) AS category_count + FROM hourly_results + GROUP BY device_id, space_id, event_date, co2_category + + UNION ALL + + SELECT device_id, space_id, event_date, ch2o_category AS category, 'ch2o' AS pollutant, COUNT(*) AS category_count + FROM hourly_results + GROUP BY device_id, space_id, event_date, ch2o_category +), --- Aggregate Total Counts per Day daily_totals AS ( - select - device_id, + SELECT + device_id, space_id, - event_day, + event_date, SUM(category_count) AS total_count FROM daily_category_counts - GROUP BY device_id, space_id, event_day -), --- Calculate the daily of the daily min max avg AQI values -daily_averages AS ( - select - device_id, - space_id, - date_trunc('day', event_hour) AS event_day, - ROUND(AVG(hourly_avg_aqi)::numeric, 2) AS daily_avg_aqi, - ROUND(AVG(hourly_max_aqi)::numeric, 2) AS daily_max_aqi, - ROUND(AVG(hourly_min_aqi)::numeric, 2) AS daily_min_aqi - FROM hourly_results - GROUP BY device_id, space_id, event_day + where pollutant = 'aqi' + GROUP BY device_id, space_id, event_date ), -- Pivot Categories into Columns @@ -228,35 +256,107 @@ daily_percentages AS ( select dt.device_id, dt.space_id, - dt.event_day, - ROUND(COALESCE(SUM(CASE WHEN dcc.aqi_category = 'Good' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.aqi_category = 'Moderate' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.aqi_category = 'Unhealthy for Sensitive Groups' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.aqi_category = 'Unhealthy' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.aqi_category = 'Very Unhealthy' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.aqi_category = 'Hazardous' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_percentage + dt.event_date, + -- AQI CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_aqi_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_aqi_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_aqi_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_aqi_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_aqi_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_aqi_percentage, + -- PM25 CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_pm25_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_pm25_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_pm25_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_pm25_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_pm25_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_pm25_percentage, + -- PM10 CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_pm10_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_pm10_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_pm10_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_pm10_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_pm10_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_pm10_percentage, + -- VOC CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_voc_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_voc_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_voc_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_voc_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_voc_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_voc_percentage, + -- CO2 CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_co2_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_co2_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_co2_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_co2_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_co2_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_co2_percentage, + -- CH20 CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_ch2o_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_ch2o_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_ch2o_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_ch2o_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_ch2o_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_ch2o_percentage FROM daily_totals dt LEFT JOIN daily_category_counts dcc - ON dt.device_id = dcc.device_id AND dt.event_day = dcc.event_day - GROUP BY dt.device_id, dt.space_id, dt.event_day, dt.total_count -) + ON dt.device_id = dcc.device_id AND dt.event_date = dcc.event_date + GROUP BY dt.device_id, dt.space_id, dt.event_date, dt.total_count +), --- Final Output +daily_averages AS ( + SELECT + device_id, + space_id, + event_date, + -- AQI + ROUND(AVG(hourly_min_aqi)::numeric, 2) AS daily_min_aqi, + ROUND(AVG(hourly_avg_aqi)::numeric, 2) AS daily_avg_aqi, + ROUND(AVG(hourly_max_aqi)::numeric, 2) AS daily_max_aqi, + -- PM25 + ROUND(AVG(pm25_min_f)::numeric, 2) AS daily_min_pm25, + ROUND(AVG(pm25_avg_f)::numeric, 2) AS daily_avg_pm25, + ROUND(AVG(pm25_max_f)::numeric, 2) AS daily_max_pm25, + -- PM10 + ROUND(AVG(pm10_min_f)::numeric, 2) AS daily_min_pm10, + ROUND(AVG(pm10_avg_f)::numeric, 2) AS daily_avg_pm10, + ROUND(AVG(pm10_max_f)::numeric, 2) AS daily_max_pm10, + -- VOC + ROUND(AVG(voc_min_f)::numeric, 2) AS daily_min_voc, + ROUND(AVG(voc_avg_f)::numeric, 2) AS daily_avg_voc, + ROUND(AVG(voc_max_f)::numeric, 2) AS daily_max_voc, + -- CO2 + ROUND(AVG(co2_min_f)::numeric, 2) AS daily_min_co2, + ROUND(AVG(co2_avg_f)::numeric, 2) AS daily_avg_co2, + ROUND(AVG(co2_max_f)::numeric, 2) AS daily_max_co2, + -- CH2O + ROUND(AVG(ch2o_min_f)::numeric, 2) AS daily_min_ch2o, + ROUND(AVG(ch2o_avg_f)::numeric, 2) AS daily_avg_ch2o, + ROUND(AVG(ch2o_max_f)::numeric, 2) AS daily_max_ch2o + + FROM hourly_results + GROUP BY device_id, space_id, event_date +) SELECT p.device_id, p.space_id, - p.event_day, - p.good_percentage, - p.moderate_percentage, - p.unhealthy_sensitive_percentage, - p.unhealthy_percentage, - p.very_unhealthy_percentage, - p.hazardous_percentage, - a.daily_avg_aqi, - a.daily_max_aqi, - a.daily_min_aqi + p.event_date, + p.good_aqi_percentage, p.moderate_aqi_percentage, p.unhealthy_sensitive_aqi_percentage, p.unhealthy_aqi_percentage, p.very_unhealthy_aqi_percentage, p.hazardous_aqi_percentage, + a.daily_avg_aqi,a.daily_max_aqi, a.daily_min_aqi, + p.good_pm25_percentage, p.moderate_pm25_percentage, p.unhealthy_sensitive_pm25_percentage, p.unhealthy_pm25_percentage, p.very_unhealthy_pm25_percentage, p.hazardous_pm25_percentage, + a.daily_avg_pm25,a.daily_max_pm25, a.daily_min_pm25, + p.good_pm10_percentage, p.moderate_pm10_percentage, p.unhealthy_sensitive_pm10_percentage, p.unhealthy_pm10_percentage, p.very_unhealthy_pm10_percentage, p.hazardous_pm10_percentage, + a.daily_avg_pm10, a.daily_max_pm10, a.daily_min_pm10, + p.good_voc_percentage, p.moderate_voc_percentage, p.unhealthy_sensitive_voc_percentage, p.unhealthy_voc_percentage, p.very_unhealthy_voc_percentage, p.hazardous_voc_percentage, + a.daily_avg_voc, a.daily_max_voc, a.daily_min_voc, + p.good_co2_percentage, p.moderate_co2_percentage, p.unhealthy_sensitive_co2_percentage, p.unhealthy_co2_percentage, p.very_unhealthy_co2_percentage, p.hazardous_co2_percentage, + a.daily_avg_co2,a.daily_max_co2, a.daily_min_co2, + p.good_ch2o_percentage, p.moderate_ch2o_percentage, p.unhealthy_sensitive_ch2o_percentage, p.unhealthy_ch2o_percentage, p.very_unhealthy_ch2o_percentage, p.hazardous_ch2o_percentage, + a.daily_avg_ch2o,a.daily_max_ch2o, a.daily_min_ch2o FROM daily_percentages p LEFT JOIN daily_averages a - ON p.device_id = a.device_id AND p.event_day = a.event_day -ORDER BY p.space_id, p.event_day; + ON p.device_id = a.device_id AND p.event_date = a.event_date +ORDER BY p.space_id, p.event_date; + From 3ad81864d139d3fc2a509bc577f6c0feffc9e336 Mon Sep 17 00:00:00 2001 From: khuss Date: Tue, 3 Jun 2025 21:05:34 -0400 Subject: [PATCH 06/17] updated space models to include suggested fixes, update final logic and column names --- .../fact_daily_space_aqi_score.sql | 383 ++++++++++-------- 1 file changed, 222 insertions(+), 161 deletions(-) diff --git a/libs/common/src/sql/queries/fact_daily_space_aqi_score/fact_daily_space_aqi_score.sql b/libs/common/src/sql/queries/fact_daily_space_aqi_score/fact_daily_space_aqi_score.sql index ccee7f4..a74e66b 100644 --- a/libs/common/src/sql/queries/fact_daily_space_aqi_score/fact_daily_space_aqi_score.sql +++ b/libs/common/src/sql/queries/fact_daily_space_aqi_score/fact_daily_space_aqi_score.sql @@ -1,14 +1,11 @@ --- CTE for device + status log + space - +-- Query Pipeline Starts Here WITH device_space AS ( SELECT device.uuid AS device_id, - device.created_at, device.space_device_uuid AS space_id, "device-status-log".event_time::timestamp AS event_time, "device-status-log".code, - "device-status-log".value, - "device-status-log".log + "device-status-log".value FROM device LEFT JOIN "device-status-log" ON device.uuid = "device-status-log".device_id @@ -17,198 +14,262 @@ WITH device_space AS ( WHERE product.cat_name = 'hjjcy' ), --- Getting the hourly pollutants max min avg for each space average_pollutants AS ( SELECT + event_time::date AS event_date, date_trunc('hour', event_time) AS event_hour, space_id, - - -- AVG READINGS - AVG(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_avg, - AVG(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_avg, - AVG(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_avg, - AVG(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_avg, - AVG(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_avg, - AVG(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_avg, - AVG(CASE WHEN code = 'air_quality_index' THEN level_to_numeric(value) END) AS air_quality_index_avg, - - -- MIN READINGS - MIN(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_min, - MIN(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_min, - MIN(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_min, - MIN(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_min, - MIN(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_min, - MIN(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_min, - MIN(CASE WHEN code = 'air_quality_index' THEN level_to_numeric(value) END) AS air_quality_index_min, - - -- MAX READINGS - MAX(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_max, - MAX(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_max, - MAX(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_max, - MAX(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_max, - MAX(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_max, - MAX(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_max, - MAX(CASE WHEN code = 'air_quality_index' THEN level_to_numeric(value) END) AS air_quality_index_max + -- PM1 + MIN(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_min, + AVG(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_avg, + MAX(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_max, + + -- PM25 + MIN(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_min, + AVG(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_avg, + MAX(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_max, + + -- PM10 + MIN(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_min, + AVG(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_avg, + MAX(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_max, + + -- VOC + MIN(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_min, + AVG(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_avg, + MAX(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_max, + + -- CH2O + MIN(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_min, + AVG(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_avg, + MAX(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_max, + + -- CO2 + MIN(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_min, + AVG(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_avg, + MAX(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_max FROM device_space - GROUP BY space_id, event_hour + GROUP BY space_id, event_hour, event_date ), --- Fill NULLs due to missing log values filled_pollutants AS ( SELECT *, - -- Forward-fill nulls using LAG() over partitioned data - COALESCE(pm25_avg, LAG(pm25_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm25_filled_avg, - COALESCE(pm10_avg, LAG(pm10_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm10_filled_avg, - COALESCE(voc_avg, LAG(voc_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS voc_filled_avg, - COALESCE(co2_avg, LAG(co2_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS co2_filled_avg, - COALESCE(ch2o_avg, LAG(ch2o_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS ch2o_filled_avg, - COALESCE(air_quality_index_avg, LAG(air_quality_index_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS aqi_filled_avg, - - COALESCE(pm25_min, LAG(pm25_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm25_min_filled, - COALESCE(pm10_min, LAG(pm10_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm10_min_filled, - COALESCE(voc_min, LAG(voc_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS voc_min_filled, - COALESCE(co2_min, LAG(co2_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS co2_min_filled, - COALESCE(ch2o_min, LAG(ch2o_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS ch2o_min_filled, - COALESCE(air_quality_index_min, LAG(air_quality_index_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS aqi_min_filled, - - COALESCE(pm25_max, LAG(pm25_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm25_max_filled, - COALESCE(pm10_max, LAG(pm10_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm10_max_filled, - COALESCE(voc_max, LAG(voc_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS voc_max_filled, - COALESCE(co2_max, LAG(co2_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS co2_max_filled, - COALESCE(ch2o_max, LAG(ch2o_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS ch2o_max_filled, - COALESCE(air_quality_index_max, LAG(air_quality_index_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS aqi_max_filled + -- AVG + COALESCE(pm25_avg, LAG(pm25_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm25_avg_f, + COALESCE(pm10_avg, LAG(pm10_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm10_avg_f, + COALESCE(voc_avg, LAG(voc_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS voc_avg_f, + COALESCE(co2_avg, LAG(co2_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS co2_avg_f, + COALESCE(ch2o_avg, LAG(ch2o_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS ch2o_avg_f, + -- MIN + COALESCE(pm25_min, LAG(pm25_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm25_min_f, + COALESCE(pm10_min, LAG(pm10_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm10_min_f, + COALESCE(voc_min, LAG(voc_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS voc_min_f, + COALESCE(co2_min, LAG(co2_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS co2_min_f, + COALESCE(ch2o_min, LAG(ch2o_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS ch2o_min_f, + -- MAX + COALESCE(pm25_max, LAG(pm25_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm25_max_f, + COALESCE(pm10_max, LAG(pm10_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm10_max_f, + COALESCE(voc_max, LAG(voc_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS voc_max_f, + COALESCE(co2_max, LAG(co2_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS co2_max_f, + COALESCE(ch2o_max, LAG(ch2o_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS ch2o_max_f FROM average_pollutants ), --- Calculate max, min, avg hourly AQI for each space -hourly_results as ( - SELECT - space_id, - event_hour, - --voc_filled, - pm25_filled_avg, - pm10_filled_avg, - -- co2_filled, - --ch2o_filled, - --aqi_filled, - pm25_max_filled, - pm10_max_filled, - pm25_min_filled, - pm10_min_filled, - air_quality_index_avg, - GREATEST( - calculate_aqi('pm25', pm25_filled_avg), - calculate_aqi('pm10', pm10_filled_avg) - --calculate_aqi('voc_value', voc_filled_avg), - --calculate_aqi('co2_value', co2_filled_avg), - --calculate_aqi('ch2o_value', ch2o_filled_avg) - -- Add more AQI sources as needed - ) AS hourly_avg_aqi, - GREATEST( - calculate_aqi('pm25', pm25_max_filled), - calculate_aqi('pm10', pm10_max_filled) - ) AS hourly_max_aqi, - GREATEST( - calculate_aqi('pm25', pm25_min_filled), - calculate_aqi('pm10', pm10_min_filled) - ) AS hourly_min_aqi, - case - when GREATEST( - calculate_aqi('pm25', pm25_filled_avg), - calculate_aqi('pm10', pm10_filled_avg) - ) <= 50 then 'Good' - when GREATEST( - calculate_aqi('pm25', pm25_filled_avg), - calculate_aqi('pm10', pm10_filled_avg) - ) <= 100 then 'Moderate' - when GREATEST( - calculate_aqi('pm25', pm25_filled_avg), - calculate_aqi('pm10', pm10_filled_avg) - ) <= 150 then 'Unhealthy for Sensitive Groups' - when GREATEST( - calculate_aqi('pm25', pm25_filled_avg), - calculate_aqi('pm10', pm10_filled_avg) - ) <= 200 then 'Unhealthy' - when GREATEST( - calculate_aqi('pm25', pm25_filled_avg), - calculate_aqi('pm10', pm10_filled_avg) - ) <= 300 then 'Very Unhealthy' - else 'Hazardous' - end as aqi_category - - FROM filled_pollutants - ORDER BY space_id, event_hour +hourly_results AS ( + SELECT + space_id, + event_date, + event_hour, + pm1_min, pm1_avg, pm1_max, + pm25_min_f, pm25_avg_f, pm25_max_f, + pm10_min_f, pm10_avg_f, pm10_max_f, + voc_min_f, voc_avg_f, voc_max_f, + co2_min_f, co2_avg_f, co2_max_f, + ch2o_min_f, ch2o_avg_f, ch2o_max_f, + + GREATEST( + calculate_aqi('pm25', pm25_min_f), + calculate_aqi('pm10', pm10_min_f) + ) AS hourly_min_aqi, + + GREATEST( + calculate_aqi('pm25', pm25_avg_f), + calculate_aqi('pm10', pm10_avg_f) + ) AS hourly_avg_aqi, + + GREATEST( + calculate_aqi('pm25', pm25_max_f), + calculate_aqi('pm10', pm10_max_f) + ) AS hourly_max_aqi, + + classify_aqi(GREATEST( + calculate_aqi('pm25', pm25_avg_f), + calculate_aqi('pm10', pm10_avg_f) + )) AS aqi_category, + + classify_aqi(calculate_aqi('pm25',pm25_avg_f)) as pm25_category, + classify_aqi(calculate_aqi('pm10',pm10_avg_f)) as pm10_category, + classify_aqi(calculate_aqi('voc',voc_avg_f)) as voc_category, + classify_aqi(calculate_aqi('co2',co2_avg_f)) as co2_category, + classify_aqi(calculate_aqi('ch2o',ch2o_avg_f)) as ch2o_category + + FROM filled_pollutants ), --- counting how many categories/hours are there in total (in case device(s) was disconnected could be less than 24) daily_category_counts AS ( - SELECT - space_id, - date_trunc('day', event_hour) AS event_day, - aqi_category, - COUNT(*) AS category_count + SELECT space_id, event_date, aqi_category AS category, 'aqi' AS pollutant, COUNT(*) AS category_count FROM hourly_results - GROUP BY space_id, event_day, aqi_category -), + GROUP BY space_id, event_date, aqi_category + + UNION ALL + + SELECT space_id, event_date, pm25_category AS category, 'pm25' AS pollutant, COUNT(*) AS category_count + FROM hourly_results + GROUP BY space_id, event_date, pm25_category + + UNION ALL + + SELECT space_id, event_date, pm10_category AS category, 'pm10' AS pollutant, COUNT(*) AS category_count + FROM hourly_results + GROUP BY space_id, event_date, pm10_category + + UNION ALL + + SELECT space_id, event_date, voc_category AS category, 'voc' AS pollutant, COUNT(*) AS category_count + FROM hourly_results + GROUP BY space_id, event_date, voc_category + + UNION ALL + + SELECT space_id, event_date, co2_category AS category, 'co2' AS pollutant, COUNT(*) AS category_count + FROM hourly_results + GROUP BY space_id, event_date, co2_category + + UNION ALL + + SELECT space_id, event_date, ch2o_category AS category, 'ch2o' AS pollutant, COUNT(*) AS category_count + FROM hourly_results + GROUP BY space_id, event_date, ch2o_category +), --- Aggregate Total Counts per Day daily_totals AS ( SELECT space_id, - event_day, + event_date, SUM(category_count) AS total_count FROM daily_category_counts - GROUP BY space_id, event_day -), - --- Calculate the daily of the daily min max avg AQI values -daily_averages AS ( - SELECT - space_id, - date_trunc('day', event_hour) AS event_day, - ROUND(AVG(hourly_avg_aqi)::numeric, 2) AS daily_avg_aqi, - ROUND(AVG(hourly_max_aqi)::numeric, 2) AS daily_max_aqi, - ROUND(AVG(hourly_min_aqi)::numeric, 2) AS daily_min_aqi - FROM hourly_results - GROUP BY space_id, event_day + where pollutant = 'aqi' + GROUP BY space_id, event_date ), -- Pivot Categories into Columns daily_percentages AS ( - SELECT + select dt.space_id, - dt.event_day, - ROUND(COALESCE(SUM(CASE WHEN dcc.aqi_category = 'Good' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.aqi_category = 'Moderate' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.aqi_category = 'Unhealthy for Sensitive Groups' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.aqi_category = 'Unhealthy' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.aqi_category = 'Very Unhealthy' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.aqi_category = 'Hazardous' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_percentage + dt.event_date, + -- AQI CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_aqi_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_aqi_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_aqi_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_aqi_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_aqi_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_aqi_percentage, + -- PM25 CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_pm25_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_pm25_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_pm25_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_pm25_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_pm25_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_pm25_percentage, + -- PM10 CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_pm10_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_pm10_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_pm10_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_pm10_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_pm10_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_pm10_percentage, + -- VOC CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_voc_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_voc_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_voc_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_voc_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_voc_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_voc_percentage, + -- CO2 CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_co2_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_co2_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_co2_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_co2_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_co2_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_co2_percentage, + -- CH20 CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_ch2o_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_ch2o_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_ch2o_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_ch2o_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_ch2o_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_ch2o_percentage FROM daily_totals dt LEFT JOIN daily_category_counts dcc - ON dt.space_id = dcc.space_id AND dt.event_day = dcc.event_day - GROUP BY dt.space_id, dt.event_day, dt.total_count -) + ON dt.space_id = dcc.space_id AND dt.event_date = dcc.event_date + GROUP BY dt.space_id, dt.event_date, dt.total_count +), --- Final Output +daily_averages AS ( + SELECT + space_id, + event_date, + -- AQI + ROUND(AVG(hourly_min_aqi)::numeric, 2) AS daily_min_aqi, + ROUND(AVG(hourly_avg_aqi)::numeric, 2) AS daily_avg_aqi, + ROUND(AVG(hourly_max_aqi)::numeric, 2) AS daily_max_aqi, + -- PM25 + ROUND(AVG(pm25_min_f)::numeric, 2) AS daily_min_pm25, + ROUND(AVG(pm25_avg_f)::numeric, 2) AS daily_avg_pm25, + ROUND(AVG(pm25_max_f)::numeric, 2) AS daily_max_pm25, + -- PM10 + ROUND(AVG(pm10_min_f)::numeric, 2) AS daily_min_pm10, + ROUND(AVG(pm10_avg_f)::numeric, 2) AS daily_avg_pm10, + ROUND(AVG(pm10_max_f)::numeric, 2) AS daily_max_pm10, + -- VOC + ROUND(AVG(voc_min_f)::numeric, 2) AS daily_min_voc, + ROUND(AVG(voc_avg_f)::numeric, 2) AS daily_avg_voc, + ROUND(AVG(voc_max_f)::numeric, 2) AS daily_max_voc, + -- CO2 + ROUND(AVG(co2_min_f)::numeric, 2) AS daily_min_co2, + ROUND(AVG(co2_avg_f)::numeric, 2) AS daily_avg_co2, + ROUND(AVG(co2_max_f)::numeric, 2) AS daily_max_co2, + -- CH2O + ROUND(AVG(ch2o_min_f)::numeric, 2) AS daily_min_ch2o, + ROUND(AVG(ch2o_avg_f)::numeric, 2) AS daily_avg_ch2o, + ROUND(AVG(ch2o_max_f)::numeric, 2) AS daily_max_ch2o + + FROM hourly_results + GROUP BY space_id, event_date +) SELECT p.space_id, - p.event_day, - p.good_percentage, - p.moderate_percentage, - p.unhealthy_sensitive_percentage, - p.unhealthy_percentage, - p.very_unhealthy_percentage, - p.hazardous_percentage, - a.daily_avg_aqi, - a.daily_max_aqi, - a.daily_min_aqi + p.event_date, + p.good_aqi_percentage, p.moderate_aqi_percentage, p.unhealthy_sensitive_aqi_percentage, p.unhealthy_aqi_percentage, p.very_unhealthy_aqi_percentage, p.hazardous_aqi_percentage, + a.daily_avg_aqi,a.daily_max_aqi, a.daily_min_aqi, + p.good_pm25_percentage, p.moderate_pm25_percentage, p.unhealthy_sensitive_pm25_percentage, p.unhealthy_pm25_percentage, p.very_unhealthy_pm25_percentage, p.hazardous_pm25_percentage, + a.daily_avg_pm25,a.daily_max_pm25, a.daily_min_pm25, + p.good_pm10_percentage, p.moderate_pm10_percentage, p.unhealthy_sensitive_pm10_percentage, p.unhealthy_pm10_percentage, p.very_unhealthy_pm10_percentage, p.hazardous_pm10_percentage, + a.daily_avg_pm10, a.daily_max_pm10, a.daily_min_pm10, + p.good_voc_percentage, p.moderate_voc_percentage, p.unhealthy_sensitive_voc_percentage, p.unhealthy_voc_percentage, p.very_unhealthy_voc_percentage, p.hazardous_voc_percentage, + a.daily_avg_voc, a.daily_max_voc, a.daily_min_voc, + p.good_co2_percentage, p.moderate_co2_percentage, p.unhealthy_sensitive_co2_percentage, p.unhealthy_co2_percentage, p.very_unhealthy_co2_percentage, p.hazardous_co2_percentage, + a.daily_avg_co2,a.daily_max_co2, a.daily_min_co2, + p.good_ch2o_percentage, p.moderate_ch2o_percentage, p.unhealthy_sensitive_ch2o_percentage, p.unhealthy_ch2o_percentage, p.very_unhealthy_ch2o_percentage, p.hazardous_ch2o_percentage, + a.daily_avg_ch2o,a.daily_max_ch2o, a.daily_min_ch2o FROM daily_percentages p LEFT JOIN daily_averages a - ON p.space_id = a.space_id AND p.event_day = a.event_day -ORDER BY p.space_id, p.event_day; \ No newline at end of file + ON p.space_id = a.space_id AND p.event_date = a.event_date +ORDER BY p.space_id, p.event_date; + + From cd3e9016f24c9771c5a698d07d7fc5ace97eb454 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Wed, 4 Jun 2025 01:08:33 -0600 Subject: [PATCH 07/17] fix: improve error handling in fetchWeatherDetails method --- src/space/services/space.service.ts | 31 +++++++++--------- src/weather/services/weather.service.ts | 43 +++++++++++++++---------- 2 files changed, 42 insertions(+), 32 deletions(-) diff --git a/src/space/services/space.service.ts b/src/space/services/space.service.ts index 246f583..c001394 100644 --- a/src/space/services/space.service.ts +++ b/src/space/services/space.service.ts @@ -213,8 +213,8 @@ export class SpaceService { { incomingConnectionDisabled: false }, ) .leftJoinAndSelect('space.productAllocations', 'productAllocations') - .leftJoinAndSelect('productAllocations.tags', 'tags') - .leftJoinAndSelect('tags.product', 'tagProduct') + // .leftJoinAndSelect('productAllocations.tags', 'tags') + // .leftJoinAndSelect('productAllocations.product', 'product') .leftJoinAndSelect( 'space.subspaces', 'subspaces', @@ -225,8 +225,11 @@ export class SpaceService { 'subspaces.productAllocations', 'subspaceProductAllocations', ) - .leftJoinAndSelect('subspaceProductAllocations.tags', 'subspaceTags') - .leftJoinAndSelect('subspaceTags.product', 'subspaceTagProduct') + // .leftJoinAndSelect('subspaceProductAllocations.tags', 'subspaceTag') + // .leftJoinAndSelect( + // 'subspaceProductAllocations.product', + // 'subspaceProduct', + // ) .leftJoinAndSelect('space.spaceModel', 'spaceModel') .where('space.community_id = :communityUuid', { communityUuid }) .andWhere('space.spaceName != :orphanSpaceName', { @@ -264,9 +267,7 @@ export class SpaceService { }), ); } - - const transformedSpaces = spaces.map(this.transformSpace); - const spaceHierarchy = this.buildSpaceHierarchy(transformedSpaces); + const spaceHierarchy = this.buildSpaceHierarchy(spaces); return new SuccessResponseDto({ message: `Spaces in community ${communityUuid} successfully fetched in hierarchy`, @@ -326,13 +327,13 @@ export class SpaceService { 'incomingConnections.disabled = :incomingConnectionDisabled', { incomingConnectionDisabled: false }, ) - .leftJoinAndSelect( - 'space.tags', - 'tags', - 'tags.disabled = :tagDisabled', - { tagDisabled: false }, - ) - .leftJoinAndSelect('tags.product', 'tagProduct') + // .leftJoinAndSelect( + // 'space.tags', + // 'tags', + // 'tags.disabled = :tagDisabled', + // { tagDisabled: false }, + // ) + // .leftJoinAndSelect('tags.product', 'tagProduct') .leftJoinAndSelect( 'space.subspaces', 'subspaces', @@ -345,7 +346,7 @@ export class SpaceService { 'subspaceTags.disabled = :subspaceTagsDisabled', { subspaceTagsDisabled: false }, ) - .leftJoinAndSelect('subspaceTags.product', 'subspaceTagProduct') + // .leftJoinAndSelect('subspaceTags.product', 'subspaceTagProduct') .where('space.community_id = :communityUuid', { communityUuid }) .andWhere('space.spaceName != :orphanSpaceName', { orphanSpaceName: ORPHAN_SPACE_NAME, diff --git a/src/weather/services/weather.service.ts b/src/weather/services/weather.service.ts index 11e1677..d534057 100644 --- a/src/weather/services/weather.service.ts +++ b/src/weather/services/weather.service.ts @@ -1,4 +1,4 @@ -import { HttpStatus, Injectable } from '@nestjs/common'; +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; import { HttpService } from '@nestjs/axios'; import { ConfigService } from '@nestjs/config'; import { firstValueFrom } from 'rxjs'; @@ -17,23 +17,32 @@ export class WeatherService { async fetchWeatherDetails( query: GetWeatherDetailsDto, ): Promise { - const { lat, lon } = query; - const weatherApiKey = this.configService.get( - 'OPEN_WEATHER_MAP_API_KEY', - ); - const url = `http://api.weatherapi.com/v1/current.json?key=${weatherApiKey}&q=${lat},${lon}&aqi=yes`; + try { + const { lat, lon } = query; + const weatherApiKey = this.configService.get( + 'OPEN_WEATHER_MAP_API_KEY', + ); + const url = `http://api.weatherapi.com/v1/current.json?key=${weatherApiKey}&q=${lat},${lon}&aqi=yes`; - const response = await firstValueFrom(this.httpService.get(url)); - const pm2_5 = response.data.current.air_quality.pm2_5; // Raw PM2.5 (µg/m³) + const response = await firstValueFrom(this.httpService.get(url)); + const pm2_5 = response.data.current.air_quality.pm2_5; // Raw PM2.5 (µg/m³) - return new SuccessResponseDto({ - message: `Weather details fetched successfully`, - data: { - aqi: calculateAQI(pm2_5), // Converted AQI (0-500) - temperature: response.data.current.temp_c, - humidity: response.data.current.humidity, - }, - statusCode: HttpStatus.OK, - }); + return new SuccessResponseDto({ + message: `Weather details fetched successfully`, + data: { + aqi: calculateAQI(pm2_5), // Converted AQI (0-500) + temperature: response.data.current.temp_c, + humidity: response.data.current.humidity, + }, + statusCode: HttpStatus.OK, + }); + } catch (error) { + console.log(`Error fetching weather data: ${error}`); + + throw new HttpException( + `Wrong latitude or longitude provided`, + error.response?.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } } } From ea021ad2280c113b8981627cf0e3d4e2913533fb Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Wed, 4 Jun 2025 01:09:49 -0600 Subject: [PATCH 08/17] fix: update error message for invalid latitude and longitude in fetchWeatherDetails method --- src/weather/services/weather.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/weather/services/weather.service.ts b/src/weather/services/weather.service.ts index d534057..aa4ea0d 100644 --- a/src/weather/services/weather.service.ts +++ b/src/weather/services/weather.service.ts @@ -40,7 +40,7 @@ export class WeatherService { console.log(`Error fetching weather data: ${error}`); throw new HttpException( - `Wrong latitude or longitude provided`, + `Api can't handle these lat and lon values`, error.response?.status || HttpStatus.INTERNAL_SERVER_ERROR, ); } From 43dfaaa90d0e2775fbf979edbe282161b489f800 Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Wed, 4 Jun 2025 01:32:01 -0600 Subject: [PATCH 09/17] fix: utilize WEATHER_API_URL in WeatherService for dynamic API endpoint --- src/config/weather.open.config.ts | 1 + src/weather/services/weather.service.ts | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/config/weather.open.config.ts b/src/config/weather.open.config.ts index 2182490..94b77d5 100644 --- a/src/config/weather.open.config.ts +++ b/src/config/weather.open.config.ts @@ -4,5 +4,6 @@ export default registerAs( 'openweather-config', (): Record => ({ OPEN_WEATHER_MAP_API_KEY: process.env.OPEN_WEATHER_MAP_API_KEY, + WEATHER_API_URL: process.env.WEATHER_API_URL, }), ); diff --git a/src/weather/services/weather.service.ts b/src/weather/services/weather.service.ts index aa4ea0d..3463b5c 100644 --- a/src/weather/services/weather.service.ts +++ b/src/weather/services/weather.service.ts @@ -9,10 +9,13 @@ import { SuccessResponseDto } from '@app/common/dto/success.response.dto'; @Injectable() export class WeatherService { + private readonly weatherApiUrl: string; constructor( private readonly configService: ConfigService, private readonly httpService: HttpService, - ) {} + ) { + this.weatherApiUrl = this.configService.get('WEATHER_API_URL'); + } async fetchWeatherDetails( query: GetWeatherDetailsDto, @@ -22,7 +25,7 @@ export class WeatherService { const weatherApiKey = this.configService.get( 'OPEN_WEATHER_MAP_API_KEY', ); - const url = `http://api.weatherapi.com/v1/current.json?key=${weatherApiKey}&q=${lat},${lon}&aqi=yes`; + const url = `${this.weatherApiUrl}/current.json?key=${weatherApiKey}&q=${lat},${lon}&aqi=yes`; const response = await firstValueFrom(this.httpService.get(url)); const pm2_5 = response.data.current.air_quality.pm2_5; // Raw PM2.5 (µg/m³) From 466863e71f00961fc947b46ca44ad87f76496785 Mon Sep 17 00:00:00 2001 From: khuss Date: Wed, 4 Jun 2025 16:33:55 -0400 Subject: [PATCH 10/17] Procedures insert-all, update, and select for daily space air quality stats --- .../proceduce_select_daily_space_aqi.sql | 374 ++++++++++++++++++ .../proceduce_update_daily_space_aqi.sql | 374 ++++++++++++++++++ .../procedure_insert_all_daily_space_aqi.sql | 367 +++++++++++++++++ 3 files changed, 1115 insertions(+) create mode 100644 libs/common/src/sql/procedures/fact_daily_space_aqi/proceduce_select_daily_space_aqi.sql create mode 100644 libs/common/src/sql/procedures/fact_daily_space_aqi/proceduce_update_daily_space_aqi.sql create mode 100644 libs/common/src/sql/procedures/fact_daily_space_aqi/procedure_insert_all_daily_space_aqi.sql diff --git a/libs/common/src/sql/procedures/fact_daily_space_aqi/proceduce_select_daily_space_aqi.sql b/libs/common/src/sql/procedures/fact_daily_space_aqi/proceduce_select_daily_space_aqi.sql new file mode 100644 index 0000000..993f3d0 --- /dev/null +++ b/libs/common/src/sql/procedures/fact_daily_space_aqi/proceduce_select_daily_space_aqi.sql @@ -0,0 +1,374 @@ +WITH params AS ( + SELECT + TO_DATE(NULLIF($1, ''), 'YYYY-MM-DD') AS event_date, + $2::uuid AS space_id +), + +-- Query Pipeline Starts Here +WITH device_space AS ( + SELECT + device.uuid AS device_id, + device.space_device_uuid AS space_id, + "device-status-log".event_time::timestamp AS event_time, + "device-status-log".code, + "device-status-log".value + FROM device + LEFT JOIN "device-status-log" + ON device.uuid = "device-status-log".device_id + LEFT JOIN product + ON product.uuid = device.product_device_uuid + WHERE product.cat_name = 'hjjcy' +), + +average_pollutants AS ( + SELECT + event_time::date AS event_date, + date_trunc('hour', event_time) AS event_hour, + space_id, + + -- PM1 + MIN(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_min, + AVG(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_avg, + MAX(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_max, + + -- PM25 + MIN(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_min, + AVG(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_avg, + MAX(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_max, + + -- PM10 + MIN(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_min, + AVG(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_avg, + MAX(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_max, + + -- VOC + MIN(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_min, + AVG(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_avg, + MAX(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_max, + + -- CH2O + MIN(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_min, + AVG(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_avg, + MAX(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_max, + + -- CO2 + MIN(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_min, + AVG(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_avg, + MAX(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_max + + FROM device_space + GROUP BY space_id, event_hour, event_date +), + +filled_pollutants AS ( + SELECT + *, + -- AVG + COALESCE(pm25_avg, LAG(pm25_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm25_avg_f, + COALESCE(pm10_avg, LAG(pm10_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm10_avg_f, + COALESCE(voc_avg, LAG(voc_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS voc_avg_f, + COALESCE(co2_avg, LAG(co2_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS co2_avg_f, + COALESCE(ch2o_avg, LAG(ch2o_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS ch2o_avg_f, + + -- MIN + COALESCE(pm25_min, LAG(pm25_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm25_min_f, + COALESCE(pm10_min, LAG(pm10_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm10_min_f, + COALESCE(voc_min, LAG(voc_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS voc_min_f, + COALESCE(co2_min, LAG(co2_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS co2_min_f, + COALESCE(ch2o_min, LAG(ch2o_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS ch2o_min_f, + + -- MAX + COALESCE(pm25_max, LAG(pm25_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm25_max_f, + COALESCE(pm10_max, LAG(pm10_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm10_max_f, + COALESCE(voc_max, LAG(voc_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS voc_max_f, + COALESCE(co2_max, LAG(co2_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS co2_max_f, + COALESCE(ch2o_max, LAG(ch2o_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS ch2o_max_f + FROM average_pollutants +), + +hourly_results AS ( + SELECT + space_id, + event_date, + event_hour, + pm1_min, pm1_avg, pm1_max, + pm25_min_f, pm25_avg_f, pm25_max_f, + pm10_min_f, pm10_avg_f, pm10_max_f, + voc_min_f, voc_avg_f, voc_max_f, + co2_min_f, co2_avg_f, co2_max_f, + ch2o_min_f, ch2o_avg_f, ch2o_max_f, + + GREATEST( + calculate_aqi('pm25', pm25_min_f), + calculate_aqi('pm10', pm10_min_f) + ) AS hourly_min_aqi, + + GREATEST( + calculate_aqi('pm25', pm25_avg_f), + calculate_aqi('pm10', pm10_avg_f) + ) AS hourly_avg_aqi, + + GREATEST( + calculate_aqi('pm25', pm25_max_f), + calculate_aqi('pm10', pm10_max_f) + ) AS hourly_max_aqi, + + classify_aqi(GREATEST( + calculate_aqi('pm25', pm25_avg_f), + calculate_aqi('pm10', pm10_avg_f) + )) AS aqi_category, + + classify_aqi(calculate_aqi('pm25',pm25_avg_f)) as pm25_category, + classify_aqi(calculate_aqi('pm10',pm10_avg_f)) as pm10_category, + classify_aqi(calculate_aqi('voc',voc_avg_f)) as voc_category, + classify_aqi(calculate_aqi('co2',co2_avg_f)) as co2_category, + classify_aqi(calculate_aqi('ch2o',ch2o_avg_f)) as ch2o_category + + FROM filled_pollutants +), + +daily_category_counts AS ( + SELECT space_id, event_date, aqi_category AS category, 'aqi' AS pollutant, COUNT(*) AS category_count + FROM hourly_results + GROUP BY space_id, event_date, aqi_category + + UNION ALL + + SELECT space_id, event_date, pm25_category AS category, 'pm25' AS pollutant, COUNT(*) AS category_count + FROM hourly_results + GROUP BY space_id, event_date, pm25_category + + UNION ALL + + SELECT space_id, event_date, pm10_category AS category, 'pm10' AS pollutant, COUNT(*) AS category_count + FROM hourly_results + GROUP BY space_id, event_date, pm10_category + + UNION ALL + + SELECT space_id, event_date, voc_category AS category, 'voc' AS pollutant, COUNT(*) AS category_count + FROM hourly_results + GROUP BY space_id, event_date, voc_category + + UNION ALL + + SELECT space_id, event_date, co2_category AS category, 'co2' AS pollutant, COUNT(*) AS category_count + FROM hourly_results + GROUP BY space_id, event_date, co2_category + + UNION ALL + + SELECT space_id, event_date, ch2o_category AS category, 'ch2o' AS pollutant, COUNT(*) AS category_count + FROM hourly_results + GROUP BY space_id, event_date, ch2o_category +), + +daily_totals AS ( + SELECT + space_id, + event_date, + SUM(category_count) AS total_count + FROM daily_category_counts + where pollutant = 'aqi' + GROUP BY space_id, event_date +), + +-- Pivot Categories into Columns +daily_percentages AS ( + select + dt.space_id, + dt.event_date, + -- AQI CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_aqi_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_aqi_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_aqi_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_aqi_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_aqi_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_aqi_percentage, + -- PM25 CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_pm25_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_pm25_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_pm25_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_pm25_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_pm25_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_pm25_percentage, + -- PM10 CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_pm10_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_pm10_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_pm10_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_pm10_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_pm10_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_pm10_percentage, + -- VOC CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_voc_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_voc_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_voc_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_voc_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_voc_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_voc_percentage, + -- CO2 CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_co2_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_co2_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_co2_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_co2_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_co2_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_co2_percentage, + -- CH20 CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_ch2o_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_ch2o_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_ch2o_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_ch2o_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_ch2o_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_ch2o_percentage + FROM daily_totals dt + LEFT JOIN daily_category_counts dcc + ON dt.space_id = dcc.space_id AND dt.event_date = dcc.event_date + GROUP BY dt.space_id, dt.event_date, dt.total_count +), + +daily_averages AS ( + SELECT + space_id, + event_date, + -- AQI + ROUND(AVG(hourly_min_aqi)::numeric, 2) AS daily_min_aqi, + ROUND(AVG(hourly_avg_aqi)::numeric, 2) AS daily_avg_aqi, + ROUND(AVG(hourly_max_aqi)::numeric, 2) AS daily_max_aqi, + -- PM25 + ROUND(AVG(pm25_min_f)::numeric, 2) AS daily_min_pm25, + ROUND(AVG(pm25_avg_f)::numeric, 2) AS daily_avg_pm25, + ROUND(AVG(pm25_max_f)::numeric, 2) AS daily_max_pm25, + -- PM10 + ROUND(AVG(pm10_min_f)::numeric, 2) AS daily_min_pm10, + ROUND(AVG(pm10_avg_f)::numeric, 2) AS daily_avg_pm10, + ROUND(AVG(pm10_max_f)::numeric, 2) AS daily_max_pm10, + -- VOC + ROUND(AVG(voc_min_f)::numeric, 2) AS daily_min_voc, + ROUND(AVG(voc_avg_f)::numeric, 2) AS daily_avg_voc, + ROUND(AVG(voc_max_f)::numeric, 2) AS daily_max_voc, + -- CO2 + ROUND(AVG(co2_min_f)::numeric, 2) AS daily_min_co2, + ROUND(AVG(co2_avg_f)::numeric, 2) AS daily_avg_co2, + ROUND(AVG(co2_max_f)::numeric, 2) AS daily_max_co2, + -- CH2O + ROUND(AVG(ch2o_min_f)::numeric, 2) AS daily_min_ch2o, + ROUND(AVG(ch2o_avg_f)::numeric, 2) AS daily_avg_ch2o, + ROUND(AVG(ch2o_max_f)::numeric, 2) AS daily_max_ch2o + + FROM hourly_results + GROUP BY space_id, event_date +), + +final_data as( +SELECT + p.space_id, + p.event_date, + p.good_aqi_percentage, p.moderate_aqi_percentage, p.unhealthy_sensitive_aqi_percentage, p.unhealthy_aqi_percentage, p.very_unhealthy_aqi_percentage, p.hazardous_aqi_percentage, + a.daily_avg_aqi,a.daily_max_aqi, a.daily_min_aqi, + p.good_pm25_percentage, p.moderate_pm25_percentage, p.unhealthy_sensitive_pm25_percentage, p.unhealthy_pm25_percentage, p.very_unhealthy_pm25_percentage, p.hazardous_pm25_percentage, + a.daily_avg_pm25,a.daily_max_pm25, a.daily_min_pm25, + p.good_pm10_percentage, p.moderate_pm10_percentage, p.unhealthy_sensitive_pm10_percentage, p.unhealthy_pm10_percentage, p.very_unhealthy_pm10_percentage, p.hazardous_pm10_percentage, + a.daily_avg_pm10, a.daily_max_pm10, a.daily_min_pm10, + p.good_voc_percentage, p.moderate_voc_percentage, p.unhealthy_sensitive_voc_percentage, p.unhealthy_voc_percentage, p.very_unhealthy_voc_percentage, p.hazardous_voc_percentage, + a.daily_avg_voc, a.daily_max_voc, a.daily_min_voc, + p.good_co2_percentage, p.moderate_co2_percentage, p.unhealthy_sensitive_co2_percentage, p.unhealthy_co2_percentage, p.very_unhealthy_co2_percentage, p.hazardous_co2_percentage, + a.daily_avg_co2,a.daily_max_co2, a.daily_min_co2, + p.good_ch2o_percentage, p.moderate_ch2o_percentage, p.unhealthy_sensitive_ch2o_percentage, p.unhealthy_ch2o_percentage, p.very_unhealthy_ch2o_percentage, p.hazardous_ch2o_percentage, + a.daily_avg_ch2o,a.daily_max_ch2o, a.daily_min_ch2o +FROM daily_percentages p +LEFT JOIN daily_averages a + ON p.space_id = a.space_id AND p.event_date = a.event_date +ORDER BY p.space_id, p.event_date) + + +INSERT INTO public.space_daily_pollutant_stats ( + space_uuid, + event_date, + good_aqi_percentage, moderate_aqi_percentage, unhealthy_sensitive_aqi_percentage, unhealthy_aqi_percentage, very_unhealthy_aqi_percentage, hazardous_aqi_percentage, + daily_avg_aqi, daily_max_aqi, daily_min_aqi, + good_pm25_percentage, moderate_pm25_percentage, unhealthy_sensitive_pm25_percentage, unhealthy_pm25_percentage, very_unhealthy_pm25_percentage, hazardous_pm25_percentage, + daily_avg_pm25, daily_max_pm25, daily_min_pm25, + good_pm10_percentage, moderate_pm10_percentage, unhealthy_sensitive_pm10_percentage, unhealthy_pm10_percentage, very_unhealthy_pm10_percentage, hazardous_pm10_percentage, + daily_avg_pm10, daily_max_pm10, daily_min_pm10, + good_voc_percentage, moderate_voc_percentage, unhealthy_sensitive_voc_percentage, unhealthy_voc_percentage, very_unhealthy_voc_percentage, hazardous_voc_percentage, + daily_avg_voc, daily_max_voc, daily_min_voc, + good_co2_percentage, moderate_co2_percentage, unhealthy_sensitive_co2_percentage, unhealthy_co2_percentage, very_unhealthy_co2_percentage, hazardous_co2_percentage, + daily_avg_co2, daily_max_co2, daily_min_co2, + good_ch2o_percentage, moderate_ch2o_percentage, unhealthy_sensitive_ch2o_percentage, unhealthy_ch2o_percentage, very_unhealthy_ch2o_percentage, hazardous_ch2o_percentage, + daily_avg_ch2o, daily_max_ch2o, daily_min_ch2o +) +SELECT + space_id, + event_date, + good_aqi_percentage, moderate_aqi_percentage, unhealthy_sensitive_aqi_percentage, unhealthy_aqi_percentage, very_unhealthy_aqi_percentage, hazardous_aqi_percentage, + daily_avg_aqi, daily_max_aqi, daily_min_aqi, + good_pm25_percentage, moderate_pm25_percentage, unhealthy_sensitive_pm25_percentage, unhealthy_pm25_percentage, very_unhealthy_pm25_percentage, hazardous_pm25_percentage, + daily_avg_pm25, daily_max_pm25, daily_min_pm25, + good_pm10_percentage, moderate_pm10_percentage, unhealthy_sensitive_pm10_percentage, unhealthy_pm10_percentage, very_unhealthy_pm10_percentage, hazardous_pm10_percentage, + daily_avg_pm10, daily_max_pm10, daily_min_pm10, + good_voc_percentage, moderate_voc_percentage, unhealthy_sensitive_voc_percentage, unhealthy_voc_percentage, very_unhealthy_voc_percentage, hazardous_voc_percentage, + daily_avg_voc, daily_max_voc, daily_min_voc, + good_co2_percentage, moderate_co2_percentage, unhealthy_sensitive_co2_percentage, unhealthy_co2_percentage, very_unhealthy_co2_percentage, hazardous_co2_percentage, + daily_avg_co2, daily_max_co2, daily_min_co2, + good_ch2o_percentage, moderate_ch2o_percentage, unhealthy_sensitive_ch2o_percentage, unhealthy_ch2o_percentage, very_unhealthy_ch2o_percentage, hazardous_ch2o_percentage, + daily_avg_ch2o, daily_max_ch2o, daily_min_ch2o +FROM final_data +ON CONFLICT (space_uuid, event_date) DO UPDATE +SET + good_aqi_percentage = EXCLUDED.good_aqi_percentage, + moderate_aqi_percentage = EXCLUDED.moderate_aqi_percentage, + unhealthy_sensitive_aqi_percentage = EXCLUDED.unhealthy_sensitive_aqi_percentage, + unhealthy_aqi_percentage = EXCLUDED.unhealthy_aqi_percentage, + very_unhealthy_aqi_percentage = EXCLUDED.very_unhealthy_aqi_percentage, + hazardous_aqi_percentage = EXCLUDED.hazardous_aqi_percentage, + daily_avg_aqi = EXCLUDED.daily_avg_aqi, + daily_max_aqi = EXCLUDED.daily_max_aqi, + daily_min_aqi = EXCLUDED.daily_min_aqi, + good_pm25_percentage = EXCLUDED.good_pm25_percentage, + moderate_pm25_percentage = EXCLUDED.moderate_pm25_percentage, + unhealthy_sensitive_pm25_percentage = EXCLUDED.unhealthy_sensitive_pm25_percentage, + unhealthy_pm25_percentage = EXCLUDED.unhealthy_pm25_percentage, + very_unhealthy_pm25_percentage = EXCLUDED.very_unhealthy_pm25_percentage, + hazardous_pm25_percentage = EXCLUDED.hazardous_pm25_percentage, + daily_avg_pm25 = EXCLUDED.daily_avg_pm25, + daily_max_pm25 = EXCLUDED.daily_max_pm25, + daily_min_pm25 = EXCLUDED.daily_min_pm25, + good_pm10_percentage = EXCLUDED.good_pm10_percentage, + moderate_pm10_percentage = EXCLUDED.moderate_pm10_percentage, + unhealthy_sensitive_pm10_percentage = EXCLUDED.unhealthy_sensitive_pm10_percentage, + unhealthy_pm10_percentage = EXCLUDED.unhealthy_pm10_percentage, + very_unhealthy_pm10_percentage = EXCLUDED.very_unhealthy_pm10_percentage, + hazardous_pm10_percentage = EXCLUDED.hazardous_pm10_percentage, + daily_avg_pm10 = EXCLUDED.daily_avg_pm10, + daily_max_pm10 = EXCLUDED.daily_max_pm10, + daily_min_pm10 = EXCLUDED.daily_min_pm10, + good_voc_percentage = EXCLUDED.good_voc_percentage, + moderate_voc_percentage = EXCLUDED.moderate_voc_percentage, + unhealthy_sensitive_voc_percentage = EXCLUDED.unhealthy_sensitive_voc_percentage, + unhealthy_voc_percentage = EXCLUDED.unhealthy_voc_percentage, + very_unhealthy_voc_percentage = EXCLUDED.very_unhealthy_voc_percentage, + hazardous_voc_percentage = EXCLUDED.hazardous_voc_percentage, + daily_avg_voc = EXCLUDED.daily_avg_voc, + daily_max_voc = EXCLUDED.daily_max_voc, + daily_min_voc = EXCLUDED.daily_min_voc, + good_co2_percentage = EXCLUDED.good_co2_percentage, + moderate_co2_percentage = EXCLUDED.moderate_co2_percentage, + unhealthy_sensitive_co2_percentage = EXCLUDED.unhealthy_sensitive_co2_percentage, + unhealthy_co2_percentage = EXCLUDED.unhealthy_co2_percentage, + very_unhealthy_co2_percentage = EXCLUDED.very_unhealthy_co2_percentage, + hazardous_co2_percentage = EXCLUDED.hazardous_co2_percentage, + daily_avg_co2 = EXCLUDED.daily_avg_co2, + daily_max_co2 = EXCLUDED.daily_max_co2, + daily_min_co2 = EXCLUDED.daily_min_co2, + good_ch2o_percentage = EXCLUDED.good_ch2o_percentage, + moderate_ch2o_percentage = EXCLUDED.moderate_ch2o_percentage, + unhealthy_sensitive_ch2o_percentage = EXCLUDED.unhealthy_sensitive_ch2o_percentage, + unhealthy_ch2o_percentage = EXCLUDED.unhealthy_ch2o_percentage, + very_unhealthy_ch2o_percentage = EXCLUDED.very_unhealthy_ch2o_percentage, + hazardous_ch2o_percentage = EXCLUDED.hazardous_ch2o_percentage, + daily_avg_ch2o = EXCLUDED.daily_avg_ch2o, + daily_max_ch2o = EXCLUDED.daily_max_ch2o, + daily_min_ch2o = EXCLUDED.daily_min_ch2o; + + + diff --git a/libs/common/src/sql/procedures/fact_daily_space_aqi/proceduce_update_daily_space_aqi.sql b/libs/common/src/sql/procedures/fact_daily_space_aqi/proceduce_update_daily_space_aqi.sql new file mode 100644 index 0000000..993f3d0 --- /dev/null +++ b/libs/common/src/sql/procedures/fact_daily_space_aqi/proceduce_update_daily_space_aqi.sql @@ -0,0 +1,374 @@ +WITH params AS ( + SELECT + TO_DATE(NULLIF($1, ''), 'YYYY-MM-DD') AS event_date, + $2::uuid AS space_id +), + +-- Query Pipeline Starts Here +WITH device_space AS ( + SELECT + device.uuid AS device_id, + device.space_device_uuid AS space_id, + "device-status-log".event_time::timestamp AS event_time, + "device-status-log".code, + "device-status-log".value + FROM device + LEFT JOIN "device-status-log" + ON device.uuid = "device-status-log".device_id + LEFT JOIN product + ON product.uuid = device.product_device_uuid + WHERE product.cat_name = 'hjjcy' +), + +average_pollutants AS ( + SELECT + event_time::date AS event_date, + date_trunc('hour', event_time) AS event_hour, + space_id, + + -- PM1 + MIN(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_min, + AVG(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_avg, + MAX(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_max, + + -- PM25 + MIN(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_min, + AVG(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_avg, + MAX(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_max, + + -- PM10 + MIN(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_min, + AVG(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_avg, + MAX(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_max, + + -- VOC + MIN(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_min, + AVG(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_avg, + MAX(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_max, + + -- CH2O + MIN(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_min, + AVG(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_avg, + MAX(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_max, + + -- CO2 + MIN(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_min, + AVG(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_avg, + MAX(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_max + + FROM device_space + GROUP BY space_id, event_hour, event_date +), + +filled_pollutants AS ( + SELECT + *, + -- AVG + COALESCE(pm25_avg, LAG(pm25_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm25_avg_f, + COALESCE(pm10_avg, LAG(pm10_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm10_avg_f, + COALESCE(voc_avg, LAG(voc_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS voc_avg_f, + COALESCE(co2_avg, LAG(co2_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS co2_avg_f, + COALESCE(ch2o_avg, LAG(ch2o_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS ch2o_avg_f, + + -- MIN + COALESCE(pm25_min, LAG(pm25_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm25_min_f, + COALESCE(pm10_min, LAG(pm10_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm10_min_f, + COALESCE(voc_min, LAG(voc_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS voc_min_f, + COALESCE(co2_min, LAG(co2_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS co2_min_f, + COALESCE(ch2o_min, LAG(ch2o_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS ch2o_min_f, + + -- MAX + COALESCE(pm25_max, LAG(pm25_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm25_max_f, + COALESCE(pm10_max, LAG(pm10_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm10_max_f, + COALESCE(voc_max, LAG(voc_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS voc_max_f, + COALESCE(co2_max, LAG(co2_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS co2_max_f, + COALESCE(ch2o_max, LAG(ch2o_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS ch2o_max_f + FROM average_pollutants +), + +hourly_results AS ( + SELECT + space_id, + event_date, + event_hour, + pm1_min, pm1_avg, pm1_max, + pm25_min_f, pm25_avg_f, pm25_max_f, + pm10_min_f, pm10_avg_f, pm10_max_f, + voc_min_f, voc_avg_f, voc_max_f, + co2_min_f, co2_avg_f, co2_max_f, + ch2o_min_f, ch2o_avg_f, ch2o_max_f, + + GREATEST( + calculate_aqi('pm25', pm25_min_f), + calculate_aqi('pm10', pm10_min_f) + ) AS hourly_min_aqi, + + GREATEST( + calculate_aqi('pm25', pm25_avg_f), + calculate_aqi('pm10', pm10_avg_f) + ) AS hourly_avg_aqi, + + GREATEST( + calculate_aqi('pm25', pm25_max_f), + calculate_aqi('pm10', pm10_max_f) + ) AS hourly_max_aqi, + + classify_aqi(GREATEST( + calculate_aqi('pm25', pm25_avg_f), + calculate_aqi('pm10', pm10_avg_f) + )) AS aqi_category, + + classify_aqi(calculate_aqi('pm25',pm25_avg_f)) as pm25_category, + classify_aqi(calculate_aqi('pm10',pm10_avg_f)) as pm10_category, + classify_aqi(calculate_aqi('voc',voc_avg_f)) as voc_category, + classify_aqi(calculate_aqi('co2',co2_avg_f)) as co2_category, + classify_aqi(calculate_aqi('ch2o',ch2o_avg_f)) as ch2o_category + + FROM filled_pollutants +), + +daily_category_counts AS ( + SELECT space_id, event_date, aqi_category AS category, 'aqi' AS pollutant, COUNT(*) AS category_count + FROM hourly_results + GROUP BY space_id, event_date, aqi_category + + UNION ALL + + SELECT space_id, event_date, pm25_category AS category, 'pm25' AS pollutant, COUNT(*) AS category_count + FROM hourly_results + GROUP BY space_id, event_date, pm25_category + + UNION ALL + + SELECT space_id, event_date, pm10_category AS category, 'pm10' AS pollutant, COUNT(*) AS category_count + FROM hourly_results + GROUP BY space_id, event_date, pm10_category + + UNION ALL + + SELECT space_id, event_date, voc_category AS category, 'voc' AS pollutant, COUNT(*) AS category_count + FROM hourly_results + GROUP BY space_id, event_date, voc_category + + UNION ALL + + SELECT space_id, event_date, co2_category AS category, 'co2' AS pollutant, COUNT(*) AS category_count + FROM hourly_results + GROUP BY space_id, event_date, co2_category + + UNION ALL + + SELECT space_id, event_date, ch2o_category AS category, 'ch2o' AS pollutant, COUNT(*) AS category_count + FROM hourly_results + GROUP BY space_id, event_date, ch2o_category +), + +daily_totals AS ( + SELECT + space_id, + event_date, + SUM(category_count) AS total_count + FROM daily_category_counts + where pollutant = 'aqi' + GROUP BY space_id, event_date +), + +-- Pivot Categories into Columns +daily_percentages AS ( + select + dt.space_id, + dt.event_date, + -- AQI CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_aqi_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_aqi_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_aqi_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_aqi_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_aqi_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_aqi_percentage, + -- PM25 CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_pm25_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_pm25_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_pm25_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_pm25_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_pm25_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_pm25_percentage, + -- PM10 CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_pm10_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_pm10_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_pm10_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_pm10_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_pm10_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_pm10_percentage, + -- VOC CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_voc_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_voc_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_voc_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_voc_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_voc_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_voc_percentage, + -- CO2 CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_co2_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_co2_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_co2_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_co2_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_co2_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_co2_percentage, + -- CH20 CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_ch2o_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_ch2o_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_ch2o_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_ch2o_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_ch2o_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_ch2o_percentage + FROM daily_totals dt + LEFT JOIN daily_category_counts dcc + ON dt.space_id = dcc.space_id AND dt.event_date = dcc.event_date + GROUP BY dt.space_id, dt.event_date, dt.total_count +), + +daily_averages AS ( + SELECT + space_id, + event_date, + -- AQI + ROUND(AVG(hourly_min_aqi)::numeric, 2) AS daily_min_aqi, + ROUND(AVG(hourly_avg_aqi)::numeric, 2) AS daily_avg_aqi, + ROUND(AVG(hourly_max_aqi)::numeric, 2) AS daily_max_aqi, + -- PM25 + ROUND(AVG(pm25_min_f)::numeric, 2) AS daily_min_pm25, + ROUND(AVG(pm25_avg_f)::numeric, 2) AS daily_avg_pm25, + ROUND(AVG(pm25_max_f)::numeric, 2) AS daily_max_pm25, + -- PM10 + ROUND(AVG(pm10_min_f)::numeric, 2) AS daily_min_pm10, + ROUND(AVG(pm10_avg_f)::numeric, 2) AS daily_avg_pm10, + ROUND(AVG(pm10_max_f)::numeric, 2) AS daily_max_pm10, + -- VOC + ROUND(AVG(voc_min_f)::numeric, 2) AS daily_min_voc, + ROUND(AVG(voc_avg_f)::numeric, 2) AS daily_avg_voc, + ROUND(AVG(voc_max_f)::numeric, 2) AS daily_max_voc, + -- CO2 + ROUND(AVG(co2_min_f)::numeric, 2) AS daily_min_co2, + ROUND(AVG(co2_avg_f)::numeric, 2) AS daily_avg_co2, + ROUND(AVG(co2_max_f)::numeric, 2) AS daily_max_co2, + -- CH2O + ROUND(AVG(ch2o_min_f)::numeric, 2) AS daily_min_ch2o, + ROUND(AVG(ch2o_avg_f)::numeric, 2) AS daily_avg_ch2o, + ROUND(AVG(ch2o_max_f)::numeric, 2) AS daily_max_ch2o + + FROM hourly_results + GROUP BY space_id, event_date +), + +final_data as( +SELECT + p.space_id, + p.event_date, + p.good_aqi_percentage, p.moderate_aqi_percentage, p.unhealthy_sensitive_aqi_percentage, p.unhealthy_aqi_percentage, p.very_unhealthy_aqi_percentage, p.hazardous_aqi_percentage, + a.daily_avg_aqi,a.daily_max_aqi, a.daily_min_aqi, + p.good_pm25_percentage, p.moderate_pm25_percentage, p.unhealthy_sensitive_pm25_percentage, p.unhealthy_pm25_percentage, p.very_unhealthy_pm25_percentage, p.hazardous_pm25_percentage, + a.daily_avg_pm25,a.daily_max_pm25, a.daily_min_pm25, + p.good_pm10_percentage, p.moderate_pm10_percentage, p.unhealthy_sensitive_pm10_percentage, p.unhealthy_pm10_percentage, p.very_unhealthy_pm10_percentage, p.hazardous_pm10_percentage, + a.daily_avg_pm10, a.daily_max_pm10, a.daily_min_pm10, + p.good_voc_percentage, p.moderate_voc_percentage, p.unhealthy_sensitive_voc_percentage, p.unhealthy_voc_percentage, p.very_unhealthy_voc_percentage, p.hazardous_voc_percentage, + a.daily_avg_voc, a.daily_max_voc, a.daily_min_voc, + p.good_co2_percentage, p.moderate_co2_percentage, p.unhealthy_sensitive_co2_percentage, p.unhealthy_co2_percentage, p.very_unhealthy_co2_percentage, p.hazardous_co2_percentage, + a.daily_avg_co2,a.daily_max_co2, a.daily_min_co2, + p.good_ch2o_percentage, p.moderate_ch2o_percentage, p.unhealthy_sensitive_ch2o_percentage, p.unhealthy_ch2o_percentage, p.very_unhealthy_ch2o_percentage, p.hazardous_ch2o_percentage, + a.daily_avg_ch2o,a.daily_max_ch2o, a.daily_min_ch2o +FROM daily_percentages p +LEFT JOIN daily_averages a + ON p.space_id = a.space_id AND p.event_date = a.event_date +ORDER BY p.space_id, p.event_date) + + +INSERT INTO public.space_daily_pollutant_stats ( + space_uuid, + event_date, + good_aqi_percentage, moderate_aqi_percentage, unhealthy_sensitive_aqi_percentage, unhealthy_aqi_percentage, very_unhealthy_aqi_percentage, hazardous_aqi_percentage, + daily_avg_aqi, daily_max_aqi, daily_min_aqi, + good_pm25_percentage, moderate_pm25_percentage, unhealthy_sensitive_pm25_percentage, unhealthy_pm25_percentage, very_unhealthy_pm25_percentage, hazardous_pm25_percentage, + daily_avg_pm25, daily_max_pm25, daily_min_pm25, + good_pm10_percentage, moderate_pm10_percentage, unhealthy_sensitive_pm10_percentage, unhealthy_pm10_percentage, very_unhealthy_pm10_percentage, hazardous_pm10_percentage, + daily_avg_pm10, daily_max_pm10, daily_min_pm10, + good_voc_percentage, moderate_voc_percentage, unhealthy_sensitive_voc_percentage, unhealthy_voc_percentage, very_unhealthy_voc_percentage, hazardous_voc_percentage, + daily_avg_voc, daily_max_voc, daily_min_voc, + good_co2_percentage, moderate_co2_percentage, unhealthy_sensitive_co2_percentage, unhealthy_co2_percentage, very_unhealthy_co2_percentage, hazardous_co2_percentage, + daily_avg_co2, daily_max_co2, daily_min_co2, + good_ch2o_percentage, moderate_ch2o_percentage, unhealthy_sensitive_ch2o_percentage, unhealthy_ch2o_percentage, very_unhealthy_ch2o_percentage, hazardous_ch2o_percentage, + daily_avg_ch2o, daily_max_ch2o, daily_min_ch2o +) +SELECT + space_id, + event_date, + good_aqi_percentage, moderate_aqi_percentage, unhealthy_sensitive_aqi_percentage, unhealthy_aqi_percentage, very_unhealthy_aqi_percentage, hazardous_aqi_percentage, + daily_avg_aqi, daily_max_aqi, daily_min_aqi, + good_pm25_percentage, moderate_pm25_percentage, unhealthy_sensitive_pm25_percentage, unhealthy_pm25_percentage, very_unhealthy_pm25_percentage, hazardous_pm25_percentage, + daily_avg_pm25, daily_max_pm25, daily_min_pm25, + good_pm10_percentage, moderate_pm10_percentage, unhealthy_sensitive_pm10_percentage, unhealthy_pm10_percentage, very_unhealthy_pm10_percentage, hazardous_pm10_percentage, + daily_avg_pm10, daily_max_pm10, daily_min_pm10, + good_voc_percentage, moderate_voc_percentage, unhealthy_sensitive_voc_percentage, unhealthy_voc_percentage, very_unhealthy_voc_percentage, hazardous_voc_percentage, + daily_avg_voc, daily_max_voc, daily_min_voc, + good_co2_percentage, moderate_co2_percentage, unhealthy_sensitive_co2_percentage, unhealthy_co2_percentage, very_unhealthy_co2_percentage, hazardous_co2_percentage, + daily_avg_co2, daily_max_co2, daily_min_co2, + good_ch2o_percentage, moderate_ch2o_percentage, unhealthy_sensitive_ch2o_percentage, unhealthy_ch2o_percentage, very_unhealthy_ch2o_percentage, hazardous_ch2o_percentage, + daily_avg_ch2o, daily_max_ch2o, daily_min_ch2o +FROM final_data +ON CONFLICT (space_uuid, event_date) DO UPDATE +SET + good_aqi_percentage = EXCLUDED.good_aqi_percentage, + moderate_aqi_percentage = EXCLUDED.moderate_aqi_percentage, + unhealthy_sensitive_aqi_percentage = EXCLUDED.unhealthy_sensitive_aqi_percentage, + unhealthy_aqi_percentage = EXCLUDED.unhealthy_aqi_percentage, + very_unhealthy_aqi_percentage = EXCLUDED.very_unhealthy_aqi_percentage, + hazardous_aqi_percentage = EXCLUDED.hazardous_aqi_percentage, + daily_avg_aqi = EXCLUDED.daily_avg_aqi, + daily_max_aqi = EXCLUDED.daily_max_aqi, + daily_min_aqi = EXCLUDED.daily_min_aqi, + good_pm25_percentage = EXCLUDED.good_pm25_percentage, + moderate_pm25_percentage = EXCLUDED.moderate_pm25_percentage, + unhealthy_sensitive_pm25_percentage = EXCLUDED.unhealthy_sensitive_pm25_percentage, + unhealthy_pm25_percentage = EXCLUDED.unhealthy_pm25_percentage, + very_unhealthy_pm25_percentage = EXCLUDED.very_unhealthy_pm25_percentage, + hazardous_pm25_percentage = EXCLUDED.hazardous_pm25_percentage, + daily_avg_pm25 = EXCLUDED.daily_avg_pm25, + daily_max_pm25 = EXCLUDED.daily_max_pm25, + daily_min_pm25 = EXCLUDED.daily_min_pm25, + good_pm10_percentage = EXCLUDED.good_pm10_percentage, + moderate_pm10_percentage = EXCLUDED.moderate_pm10_percentage, + unhealthy_sensitive_pm10_percentage = EXCLUDED.unhealthy_sensitive_pm10_percentage, + unhealthy_pm10_percentage = EXCLUDED.unhealthy_pm10_percentage, + very_unhealthy_pm10_percentage = EXCLUDED.very_unhealthy_pm10_percentage, + hazardous_pm10_percentage = EXCLUDED.hazardous_pm10_percentage, + daily_avg_pm10 = EXCLUDED.daily_avg_pm10, + daily_max_pm10 = EXCLUDED.daily_max_pm10, + daily_min_pm10 = EXCLUDED.daily_min_pm10, + good_voc_percentage = EXCLUDED.good_voc_percentage, + moderate_voc_percentage = EXCLUDED.moderate_voc_percentage, + unhealthy_sensitive_voc_percentage = EXCLUDED.unhealthy_sensitive_voc_percentage, + unhealthy_voc_percentage = EXCLUDED.unhealthy_voc_percentage, + very_unhealthy_voc_percentage = EXCLUDED.very_unhealthy_voc_percentage, + hazardous_voc_percentage = EXCLUDED.hazardous_voc_percentage, + daily_avg_voc = EXCLUDED.daily_avg_voc, + daily_max_voc = EXCLUDED.daily_max_voc, + daily_min_voc = EXCLUDED.daily_min_voc, + good_co2_percentage = EXCLUDED.good_co2_percentage, + moderate_co2_percentage = EXCLUDED.moderate_co2_percentage, + unhealthy_sensitive_co2_percentage = EXCLUDED.unhealthy_sensitive_co2_percentage, + unhealthy_co2_percentage = EXCLUDED.unhealthy_co2_percentage, + very_unhealthy_co2_percentage = EXCLUDED.very_unhealthy_co2_percentage, + hazardous_co2_percentage = EXCLUDED.hazardous_co2_percentage, + daily_avg_co2 = EXCLUDED.daily_avg_co2, + daily_max_co2 = EXCLUDED.daily_max_co2, + daily_min_co2 = EXCLUDED.daily_min_co2, + good_ch2o_percentage = EXCLUDED.good_ch2o_percentage, + moderate_ch2o_percentage = EXCLUDED.moderate_ch2o_percentage, + unhealthy_sensitive_ch2o_percentage = EXCLUDED.unhealthy_sensitive_ch2o_percentage, + unhealthy_ch2o_percentage = EXCLUDED.unhealthy_ch2o_percentage, + very_unhealthy_ch2o_percentage = EXCLUDED.very_unhealthy_ch2o_percentage, + hazardous_ch2o_percentage = EXCLUDED.hazardous_ch2o_percentage, + daily_avg_ch2o = EXCLUDED.daily_avg_ch2o, + daily_max_ch2o = EXCLUDED.daily_max_ch2o, + daily_min_ch2o = EXCLUDED.daily_min_ch2o; + + + diff --git a/libs/common/src/sql/procedures/fact_daily_space_aqi/procedure_insert_all_daily_space_aqi.sql b/libs/common/src/sql/procedures/fact_daily_space_aqi/procedure_insert_all_daily_space_aqi.sql new file mode 100644 index 0000000..aeb211b --- /dev/null +++ b/libs/common/src/sql/procedures/fact_daily_space_aqi/procedure_insert_all_daily_space_aqi.sql @@ -0,0 +1,367 @@ +-- Query Pipeline Starts Here +WITH device_space AS ( + SELECT + device.uuid AS device_id, + device.space_device_uuid AS space_id, + "device-status-log".event_time::timestamp AS event_time, + "device-status-log".code, + "device-status-log".value + FROM device + LEFT JOIN "device-status-log" + ON device.uuid = "device-status-log".device_id + LEFT JOIN product + ON product.uuid = device.product_device_uuid + WHERE product.cat_name = 'hjjcy' +), + +average_pollutants AS ( + SELECT + event_time::date AS event_date, + date_trunc('hour', event_time) AS event_hour, + space_id, + + -- PM1 + MIN(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_min, + AVG(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_avg, + MAX(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_max, + + -- PM25 + MIN(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_min, + AVG(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_avg, + MAX(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_max, + + -- PM10 + MIN(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_min, + AVG(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_avg, + MAX(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_max, + + -- VOC + MIN(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_min, + AVG(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_avg, + MAX(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_max, + + -- CH2O + MIN(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_min, + AVG(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_avg, + MAX(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_max, + + -- CO2 + MIN(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_min, + AVG(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_avg, + MAX(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_max + + FROM device_space + GROUP BY space_id, event_hour, event_date +), + +filled_pollutants AS ( + SELECT + *, + -- AVG + COALESCE(pm25_avg, LAG(pm25_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm25_avg_f, + COALESCE(pm10_avg, LAG(pm10_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm10_avg_f, + COALESCE(voc_avg, LAG(voc_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS voc_avg_f, + COALESCE(co2_avg, LAG(co2_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS co2_avg_f, + COALESCE(ch2o_avg, LAG(ch2o_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS ch2o_avg_f, + + -- MIN + COALESCE(pm25_min, LAG(pm25_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm25_min_f, + COALESCE(pm10_min, LAG(pm10_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm10_min_f, + COALESCE(voc_min, LAG(voc_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS voc_min_f, + COALESCE(co2_min, LAG(co2_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS co2_min_f, + COALESCE(ch2o_min, LAG(ch2o_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS ch2o_min_f, + + -- MAX + COALESCE(pm25_max, LAG(pm25_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm25_max_f, + COALESCE(pm10_max, LAG(pm10_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm10_max_f, + COALESCE(voc_max, LAG(voc_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS voc_max_f, + COALESCE(co2_max, LAG(co2_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS co2_max_f, + COALESCE(ch2o_max, LAG(ch2o_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS ch2o_max_f + FROM average_pollutants +), + +hourly_results AS ( + SELECT + space_id, + event_date, + event_hour, + pm1_min, pm1_avg, pm1_max, + pm25_min_f, pm25_avg_f, pm25_max_f, + pm10_min_f, pm10_avg_f, pm10_max_f, + voc_min_f, voc_avg_f, voc_max_f, + co2_min_f, co2_avg_f, co2_max_f, + ch2o_min_f, ch2o_avg_f, ch2o_max_f, + + GREATEST( + calculate_aqi('pm25', pm25_min_f), + calculate_aqi('pm10', pm10_min_f) + ) AS hourly_min_aqi, + + GREATEST( + calculate_aqi('pm25', pm25_avg_f), + calculate_aqi('pm10', pm10_avg_f) + ) AS hourly_avg_aqi, + + GREATEST( + calculate_aqi('pm25', pm25_max_f), + calculate_aqi('pm10', pm10_max_f) + ) AS hourly_max_aqi, + + classify_aqi(GREATEST( + calculate_aqi('pm25', pm25_avg_f), + calculate_aqi('pm10', pm10_avg_f) + )) AS aqi_category, + + classify_aqi(calculate_aqi('pm25',pm25_avg_f)) as pm25_category, + classify_aqi(calculate_aqi('pm10',pm10_avg_f)) as pm10_category, + classify_aqi(calculate_aqi('voc',voc_avg_f)) as voc_category, + classify_aqi(calculate_aqi('co2',co2_avg_f)) as co2_category, + classify_aqi(calculate_aqi('ch2o',ch2o_avg_f)) as ch2o_category + + FROM filled_pollutants +), + +daily_category_counts AS ( + SELECT space_id, event_date, aqi_category AS category, 'aqi' AS pollutant, COUNT(*) AS category_count + FROM hourly_results + GROUP BY space_id, event_date, aqi_category + + UNION ALL + + SELECT space_id, event_date, pm25_category AS category, 'pm25' AS pollutant, COUNT(*) AS category_count + FROM hourly_results + GROUP BY space_id, event_date, pm25_category + + UNION ALL + + SELECT space_id, event_date, pm10_category AS category, 'pm10' AS pollutant, COUNT(*) AS category_count + FROM hourly_results + GROUP BY space_id, event_date, pm10_category + + UNION ALL + + SELECT space_id, event_date, voc_category AS category, 'voc' AS pollutant, COUNT(*) AS category_count + FROM hourly_results + GROUP BY space_id, event_date, voc_category + + UNION ALL + + SELECT space_id, event_date, co2_category AS category, 'co2' AS pollutant, COUNT(*) AS category_count + FROM hourly_results + GROUP BY space_id, event_date, co2_category + + UNION ALL + + SELECT space_id, event_date, ch2o_category AS category, 'ch2o' AS pollutant, COUNT(*) AS category_count + FROM hourly_results + GROUP BY space_id, event_date, ch2o_category +), + +daily_totals AS ( + SELECT + space_id, + event_date, + SUM(category_count) AS total_count + FROM daily_category_counts + where pollutant = 'aqi' + GROUP BY space_id, event_date +), + +-- Pivot Categories into Columns +daily_percentages AS ( + select + dt.space_id, + dt.event_date, + -- AQI CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_aqi_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_aqi_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_aqi_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_aqi_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_aqi_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_aqi_percentage, + -- PM25 CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_pm25_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_pm25_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_pm25_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_pm25_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_pm25_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_pm25_percentage, + -- PM10 CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_pm10_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_pm10_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_pm10_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_pm10_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_pm10_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_pm10_percentage, + -- VOC CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_voc_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_voc_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_voc_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_voc_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_voc_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_voc_percentage, + -- CO2 CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_co2_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_co2_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_co2_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_co2_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_co2_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_co2_percentage, + -- CH20 CATEGORIES + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_ch2o_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_ch2o_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_ch2o_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_ch2o_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_ch2o_percentage, + ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_ch2o_percentage + FROM daily_totals dt + LEFT JOIN daily_category_counts dcc + ON dt.space_id = dcc.space_id AND dt.event_date = dcc.event_date + GROUP BY dt.space_id, dt.event_date, dt.total_count +), + +daily_averages AS ( + SELECT + space_id, + event_date, + -- AQI + ROUND(AVG(hourly_min_aqi)::numeric, 2) AS daily_min_aqi, + ROUND(AVG(hourly_avg_aqi)::numeric, 2) AS daily_avg_aqi, + ROUND(AVG(hourly_max_aqi)::numeric, 2) AS daily_max_aqi, + -- PM25 + ROUND(AVG(pm25_min_f)::numeric, 2) AS daily_min_pm25, + ROUND(AVG(pm25_avg_f)::numeric, 2) AS daily_avg_pm25, + ROUND(AVG(pm25_max_f)::numeric, 2) AS daily_max_pm25, + -- PM10 + ROUND(AVG(pm10_min_f)::numeric, 2) AS daily_min_pm10, + ROUND(AVG(pm10_avg_f)::numeric, 2) AS daily_avg_pm10, + ROUND(AVG(pm10_max_f)::numeric, 2) AS daily_max_pm10, + -- VOC + ROUND(AVG(voc_min_f)::numeric, 2) AS daily_min_voc, + ROUND(AVG(voc_avg_f)::numeric, 2) AS daily_avg_voc, + ROUND(AVG(voc_max_f)::numeric, 2) AS daily_max_voc, + -- CO2 + ROUND(AVG(co2_min_f)::numeric, 2) AS daily_min_co2, + ROUND(AVG(co2_avg_f)::numeric, 2) AS daily_avg_co2, + ROUND(AVG(co2_max_f)::numeric, 2) AS daily_max_co2, + -- CH2O + ROUND(AVG(ch2o_min_f)::numeric, 2) AS daily_min_ch2o, + ROUND(AVG(ch2o_avg_f)::numeric, 2) AS daily_avg_ch2o, + ROUND(AVG(ch2o_max_f)::numeric, 2) AS daily_max_ch2o + + FROM hourly_results + GROUP BY space_id, event_date +), + +final_data as( +SELECT + p.space_id, + p.event_date, + p.good_aqi_percentage, p.moderate_aqi_percentage, p.unhealthy_sensitive_aqi_percentage, p.unhealthy_aqi_percentage, p.very_unhealthy_aqi_percentage, p.hazardous_aqi_percentage, + a.daily_avg_aqi,a.daily_max_aqi, a.daily_min_aqi, + p.good_pm25_percentage, p.moderate_pm25_percentage, p.unhealthy_sensitive_pm25_percentage, p.unhealthy_pm25_percentage, p.very_unhealthy_pm25_percentage, p.hazardous_pm25_percentage, + a.daily_avg_pm25,a.daily_max_pm25, a.daily_min_pm25, + p.good_pm10_percentage, p.moderate_pm10_percentage, p.unhealthy_sensitive_pm10_percentage, p.unhealthy_pm10_percentage, p.very_unhealthy_pm10_percentage, p.hazardous_pm10_percentage, + a.daily_avg_pm10, a.daily_max_pm10, a.daily_min_pm10, + p.good_voc_percentage, p.moderate_voc_percentage, p.unhealthy_sensitive_voc_percentage, p.unhealthy_voc_percentage, p.very_unhealthy_voc_percentage, p.hazardous_voc_percentage, + a.daily_avg_voc, a.daily_max_voc, a.daily_min_voc, + p.good_co2_percentage, p.moderate_co2_percentage, p.unhealthy_sensitive_co2_percentage, p.unhealthy_co2_percentage, p.very_unhealthy_co2_percentage, p.hazardous_co2_percentage, + a.daily_avg_co2,a.daily_max_co2, a.daily_min_co2, + p.good_ch2o_percentage, p.moderate_ch2o_percentage, p.unhealthy_sensitive_ch2o_percentage, p.unhealthy_ch2o_percentage, p.very_unhealthy_ch2o_percentage, p.hazardous_ch2o_percentage, + a.daily_avg_ch2o,a.daily_max_ch2o, a.daily_min_ch2o +FROM daily_percentages p +LEFT JOIN daily_averages a + ON p.space_id = a.space_id AND p.event_date = a.event_date +ORDER BY p.space_id, p.event_date) + + +INSERT INTO public.space_daily_pollutant_stats ( + space_uuid, + event_date, + good_aqi_percentage, moderate_aqi_percentage, unhealthy_sensitive_aqi_percentage, unhealthy_aqi_percentage, very_unhealthy_aqi_percentage, hazardous_aqi_percentage, + daily_avg_aqi, daily_max_aqi, daily_min_aqi, + good_pm25_percentage, moderate_pm25_percentage, unhealthy_sensitive_pm25_percentage, unhealthy_pm25_percentage, very_unhealthy_pm25_percentage, hazardous_pm25_percentage, + daily_avg_pm25, daily_max_pm25, daily_min_pm25, + good_pm10_percentage, moderate_pm10_percentage, unhealthy_sensitive_pm10_percentage, unhealthy_pm10_percentage, very_unhealthy_pm10_percentage, hazardous_pm10_percentage, + daily_avg_pm10, daily_max_pm10, daily_min_pm10, + good_voc_percentage, moderate_voc_percentage, unhealthy_sensitive_voc_percentage, unhealthy_voc_percentage, very_unhealthy_voc_percentage, hazardous_voc_percentage, + daily_avg_voc, daily_max_voc, daily_min_voc, + good_co2_percentage, moderate_co2_percentage, unhealthy_sensitive_co2_percentage, unhealthy_co2_percentage, very_unhealthy_co2_percentage, hazardous_co2_percentage, + daily_avg_co2, daily_max_co2, daily_min_co2, + good_ch2o_percentage, moderate_ch2o_percentage, unhealthy_sensitive_ch2o_percentage, unhealthy_ch2o_percentage, very_unhealthy_ch2o_percentage, hazardous_ch2o_percentage, + daily_avg_ch2o, daily_max_ch2o, daily_min_ch2o +) +SELECT + space_id, + event_date, + good_aqi_percentage, moderate_aqi_percentage, unhealthy_sensitive_aqi_percentage, unhealthy_aqi_percentage, very_unhealthy_aqi_percentage, hazardous_aqi_percentage, + daily_avg_aqi, daily_max_aqi, daily_min_aqi, + good_pm25_percentage, moderate_pm25_percentage, unhealthy_sensitive_pm25_percentage, unhealthy_pm25_percentage, very_unhealthy_pm25_percentage, hazardous_pm25_percentage, + daily_avg_pm25, daily_max_pm25, daily_min_pm25, + good_pm10_percentage, moderate_pm10_percentage, unhealthy_sensitive_pm10_percentage, unhealthy_pm10_percentage, very_unhealthy_pm10_percentage, hazardous_pm10_percentage, + daily_avg_pm10, daily_max_pm10, daily_min_pm10, + good_voc_percentage, moderate_voc_percentage, unhealthy_sensitive_voc_percentage, unhealthy_voc_percentage, very_unhealthy_voc_percentage, hazardous_voc_percentage, + daily_avg_voc, daily_max_voc, daily_min_voc, + good_co2_percentage, moderate_co2_percentage, unhealthy_sensitive_co2_percentage, unhealthy_co2_percentage, very_unhealthy_co2_percentage, hazardous_co2_percentage, + daily_avg_co2, daily_max_co2, daily_min_co2, + good_ch2o_percentage, moderate_ch2o_percentage, unhealthy_sensitive_ch2o_percentage, unhealthy_ch2o_percentage, very_unhealthy_ch2o_percentage, hazardous_ch2o_percentage, + daily_avg_ch2o, daily_max_ch2o, daily_min_ch2o +FROM final_data +ON CONFLICT (space_uuid, event_date) DO UPDATE +SET + good_aqi_percentage = EXCLUDED.good_aqi_percentage, + moderate_aqi_percentage = EXCLUDED.moderate_aqi_percentage, + unhealthy_sensitive_aqi_percentage = EXCLUDED.unhealthy_sensitive_aqi_percentage, + unhealthy_aqi_percentage = EXCLUDED.unhealthy_aqi_percentage, + very_unhealthy_aqi_percentage = EXCLUDED.very_unhealthy_aqi_percentage, + hazardous_aqi_percentage = EXCLUDED.hazardous_aqi_percentage, + daily_avg_aqi = EXCLUDED.daily_avg_aqi, + daily_max_aqi = EXCLUDED.daily_max_aqi, + daily_min_aqi = EXCLUDED.daily_min_aqi, + good_pm25_percentage = EXCLUDED.good_pm25_percentage, + moderate_pm25_percentage = EXCLUDED.moderate_pm25_percentage, + unhealthy_sensitive_pm25_percentage = EXCLUDED.unhealthy_sensitive_pm25_percentage, + unhealthy_pm25_percentage = EXCLUDED.unhealthy_pm25_percentage, + very_unhealthy_pm25_percentage = EXCLUDED.very_unhealthy_pm25_percentage, + hazardous_pm25_percentage = EXCLUDED.hazardous_pm25_percentage, + daily_avg_pm25 = EXCLUDED.daily_avg_pm25, + daily_max_pm25 = EXCLUDED.daily_max_pm25, + daily_min_pm25 = EXCLUDED.daily_min_pm25, + good_pm10_percentage = EXCLUDED.good_pm10_percentage, + moderate_pm10_percentage = EXCLUDED.moderate_pm10_percentage, + unhealthy_sensitive_pm10_percentage = EXCLUDED.unhealthy_sensitive_pm10_percentage, + unhealthy_pm10_percentage = EXCLUDED.unhealthy_pm10_percentage, + very_unhealthy_pm10_percentage = EXCLUDED.very_unhealthy_pm10_percentage, + hazardous_pm10_percentage = EXCLUDED.hazardous_pm10_percentage, + daily_avg_pm10 = EXCLUDED.daily_avg_pm10, + daily_max_pm10 = EXCLUDED.daily_max_pm10, + daily_min_pm10 = EXCLUDED.daily_min_pm10, + good_voc_percentage = EXCLUDED.good_voc_percentage, + moderate_voc_percentage = EXCLUDED.moderate_voc_percentage, + unhealthy_sensitive_voc_percentage = EXCLUDED.unhealthy_sensitive_voc_percentage, + unhealthy_voc_percentage = EXCLUDED.unhealthy_voc_percentage, + very_unhealthy_voc_percentage = EXCLUDED.very_unhealthy_voc_percentage, + hazardous_voc_percentage = EXCLUDED.hazardous_voc_percentage, + daily_avg_voc = EXCLUDED.daily_avg_voc, + daily_max_voc = EXCLUDED.daily_max_voc, + daily_min_voc = EXCLUDED.daily_min_voc, + good_co2_percentage = EXCLUDED.good_co2_percentage, + moderate_co2_percentage = EXCLUDED.moderate_co2_percentage, + unhealthy_sensitive_co2_percentage = EXCLUDED.unhealthy_sensitive_co2_percentage, + unhealthy_co2_percentage = EXCLUDED.unhealthy_co2_percentage, + very_unhealthy_co2_percentage = EXCLUDED.very_unhealthy_co2_percentage, + hazardous_co2_percentage = EXCLUDED.hazardous_co2_percentage, + daily_avg_co2 = EXCLUDED.daily_avg_co2, + daily_max_co2 = EXCLUDED.daily_max_co2, + daily_min_co2 = EXCLUDED.daily_min_co2, + good_ch2o_percentage = EXCLUDED.good_ch2o_percentage, + moderate_ch2o_percentage = EXCLUDED.moderate_ch2o_percentage, + unhealthy_sensitive_ch2o_percentage = EXCLUDED.unhealthy_sensitive_ch2o_percentage, + unhealthy_ch2o_percentage = EXCLUDED.unhealthy_ch2o_percentage, + very_unhealthy_ch2o_percentage = EXCLUDED.very_unhealthy_ch2o_percentage, + hazardous_ch2o_percentage = EXCLUDED.hazardous_ch2o_percentage, + daily_avg_ch2o = EXCLUDED.daily_avg_ch2o, + daily_max_ch2o = EXCLUDED.daily_max_ch2o, + daily_min_ch2o = EXCLUDED.daily_min_ch2o; + + From 80e89dd035d2acb356bc412bec51b5343fffe2aa Mon Sep 17 00:00:00 2001 From: khuss Date: Wed, 4 Jun 2025 17:20:25 -0400 Subject: [PATCH 11/17] fix name of snapshot table --- .../proceduce_select_daily_space_aqi.sql | 401 ++---------------- .../proceduce_update_daily_space_aqi.sql | 2 +- .../procedure_insert_all_daily_space_aqi.sql | 2 +- 3 files changed, 35 insertions(+), 370 deletions(-) diff --git a/libs/common/src/sql/procedures/fact_daily_space_aqi/proceduce_select_daily_space_aqi.sql b/libs/common/src/sql/procedures/fact_daily_space_aqi/proceduce_select_daily_space_aqi.sql index 993f3d0..4c4c6ab 100644 --- a/libs/common/src/sql/procedures/fact_daily_space_aqi/proceduce_select_daily_space_aqi.sql +++ b/libs/common/src/sql/procedures/fact_daily_space_aqi/proceduce_select_daily_space_aqi.sql @@ -1,374 +1,39 @@ -WITH params AS ( - SELECT - TO_DATE(NULLIF($1, ''), 'YYYY-MM-DD') AS event_date, - $2::uuid AS space_id -), - --- Query Pipeline Starts Here -WITH device_space AS ( - SELECT - device.uuid AS device_id, - device.space_device_uuid AS space_id, - "device-status-log".event_time::timestamp AS event_time, - "device-status-log".code, - "device-status-log".value - FROM device - LEFT JOIN "device-status-log" - ON device.uuid = "device-status-log".device_id - LEFT JOIN product - ON product.uuid = device.product_device_uuid - WHERE product.cat_name = 'hjjcy' -), - -average_pollutants AS ( - SELECT - event_time::date AS event_date, - date_trunc('hour', event_time) AS event_hour, - space_id, - - -- PM1 - MIN(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_min, - AVG(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_avg, - MAX(CASE WHEN code = 'pm1' THEN value::numeric END) AS pm1_max, - - -- PM25 - MIN(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_min, - AVG(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_avg, - MAX(CASE WHEN code = 'pm25_value' THEN value::numeric END) AS pm25_max, - - -- PM10 - MIN(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_min, - AVG(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_avg, - MAX(CASE WHEN code = 'pm10' THEN value::numeric END) AS pm10_max, - - -- VOC - MIN(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_min, - AVG(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_avg, - MAX(CASE WHEN code = 'voc_value' THEN value::numeric END) AS voc_max, - - -- CH2O - MIN(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_min, - AVG(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_avg, - MAX(CASE WHEN code = 'ch2o_value' THEN value::numeric END) AS ch2o_max, - - -- CO2 - MIN(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_min, - AVG(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_avg, - MAX(CASE WHEN code = 'co2_value' THEN value::numeric END) AS co2_max - - FROM device_space - GROUP BY space_id, event_hour, event_date -), - -filled_pollutants AS ( - SELECT - *, - -- AVG - COALESCE(pm25_avg, LAG(pm25_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm25_avg_f, - COALESCE(pm10_avg, LAG(pm10_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm10_avg_f, - COALESCE(voc_avg, LAG(voc_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS voc_avg_f, - COALESCE(co2_avg, LAG(co2_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS co2_avg_f, - COALESCE(ch2o_avg, LAG(ch2o_avg) OVER (PARTITION BY space_id ORDER BY event_hour)) AS ch2o_avg_f, - - -- MIN - COALESCE(pm25_min, LAG(pm25_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm25_min_f, - COALESCE(pm10_min, LAG(pm10_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm10_min_f, - COALESCE(voc_min, LAG(voc_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS voc_min_f, - COALESCE(co2_min, LAG(co2_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS co2_min_f, - COALESCE(ch2o_min, LAG(ch2o_min) OVER (PARTITION BY space_id ORDER BY event_hour)) AS ch2o_min_f, - - -- MAX - COALESCE(pm25_max, LAG(pm25_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm25_max_f, - COALESCE(pm10_max, LAG(pm10_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS pm10_max_f, - COALESCE(voc_max, LAG(voc_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS voc_max_f, - COALESCE(co2_max, LAG(co2_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS co2_max_f, - COALESCE(ch2o_max, LAG(ch2o_max) OVER (PARTITION BY space_id ORDER BY event_hour)) AS ch2o_max_f - FROM average_pollutants -), - -hourly_results AS ( +ITH params AS ( SELECT - space_id, - event_date, - event_hour, - pm1_min, pm1_avg, pm1_max, - pm25_min_f, pm25_avg_f, pm25_max_f, - pm10_min_f, pm10_avg_f, pm10_max_f, - voc_min_f, voc_avg_f, voc_max_f, - co2_min_f, co2_avg_f, co2_max_f, - ch2o_min_f, ch2o_avg_f, ch2o_max_f, - - GREATEST( - calculate_aqi('pm25', pm25_min_f), - calculate_aqi('pm10', pm10_min_f) - ) AS hourly_min_aqi, - - GREATEST( - calculate_aqi('pm25', pm25_avg_f), - calculate_aqi('pm10', pm10_avg_f) - ) AS hourly_avg_aqi, - - GREATEST( - calculate_aqi('pm25', pm25_max_f), - calculate_aqi('pm10', pm10_max_f) - ) AS hourly_max_aqi, - - classify_aqi(GREATEST( - calculate_aqi('pm25', pm25_avg_f), - calculate_aqi('pm10', pm10_avg_f) - )) AS aqi_category, - - classify_aqi(calculate_aqi('pm25',pm25_avg_f)) as pm25_category, - classify_aqi(calculate_aqi('pm10',pm10_avg_f)) as pm10_category, - classify_aqi(calculate_aqi('voc',voc_avg_f)) as voc_category, - classify_aqi(calculate_aqi('co2',co2_avg_f)) as co2_category, - classify_aqi(calculate_aqi('ch2o',ch2o_avg_f)) as ch2o_category - - FROM filled_pollutants -), - -daily_category_counts AS ( - SELECT space_id, event_date, aqi_category AS category, 'aqi' AS pollutant, COUNT(*) AS category_count - FROM hourly_results - GROUP BY space_id, event_date, aqi_category - - UNION ALL - - SELECT space_id, event_date, pm25_category AS category, 'pm25' AS pollutant, COUNT(*) AS category_count - FROM hourly_results - GROUP BY space_id, event_date, pm25_category - - UNION ALL - - SELECT space_id, event_date, pm10_category AS category, 'pm10' AS pollutant, COUNT(*) AS category_count - FROM hourly_results - GROUP BY space_id, event_date, pm10_category - - UNION ALL - - SELECT space_id, event_date, voc_category AS category, 'voc' AS pollutant, COUNT(*) AS category_count - FROM hourly_results - GROUP BY space_id, event_date, voc_category - - UNION ALL - - SELECT space_id, event_date, co2_category AS category, 'co2' AS pollutant, COUNT(*) AS category_count - FROM hourly_results - GROUP BY space_id, event_date, co2_category - - UNION ALL - - SELECT space_id, event_date, ch2o_category AS category, 'ch2o' AS pollutant, COUNT(*) AS category_count - FROM hourly_results - GROUP BY space_id, event_date, ch2o_category -), - -daily_totals AS ( - SELECT - space_id, - event_date, - SUM(category_count) AS total_count - FROM daily_category_counts - where pollutant = 'aqi' - GROUP BY space_id, event_date -), - --- Pivot Categories into Columns -daily_percentages AS ( - select - dt.space_id, - dt.event_date, - -- AQI CATEGORIES - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_aqi_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_aqi_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_aqi_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_aqi_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_aqi_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'aqi' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_aqi_percentage, - -- PM25 CATEGORIES - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_pm25_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_pm25_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_pm25_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_pm25_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_pm25_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'pm25' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_pm25_percentage, - -- PM10 CATEGORIES - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_pm10_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_pm10_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_pm10_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_pm10_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_pm10_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'pm10' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_pm10_percentage, - -- VOC CATEGORIES - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_voc_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_voc_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_voc_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_voc_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_voc_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'voc' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_voc_percentage, - -- CO2 CATEGORIES - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_co2_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_co2_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_co2_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_co2_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_co2_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'co2' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_co2_percentage, - -- CH20 CATEGORIES - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Good' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS good_ch2o_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Moderate' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS moderate_ch2o_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy for Sensitive Groups' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_sensitive_ch2o_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Unhealthy' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS unhealthy_ch2o_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Very Unhealthy' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS very_unhealthy_ch2o_percentage, - ROUND(COALESCE(SUM(CASE WHEN dcc.category = 'Hazardous' and dcc.pollutant = 'ch2o' THEN dcc.category_count ELSE 0 END) * 100.0 / dt.total_count, 0), 2) AS hazardous_ch2o_percentage - FROM daily_totals dt - LEFT JOIN daily_category_counts dcc - ON dt.space_id = dcc.space_id AND dt.event_date = dcc.event_date - GROUP BY dt.space_id, dt.event_date, dt.total_count -), - -daily_averages AS ( - SELECT - space_id, - event_date, - -- AQI - ROUND(AVG(hourly_min_aqi)::numeric, 2) AS daily_min_aqi, - ROUND(AVG(hourly_avg_aqi)::numeric, 2) AS daily_avg_aqi, - ROUND(AVG(hourly_max_aqi)::numeric, 2) AS daily_max_aqi, - -- PM25 - ROUND(AVG(pm25_min_f)::numeric, 2) AS daily_min_pm25, - ROUND(AVG(pm25_avg_f)::numeric, 2) AS daily_avg_pm25, - ROUND(AVG(pm25_max_f)::numeric, 2) AS daily_max_pm25, - -- PM10 - ROUND(AVG(pm10_min_f)::numeric, 2) AS daily_min_pm10, - ROUND(AVG(pm10_avg_f)::numeric, 2) AS daily_avg_pm10, - ROUND(AVG(pm10_max_f)::numeric, 2) AS daily_max_pm10, - -- VOC - ROUND(AVG(voc_min_f)::numeric, 2) AS daily_min_voc, - ROUND(AVG(voc_avg_f)::numeric, 2) AS daily_avg_voc, - ROUND(AVG(voc_max_f)::numeric, 2) AS daily_max_voc, - -- CO2 - ROUND(AVG(co2_min_f)::numeric, 2) AS daily_min_co2, - ROUND(AVG(co2_avg_f)::numeric, 2) AS daily_avg_co2, - ROUND(AVG(co2_max_f)::numeric, 2) AS daily_max_co2, - -- CH2O - ROUND(AVG(ch2o_min_f)::numeric, 2) AS daily_min_ch2o, - ROUND(AVG(ch2o_avg_f)::numeric, 2) AS daily_avg_ch2o, - ROUND(AVG(ch2o_max_f)::numeric, 2) AS daily_max_ch2o - - FROM hourly_results - GROUP BY space_id, event_date -), - -final_data as( -SELECT - p.space_id, - p.event_date, - p.good_aqi_percentage, p.moderate_aqi_percentage, p.unhealthy_sensitive_aqi_percentage, p.unhealthy_aqi_percentage, p.very_unhealthy_aqi_percentage, p.hazardous_aqi_percentage, - a.daily_avg_aqi,a.daily_max_aqi, a.daily_min_aqi, - p.good_pm25_percentage, p.moderate_pm25_percentage, p.unhealthy_sensitive_pm25_percentage, p.unhealthy_pm25_percentage, p.very_unhealthy_pm25_percentage, p.hazardous_pm25_percentage, - a.daily_avg_pm25,a.daily_max_pm25, a.daily_min_pm25, - p.good_pm10_percentage, p.moderate_pm10_percentage, p.unhealthy_sensitive_pm10_percentage, p.unhealthy_pm10_percentage, p.very_unhealthy_pm10_percentage, p.hazardous_pm10_percentage, - a.daily_avg_pm10, a.daily_max_pm10, a.daily_min_pm10, - p.good_voc_percentage, p.moderate_voc_percentage, p.unhealthy_sensitive_voc_percentage, p.unhealthy_voc_percentage, p.very_unhealthy_voc_percentage, p.hazardous_voc_percentage, - a.daily_avg_voc, a.daily_max_voc, a.daily_min_voc, - p.good_co2_percentage, p.moderate_co2_percentage, p.unhealthy_sensitive_co2_percentage, p.unhealthy_co2_percentage, p.very_unhealthy_co2_percentage, p.hazardous_co2_percentage, - a.daily_avg_co2,a.daily_max_co2, a.daily_min_co2, - p.good_ch2o_percentage, p.moderate_ch2o_percentage, p.unhealthy_sensitive_ch2o_percentage, p.unhealthy_ch2o_percentage, p.very_unhealthy_ch2o_percentage, p.hazardous_ch2o_percentage, - a.daily_avg_ch2o,a.daily_max_ch2o, a.daily_min_ch2o -FROM daily_percentages p -LEFT JOIN daily_averages a - ON p.space_id = a.space_id AND p.event_date = a.event_date -ORDER BY p.space_id, p.event_date) - - -INSERT INTO public.space_daily_pollutant_stats ( - space_uuid, - event_date, - good_aqi_percentage, moderate_aqi_percentage, unhealthy_sensitive_aqi_percentage, unhealthy_aqi_percentage, very_unhealthy_aqi_percentage, hazardous_aqi_percentage, - daily_avg_aqi, daily_max_aqi, daily_min_aqi, - good_pm25_percentage, moderate_pm25_percentage, unhealthy_sensitive_pm25_percentage, unhealthy_pm25_percentage, very_unhealthy_pm25_percentage, hazardous_pm25_percentage, - daily_avg_pm25, daily_max_pm25, daily_min_pm25, - good_pm10_percentage, moderate_pm10_percentage, unhealthy_sensitive_pm10_percentage, unhealthy_pm10_percentage, very_unhealthy_pm10_percentage, hazardous_pm10_percentage, - daily_avg_pm10, daily_max_pm10, daily_min_pm10, - good_voc_percentage, moderate_voc_percentage, unhealthy_sensitive_voc_percentage, unhealthy_voc_percentage, very_unhealthy_voc_percentage, hazardous_voc_percentage, - daily_avg_voc, daily_max_voc, daily_min_voc, - good_co2_percentage, moderate_co2_percentage, unhealthy_sensitive_co2_percentage, unhealthy_co2_percentage, very_unhealthy_co2_percentage, hazardous_co2_percentage, - daily_avg_co2, daily_max_co2, daily_min_co2, - good_ch2o_percentage, moderate_ch2o_percentage, unhealthy_sensitive_ch2o_percentage, unhealthy_ch2o_percentage, very_unhealthy_ch2o_percentage, hazardous_ch2o_percentage, - daily_avg_ch2o, daily_max_ch2o, daily_min_ch2o + $1::uuid AS space_uuid, + TO_DATE(NULLIF($2, ''), 'YYYY-MM') AS event_month ) + SELECT - space_id, - event_date, - good_aqi_percentage, moderate_aqi_percentage, unhealthy_sensitive_aqi_percentage, unhealthy_aqi_percentage, very_unhealthy_aqi_percentage, hazardous_aqi_percentage, - daily_avg_aqi, daily_max_aqi, daily_min_aqi, - good_pm25_percentage, moderate_pm25_percentage, unhealthy_sensitive_pm25_percentage, unhealthy_pm25_percentage, very_unhealthy_pm25_percentage, hazardous_pm25_percentage, - daily_avg_pm25, daily_max_pm25, daily_min_pm25, - good_pm10_percentage, moderate_pm10_percentage, unhealthy_sensitive_pm10_percentage, unhealthy_pm10_percentage, very_unhealthy_pm10_percentage, hazardous_pm10_percentage, - daily_avg_pm10, daily_max_pm10, daily_min_pm10, - good_voc_percentage, moderate_voc_percentage, unhealthy_sensitive_voc_percentage, unhealthy_voc_percentage, very_unhealthy_voc_percentage, hazardous_voc_percentage, - daily_avg_voc, daily_max_voc, daily_min_voc, - good_co2_percentage, moderate_co2_percentage, unhealthy_sensitive_co2_percentage, unhealthy_co2_percentage, very_unhealthy_co2_percentage, hazardous_co2_percentage, - daily_avg_co2, daily_max_co2, daily_min_co2, - good_ch2o_percentage, moderate_ch2o_percentage, unhealthy_sensitive_ch2o_percentage, unhealthy_ch2o_percentage, very_unhealthy_ch2o_percentage, hazardous_ch2o_percentage, - daily_avg_ch2o, daily_max_ch2o, daily_min_ch2o -FROM final_data -ON CONFLICT (space_uuid, event_date) DO UPDATE -SET - good_aqi_percentage = EXCLUDED.good_aqi_percentage, - moderate_aqi_percentage = EXCLUDED.moderate_aqi_percentage, - unhealthy_sensitive_aqi_percentage = EXCLUDED.unhealthy_sensitive_aqi_percentage, - unhealthy_aqi_percentage = EXCLUDED.unhealthy_aqi_percentage, - very_unhealthy_aqi_percentage = EXCLUDED.very_unhealthy_aqi_percentage, - hazardous_aqi_percentage = EXCLUDED.hazardous_aqi_percentage, - daily_avg_aqi = EXCLUDED.daily_avg_aqi, - daily_max_aqi = EXCLUDED.daily_max_aqi, - daily_min_aqi = EXCLUDED.daily_min_aqi, - good_pm25_percentage = EXCLUDED.good_pm25_percentage, - moderate_pm25_percentage = EXCLUDED.moderate_pm25_percentage, - unhealthy_sensitive_pm25_percentage = EXCLUDED.unhealthy_sensitive_pm25_percentage, - unhealthy_pm25_percentage = EXCLUDED.unhealthy_pm25_percentage, - very_unhealthy_pm25_percentage = EXCLUDED.very_unhealthy_pm25_percentage, - hazardous_pm25_percentage = EXCLUDED.hazardous_pm25_percentage, - daily_avg_pm25 = EXCLUDED.daily_avg_pm25, - daily_max_pm25 = EXCLUDED.daily_max_pm25, - daily_min_pm25 = EXCLUDED.daily_min_pm25, - good_pm10_percentage = EXCLUDED.good_pm10_percentage, - moderate_pm10_percentage = EXCLUDED.moderate_pm10_percentage, - unhealthy_sensitive_pm10_percentage = EXCLUDED.unhealthy_sensitive_pm10_percentage, - unhealthy_pm10_percentage = EXCLUDED.unhealthy_pm10_percentage, - very_unhealthy_pm10_percentage = EXCLUDED.very_unhealthy_pm10_percentage, - hazardous_pm10_percentage = EXCLUDED.hazardous_pm10_percentage, - daily_avg_pm10 = EXCLUDED.daily_avg_pm10, - daily_max_pm10 = EXCLUDED.daily_max_pm10, - daily_min_pm10 = EXCLUDED.daily_min_pm10, - good_voc_percentage = EXCLUDED.good_voc_percentage, - moderate_voc_percentage = EXCLUDED.moderate_voc_percentage, - unhealthy_sensitive_voc_percentage = EXCLUDED.unhealthy_sensitive_voc_percentage, - unhealthy_voc_percentage = EXCLUDED.unhealthy_voc_percentage, - very_unhealthy_voc_percentage = EXCLUDED.very_unhealthy_voc_percentage, - hazardous_voc_percentage = EXCLUDED.hazardous_voc_percentage, - daily_avg_voc = EXCLUDED.daily_avg_voc, - daily_max_voc = EXCLUDED.daily_max_voc, - daily_min_voc = EXCLUDED.daily_min_voc, - good_co2_percentage = EXCLUDED.good_co2_percentage, - moderate_co2_percentage = EXCLUDED.moderate_co2_percentage, - unhealthy_sensitive_co2_percentage = EXCLUDED.unhealthy_sensitive_co2_percentage, - unhealthy_co2_percentage = EXCLUDED.unhealthy_co2_percentage, - very_unhealthy_co2_percentage = EXCLUDED.very_unhealthy_co2_percentage, - hazardous_co2_percentage = EXCLUDED.hazardous_co2_percentage, - daily_avg_co2 = EXCLUDED.daily_avg_co2, - daily_max_co2 = EXCLUDED.daily_max_co2, - daily_min_co2 = EXCLUDED.daily_min_co2, - good_ch2o_percentage = EXCLUDED.good_ch2o_percentage, - moderate_ch2o_percentage = EXCLUDED.moderate_ch2o_percentage, - unhealthy_sensitive_ch2o_percentage = EXCLUDED.unhealthy_sensitive_ch2o_percentage, - unhealthy_ch2o_percentage = EXCLUDED.unhealthy_ch2o_percentage, - very_unhealthy_ch2o_percentage = EXCLUDED.very_unhealthy_ch2o_percentage, - hazardous_ch2o_percentage = EXCLUDED.hazardous_ch2o_percentage, - daily_avg_ch2o = EXCLUDED.daily_avg_ch2o, - daily_max_ch2o = EXCLUDED.daily_max_ch2o, - daily_min_ch2o = EXCLUDED.daily_min_ch2o; + sdp.space_uuid, + sdp.event_date, + sdp.good_aqi_percentage, sdp.moderate_aqi_percentage, sdp.unhealthy_sensitive_aqi_percentage, sdp.unhealthy_aqi_percentage, + sdp.very_unhealthy_aqi_percentage, sdp.hazardous_aqi_percentage, + sdp.daily_avg_aqi, sdp.daily_max_aqi, sdp.daily_min_aqi, + sdp.good_pm25_percentage, sdp.moderate_pm25_percentage, sdp.unhealthy_sensitive_pm25_percentage, sdp.unhealthy_pm25_percentage, + sdp.very_unhealthy_pm25_percentage, sdp.hazardous_pm25_percentage, + sdp.daily_avg_pm25, sdp.daily_max_pm25, sdp.daily_min_pm25, + sdp.good_pm10_percentage, sdp.moderate_pm10_percentage, sdp.unhealthy_sensitive_pm10_percentage, sdp.unhealthy_pm10_percentage, + sdp.very_unhealthy_pm10_percentage, sdp.hazardous_pm10_percentage, + sdp.daily_avg_pm10, sdp.daily_max_pm10, sdp.daily_min_pm10, + sdp.good_voc_percentage, sdp.moderate_voc_percentage, sdp.unhealthy_sensitive_voc_percentage, sdp.unhealthy_voc_percentage, + sdp.very_unhealthy_voc_percentage, sdp.hazardous_voc_percentage, + sdp.daily_avg_voc, sdp.daily_max_voc, sdp.daily_min_voc, + + sdp.good_co2_percentage, sdp.moderate_co2_percentage, sdp.unhealthy_sensitive_co2_percentage, sdp.unhealthy_co2_percentage, + sdp.very_unhealthy_co2_percentage, sdp.hazardous_co2_percentage, + sdp.daily_avg_co2, sdp.daily_max_co2, sdp.daily_min_co2, + + sdp.good_ch2o_percentage, sdp.moderate_ch2o_percentage, sdp.unhealthy_sensitive_ch2o_percentage, sdp.unhealthy_ch2o_percentage, + sdp.very_unhealthy_ch2o_percentage, sdp.hazardous_ch2o_percentage, + sdp.daily_avg_ch2o, sdp.daily_max_ch2o, sdp.daily_min_ch2o + +FROM public."space-daily-pollutant-stats" AS sdp +CROSS JOIN params p +WHERE + (p.space_uuid IS NULL OR sdp.space_uuid = p.space_uuid) + AND (p.event_month IS NULL OR TO_CHAR(sdp.event_date, 'YYYY-MM') = TO_CHAR(p.event_month, 'YYYY-MM')) +ORDER BY sdp.space_uuid, sdp.event_date; \ No newline at end of file diff --git a/libs/common/src/sql/procedures/fact_daily_space_aqi/proceduce_update_daily_space_aqi.sql b/libs/common/src/sql/procedures/fact_daily_space_aqi/proceduce_update_daily_space_aqi.sql index 993f3d0..e2466f2 100644 --- a/libs/common/src/sql/procedures/fact_daily_space_aqi/proceduce_update_daily_space_aqi.sql +++ b/libs/common/src/sql/procedures/fact_daily_space_aqi/proceduce_update_daily_space_aqi.sql @@ -281,7 +281,7 @@ LEFT JOIN daily_averages a ORDER BY p.space_id, p.event_date) -INSERT INTO public.space_daily_pollutant_stats ( +INSERT INTO public."space-daily-pollutant-stats" ( space_uuid, event_date, good_aqi_percentage, moderate_aqi_percentage, unhealthy_sensitive_aqi_percentage, unhealthy_aqi_percentage, very_unhealthy_aqi_percentage, hazardous_aqi_percentage, diff --git a/libs/common/src/sql/procedures/fact_daily_space_aqi/procedure_insert_all_daily_space_aqi.sql b/libs/common/src/sql/procedures/fact_daily_space_aqi/procedure_insert_all_daily_space_aqi.sql index aeb211b..0ed029b 100644 --- a/libs/common/src/sql/procedures/fact_daily_space_aqi/procedure_insert_all_daily_space_aqi.sql +++ b/libs/common/src/sql/procedures/fact_daily_space_aqi/procedure_insert_all_daily_space_aqi.sql @@ -275,7 +275,7 @@ LEFT JOIN daily_averages a ORDER BY p.space_id, p.event_date) -INSERT INTO public.space_daily_pollutant_stats ( +INSERT INTO public."space-daily-pollutant-stats" ( space_uuid, event_date, good_aqi_percentage, moderate_aqi_percentage, unhealthy_sensitive_aqi_percentage, unhealthy_aqi_percentage, very_unhealthy_aqi_percentage, hazardous_aqi_percentage, From ee0261d102015af5cce07b04edf5a5996fcb1ed9 Mon Sep 17 00:00:00 2001 From: khuss Date: Wed, 4 Jun 2025 17:32:50 -0400 Subject: [PATCH 12/17] Fix typos procedure select and update --- .../fact_daily_space_aqi/proceduce_select_daily_space_aqi.sql | 2 +- .../fact_daily_space_aqi/proceduce_update_daily_space_aqi.sql | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/common/src/sql/procedures/fact_daily_space_aqi/proceduce_select_daily_space_aqi.sql b/libs/common/src/sql/procedures/fact_daily_space_aqi/proceduce_select_daily_space_aqi.sql index 4c4c6ab..83fc840 100644 --- a/libs/common/src/sql/procedures/fact_daily_space_aqi/proceduce_select_daily_space_aqi.sql +++ b/libs/common/src/sql/procedures/fact_daily_space_aqi/proceduce_select_daily_space_aqi.sql @@ -1,4 +1,4 @@ -ITH params AS ( +WITH params AS ( SELECT $1::uuid AS space_uuid, TO_DATE(NULLIF($2, ''), 'YYYY-MM') AS event_month diff --git a/libs/common/src/sql/procedures/fact_daily_space_aqi/proceduce_update_daily_space_aqi.sql b/libs/common/src/sql/procedures/fact_daily_space_aqi/proceduce_update_daily_space_aqi.sql index e2466f2..04fb661 100644 --- a/libs/common/src/sql/procedures/fact_daily_space_aqi/proceduce_update_daily_space_aqi.sql +++ b/libs/common/src/sql/procedures/fact_daily_space_aqi/proceduce_update_daily_space_aqi.sql @@ -5,7 +5,7 @@ WITH params AS ( ), -- Query Pipeline Starts Here -WITH device_space AS ( +device_space AS ( SELECT device.uuid AS device_id, device.space_device_uuid AS space_id, From 7eb13088acb0916cfa965376e67a217ac4542c8c Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Mon, 9 Jun 2025 04:50:58 -0600 Subject: [PATCH 13/17] Refactor DeviceStatusLogEntity: update unique constraint and primary key definition --- .../entities/device-status-log.entity.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libs/common/src/modules/device-status-log/entities/device-status-log.entity.ts b/libs/common/src/modules/device-status-log/entities/device-status-log.entity.ts index b40c393..595db05 100644 --- a/libs/common/src/modules/device-status-log/entities/device-status-log.entity.ts +++ b/libs/common/src/modules/device-status-log/entities/device-status-log.entity.ts @@ -1,16 +1,16 @@ import { SourceType } from '@app/common/constants/source-type.enum'; -import { Entity, Column, PrimaryColumn, Unique } from 'typeorm'; +import { Entity, Column, Unique, PrimaryGeneratedColumn } from 'typeorm'; @Entity('device-status-log') -@Unique('event_time_idx', ['eventTime']) +@Unique('device_event_time_unique', ['deviceId', 'eventTime']) export class DeviceStatusLogEntity { - @Column({ type: 'int', generated: true, unsigned: true }) + @PrimaryGeneratedColumn() id: number; @Column({ type: 'text' }) eventId: string; - @PrimaryColumn({ type: 'timestamptz' }) + @Column({ type: 'timestamptz' }) eventTime: Date; @Column({ From c1065126aab117ee508cca9d65dd4e7b5bbf456c Mon Sep 17 00:00:00 2001 From: faljawhary Date: Tue, 10 Jun 2025 01:03:45 -0600 Subject: [PATCH 14/17] =?UTF-8?q?Revert=20"Refactor=20DeviceStatusLogEntit?= =?UTF-8?q?y:=20update=20unique=20constraint=20and=20primary=20=E2=80=A6"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../entities/device-status-log.entity.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libs/common/src/modules/device-status-log/entities/device-status-log.entity.ts b/libs/common/src/modules/device-status-log/entities/device-status-log.entity.ts index 595db05..b40c393 100644 --- a/libs/common/src/modules/device-status-log/entities/device-status-log.entity.ts +++ b/libs/common/src/modules/device-status-log/entities/device-status-log.entity.ts @@ -1,16 +1,16 @@ import { SourceType } from '@app/common/constants/source-type.enum'; -import { Entity, Column, Unique, PrimaryGeneratedColumn } from 'typeorm'; +import { Entity, Column, PrimaryColumn, Unique } from 'typeorm'; @Entity('device-status-log') -@Unique('device_event_time_unique', ['deviceId', 'eventTime']) +@Unique('event_time_idx', ['eventTime']) export class DeviceStatusLogEntity { - @PrimaryGeneratedColumn() + @Column({ type: 'int', generated: true, unsigned: true }) id: number; @Column({ type: 'text' }) eventId: string; - @Column({ type: 'timestamptz' }) + @PrimaryColumn({ type: 'timestamptz' }) eventTime: Date; @Column({ From b6321c253058f18ebe5d78a6d8909b042e6ff5db Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 10 Jun 2025 01:18:58 -0600 Subject: [PATCH 15/17] Refactor DeviceStatusLogEntity: update unique constraint to include deviceId --- .../device-status-log/entities/device-status-log.entity.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/common/src/modules/device-status-log/entities/device-status-log.entity.ts b/libs/common/src/modules/device-status-log/entities/device-status-log.entity.ts index b40c393..a97565a 100644 --- a/libs/common/src/modules/device-status-log/entities/device-status-log.entity.ts +++ b/libs/common/src/modules/device-status-log/entities/device-status-log.entity.ts @@ -2,15 +2,15 @@ import { SourceType } from '@app/common/constants/source-type.enum'; import { Entity, Column, PrimaryColumn, Unique } from 'typeorm'; @Entity('device-status-log') -@Unique('event_time_idx', ['eventTime']) +@Unique('event_time_device_idx', ['eventTime', 'deviceId']) export class DeviceStatusLogEntity { - @Column({ type: 'int', generated: true, unsigned: true }) + @PrimaryColumn({ type: 'int', generated: true, unsigned: true }) id: number; @Column({ type: 'text' }) eventId: string; - @PrimaryColumn({ type: 'timestamptz' }) + @Column({ type: 'timestamptz' }) eventTime: Date; @Column({ From 0793441e06534b2b1ade526c1516ed5478c1deaf Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 10 Jun 2025 01:24:33 -0600 Subject: [PATCH 16/17] Refactor DeviceStatusLogEntity: correct unique constraint name for event time and device ID --- .../device-status-log/entities/device-status-log.entity.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/common/src/modules/device-status-log/entities/device-status-log.entity.ts b/libs/common/src/modules/device-status-log/entities/device-status-log.entity.ts index a97565a..3b55e31 100644 --- a/libs/common/src/modules/device-status-log/entities/device-status-log.entity.ts +++ b/libs/common/src/modules/device-status-log/entities/device-status-log.entity.ts @@ -2,7 +2,7 @@ import { SourceType } from '@app/common/constants/source-type.enum'; import { Entity, Column, PrimaryColumn, Unique } from 'typeorm'; @Entity('device-status-log') -@Unique('event_time_device_idx', ['eventTime', 'deviceId']) +@Unique('event_time_idx', ['eventTime', 'deviceId']) export class DeviceStatusLogEntity { @PrimaryColumn({ type: 'int', generated: true, unsigned: true }) id: number; From 97e14e70f7cb4d452383b383723935fb04d46fbb Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Tue, 10 Jun 2025 01:41:41 -0600 Subject: [PATCH 17/17] Refactor DeviceStatusLogEntity: expand unique constraint to include code and value --- .../device-status-log/entities/device-status-log.entity.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/common/src/modules/device-status-log/entities/device-status-log.entity.ts b/libs/common/src/modules/device-status-log/entities/device-status-log.entity.ts index 3b55e31..07fc8d2 100644 --- a/libs/common/src/modules/device-status-log/entities/device-status-log.entity.ts +++ b/libs/common/src/modules/device-status-log/entities/device-status-log.entity.ts @@ -2,7 +2,7 @@ import { SourceType } from '@app/common/constants/source-type.enum'; import { Entity, Column, PrimaryColumn, Unique } from 'typeorm'; @Entity('device-status-log') -@Unique('event_time_idx', ['eventTime', 'deviceId']) +@Unique('event_time_idx', ['eventTime', 'deviceId', 'code', 'value']) export class DeviceStatusLogEntity { @PrimaryColumn({ type: 'int', generated: true, unsigned: true }) id: number;