[ 'read_id', 'sync' ], 'public' => [] ]; public static $cache = [ // Can't have these on right now because beestat loads and fires this API // call off. Then it syncs all the data and calls these again to get updated // data...if they're cached that just returns empty. So need to be able to // bypass the cache or something to get that to work. Or just don't call the // API call to begin with on first load if there's nothing there. // 'read' => 3600, // 1 hour // 'read_id' => 3600, // 1 hour ]; /** * Read from the runtime_thermostat_summary table. Fixes temperature columns * to return as decimals. * * @param array $attributes * @param array $columns * * @return array */ public function read($attributes = [], $columns = []) { $thermostats = $this->api('thermostat', 'read_id'); if(isset($attributes['thermostat_id']) === true) { $thermostat_ids = [$attributes['thermostat_id']]; } else { $thermostat_ids = array_keys($thermostats); } $runtime_thermostat_summaries = []; foreach($thermostat_ids as $thermostat_id) { $thermostat = $thermostats[$thermostat_id]; $attributes['thermostat_id'] = $thermostat_id; // Get a base time for relative calculations $local_time_zone = new DateTimeZone($thermostat['time_zone']); $utc_time_zone = new DateTimeZone('UTC'); $date_time = new DateTime(date('Y-m-d H:i:s'), $utc_time_zone); $date_time->setTimezone($local_time_zone); if($date_time->getOffset() < 0) { $base = strtotime($date_time->getOffset() . ' seconds'); } else { $base = strtotime('+' . $date_time->getOffset() . ' seconds'); } // If a date was given, apply the base time adjustment to correct for time // zones. if(isset($attributes['date']) === true) { if(is_array($attributes['date']) === true) { $attributes['date']['value'] = date( 'Y-m-d', strtotime($attributes['date']['value'], $base) ); } else { $attributes['date'] = date( 'Y-m-d', strtotime($attributes['date'], $base) ); } } $runtime_thermostat_summaries = array_merge( $runtime_thermostat_summaries, parent::read($attributes, $columns) ); } foreach($runtime_thermostat_summaries as &$runtime_thermostat_summary) { $runtime_thermostat_summary['avg_outdoor_temperature'] /= 10; $runtime_thermostat_summary['min_outdoor_temperature'] /= 10; $runtime_thermostat_summary['max_outdoor_temperature'] /= 10; $runtime_thermostat_summary['avg_indoor_temperature'] /= 10; $runtime_thermostat_summary['sum_heating_degree_days'] /= 10; $runtime_thermostat_summary['sum_cooling_degree_days'] /= 10; } return $runtime_thermostat_summaries; } /** * Populate from the max populated date until now. * * @param int $thermostat_id */ public function populate_forwards($thermostat_id) { $thermostat = $this->api('thermostat', 'get', $thermostat_id); $query = ' select max(`date`) `max_date` #"2021-01-20" `max_date` from `runtime_thermostat_summary` where `user_id` = ' . $this->database->escape($this->session->get_user_id()) . ' and `thermostat_id` = ' . $this->database->escape($thermostat_id) . ' '; $result = $this->database->query($query); $row = $result->fetch_assoc(); if($row['max_date'] === null) { if($thermostat['data_begin'] === null) { $populate_begin = strtotime($thermostat['first_connected']); } else { $populate_begin = strtotime($thermostat['data_begin']); // Just grab everything } } else { $populate_begin = strtotime($row['max_date']); } $populate_end = time(); $populate_begin = date('Y-m-d', $populate_begin); $populate_end = date('Y-m-d', $populate_end); return $this->populate($thermostat_id, $populate_begin, $populate_end); } /** * Populate from the beginning of time until the min populated date. * * @param int $thermostat_id */ public function populate_backwards($thermostat_id) { $thermostat = $this->api('thermostat', 'get', $thermostat_id); // If there's no data do nothing. if ($thermostat['data_begin'] === null) { return; } $query = ' select min(`date`) `min_date` from `runtime_thermostat_summary` where `user_id` = ' . $this->database->escape($this->session->get_user_id()) . ' and `thermostat_id` = ' . $this->database->escape($thermostat_id) . ' '; $result = $this->database->query($query); $row = $result->fetch_assoc(); if($row['min_date'] === null) { $populate_end = time(); } else { // Include $populate_end = strtotime($row['min_date']); } $populate_begin = strtotime($thermostat['data_begin']); $populate_begin = date('Y-m-d', $populate_begin); $populate_end = date('Y-m-d', $populate_end); $this->populate($thermostat_id, $populate_begin, $populate_end); } /** * Populate the runtime_thermostat_summary table. * * @param int $thermostat_id * @param string $populate_begin Local date to begin populating, inclusive. * @param string $populate_end Local date to end populating, inclusive. */ public function populate($thermostat_id, $populate_begin, $populate_end) { $degree_days_base_temperature = 650; // 65°F * 10 (database storage) $degree_days_base_time_interval = 5 / 1440; // 5 minutes out of 1440 minutes per day. $thermostat = $this->api('thermostat', 'get', $thermostat_id); // Convert date strings to timestamps to make them easier to work with. $populate_begin = strtotime($populate_begin . ' 00:00:00'); $populate_end = strtotime($populate_end . ' 23:59:59'); $chunk_begin = $populate_begin; $chunk_end = $populate_begin; $data = []; do { $chunk_end = strtotime('+1 week', $chunk_begin); // MySQL "between" is inclusive so go back 5 minutes to avoid // double-counting rows. $chunk_end = strtotime('-5 minute', $chunk_end); // Don't overshoot into data that's already populated $chunk_end = min($chunk_end, $populate_end); $chunk_begin_datetime = get_utc_datetime( date('Y-m-d H:i:s', $chunk_begin), $thermostat['time_zone'] ); $chunk_end_datetime = get_utc_datetime( date('Y-m-d H:i:s', $chunk_end), $thermostat['time_zone'] ); $runtime_thermostats = $this->api( 'runtime_thermostat', 'read', [ 'attributes' => [ 'thermostat_id' => $thermostat['thermostat_id'], 'timestamp' => [ 'operator' => 'between', 'value' => [$chunk_begin_datetime, $chunk_end_datetime] ] ] ] ); foreach($runtime_thermostats as $runtime_thermostat) { $date = get_local_datetime( $runtime_thermostat['timestamp'], $thermostat['time_zone'], 'Y-m-d' ); if(isset($data[$date]) === false) { $data[$date] = [ 'count' => 0, 'sum_fan' => 0, 'min_outdoor_temperature' => INF, 'max_outdoor_temperature' => -INF, 'sum_auxiliary_heat_1' => 0, 'sum_auxiliary_heat_2' => 0, 'sum_compressor_cool_1' => 0, 'sum_compressor_cool_2' => 0, 'sum_compressor_heat_1' => 0, 'sum_compressor_heat_2' => 0, 'sum_humidifier' => 0, 'sum_dehumidifier' => 0, 'sum_ventilator' => 0, 'sum_economizer' => 0, 'sum_heating_degree_days' => 0, 'sum_cooling_degree_days' => 0, 'avg_outdoor_temperature' => [], 'avg_outdoor_humidity' => [], 'avg_indoor_temperature' => [], 'avg_indoor_humidity' => [] ]; } if($runtime_thermostat['outdoor_temperature'] !== null) { $runtime_thermostat['outdoor_temperature'] *= 10; } $runtime_thermostat['indoor_temperature'] *= 10; $data[$date]['count']++; $data[$date]['sum_fan'] += $runtime_thermostat['fan']; if ($runtime_thermostat['outdoor_temperature'] !== null) { $data[$date]['min_outdoor_temperature'] = min($runtime_thermostat['outdoor_temperature'], $data[$date]['min_outdoor_temperature']); $data[$date]['max_outdoor_temperature'] = max($runtime_thermostat['outdoor_temperature'], $data[$date]['max_outdoor_temperature']); } $data[$date]['sum_auxiliary_heat_1'] += $runtime_thermostat['auxiliary_heat_1']; $data[$date]['sum_auxiliary_heat_2'] += $runtime_thermostat['auxiliary_heat_2']; if($runtime_thermostat['compressor_mode'] === 'cool') { $data[$date]['sum_compressor_cool_1'] += $runtime_thermostat['compressor_1']; $data[$date]['sum_compressor_cool_2'] += $runtime_thermostat['compressor_2']; } else if($runtime_thermostat['compressor_mode'] === 'heat') { $data[$date]['sum_compressor_heat_1'] += $runtime_thermostat['compressor_1']; $data[$date]['sum_compressor_heat_2'] += $runtime_thermostat['compressor_2']; } if($runtime_thermostat['accessory_type'] === 'humidifier') { $data[$date]['sum_humidifier'] += $runtime_thermostat['accessory']; } else if($runtime_thermostat['accessory_type'] === 'dehumidifier') { $data[$date]['sum_dehumidifier'] += $runtime_thermostat['accessory']; } else if($runtime_thermostat['accessory_type'] === 'ventilator') { $data[$date]['sum_ventilator'] += $runtime_thermostat['accessory']; } else if($runtime_thermostat['accessory_type'] === 'economizer') { $data[$date]['sum_economizer'] += $runtime_thermostat['accessory']; } if ($runtime_thermostat['outdoor_temperature'] !== null) { $data[$date]['avg_outdoor_temperature'][] = $runtime_thermostat['outdoor_temperature']; // CDD if($runtime_thermostat['outdoor_temperature'] > $degree_days_base_temperature) { $data[$date]['sum_cooling_degree_days'] += (($runtime_thermostat['outdoor_temperature']) - $degree_days_base_temperature) * $degree_days_base_time_interval; } // HDD if($runtime_thermostat['outdoor_temperature'] < $degree_days_base_temperature) { $data[$date]['sum_heating_degree_days'] += ($degree_days_base_temperature - $runtime_thermostat['outdoor_temperature']) * $degree_days_base_time_interval; } } if ($runtime_thermostat['outdoor_humidity'] !== null) { $data[$date]['avg_outdoor_humidity'][] = $runtime_thermostat['outdoor_humidity']; } $data[$date]['avg_indoor_temperature'][] = $runtime_thermostat['indoor_temperature']; $data[$date]['avg_indoor_humidity'][] = $runtime_thermostat['indoor_humidity']; } $chunk_begin = strtotime('+5 minute', $chunk_end); } while ($chunk_end < $populate_end); // Write to the database. foreach($data as $date => &$row) { if (count($row['avg_outdoor_temperature']) > 0) { $row['avg_outdoor_temperature'] = round(array_sum($row['avg_outdoor_temperature']) / count($row['avg_outdoor_temperature'])); } else { $row['avg_outdoor_temperature'] = null; } if (count($row['avg_outdoor_humidity']) > 0) { $row['avg_outdoor_humidity'] = round(array_sum($row['avg_outdoor_humidity']) / count($row['avg_outdoor_humidity'])); } else { $row['avg_outdoor_humidity'] = null; } if ($row['min_outdoor_temperature'] === INF) { $row['min_outdoor_temperature'] = null; } if ($row['max_outdoor_temperature'] === -INF) { $row['max_outdoor_temperature'] = null; } $row['avg_indoor_temperature'] = round(array_sum($row['avg_indoor_temperature']) / count($row['avg_indoor_temperature'])); $row['avg_indoor_humidity'] = round(array_sum($row['avg_indoor_humidity']) / count($row['avg_indoor_humidity'])); $row['date'] = $date; $row['user_id'] = $thermostat['user_id']; $row['thermostat_id'] = $thermostat['thermostat_id']; $existing_runtime_thermostat_summary = $this->get([ 'thermostat_id' => $thermostat['thermostat_id'], 'date' => $date ]); if($existing_runtime_thermostat_summary === null) { $this->create($row); } else { $row['runtime_thermostat_summary_id'] = $existing_runtime_thermostat_summary['runtime_thermostat_summary_id']; $this->update($row); } } } }