mirror of
https://github.com/beestat/app.git
synced 2025-05-24 02:14:03 -04:00
Added settings page and all the things that go along with it.
This commit is contained in:
parent
65cfc07220
commit
06073bfc25
@ -57,6 +57,28 @@ class api_cache extends crud {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the cache for a specific API call.
|
||||
*
|
||||
* @param $api_call The API call to clear the cache for.
|
||||
*
|
||||
* @return mixed The updated cache row or null if it wasn't cached.
|
||||
*/
|
||||
public function clear_cache($api_call) {
|
||||
$key = $this->generate_key($api_call);
|
||||
$cache_hits = $this->read(['key' => $key]);
|
||||
|
||||
if(count($cache_hits) > 0) {
|
||||
$cache_hit = $cache_hits[0];
|
||||
$attributes = [];
|
||||
$attributes['expires_at'] = date('Y-m-d H:i:s', strtotime('1970-01-01 00:00:01'));
|
||||
$attributes['api_cache_id'] = $cache_hit['api_cache_id'];
|
||||
return $this->update($attributes);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a cache entry with a matching key that is not expired.
|
||||
*
|
||||
|
@ -37,6 +37,27 @@ final class api_call {
|
||||
*/
|
||||
private $alias;
|
||||
|
||||
/**
|
||||
* Bypass the cache read.
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
private $bypass_cache_read;
|
||||
|
||||
/**
|
||||
* Bypass the cache write.
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
private $bypass_cache_write;
|
||||
|
||||
/**
|
||||
* Clear the cache for this API call, don't actually run the call.
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
private $clear_cache;
|
||||
|
||||
/**
|
||||
* The current auto-alias. If an alias is not provided, an auto-alias is
|
||||
* assigned.
|
||||
@ -87,6 +108,29 @@ final class api_call {
|
||||
} else {
|
||||
$this->alias = $this->get_auto_alias();
|
||||
}
|
||||
|
||||
/**
|
||||
* Note the following three parameters will come in as strings when not in
|
||||
* a batch and boolean values when in a batch because of the JSON. Cast to
|
||||
* boolean to support various representations.
|
||||
*/
|
||||
if(isset($api_call['bypass_cache_read']) === true) {
|
||||
$this->bypass_cache_read = ((bool) $api_call['bypass_cache_read'] === true);
|
||||
} else {
|
||||
$this->bypass_cache_read = false;
|
||||
}
|
||||
|
||||
if(isset($api_call['bypass_cache_write']) === true) {
|
||||
$this->bypass_cache_write = ((bool) $api_call['bypass_cache_write'] === true);
|
||||
} else {
|
||||
$this->bypass_cache_write = false;
|
||||
}
|
||||
|
||||
if(isset($api_call['clear_cache']) === true) {
|
||||
$this->clear_cache = ((bool) $api_call['clear_cache'] === true);
|
||||
} else {
|
||||
$this->clear_cache = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -103,31 +147,37 @@ final class api_call {
|
||||
}
|
||||
|
||||
// Caching! If this API call is configured for caching,
|
||||
// $cache_config = $this->setting->get('cache');
|
||||
if( // Is cacheable
|
||||
isset($this->resource::$cache) === true &&
|
||||
isset($this->resource::$cache[$this->method]) === true
|
||||
) {
|
||||
$api_cache_instance = new api_cache();
|
||||
$api_cache = $api_cache_instance->retrieve($this);
|
||||
|
||||
if($api_cache !== null) {
|
||||
// If there was a cache entry available, use that.
|
||||
$this->response = $api_cache['response_data'];
|
||||
$this->cached_until = date('Y-m-d H:i:s', strtotime($api_cache['expires_at']));
|
||||
if($this->clear_cache === true) {
|
||||
$this->response = $api_cache_instance->clear_cache($this);
|
||||
$this->cached_until = date('Y-m-d H:i:s', strtotime('1970-01-01 00:00:01'));
|
||||
} else {
|
||||
// Else just run the API call, then cache it.
|
||||
$this->response = call_user_func_array(
|
||||
[$resource_instance, $this->method],
|
||||
$this->arguments
|
||||
);
|
||||
$api_cache = $api_cache_instance->retrieve($this);
|
||||
|
||||
$api_cache = $api_cache_instance->cache(
|
||||
$this,
|
||||
$this->response,
|
||||
$this->resource::$cache[$this->method]
|
||||
);
|
||||
$this->cached_until = date('Y-m-d H:i:s', strtotime($api_cache['expires_at']));
|
||||
if($api_cache !== null && $this->bypass_cache_read === false) {
|
||||
// If there was a cache entry available, use that.
|
||||
$this->response = $api_cache['response_data'];
|
||||
$this->cached_until = date('Y-m-d H:i:s', strtotime($api_cache['expires_at']));
|
||||
} else {
|
||||
// Else just run the API call, then cache it.
|
||||
$this->response = call_user_func_array(
|
||||
[$resource_instance, $this->method],
|
||||
$this->arguments
|
||||
);
|
||||
|
||||
if($this->bypass_cache_write === false) {
|
||||
$api_cache = $api_cache_instance->cache(
|
||||
$this,
|
||||
$this->response,
|
||||
$this->resource::$cache[$this->method]
|
||||
);
|
||||
$this->cached_until = date('Y-m-d H:i:s', strtotime($api_cache['expires_at']));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else { // Not cacheable
|
||||
|
@ -114,14 +114,18 @@ class profile extends cora\api {
|
||||
*/
|
||||
$max_lookahead = 1800; // 30 min
|
||||
|
||||
// Get some stuff
|
||||
$thermostat = $this->api('thermostat', 'get', $thermostat_id);
|
||||
|
||||
/**
|
||||
* Attempt to ignore the effects of solar heating by only looking at
|
||||
* samples when the sun is down.
|
||||
*/
|
||||
$ignore_solar_heating = false;
|
||||
|
||||
// Get some stuff
|
||||
$thermostat = $this->api('thermostat', 'get', $thermostat_id);
|
||||
$ignore_solar_gain = $this->api(
|
||||
'user',
|
||||
'get_setting',
|
||||
'thermostat.' . $thermostat_id . '.profile.ignore_solar_gain'
|
||||
);
|
||||
|
||||
if($thermostat['system_type']['reported']['heat']['equipment'] !== null) {
|
||||
$system_type_heat = $thermostat['system_type']['reported']['heat']['equipment'];
|
||||
@ -165,6 +169,26 @@ class profile extends cora\api {
|
||||
]
|
||||
);
|
||||
|
||||
// Get latitude/longitude. If that's not possible, disable solar gain
|
||||
// check.
|
||||
if($ignore_solar_gain === true) {
|
||||
if($thermostat['address_id'] === null) {
|
||||
$ignore_solar_gain = false;
|
||||
} else {
|
||||
$address = $this->api('address', 'get', $thermostat['address_id']);
|
||||
if(
|
||||
isset($address['normalized']['metadata']) === false ||
|
||||
isset($address['normalized']['metadata']['latitude']) === false ||
|
||||
isset($address['normalized']['metadata']['longitude']) === false
|
||||
) {
|
||||
$ignore_solar_gain = false;
|
||||
} else {
|
||||
$latitude = $address['normalized']['metadata']['latitude'];
|
||||
$longitude = $address['normalized']['metadata']['longitude'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get all of the relevant data
|
||||
$thermostat_ids = [];
|
||||
foreach($group_thermostats as $group_thermostat) {
|
||||
@ -254,7 +278,6 @@ class profile extends cora\api {
|
||||
$degree_days_temperatures = [];
|
||||
while($row = $result->fetch_assoc()) {
|
||||
$timestamp = strtotime($row['timestamp']);
|
||||
$hour = date('G', $timestamp);
|
||||
$date = date('Y-m-d', $timestamp);
|
||||
|
||||
// Degree days
|
||||
@ -308,12 +331,15 @@ class profile extends cora\api {
|
||||
$runtime_seconds['cool_1'] += $row['cool_1'];
|
||||
$runtime_seconds['cool_2'] += $row['cool_2'];
|
||||
|
||||
if (
|
||||
$ignore_solar_heating === true &&
|
||||
$hour > 6 &&
|
||||
$hour < 22
|
||||
) {
|
||||
continue;
|
||||
// Ignore data between sunrise and sunset.
|
||||
if($ignore_solar_gain === true) {
|
||||
$sun_info = date_sun_info($timestamp, $latitude, $longitude);
|
||||
if(
|
||||
$timestamp > $sun_info['sunrise'] &&
|
||||
$timestamp < $sun_info['sunset']
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($runtime[$timestamp]) === false) {
|
||||
|
@ -16,7 +16,8 @@ class thermostat extends cora\crud {
|
||||
'set_reported_system_types',
|
||||
'generate_profile',
|
||||
'generate_profiles',
|
||||
'get_metrics'
|
||||
'get_metrics',
|
||||
'update'
|
||||
],
|
||||
'public' => []
|
||||
];
|
||||
@ -240,7 +241,8 @@ class thermostat extends cora\crud {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new profile for this thermostat.
|
||||
* Generate a new profile for this thermostat. This is called from the GUI
|
||||
* often but is cached.
|
||||
*
|
||||
* @param int $thermostat_id
|
||||
*/
|
||||
|
69
api/user.php
69
api/user.php
@ -166,8 +166,9 @@ class user extends cora\crud {
|
||||
$settings = $user['settings'];
|
||||
}
|
||||
|
||||
$settings[$key] = $value;
|
||||
$settings = $this->update_setting_($settings, $key, $value);
|
||||
|
||||
// Disallow setting changes in the demo.
|
||||
if($this->setting->is_demo() === false) {
|
||||
$this->update(
|
||||
[
|
||||
@ -180,6 +181,72 @@ class user extends cora\crud {
|
||||
return $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively update the setting array.
|
||||
*
|
||||
* @param array $settings Settings array
|
||||
* @param string $key Key to update. Dots indicate a path.
|
||||
* @param mixed $value Value to set.
|
||||
*
|
||||
* @return array Updated settings array.
|
||||
*/
|
||||
private function update_setting_($settings, $key, $value) {
|
||||
$path = explode('.', $key);
|
||||
if(count($path) > 1) {
|
||||
$this_key = array_shift($path);
|
||||
if(isset($settings[$this_key]) === false) {
|
||||
$settings[$this_key] = [];
|
||||
}
|
||||
$settings[$this_key] = $this->update_setting_(
|
||||
$settings[$this_key],
|
||||
implode('.', $path),
|
||||
$value
|
||||
);
|
||||
} else {
|
||||
$settings[$key] = $value;
|
||||
}
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific setting.
|
||||
*
|
||||
* @param string $key The setting to get. Supports dotted paths.
|
||||
*
|
||||
* @return mixed The setting. Null if not set.
|
||||
*/
|
||||
public function get_setting($key) {
|
||||
$user = $this->get($this->session->get_user_id());
|
||||
return $this->get_setting_($user['settings'], $key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursive helper function for getting a setting.
|
||||
*
|
||||
* @param array $settings Settings array
|
||||
* @param string $key The key of the setting to get.
|
||||
*
|
||||
* @return mixed The setting. Null if not set.
|
||||
*/
|
||||
private function get_setting_($settings, $key) {
|
||||
$path = explode('.', $key);
|
||||
if(count($path) > 1) {
|
||||
$this_key = array_shift($path);
|
||||
if(isset($settings[$this_key]) === true) {
|
||||
return $this->get_setting_($settings[$this_key], implode('.', $path));
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
if(isset($settings[$key]) === true) {
|
||||
return $settings[$key];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a sync_status on a user to the current datetime.
|
||||
*
|
||||
|
@ -218,6 +218,56 @@ a.inverted:active {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Inputs
|
||||
*/
|
||||
input[type=checkbox] {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
position: relative;
|
||||
user-select: none;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.checkbox label {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border-radius: 4px;
|
||||
box-shadow: inset 0px 1px 1px rgba(0,0,0,0.5), 0px 1px 0px rgba(255, 255, 255, 0.4);
|
||||
background: #37474f; /* bluegray light */
|
||||
}
|
||||
|
||||
.checkbox label::after {
|
||||
opacity: 0;
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 12px;
|
||||
height: 8px;
|
||||
background: transparent;
|
||||
top: 5px;
|
||||
left: 4px;
|
||||
border: 3px solid #20bf6b;
|
||||
border-top: none;
|
||||
border-right: none;
|
||||
transform: rotate(-45deg);
|
||||
transition: opacity 200ms ease;
|
||||
|
||||
}
|
||||
|
||||
.checkbox input[type=checkbox]:checked + label:after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.checkbox input[type=checkbox]:disabled + label:after {
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This is a stripped down version of https://flexgridlite.elliotdahl.com/
|
||||
@ -344,6 +394,7 @@ a.inverted:active {
|
||||
.icon.close_network:before { content: "\F015B"; }
|
||||
.icon.cloud_question:before { content: "\F0A39"; }
|
||||
.icon.code_tags:before { content: "\F0174"; }
|
||||
.icon.cog:before { content: "\F0493"; }
|
||||
.icon.dots_vertical:before { content: "\F01D9"; }
|
||||
.icon.download:before { content: "\F01DA"; }
|
||||
.icon.earth:before { content: "\F01E7"; }
|
||||
|
@ -114,18 +114,30 @@ beestat.api.prototype.send = function(opt_api_call) {
|
||||
* @param {string} method The method.
|
||||
* @param {Object=} opt_args Optional arguments.
|
||||
* @param {string=} opt_alias Optional alias (required for batch API calls).
|
||||
* @param {boolean=} opt_bypass_cache_read Optional bypass of cache read.
|
||||
* @param {boolean=} opt_bypass_cache_write Optional bypass of cache write.
|
||||
*
|
||||
* @return {beestat.api} This.
|
||||
*/
|
||||
beestat.api.prototype.add_call = function(resource, method, opt_args, opt_alias) {
|
||||
beestat.api.prototype.add_call = function(resource, method, opt_args, opt_alias, opt_bypass_cache_read, opt_bypass_cache_write, opt_clear_cache) {
|
||||
var api_call = {
|
||||
'resource': resource,
|
||||
'method': method,
|
||||
'arguments': JSON.stringify(beestat.default_value(opt_args, {}))
|
||||
};
|
||||
|
||||
if (opt_alias !== undefined) {
|
||||
api_call.alias = opt_alias;
|
||||
}
|
||||
if (opt_bypass_cache_read !== undefined) {
|
||||
api_call.bypass_cache_read = opt_bypass_cache_read;
|
||||
}
|
||||
if (opt_bypass_cache_write !== undefined) {
|
||||
api_call.bypass_cache_write = opt_bypass_cache_write;
|
||||
}
|
||||
if (opt_clear_cache !== undefined) {
|
||||
api_call.clear_cache = opt_clear_cache;
|
||||
}
|
||||
|
||||
this.api_calls_.push(api_call);
|
||||
|
||||
@ -296,7 +308,9 @@ beestat.api.prototype.get_cached_ = function(api_call) {
|
||||
var cached = beestat.api.cache[this.get_key_(api_call)];
|
||||
if (
|
||||
cached !== undefined &&
|
||||
moment().isAfter(cached.until) === false
|
||||
moment().isAfter(cached.until) === false &&
|
||||
api_call.bypass_cache_read !== true &&
|
||||
api_call.clear_cache !== true
|
||||
) {
|
||||
return cached;
|
||||
}
|
||||
|
@ -1,17 +1,30 @@
|
||||
/**
|
||||
* Get or set a setting.
|
||||
* Get or set a setting. ESLint Forgive my variable naming sins for the sake
|
||||
* of no-shadow.
|
||||
*
|
||||
* @param {mixed} key If a string, get/set that specific key. If an object, set all the specified keys in the object.
|
||||
* @param {mixed} argument_1 If a string, get/set that specific key. If an
|
||||
* object, set all the specified keys in the object.
|
||||
* @param {mixed} opt_value If a string, set the specified key to this value.
|
||||
* @param {mixed} opt_callback Optional callback.
|
||||
*
|
||||
* @return {mixed} The setting if requesting (undefined if not set), undefined
|
||||
* otherwise.
|
||||
*/
|
||||
beestat.setting = function(key, opt_value, opt_callback) {
|
||||
var user = beestat.user.get();
|
||||
beestat.setting = function(argument_1, opt_value, opt_callback) {
|
||||
const user = beestat.user.get();
|
||||
if (user.settings === null) {
|
||||
user.settings = {};
|
||||
}
|
||||
|
||||
var defaults = {
|
||||
// TODO Some of these are still strings instead of ints in the database.
|
||||
if (user.settings.thermostat_id !== undefined) {
|
||||
user.settings.thermostat_id = parseInt(
|
||||
user.settings.thermostat_id,
|
||||
10
|
||||
);
|
||||
}
|
||||
|
||||
const defaults = {
|
||||
'runtime_thermostat_detail_range_type': 'dynamic',
|
||||
'runtime_thermostat_detail_range_static_begin': moment()
|
||||
.subtract(3, 'day')
|
||||
@ -37,49 +50,113 @@ beestat.setting = function(key, opt_value, opt_callback) {
|
||||
|
||||
'temperature_unit': '°F',
|
||||
|
||||
'first_run': true
|
||||
'first_run': true,
|
||||
|
||||
'thermostat.#.profile.ignore_solar_gain': false
|
||||
};
|
||||
|
||||
if (user.settings === null) {
|
||||
user.settings = {};
|
||||
}
|
||||
|
||||
/*
|
||||
* TODO This is temporary until I get all the setting data types under
|
||||
* control. Just doing this so other parts of the application can be built out
|
||||
* properly.
|
||||
*/
|
||||
if (user.settings.thermostat_id !== undefined) {
|
||||
user.settings.thermostat_id = parseInt(
|
||||
user.settings.thermostat_id,
|
||||
10
|
||||
);
|
||||
}
|
||||
|
||||
if (opt_value === undefined && typeof key !== 'object') {
|
||||
if (user.settings[key] !== undefined) {
|
||||
return user.settings[key];
|
||||
} else if (defaults[key] !== undefined) {
|
||||
return defaults[key];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
var settings;
|
||||
if (typeof key === 'object') {
|
||||
settings = key;
|
||||
// Figure out what we're trying to do.
|
||||
let settings;
|
||||
let key;
|
||||
let mode;
|
||||
if (typeof argument_1 === 'object') {
|
||||
settings = argument_1;
|
||||
} else {
|
||||
settings = {};
|
||||
settings[key] = opt_value;
|
||||
key = argument_1;
|
||||
|
||||
if (opt_value !== undefined) {
|
||||
settings = {};
|
||||
settings[key] = opt_value;
|
||||
}
|
||||
}
|
||||
mode = (settings !== undefined || opt_value !== undefined) ? 'set' : 'get';
|
||||
|
||||
// Get the requested value.
|
||||
if (mode === 'get') {
|
||||
/**
|
||||
* Get a value nested in an object from a string path.
|
||||
*
|
||||
* @param {object} o The object to search in.
|
||||
* @param {string} p) The path (ex: thermostat.1.profile.ignore_solar_gain)
|
||||
*
|
||||
* @throws {exception} If the path is invalid.
|
||||
* @return {mixed} The value, or undefined if it doesn't exist.
|
||||
*/
|
||||
const get_value_from_path = (o, p) => p.split('.').reduce((a, v) => a[v], o);
|
||||
|
||||
/**
|
||||
* Get the default value of a setting.
|
||||
*
|
||||
* @param {string} k The setting to get.
|
||||
*
|
||||
* @return {mixed} The default value, or undefined if there is none.
|
||||
*/
|
||||
const get_default_value = function(k) {
|
||||
// Replace any numeric key parts with a # as a placeholder.
|
||||
let old_parts = k.split('.');
|
||||
let new_parts = [];
|
||||
old_parts.forEach(function(part) {
|
||||
if (isNaN(part) === false) {
|
||||
new_parts.push('#');
|
||||
} else {
|
||||
new_parts.push(part);
|
||||
}
|
||||
});
|
||||
|
||||
return defaults[new_parts.join('.')];
|
||||
};
|
||||
|
||||
let value;
|
||||
try {
|
||||
value = get_value_from_path(user.settings, key);
|
||||
} catch (error) {
|
||||
value = undefined;
|
||||
}
|
||||
|
||||
return (value === undefined ? get_default_value(key) : value);
|
||||
}
|
||||
|
||||
var api = new beestat.api();
|
||||
// Set the requested value.
|
||||
|
||||
/**
|
||||
* Recursively update the setting object.
|
||||
*
|
||||
* @param {object} user_settings Settings object
|
||||
* @param {string} k Key to update. Dots indicate a path.
|
||||
* @param {mixed} v Value to set
|
||||
*
|
||||
* @return {object} Updated settings object.
|
||||
*/
|
||||
const update_setting = function(user_settings, k, v) {
|
||||
let path = k.split('.');
|
||||
if (path.length > 1) {
|
||||
const this_key = path.shift();
|
||||
if (user_settings[this_key] === undefined) {
|
||||
user_settings[this_key] = {};
|
||||
}
|
||||
if (typeof user_settings[this_key] !== 'object') {
|
||||
throw new Error('Tried to set sub-key of non-object setting.');
|
||||
}
|
||||
user_settings[this_key] = update_setting(
|
||||
user_settings[this_key],
|
||||
path.join('.'),
|
||||
v
|
||||
);
|
||||
} else {
|
||||
user_settings[k] = v;
|
||||
}
|
||||
|
||||
return user_settings;
|
||||
};
|
||||
|
||||
const api = new beestat.api();
|
||||
api.set_callback(opt_callback);
|
||||
|
||||
var has_calls = false;
|
||||
let has_calls = false;
|
||||
|
||||
for (var k in settings) {
|
||||
if (user.settings[k] !== settings[k]) {
|
||||
user.settings[k] = settings[k];
|
||||
for (let k in settings) {
|
||||
if (beestat.setting(k) !== settings[k]) {
|
||||
user.settings = update_setting(user.settings, k, settings[k]);
|
||||
|
||||
beestat.dispatcher.dispatchEvent('setting.' + k);
|
||||
|
||||
@ -107,4 +184,6 @@ beestat.setting = function(key, opt_value, opt_callback) {
|
||||
opt_callback();
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
@ -580,38 +580,6 @@ beestat.component.card.runtime_thermostat_summary.prototype.decorate_top_right_
|
||||
.set_callback(function() {
|
||||
self.chart_.reset_zoom();
|
||||
}));
|
||||
|
||||
if (beestat.setting('runtime_thermostat_summary_gap_fill') === true) {
|
||||
menu.add_menu_item(new beestat.component.menu_item()
|
||||
.set_text('Disable Gap Fill')
|
||||
.set_icon('basket_unfill')
|
||||
.set_callback(function() {
|
||||
beestat.setting('runtime_thermostat_summary_gap_fill', false);
|
||||
}));
|
||||
} else {
|
||||
menu.add_menu_item(new beestat.component.menu_item()
|
||||
.set_text('Enable Gap Fill')
|
||||
.set_icon('basket_fill')
|
||||
.set_callback(function() {
|
||||
beestat.setting('runtime_thermostat_summary_gap_fill', true);
|
||||
}));
|
||||
}
|
||||
|
||||
if (beestat.setting('runtime_thermostat_summary_smart_scale') === true) {
|
||||
menu.add_menu_item(new beestat.component.menu_item()
|
||||
.set_text('Disable Smart Scale')
|
||||
.set_icon('network_strength_off')
|
||||
.set_callback(function() {
|
||||
beestat.setting('runtime_thermostat_summary_smart_scale', false);
|
||||
}));
|
||||
} else {
|
||||
menu.add_menu_item(new beestat.component.menu_item()
|
||||
.set_text('Enable Smart Scale')
|
||||
.set_icon('network_strength_4')
|
||||
.set_callback(function() {
|
||||
beestat.setting('runtime_thermostat_summary_smart_scale', true);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
menu.add_menu_item(new beestat.component.menu_item()
|
||||
|
145
js/component/card/settings.js
Normal file
145
js/component/card/settings.js
Normal file
@ -0,0 +1,145 @@
|
||||
/**
|
||||
* Setting
|
||||
*/
|
||||
beestat.component.card.settings = function() {
|
||||
beestat.component.card.apply(this, arguments);
|
||||
};
|
||||
beestat.extend(beestat.component.card.settings, beestat.component.card);
|
||||
|
||||
/**
|
||||
* Decorate contents.
|
||||
*
|
||||
* @param {rocket.Elements} parent Parent
|
||||
*/
|
||||
beestat.component.card.settings.prototype.decorate_contents_ = function(parent) {
|
||||
const thermostat = beestat.cache.thermostat[
|
||||
beestat.setting('thermostat_id')
|
||||
];
|
||||
|
||||
parent.appendChild(
|
||||
$.createElement('p')
|
||||
.style('font-weight', '400')
|
||||
.innerText('Thermostat Summary')
|
||||
);
|
||||
|
||||
// Gap Fill
|
||||
const enable_gap_fill = new beestat.component.input.checkbox();
|
||||
enable_gap_fill
|
||||
.set_label('Enable Gap Fill')
|
||||
.set_value(beestat.setting('runtime_thermostat_summary_gap_fill'))
|
||||
.render(parent);
|
||||
|
||||
enable_gap_fill.addEventListener('change', function() {
|
||||
enable_gap_fill.disable();
|
||||
beestat.setting(
|
||||
'runtime_thermostat_summary_gap_fill',
|
||||
enable_gap_fill.get_value(),
|
||||
function() {
|
||||
enable_gap_fill.enable();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// Smart Scale
|
||||
const enable_smart_scale = new beestat.component.input.checkbox();
|
||||
enable_smart_scale
|
||||
.set_label('Enable Smart Scale')
|
||||
.set_value(beestat.setting('runtime_thermostat_summary_smart_scale'))
|
||||
.render(parent);
|
||||
|
||||
enable_smart_scale.addEventListener('change', function() {
|
||||
enable_smart_scale.disable();
|
||||
beestat.setting(
|
||||
'runtime_thermostat_summary_smart_scale',
|
||||
enable_smart_scale.get_value(),
|
||||
function() {
|
||||
enable_smart_scale.enable();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
parent.appendChild(
|
||||
$.createElement('p')
|
||||
.style('font-weight', '400')
|
||||
.innerText('Temperature Profiles')
|
||||
);
|
||||
const ignore_solar_gain = new beestat.component.input.checkbox();
|
||||
const ignore_solar_gain_key = 'thermostat.' + thermostat.thermostat_id + '.profile.ignore_solar_gain';
|
||||
ignore_solar_gain
|
||||
.set_label('Ignore Solar Gain')
|
||||
.set_value(beestat.setting(ignore_solar_gain_key))
|
||||
.render(parent);
|
||||
|
||||
ignore_solar_gain.addEventListener('change', function() {
|
||||
ignore_solar_gain.disable();
|
||||
beestat.setting(
|
||||
ignore_solar_gain_key,
|
||||
ignore_solar_gain.get_value(),
|
||||
function() {
|
||||
new beestat.api()
|
||||
.add_call(
|
||||
'thermostat',
|
||||
'generate_profile',
|
||||
{
|
||||
'thermostat_id': thermostat.thermostat_id
|
||||
},
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
// Clear cache
|
||||
true
|
||||
)
|
||||
.add_call(
|
||||
'thermostat',
|
||||
'update',
|
||||
{
|
||||
'attributes': {
|
||||
'thermostat_id': thermostat.thermostat_id,
|
||||
'profile': null
|
||||
}
|
||||
}
|
||||
)
|
||||
.add_call(
|
||||
'thermostat',
|
||||
'read_id',
|
||||
{
|
||||
'attributes': {
|
||||
'inactive': 0
|
||||
}
|
||||
},
|
||||
'thermostat'
|
||||
)
|
||||
.set_callback(function(response) {
|
||||
ignore_solar_gain.enable();
|
||||
beestat.cache.set('thermostat', response.thermostat);
|
||||
})
|
||||
.send();
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the title of the card.
|
||||
*
|
||||
* @return {string} The title.
|
||||
*/
|
||||
beestat.component.card.settings.prototype.get_title_ = function() {
|
||||
return 'Settings';
|
||||
};
|
||||
|
||||
/**
|
||||
* Decorate the menu
|
||||
*
|
||||
* @param {rocket.Elements} parent
|
||||
*/
|
||||
beestat.component.card.settings.prototype.decorate_top_right_ = function(parent) {
|
||||
var menu = (new beestat.component.menu()).render(parent);
|
||||
|
||||
menu.add_menu_item(new beestat.component.menu_item()
|
||||
.set_text('Help')
|
||||
.set_icon('help_circle')
|
||||
.set_callback(function() {
|
||||
window.open('https://doc.beestat.io/9d01e7256390473ca8121d4098d91c9d');
|
||||
}));
|
||||
};
|
@ -165,6 +165,7 @@ beestat.component.card.temperature_profiles.prototype.get_data_ = function() {
|
||||
parseFloat(x_fixed),
|
||||
profile.deltas[x_fixed]
|
||||
]);
|
||||
|
||||
y_min = Math.min(y_min, profile.deltas[x_fixed]);
|
||||
y_max = Math.max(y_max, profile.deltas[x_fixed]);
|
||||
}
|
||||
|
@ -184,6 +184,13 @@ beestat.component.header.prototype.decorate_ = function(parent) {
|
||||
}
|
||||
}
|
||||
|
||||
menu.add_menu_item(new beestat.component.menu_item()
|
||||
.set_text('Settings')
|
||||
.set_icon('cog')
|
||||
.set_callback(function() {
|
||||
(new beestat.layer.settings()).render();
|
||||
}));
|
||||
|
||||
menu.add_menu_item(new beestat.component.menu_item()
|
||||
.set_text('Log Out')
|
||||
.set_icon('exit_to_app')
|
||||
|
@ -2,6 +2,7 @@
|
||||
* Input parent class.
|
||||
*/
|
||||
beestat.component.input = function() {
|
||||
this.uuid_ = this.generate_uuid_();
|
||||
beestat.component.apply(this, arguments);
|
||||
};
|
||||
beestat.extend(beestat.component.input, beestat.component);
|
||||
@ -48,3 +49,15 @@ beestat.component.input.prototype.set_ = function(key, value) {
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a UUID to uniquely identify an input.
|
||||
*
|
||||
* @link https://stackoverflow.com/a/2117523
|
||||
* @return {string} The UUID;
|
||||
*/
|
||||
beestat.component.input.prototype.generate_uuid_ = function() {
|
||||
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
|
||||
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
|
||||
);
|
||||
};
|
||||
|
85
js/component/input/checkbox.js
Normal file
85
js/component/input/checkbox.js
Normal file
@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Checkbox parent class.
|
||||
*/
|
||||
beestat.component.input.checkbox = function() {
|
||||
this.input_ = $.createElement('input');
|
||||
|
||||
beestat.component.input.apply(this, arguments);
|
||||
};
|
||||
beestat.extend(beestat.component.input.checkbox, beestat.component.input);
|
||||
|
||||
/**
|
||||
* Decorate
|
||||
*
|
||||
* @param {rocket.Elements} parent
|
||||
*/
|
||||
beestat.component.input.checkbox.prototype.decorate_ = function(parent) {
|
||||
var self = this;
|
||||
|
||||
const div = $.createElement('div').addClass('checkbox');
|
||||
this.input_
|
||||
.setAttribute('id', this.uuid_)
|
||||
.setAttribute('type', 'checkbox');
|
||||
div.appendChild(this.input_);
|
||||
|
||||
const label = $.createElement('label');
|
||||
label.setAttribute('for', this.uuid_);
|
||||
div.appendChild(label);
|
||||
|
||||
const text_label = $.createElement('span')
|
||||
.style({
|
||||
'cursor': 'pointer',
|
||||
'margin-left': (beestat.style.size.gutter / 2)
|
||||
})
|
||||
.innerText(this.label_)
|
||||
.addEventListener('click', function() {
|
||||
self.input_[0].click();
|
||||
// self.input_.checked(!self.input_.checked());
|
||||
});
|
||||
div.appendChild(text_label);
|
||||
|
||||
this.input_.addEventListener('change', function() {
|
||||
// console.log('input changed');
|
||||
self.dispatchEvent('change');
|
||||
});
|
||||
|
||||
parent.appendChild(div);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the value in the input field. This bypasses the set_ function to avoid
|
||||
* rerendering when the input value is set. It's unnecessary and can also
|
||||
* cause minor issues if you try to set the value, then do something else with
|
||||
* the input immediately after.
|
||||
*
|
||||
* This will not fire off the change event listener.
|
||||
*
|
||||
* @param {string} value
|
||||
*
|
||||
* @return {beestat.component.input.checkbox} This.
|
||||
*/
|
||||
beestat.component.input.checkbox.prototype.set_value = function(value) {
|
||||
this.input_.checked(value);
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the value in the input field.
|
||||
*
|
||||
* @return {string} The value in the input field.
|
||||
*/
|
||||
beestat.component.input.checkbox.prototype.get_value = function() {
|
||||
return this.input_.checked();
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the checkbox label.
|
||||
*
|
||||
* @param {string} label
|
||||
*
|
||||
* @return {beestat.component.input.checkbox} This.
|
||||
*/
|
||||
beestat.component.input.checkbox.prototype.set_label = function(label) {
|
||||
this.label_ = label;
|
||||
return this;
|
||||
};
|
@ -20,7 +20,7 @@ beestat.component.input.text = function() {
|
||||
});
|
||||
});
|
||||
|
||||
beestat.component.apply(this, arguments);
|
||||
beestat.component.input.apply(this, arguments);
|
||||
};
|
||||
beestat.extend(beestat.component.input.text, beestat.component.input);
|
||||
|
||||
|
@ -44,6 +44,7 @@ if($setting->get('environment') === 'dev' || $setting->get('environment') === 'd
|
||||
echo '<script src="/js/layer/detail.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/layer/compare.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/layer/analyze.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/layer/settings.js"></script>' . PHP_EOL;
|
||||
|
||||
// Component
|
||||
echo '<script src="/js/component.js"></script>' . PHP_EOL;
|
||||
@ -64,6 +65,7 @@ if($setting->get('environment') === 'dev' || $setting->get('environment') === 'd
|
||||
echo '<script src="/js/component/card/system.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/card/temperature_profiles.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/card/metrics.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/card/settings.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/chart.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/chart/runtime_thermostat_summary.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/chart/temperature_profiles.js"></script>' . PHP_EOL;
|
||||
@ -95,6 +97,7 @@ if($setting->get('environment') === 'dev' || $setting->get('environment') === 'd
|
||||
echo '<script src="/js/component/modal/newsletter.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/input.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/input/text.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/input/checkbox.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/button.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/button_group.js"></script>' . PHP_EOL;
|
||||
echo '<script src="/js/component/title.js"></script>' . PHP_EOL;
|
||||
|
51
js/layer/settings.js
Normal file
51
js/layer/settings.js
Normal file
@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Setting layer.
|
||||
*/
|
||||
beestat.layer.settings = function() {
|
||||
beestat.layer.apply(this, arguments);
|
||||
};
|
||||
beestat.extend(beestat.layer.settings, beestat.layer);
|
||||
|
||||
beestat.layer.settings.prototype.decorate_ = function(parent) {
|
||||
/*
|
||||
* Set the overflow on the body so the scrollbar is always present so
|
||||
* highcharts graphs render properly.
|
||||
*/
|
||||
$('body').style({
|
||||
'overflow-y': 'scroll',
|
||||
'background': beestat.style.color.bluegray.light,
|
||||
'padding': '0 ' + beestat.style.size.gutter + 'px'
|
||||
});
|
||||
|
||||
(new beestat.component.header('setting')).render(parent);
|
||||
|
||||
// All the cards
|
||||
const cards = [];
|
||||
|
||||
if (window.is_demo === true) {
|
||||
cards.push([
|
||||
{
|
||||
'card': new beestat.component.card.demo(),
|
||||
'size': 12
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
// Settings
|
||||
cards.push([
|
||||
{
|
||||
'card': new beestat.component.card.settings(),
|
||||
'size': 12
|
||||
}
|
||||
]);
|
||||
|
||||
// Footer
|
||||
cards.push([
|
||||
{
|
||||
'card': new beestat.component.card.footer(),
|
||||
'size': 12
|
||||
}
|
||||
]);
|
||||
|
||||
(new beestat.component.layout(cards)).render(parent);
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user