mirror of
https://github.com/beestat/app.git
synced 2025-05-24 02:14:03 -04:00
New Download Data button for #180
Adds a download data button with a somewhat customization date range selector.
This commit is contained in:
parent
a8562b6878
commit
f5ff29d45e
@ -10,7 +10,8 @@ class runtime_thermostat extends cora\crud {
|
||||
public static $exposed = [
|
||||
'private' => [
|
||||
'read',
|
||||
'sync'
|
||||
'sync',
|
||||
'download'
|
||||
],
|
||||
'public' => []
|
||||
];
|
||||
@ -82,6 +83,7 @@ class runtime_thermostat extends cora\crud {
|
||||
)
|
||||
);
|
||||
} else {
|
||||
$this->user_lock($thermostat_id);
|
||||
$thermostat_ids = [$thermostat_id];
|
||||
}
|
||||
|
||||
@ -267,6 +269,8 @@ class runtime_thermostat extends cora\crud {
|
||||
* @param int $end
|
||||
*/
|
||||
private function sync_($thermostat_id, $begin, $end) {
|
||||
$this->user_lock($thermostat_id);
|
||||
|
||||
$thermostat = $this->api('thermostat', 'get', $thermostat_id);
|
||||
$ecobee_thermostat = $this->api('ecobee_thermostat', 'get', $thermostat['ecobee_thermostat_id']);
|
||||
|
||||
@ -333,7 +337,6 @@ class runtime_thermostat extends cora\crud {
|
||||
'Y-m-d H:i:s',
|
||||
strtotime($columns_begin['date'] . ' ' . $columns_begin['time'] . ' -1 hour')
|
||||
),
|
||||
// $columns_begin['date'] . ' ' . $columns_begin['time'],
|
||||
$thermostat['time_zone']
|
||||
),
|
||||
$this->get_utc_datetime(
|
||||
@ -526,8 +529,26 @@ class runtime_thermostat extends cora\crud {
|
||||
*/
|
||||
private function get_utc_datetime($local_datetime, $local_time_zone) {
|
||||
$local_time_zone = new DateTimeZone($local_time_zone);
|
||||
$utc_time_zone = new DateTimeZone('UTC');
|
||||
$date_time = new DateTime($local_datetime, $local_time_zone);
|
||||
$date_time->setTimezone(new DateTimeZone('UTC'));
|
||||
$date_time->setTimezone($utc_time_zone);
|
||||
|
||||
return $date_time->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a UTC datetime string to a UTC datetime string.
|
||||
*
|
||||
* @param string $utc_datetime Local datetime string.
|
||||
* @param string $local_time_zone The local time zone to convert from.
|
||||
*
|
||||
* @return string The UTC datetime string.
|
||||
*/
|
||||
private function get_local_datetime($utc_datetime, $local_time_zone) {
|
||||
$local_time_zone = new DateTimeZone($local_time_zone);
|
||||
$utc_time_zone = new DateTimeZone('UTC');
|
||||
$date_time = new DateTime($utc_datetime, $utc_time_zone);
|
||||
$date_time->setTimezone($local_time_zone);
|
||||
|
||||
return $date_time->format('Y-m-d H:i:s');
|
||||
}
|
||||
@ -544,7 +565,7 @@ class runtime_thermostat extends cora\crud {
|
||||
* @return array
|
||||
*/
|
||||
public function read($attributes = [], $columns = []) {
|
||||
$thermostats = $this->api('thermostat', 'read_id');
|
||||
$this->user_lock($attributes['thermostat_id']);
|
||||
|
||||
// Check for exceptions.
|
||||
if (isset($attributes['thermostat_id']) === false) {
|
||||
@ -555,10 +576,6 @@ class runtime_thermostat extends cora\crud {
|
||||
throw new \Exception('Missing required attribute: timestamp.', 10202);
|
||||
}
|
||||
|
||||
if (isset($thermostats[$attributes['thermostat_id']]) === false) {
|
||||
throw new \Exception('Invalid thermostat_id.', 10203);
|
||||
}
|
||||
|
||||
if (
|
||||
is_array($attributes['timestamp']) === true &&
|
||||
in_array($attributes['timestamp']['operator'], ['>', '>=', '<', '<=']) === true &&
|
||||
@ -571,7 +588,7 @@ class runtime_thermostat extends cora\crud {
|
||||
}
|
||||
}
|
||||
|
||||
$thermostat = $thermostats[$attributes['thermostat_id']];
|
||||
$thermostat = $this->api('thermostat', 'get', $attributes['thermostat_id']);
|
||||
$max_range = 2592000; // 30 days
|
||||
if (
|
||||
(
|
||||
@ -659,4 +676,146 @@ class runtime_thermostat extends cora\crud {
|
||||
return $runtime_thermostats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download all data that exists for a specific thermostat.
|
||||
*
|
||||
* @param int $thermostat_id
|
||||
* @param string $download_begin Optional; the date to begin the download.
|
||||
* @param string $download_end Optional; the date to end the download.
|
||||
*/
|
||||
public function download($thermostat_id, $download_begin = null, $download_end = null) {
|
||||
set_time_limit(120);
|
||||
|
||||
$this->user_lock($thermostat_id);
|
||||
|
||||
$thermostat = $this->api('thermostat', 'get', $thermostat_id);
|
||||
$ecobee_thermostat = $this->api(
|
||||
'ecobee_thermostat',
|
||||
'get',
|
||||
$thermostat['ecobee_thermostat_id']
|
||||
);
|
||||
|
||||
if($download_begin === null) {
|
||||
$download_begin = strtotime($thermostat['first_connected']);
|
||||
} else {
|
||||
$download_begin = strtotime($download_begin);
|
||||
}
|
||||
|
||||
if($download_end === null) {
|
||||
$download_end = time();
|
||||
} else {
|
||||
$download_end = strtotime($download_end);
|
||||
}
|
||||
|
||||
$chunk_begin = $download_begin;
|
||||
$chunk_end = $download_begin;
|
||||
|
||||
$bytes = 0;
|
||||
|
||||
$output = fopen('php://output', 'w');
|
||||
$needs_header = true;
|
||||
do {
|
||||
$chunk_end = strtotime('+1 week', $chunk_begin);
|
||||
$chunk_end = min($chunk_end, $download_end);
|
||||
|
||||
$runtime_thermostats = $this->database->read(
|
||||
'runtime_thermostat',
|
||||
[
|
||||
'thermostat_id' => $thermostat_id,
|
||||
'timestamp' => [
|
||||
'value' => [date('Y-m-d H:i:s', $chunk_begin), date('Y-m-d H:i:s', $chunk_end)] ,
|
||||
'operator' => 'between'
|
||||
]
|
||||
],
|
||||
[],
|
||||
'timestamp' // order by
|
||||
);
|
||||
|
||||
// Get the appropriate runtime_thermostat_texts.
|
||||
$runtime_thermostat_text_ids = array_unique(array_merge(
|
||||
array_column($runtime_thermostats, 'event_runtime_thermostat_text_id'),
|
||||
array_column($runtime_thermostats, 'climate_runtime_thermostat_text_id')
|
||||
));
|
||||
$runtime_thermostat_texts = $this->api(
|
||||
'runtime_thermostat_text',
|
||||
'read_id',
|
||||
[
|
||||
'attributes' => [
|
||||
'runtime_thermostat_text_id' => $runtime_thermostat_text_ids
|
||||
]
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
if ($needs_header === true && count($runtime_thermostats) > 0) {
|
||||
$headers = array_keys($runtime_thermostats[0]);
|
||||
|
||||
// Remove the IDs and rename two columns.
|
||||
unset($headers[array_search('runtime_thermostat_id', $headers)]);
|
||||
unset($headers[array_search('thermostat_id', $headers)]);
|
||||
$headers[array_search('event_runtime_thermostat_text_id', $headers)] = 'event';
|
||||
$headers[array_search('climate_runtime_thermostat_text_id', $headers)] = 'climate';
|
||||
|
||||
$bytes += fputcsv($output, $headers);
|
||||
$needs_header = false;
|
||||
}
|
||||
|
||||
foreach($runtime_thermostats as $runtime_thermostat) {
|
||||
unset($runtime_thermostat['runtime_thermostat_id']);
|
||||
unset($runtime_thermostat['thermostat_id']);
|
||||
|
||||
$runtime_thermostat['timestamp'] = $this->get_local_datetime(
|
||||
$runtime_thermostat['timestamp'],
|
||||
$thermostat['time_zone']
|
||||
);
|
||||
|
||||
// Return temperatures in a human-readable format.
|
||||
foreach(['indoor_temperature', 'outdoor_temperature', 'setpoint_heat', 'setpoint_cool'] as $key) {
|
||||
if($runtime_thermostat[$key] !== null) {
|
||||
$runtime_thermostat[$key] /= 10;
|
||||
}
|
||||
}
|
||||
|
||||
// Replace event and climate with their string values.
|
||||
if ($runtime_thermostat['event_runtime_thermostat_text_id'] !== null) {
|
||||
$runtime_thermostat['event_runtime_thermostat_text_id'] = $runtime_thermostat_texts[$runtime_thermostat['event_runtime_thermostat_text_id']]['value'];
|
||||
}
|
||||
|
||||
if ($runtime_thermostat['climate_runtime_thermostat_text_id'] !== null) {
|
||||
$runtime_thermostat['climate_runtime_thermostat_text_id'] = $runtime_thermostat_texts[$runtime_thermostat['climate_runtime_thermostat_text_id']]['value'];
|
||||
}
|
||||
|
||||
$bytes += fputcsv($output, $runtime_thermostat);
|
||||
}
|
||||
|
||||
$chunk_begin = strtotime('+1 day', $chunk_end);
|
||||
} while ($chunk_end < $download_end);
|
||||
fclose($output);
|
||||
|
||||
header('Content-type: text/csv');
|
||||
header('Content-Length: ' . $bytes);
|
||||
header('Content-Disposition: attachment; filename="Beestat Export - ' . $ecobee_thermostat['identifier'] . '.csv"');
|
||||
header('Pragma: no-cache');
|
||||
header('Expires: 0');
|
||||
|
||||
die();
|
||||
}
|
||||
|
||||
/**
|
||||
* Since this table does not have a user_id column, security must be handled
|
||||
* manually. Call this with a thermostat_id to verify that the current user
|
||||
* has access to the requested thermostat.
|
||||
*
|
||||
* @param int $thermostat_id
|
||||
*
|
||||
* @throws \Exception If the current user doesn't have access to the
|
||||
* requested thermostat.
|
||||
*/
|
||||
private function user_lock($thermostat_id) {
|
||||
$thermostats = $this->api('thermostat', 'read_id');
|
||||
if (isset($thermostats[$thermostat_id]) === false) {
|
||||
throw new \Exception('Invalid thermostat_id.', 10203);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -146,6 +146,13 @@ beestat.component.header.prototype.decorate_ = function(parent) {
|
||||
}
|
||||
menu.add_menu_item(announcements_menu_item);
|
||||
|
||||
menu.add_menu_item(new beestat.component.menu_item()
|
||||
.set_text('Download Data')
|
||||
.set_icon('download')
|
||||
.set_callback(function() {
|
||||
(new beestat.component.modal.download_data()).render();
|
||||
}));
|
||||
|
||||
menu.add_menu_item(new beestat.component.menu_item()
|
||||
.set_text('Log Out')
|
||||
.set_icon('exit_to_app')
|
||||
|
187
js/component/modal/download_data.js
Normal file
187
js/component/modal/download_data.js
Normal file
@ -0,0 +1,187 @@
|
||||
/**
|
||||
* Download data modal.
|
||||
*/
|
||||
beestat.component.modal.download_data = function() {
|
||||
beestat.component.modal.apply(this, arguments);
|
||||
};
|
||||
beestat.extend(beestat.component.modal.download_data, beestat.component.modal);
|
||||
|
||||
beestat.component.modal.download_data.prototype.decorate_contents_ = function(parent) {
|
||||
var self = this;
|
||||
|
||||
parent.appendChild($.createElement('p').innerHTML('Choose a custom download range.'));
|
||||
|
||||
self.state_.download_data_time_count = 1;
|
||||
|
||||
// Time count
|
||||
var time_count = new beestat.component.input.text()
|
||||
.set_style({
|
||||
'width': 75,
|
||||
'text-align': 'center',
|
||||
'border-bottom': '2px solid ' + beestat.style.color.lightblue.base
|
||||
})
|
||||
.set_attribute({
|
||||
'maxlength': 10
|
||||
})
|
||||
.set_icon('pound')
|
||||
.set_value(self.state_.download_data_time_count);
|
||||
|
||||
time_count.addEventListener('blur', function() {
|
||||
self.state_.download_data_time_count =
|
||||
parseInt(this.get_value(), 10) || 1;
|
||||
});
|
||||
|
||||
// Button groups
|
||||
var options = {
|
||||
'download_data_time_period': [
|
||||
'day',
|
||||
'week',
|
||||
'month',
|
||||
'year',
|
||||
'all'
|
||||
]
|
||||
};
|
||||
|
||||
var button_groups = {};
|
||||
|
||||
this.selected_buttons_ = {};
|
||||
for (let key in options) {
|
||||
let current_type = 'month';
|
||||
|
||||
let button_group = new beestat.component.button_group();
|
||||
options[key].forEach(function(value) {
|
||||
let text = value.replace('download_data_', '')
|
||||
.charAt(0)
|
||||
.toUpperCase() +
|
||||
value.slice(1) +
|
||||
(
|
||||
(
|
||||
key === 'download_data_time_period' &&
|
||||
value !== 'all'
|
||||
) ? 's' : ''
|
||||
);
|
||||
|
||||
let button = new beestat.component.button()
|
||||
.set_background_hover_color(beestat.style.color.lightblue.base)
|
||||
.set_text_color('#fff')
|
||||
.set_text(text)
|
||||
.addEventListener('click', function() {
|
||||
if (key === 'download_data_time_period') {
|
||||
if (value === 'all') {
|
||||
time_count.set_value('∞').disable();
|
||||
} else if (time_count.get_value() === '∞') {
|
||||
time_count
|
||||
.set_value(self.state_.download_data_time_count || '1')
|
||||
.enable();
|
||||
time_count.dispatchEvent('blur');
|
||||
}
|
||||
}
|
||||
|
||||
if (current_type !== value) {
|
||||
this.set_background_color(beestat.style.color.lightblue.base);
|
||||
if (self.selected_buttons_[key] !== undefined) {
|
||||
self.selected_buttons_[key]
|
||||
.set_background_color(beestat.style.color.bluegray.base);
|
||||
}
|
||||
self.selected_buttons_[key] = this;
|
||||
self.state_[key] = value;
|
||||
current_type = value;
|
||||
}
|
||||
});
|
||||
|
||||
if (current_type === value) {
|
||||
if (
|
||||
key === 'download_data_time_period' &&
|
||||
value === 'all'
|
||||
) {
|
||||
time_count.set_value('∞').disable();
|
||||
}
|
||||
|
||||
button.set_background_color(beestat.style.color.lightblue.base);
|
||||
self.state_[key] = value;
|
||||
self.selected_buttons_[key] = button;
|
||||
} else {
|
||||
button.set_background_color(beestat.style.color.bluegray.base);
|
||||
}
|
||||
|
||||
button_group.add_button(button);
|
||||
});
|
||||
button_groups[key] = button_group;
|
||||
}
|
||||
|
||||
// Display it all
|
||||
var row;
|
||||
var column;
|
||||
|
||||
(new beestat.component.title('Time Period')).render(parent);
|
||||
row = $.createElement('div').addClass('row');
|
||||
parent.appendChild(row);
|
||||
column = $.createElement('div').addClass(['column column_2']);
|
||||
row.appendChild(column);
|
||||
time_count.render(column);
|
||||
column = $.createElement('div').addClass(['column column_10']);
|
||||
row.appendChild(column);
|
||||
button_groups.download_data_time_period.render(column);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get title.
|
||||
*
|
||||
* @return {string} Title
|
||||
*/
|
||||
beestat.component.modal.download_data.prototype.get_title_ = function() {
|
||||
return 'Download Data';
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the buttons that go on the bottom of this modal.
|
||||
*
|
||||
* @return {[beestat.component.button]} The buttons.
|
||||
*/
|
||||
beestat.component.modal.download_data.prototype.get_buttons_ = function() {
|
||||
var self = this;
|
||||
|
||||
var cancel = new beestat.component.button()
|
||||
.set_background_color('#fff')
|
||||
.set_text_color(beestat.style.color.gray.base)
|
||||
.set_text_hover_color(beestat.style.color.red.base)
|
||||
.set_text('Cancel')
|
||||
.addEventListener('click', function() {
|
||||
self.dispose();
|
||||
});
|
||||
|
||||
var save = new beestat.component.button()
|
||||
.set_background_color(beestat.style.color.green.base)
|
||||
.set_background_hover_color(beestat.style.color.green.light)
|
||||
.set_text_color('#fff')
|
||||
.set_text('Download Data')
|
||||
.addEventListener('click', function() {
|
||||
var download_begin;
|
||||
var download_end = null;
|
||||
if (self.state_.download_data_time_period === 'all') {
|
||||
download_begin = null;
|
||||
} else {
|
||||
download_begin = moment().utc()
|
||||
.subtract(
|
||||
self.state_.download_data_time_count,
|
||||
self.state_.download_data_time_period
|
||||
)
|
||||
.format('YYYY-MM-DD HH:mm:ss');
|
||||
}
|
||||
|
||||
var download_arguments = {
|
||||
'thermostat_id': beestat.setting('thermostat_id'),
|
||||
'download_begin': download_begin,
|
||||
'download_end': download_end
|
||||
};
|
||||
|
||||
window.location.href = '/api/?resource=runtime_thermostat&method=download&arguments=' + JSON.stringify(download_arguments) + '&api_key=' + beestat.api.api_key;
|
||||
|
||||
self.dispose();
|
||||
});
|
||||
|
||||
return [
|
||||
cancel,
|
||||
save
|
||||
];
|
||||
};
|
@ -74,6 +74,7 @@ if($setting->get('environment') === 'dev' || $setting->get('environment') === 'd
|
||||
echo '<script src="/js/component/modal/announcements.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/modal/change_system_type.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/modal/change_thermostat.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/modal/download_data.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/modal/error.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/modal/filter_info.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/modal/help_runtime_thermostat_summary.js"></script>' . PHP_EOL;
|
||||
|
Loading…
x
Reference in New Issue
Block a user