From 154af5d89f9628faf3925763dc28efd6e8d60489 Mon Sep 17 00:00:00 2001 From: Jon Ziebell Date: Thu, 27 Feb 2020 20:09:11 -0500 Subject: [PATCH] Updated profile generation; added some metrics. --- api/index.php | 2 +- api/profile.php | 73 ++++++++++++++-- api/thermostat_group.php | 177 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 241 insertions(+), 11 deletions(-) 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.