diff --git a/api/index.php b/api/index.php index c6efe7f..4d048ae 100644 --- a/api/index.php +++ b/api/index.php @@ -50,7 +50,7 @@ function array_median($array) { } // Useful function -function array_average($array) { +function array_mean($array) { if (count($array) === 0) { return null; } diff --git a/api/profile.php b/api/profile.php index bb44362..22057ca 100644 --- a/api/profile.php +++ b/api/profile.php @@ -177,6 +177,16 @@ class profile extends cora\api { 'heat' => [], 'cool' => [] ]; + $runtime_seconds = [ + 'heat_1' => 0, + 'heat_2' => 0, + 'auxiliary_heat_1' => 0, + 'auxiliary_heat_2' => 0, + 'cool_1' => 0, + 'cool_2' => 0 + ]; + $degree_days_baseline = 65; + $degree_days = []; $begin_runtime = []; while($current_timestamp <= $end_timestamp) { @@ -213,22 +223,26 @@ class profile extends cora\api { // consistently represented instead of having to do this logic // throughout the generator. $runtime = []; + $degree_days_date = date('Y-m-d', $current_timestamp); + $degree_days_temperatures = []; while($row = $result->fetch_assoc()) { $timestamp = strtotime($row['timestamp']); $hour = date('G', $timestamp); + $date = date('Y-m-d', $timestamp); - if ( - $ignore_solar_heating === true && - $hour > 6 && - $hour < 22 - ) { - continue; + // Degree days + if($date !== $degree_days_date) { + $degree_days[] = (array_mean($degree_days_temperatures) / 10) - $degree_days_baseline; + $degree_days_date = $date; + $degree_days_temperatures = []; } + $degree_days_temperatures[] = $row['outdoor_temperature']; if($first_timestamp === null) { $first_timestamp = $row['timestamp']; } + // Normalizing heating and cooling a bit. if( $thermostat['system_type']['detected']['heat'] === 'compressor' || $thermostat['system_type']['detected']['heat'] === 'geothermal' @@ -255,6 +269,21 @@ class profile extends cora\api { $row['cool_2'] = 0; } + $runtime_seconds['heat_1'] += $row['heat_1']; + $runtime_seconds['heat_2'] += $row['heat_2']; + $runtime_seconds['auxiliary_heat_1'] += $row['auxiliary_heat_1']; + $runtime_seconds['auxiliary_heat_2'] += $row['auxiliary_heat_2']; + $runtime_seconds['cool_1'] += $row['cool_1']; + $runtime_seconds['cool_2'] += $row['cool_2']; + + if ( + $ignore_solar_heating === true && + $hour > 6 && + $hour < 22 + ) { + continue; + } + if (isset($runtime[$timestamp]) === false) { $runtime[$timestamp] = []; } @@ -630,8 +659,6 @@ class profile extends cora\api { $current_timestamp += $five_minutes; } - // print_r($samples); - // Process the samples $deltas_raw = []; foreach($samples as $sample) { @@ -670,6 +697,18 @@ class profile extends cora\api { 'heat' => null, 'cool' => null ], + 'degree_days' => [ + 'heat' => null, + 'cool' => null + ], + 'runtime' => [ + 'heat_1' => round($runtime_seconds['heat_1'] / 3600), + 'heat_2' => round($runtime_seconds['heat_2'] / 3600), + 'auxiliary_heat_1' => round($runtime_seconds['auxiliary_heat_1'] / 3600), + 'auxiliary_heat_2' => round($runtime_seconds['auxiliary_heat_2'] / 3600), + 'cool_1' => round($runtime_seconds['cool_1'] / 3600), + 'cool_2' => round($runtime_seconds['cool_2'] / 3600), + ], 'metadata' => [ 'generated_at' => date('c'), 'duration' => round((time() - strtotime($first_timestamp)) / 86400), @@ -730,11 +769,27 @@ class profile extends cora\api { foreach(['heat', 'cool'] as $type) { if(count($setpoints[$type]) > 0) { - $profile['setpoint'][$type] = round(array_average($setpoints[$type])) / 10; + $profile['setpoint'][$type] = round(array_mean($setpoints[$type])) / 10; $profile['metadata']['setpoint'][$type]['samples'] = count($setpoints[$type]); } } + // Heating and cooling degree days. + foreach($degree_days as $degree_day) { + if($degree_day < 0) { + $profile['degree_days']['cool'] += ($degree_day * -1); + } else { + $profile['degree_days']['heat'] += ($degree_day); + } + } + if ($profile['degree_days']['cool'] !== null) { + $profile['degree_days']['cool'] = round($profile['degree_days']['cool']); + } + if ($profile['degree_days']['heat'] !== null) { + $profile['degree_days']['heat'] = round($profile['degree_days']['heat']); + } + + return $profile; } diff --git a/api/thermostat_group.php b/api/thermostat_group.php index c045e6a..fe91530 100644 --- a/api/thermostat_group.php +++ b/api/thermostat_group.php @@ -17,6 +17,7 @@ class thermostat_group extends cora\crud { 'generate_profiles', 'generate_profile', 'get_scores', + 'get_metrics', 'update_system_types' ], 'public' => [] @@ -27,7 +28,8 @@ class thermostat_group extends cora\crud { 'generate_temperature_profiles' => 604800, // 7 Days 'generate_profile' => 604800, // 7 Days 'generate_profiles' => 604800, // 7 Days - 'get_scores' => 604800 // 7 Days + 'get_scores' => 604800, // 7 Days + 'get_metrics' => 604800 // 7 Days ]; /** @@ -75,6 +77,18 @@ class thermostat_group extends cora\crud { 'heat' => null, 'cool' => null ], + 'degree_days' => [ + 'heat' => null, + 'cool' => null + ], + 'runtime' => [ + 'heat_1' => 0, + 'heat_2' => 0, + 'auxiliary_heat_1' => 0, + 'auxiliary_heat_2' => 0, + 'cool_1' => 0, + 'cool_2' => 0 + ], 'metadata' => [ 'generated_at' => date('c'), 'duration' => null, @@ -151,6 +165,22 @@ class thermostat_group extends cora\crud { } } } + + // Degree days. + if($profile['degree_days']['heat'] !== null) { + $group_profile['degree_days']['heat'] += $profile['degree_days']['heat']; + } + if($profile['degree_days']['cool'] !== null) { + $group_profile['degree_days']['cool'] += $profile['degree_days']['cool']; + } + + // Runtime + $group_profile['runtime']['heat_1'] += $profile['runtime']['heat_1']; + $group_profile['runtime']['heat_2'] += $profile['runtime']['heat_2']; + $group_profile['runtime']['auxiliary_heat_1'] += $profile['runtime']['auxiliary_heat_1']; + $group_profile['runtime']['auxiliary_heat_2'] += $profile['runtime']['auxiliary_heat_2']; + $group_profile['runtime']['cool_1'] += $profile['runtime']['cool_1']; + $group_profile['runtime']['cool_2'] += $profile['runtime']['cool_2']; } // echo '
'; @@ -497,6 +527,151 @@ class thermostat_group extends cora\crud { return $scores; } + /** + * Compare this thermostat_group to all other matching ones. + * + * @param array $attributes The attributes to compare to. + * + * @return array + */ + public function get_metrics($type, $attributes) { + // All or none are required. + if( + ( + isset($attributes['address_latitude']) === true || + isset($attributes['address_longitude']) === true || + isset($attributes['address_radius']) === true + ) && + ( + isset($attributes['address_latitude']) === false || + isset($attributes['address_longitude']) === false || + isset($attributes['address_radius']) === false + ) + ) { + throw new Exception('If one of address_latitude, address_longitude, or address_radius are set, then all are required.'); + } + + // Pull these values out so they don't get queried; this comparison is done + // in PHP. + if(isset($attributes['address_radius']) === true) { + $address_latitude = $attributes['address_latitude']; + $address_longitude = $attributes['address_longitude']; + $address_radius = $attributes['address_radius']; + + unset($attributes['address_latitude']); + unset($attributes['address_longitude']); + unset($attributes['address_radius']); + } + + $metric_codes = [ + 'setpoint_heat', + 'setpoint_cool' + ]; + + $metrics = []; + foreach($metric_codes as $metric_code) { + $metrics[$metric_code] = [ + 'values' => [], + 'histogram' => [], + 'standard_deviation' => null, + 'median' => null + ]; + } + + $limit_start = 0; + $limit_count = 1000; + + /** + * Selecting lots of rows can eventually run PHP out of memory, so chunk + * this up into several queries to avoid that. + */ + do { + // Get all matching thermostat groups. + $other_thermostat_groups = $this->database->read( + 'thermostat_group', + $attributes, + [], // columns + [], // order_by + [$limit_start, $limit_count] // limit + ); + + // Get all the scores from the other thermostat groups + foreach($other_thermostat_groups as $other_thermostat_group) { + // Only use profiles with at least a year of data + // Only use profiles generated in the past year + // + if( + $other_thermostat_group['profile']['metadata']['duration'] >= 365 && + strtotime($other_thermostat_group['profile']['metadata']['generated_at']) > strtotime('-1 year') + ) { + // Skip thermostat_groups that are too far away. + if( + isset($address_radius) === true && + $this->haversine_great_circle_distance( + $address_latitude, + $address_longitude, + $other_thermostat_group['address_latitude'], + $other_thermostat_group['address_longitude'] + ) > $address_radius + ) { + continue; + } + + // setpoint_heat + if($other_thermostat_group['profile']['setpoint']['heat'] !== null) { + $setpoint_heat = round($other_thermostat_group['profile']['setpoint']['heat']); + if(isset($metrics['setpoint_heat']['histogram'][$setpoint_heat]) === false) { + $metrics['setpoint_heat']['histogram'][$setpoint_heat] = 0; + } + $metrics['setpoint_heat']['histogram'][$setpoint_heat]++; + $metrics['setpoint_heat']['values'][] = $setpoint_heat; + } + + // setpoint_cool + if($other_thermostat_group['profile']['setpoint']['cool'] !== null) { + $setpoint_cool = round($other_thermostat_group['profile']['setpoint']['cool']); + if(isset($metrics['setpoint_cool']['histogram'][$setpoint_cool]) === false) { + $metrics['setpoint_cool']['histogram'][$setpoint_cool] = 0; + } + $metrics['setpoint_cool']['histogram'][$setpoint_cool]++; + $metrics['setpoint_cool']['values'][] = $setpoint_cool; + } + } + } + + $limit_start += $limit_count; + } while (count($other_thermostat_groups) === $limit_count); + + // setpoint_heat + $metrics['setpoint_heat']['standard_deviation'] = round($this->standard_deviation( + $metrics['setpoint_heat']['values'] + ), 2); + $metrics['setpoint_heat']['median'] = array_median($metrics['setpoint_heat']['values']); + unset($metrics['setpoint_heat']['values']); + + // setpoint_cool + $metrics['setpoint_cool']['standard_deviation'] = round($this->standard_deviation( + $metrics['setpoint_cool']['values'] + ), 2); + $metrics['setpoint_cool']['median'] = array_median($metrics['setpoint_cool']['values']); + unset($metrics['setpoint_cool']['values']); + + return $metrics; + } + + private function standard_deviation($array) { + $count = count($array); + + $mean = array_mean($array); + + $variance = 0; + foreach($array as $i) { + $variance += pow(($i - $mean), 2); + } + + return sqrt($variance / $count); + } + /** * Calculates the great-circle distance between two points, with the * Haversine formula.