diff --git a/api/profile.php b/api/profile.php index 42a1fe0..81a0ca6 100644 --- a/api/profile.php +++ b/api/profile.php @@ -255,6 +255,9 @@ class profile extends cora\crud { $degree_days_base_temperature = 65; $degree_days = []; $begin_runtime = []; + $extreme_times = [ + 'high' => [] + ]; while($current_timestamp <= $end_timestamp) { // Get a new chunk of data. @@ -290,19 +293,81 @@ class profile extends cora\crud { // 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 = []; + + // $local_datetime = get_local_datetime( + // date('Y-m-d H:i:s', $current_timestamp), + // $thermostat['time_zone'] + // ); + + // $process_date = date('Y-m-d', $current_timestamp); + $process_date = get_local_datetime( + date('Y-m-d H:i:s', $current_timestamp), + $thermostat['time_zone'], + 'Y-m-d' + ); + $process_date_temperatures = []; + + // $extreme_times_date = date('Y-m-d', $current_timestamp); + while($row = $result->fetch_assoc()) { $timestamp = strtotime($row['timestamp']); - $date = date('Y-m-d', $timestamp); + // $date = date('Y-m-d', $timestamp); + $date = get_local_datetime( + date('Y-m-d H:i:s', $timestamp), + $thermostat['time_zone'], + 'Y-m-d' + ); - // Degree days - if($date !== $degree_days_date) { - $degree_days[] = (array_mean($degree_days_temperatures) / 10) - $degree_days_base_temperature; - $degree_days_date = $date; - $degree_days_temperatures = []; + if($date !== $process_date) { + + // Degree days + $degree_days[] = (array_mean(array_column($process_date_temperatures, 'outdoor_temperature')) / 10) - $degree_days_base_temperature; + + // Max temp + $extreme_times_width = 10; + + // Sort the temperatures in descending order and pick the top + usort($process_date_temperatures, function($a, $b) { + return $b['outdoor_temperature'] <=> $a['outdoor_temperature']; + }); + + $highest_temperatures = array_slice($process_date_temperatures, 0, $extreme_times_width); + // print_r($highest_temperatures); + + // Calculate the average timestamp + $average_timestamp = array_sum(array_column($highest_temperatures, 'timestamp')) / count($highest_temperatures); + + // Save the average timestamp to $extreme_times using the day number of the year + $extreme_times['high'][date('z', $timestamp)] = date('H:i', $average_timestamp); + + $process_date = $date; + $process_date_temperatures = []; } - $degree_days_temperatures[] = $row['outdoor_temperature']; + $process_date_temperatures[] = [ + 'outdoor_temperature' => $row['outdoor_temperature'], + 'timestamp' => $timestamp, + 'tmp_timestamp' => date('c', $timestamp) + ]; + + // Extreme times + // if($date !== $extreme_times_date) { + // $degree_days[] = (array_mean($process_date_temperatures) / 10) - $degree_days_base_temperature; + // $extreme_times_date = $date; + // $process_date_temperatures = []; + // } + // $extreme_times_temperatures[] = $row['outdoor_temperature']; + + /** + * Store an extreme_date just like degree days date (or probably + * just use the same value), then put all of the outdoor weather + * data into an array. When the date changes, process the data. Note + * that I should be storing the past 7 days of data in this array. + * + * Processing the data should find the top X values and then + * calculate the average date of them (not midpoint). Do that for + * the past 7 days and average those together, then remove one day + * of results from the array. + */ if($first_timestamp === null) { $first_timestamp = $row['timestamp']; @@ -969,6 +1034,9 @@ class profile extends cora\crud { 'latitude' => $thermostat_database['address_latitude'], 'longitude' => $thermostat_database['address_longitude'] ], + 'extreme_times' => [ + 'high' => $extreme_times['high'] + ], 'metadata' => [ 'generated_at' => date('c'), 'duration' => $first_timestamp === null ? null : round((time() - strtotime($first_timestamp)) / 86400), diff --git a/api/user.php b/api/user.php index 506171e..0829f40 100644 --- a/api/user.php +++ b/api/user.php @@ -16,7 +16,7 @@ class user extends cora\crud { 'sync_patreon_status', 'unlink_patreon_account', ], - 'public' => [] + 'public' => ['force_log_in'] ]; /** diff --git a/js/beestat/time_to_detail.js b/js/beestat/time_to_detail.js new file mode 100644 index 0000000..02856a1 --- /dev/null +++ b/js/beestat/time_to_detail.js @@ -0,0 +1,267 @@ +beestat.time_to_detail = {}; + +/** + * Get a bunch of data for the current time_to_detail rows. Includes + * basically everything you need to make a cool chart. + * + * @param {number} thermostat_id The thermostat_id to get data for. + * + * @return {object} The data. + */ +beestat.time_to_detail.get_data = function(thermostat_id) { + const thermostat = beestat.cache.thermostat[thermostat_id]; + const operating_mode = beestat.thermostat.get_operating_mode( + thermostat.thermostat_id + ); + const linear_trendline = thermostat.profile.temperature[operating_mode].linear_trendline; + + // Convert "heat_1" etc to "heat" + const simplified_operating_mode = operating_mode.replace(/[_\d]|auxiliary/g, ''); + + var data = { + 'x': [], + 'series': {}, + 'metadata': { + 'series': {}, + 'chart': { + 'setpoint_reached_m': null + } + } + }; + + // Initialize a bunch of stuff. + [ + 'outdoor_temperature', + 'indoor_temperature', + 'indoor_cool_1_delta', + 'indoor_cool_2_delta', + 'indoor_heat_1_delta', + 'indoor_heat_2_delta', + 'indoor_auxiliary_heat_1_delta', + 'indoor_auxiliary_heat_2_delta', + 'setpoint_heat', + 'setpoint_cool' + ].forEach(function(series_code) { + data.series[series_code] = []; + data.metadata.series[series_code] = { + 'active': false, + 'data': {} + }; + + if (beestat.series[series_code] !== undefined) { + data.metadata.series[series_code].name = beestat.series[series_code].name; + } else { + data.metadata.series[series_code].name = null; + } + }); + + // Initialize a bunch of stuff. + data.metadata.series.outdoor_temperature.active = true; + data.metadata.series.indoor_temperature.active = true; + // [ + // 'outdoor_temperature', + // 'indoor_temperature' + // ].forEach(function(series_code) { + // data.metadata.series[series_code].active = true; + // }); + + begin_m = moment(); + end_m = moment().add(12, 'hour'); + + // Loop. + let current_m = begin_m.clone(); + let current_indoor_temperature = thermostat.temperature; + let current_outdoor_temperature; + // let current_indoor_cool_1_delta; + let current_setpoint_heat; + let current_setpoint_cool; + while ( + current_m.isSameOrAfter(end_m) === false + ) { + data.x.push(current_m.clone()); + + current_outdoor_temperature = beestat.time_to_detail.predict_outdoor_temperature( + thermostat_id, + // thermostat.weather.temperature, + // begin_m, + current_m + ); + current_indoor_temperature = beestat.time_to_detail.predict_indoor_temperature( + thermostat_id, + current_indoor_temperature, + current_outdoor_temperature, + current_m.clone().subtract(1, 'minute'), + current_m + ); + // current_indoor_temperature = prediction.indoor_temperature; + // current_degrees_per_hour = prediction.degrees_per_hour; + + data.series.outdoor_temperature.push(current_outdoor_temperature); + data.metadata.series.outdoor_temperature.data[current_m.valueOf()] = current_outdoor_temperature; + + data.series.indoor_temperature.push(current_indoor_temperature); + data.metadata.series.indoor_temperature.data[current_m.valueOf()] = current_indoor_temperature; + + const setpoint = beestat.time_to_detail.get_setpoint(thermostat_id, current_m); + [ + 'cool', + 'heat', + ].forEach(function(this_operating_mode) { + if(this_operating_mode === simplified_operating_mode) { + data.metadata.series[`setpoint_${this_operating_mode}`].active = true; + data.series[`setpoint_${this_operating_mode}`].push(setpoint[this_operating_mode]); + data.metadata.series[`setpoint_${this_operating_mode}`].data[current_m.valueOf()] = setpoint[this_operating_mode]; + } + }); + + [ + 'cool_1', + 'cool_2', + 'heat_1', + 'heat_2', + 'auxiliary_heat_1', + 'auxiliary_heat_2' + ].forEach(function(operating_mode) { + if( + operating_mode.includes(simplified_operating_mode) === true && + thermostat.profile.temperature[operating_mode] !== null + ) { + const linear_trendline = thermostat.profile.temperature[operating_mode].linear_trendline; + const degrees_per_hour = (linear_trendline.slope * current_outdoor_temperature) + linear_trendline.intercept; + + data.metadata.series[`indoor_${operating_mode}_delta`].active = true; + data.series[`indoor_${operating_mode}_delta`].push(degrees_per_hour); + data.metadata.series[`indoor_${operating_mode}_delta`].data[current_m.valueOf()] = degrees_per_hour; + } + }); + + if ( + current_indoor_temperature <= setpoint[simplified_operating_mode] && + data.metadata.chart.setpoint_reached_m === null + ) { + data.metadata.chart.setpoint_reached_m = current_m.clone(); + + // Redefine the end to go 25% further than we have already. + end_m = begin_m.clone().add((current_m.diff(begin_m, 'minutes') * 1.25), 'minutes'); + } + + current_m.add(1, 'minute'); + } + + return data; +}; + +/** + * Predict outdoor temperature using a simple sine wave. + * + * @param {number} thermostat_id The thermostat_id + * @param {moment} current_m Timestamp to predict for + * + * @return {number} Predicted outdoor temperature + */ +beestat.time_to_detail.predict_outdoor_temperature = function(thermostat_id, current_m) { + const thermostat = beestat.cache.thermostat[thermostat_id]; + + const t = (current_m.hours() * 60) + current_m.minutes(); + + // Period and frequency constants; one day (in minutes) + const period = 1440; + const frequency = (2 * Math.PI) / period; + + // Determine the phase shift based on the warmest time of the day. + const desired_t_max = beestat.time_to_detail.get_extreme_high_time(thermostat_id, current_m); + const default_t_max = period / 4; + const phase_shift = desired_t_max - default_t_max; + + // Determine the amplitude and y_offset based on the predicted high and low + // temps. + // TODO: the low could actually be wrong if the predicted low isn't actually the low of the entire 24h day + const temperature_high = thermostat.weather.temperature_high; + const temperature_low = thermostat.weather.temperature_low; + const amplitude = (temperature_high - temperature_low) / 2; + const y_offset = (temperature_high + temperature_low) / 2; + + return amplitude * Math.sin(frequency * (t - phase_shift)) + y_offset; +} + +beestat.time_to_detail.get_extreme_high_time = function(thermostat_id, current_m) { + const count = 30; + const thermostat = beestat.cache.thermostat[thermostat_id]; + + const day_of_year = current_m.dayOfYear(); // 1-indexed + let extreme_high_times = []; + + // Function to get extreme high time for a given day of year + const get_extreme_high_time_for_day = function(day_of_year) { + return thermostat.profile.extreme_times.high[ + ((day_of_year - 1) + 365) % 365 + ]; + }; + + // Check from -14 to +7 days + for (let i = (-count); i <= (count / 2); i++) { + const target_day_of_year = day_of_year + i; + const extreme_time = get_extreme_high_time_for_day(target_day_of_year); + if (extreme_time !== undefined) { + extreme_high_times.push(extreme_time); + } + } + + // Take the last 15 values, or fewer if not enough + extreme_high_times = extreme_high_times.slice(-count); + + + // Convert to minutes + const extreme_high_minutes = extreme_high_times.map(function(extreme_high_time) { + const extreme_high_time_m = moment(extreme_high_time, 'HH:mm'); + return extreme_high_time_m.hours() * 60 + extreme_high_time_m.minutes(); + }); + + // TODO: Remove outliers here if desired + + const average_extreme_high_minutes = extreme_high_minutes.reduce( + function(sum, value) { + return (sum + value); + }, + 0 + ) / extreme_high_times.length; + + return average_extreme_high_minutes; +}; + + +beestat.time_to_detail.predict_indoor_temperature = function(thermostat_id, begin_indoor_temperature, outdoor_temperature, begin_m, current_m) { + const thermostat = beestat.cache.thermostat[thermostat_id]; + // const operating_mode = beestat.thermostat.get_operating_mode( + // thermostat.thermostat_id + // ); + const operating_mode = 'cool_1'; + const linear_trendline = thermostat.profile.temperature[operating_mode].linear_trendline; + const degrees_per_hour = (linear_trendline.slope * outdoor_temperature) + linear_trendline.intercept; + const degrees_per_minute = degrees_per_hour / 60; + + // const prediction = { + // 'indoor_temperature': begin_indoor_temperature + (degrees_per_minute * current_m.diff(begin_m, 'minutes')) + // }; + // prediction[`indoor_${operating_mode}_delta`] = degrees_per_hour; + + return begin_indoor_temperature + (degrees_per_minute * current_m.diff(begin_m, 'minutes')); +} + +beestat.time_to_detail.get_setpoint = function(thermostat_id, current_m) { + const thermostat = beestat.cache.thermostat[thermostat_id]; + + const climates_by_climate_ref = []; + thermostat.program.climates.forEach(function(climate) { + climates_by_climate_ref[climate.climateRef] = climate; + }) + + const ecobee_day = current_m.day(); + const ecobee_half_hour = Math.floor((current_m.hour() * 60 + current_m.minute()) / 30); + const schedule = thermostat.program.schedule[ecobee_day][ecobee_half_hour]; + + return { + 'heat': climates_by_climate_ref[schedule].heatTemp, + 'cool': climates_by_climate_ref[schedule].coolTemp + }; +} diff --git a/js/component/card/system.js b/js/component/card/system.js index d00faca..8d49ece 100644 --- a/js/component/card/system.js +++ b/js/component/card/system.js @@ -346,7 +346,6 @@ beestat.component.card.system.prototype.decorate_time_to_temperature_ = function const container = $.createElement('div').style({ 'background': beestat.style.color.bluegray.dark, 'padding': beestat.style.size.gutter / 2, - 'text-align': 'center', 'margin-top': beestat.style.size.gutter, 'border-radius': beestat.style.size.border_radius }); @@ -365,13 +364,13 @@ beestat.component.card.system.prototype.decorate_time_to_temperature_ = function const outdoor_temperature = thermostat.weather.temperature; const degrees_per_hour = (linear_trendline.slope * outdoor_temperature) + linear_trendline.intercept; - header_text += ' (' + - beestat.temperature({ - 'temperature': degrees_per_hour, - 'delta': true, - 'units': true - }) + - ' / h)'; + // header_text += ' (' + + // beestat.temperature({ + // 'temperature': degrees_per_hour, + // 'delta': true, + // 'units': true + // }) + + // ' / h)'; if ( ( @@ -417,12 +416,55 @@ beestat.component.card.system.prototype.decorate_time_to_temperature_ = function } } - container.appendChild( + const grid = $.createElement('div') + .style({ + 'display': 'grid', + 'grid-template-columns': '3fr 1fr', // 75% for left, 25% for right + 'grid-gap': `${beestat.style.size.gutter}px`, + 'align-items': 'center', // Center content vertically + }); + + const left = $.createElement('div'); + // .style({ + // 'overflow-wrap': 'break-word', // Allow wrapping if the content doesn't fit + // 'word-wrap': 'break-word', + // 'word-break': 'break-word', + // }); + + left.appendChild( $.createElement('div') .style('font-weight', 'bold') .innerText(header_text) ); - container.appendChild($.createElement('div').innerText(text)); + left.appendChild( + $.createElement('div') + .innerText(text) + ); + + const right = $.createElement('div') + .style({ + 'text-align': 'right', // Right-align the content of the right column + }); + + var cancel = new beestat.component.tile() + .set_icon('chart_line') + .set_shadow(false) + .set_background_hover_color('#fff') + .set_text_hover_color(beestat.style.color.bluegray.dark) + .set_text('Detail') + .addEventListener('click', function () { + (new beestat.component.modal.time_to_detail()).render(); + }) + .render(right); + + grid.appendChild(left); + grid.appendChild(right); + + container.appendChild(grid); + + // setTimeout(function() { + // (new beestat.component.modal.time_to_detail()).render(); + // }, 0); }; /** diff --git a/js/component/card/three_d.js b/js/component/card/three_d.js index 8db06d7..fafb6b9 100644 --- a/js/component/card/three_d.js +++ b/js/component/card/three_d.js @@ -511,6 +511,7 @@ beestat.component.card.three_d.prototype.decorate_controls_ = function(parent) { address.normalized.metadata.latitude, address.normalized.metadata.longitude ); + console.info(times); const sunrise_m = moment(times.sunrise); const sunrise_percentage = ((sunrise_m.hours() * 60) + sunrise_m.minutes()) / 1440 * 100; diff --git a/js/component/chart.js b/js/component/chart.js index 237e9e0..1328fbc 100644 --- a/js/component/chart.js +++ b/js/component/chart.js @@ -211,6 +211,7 @@ beestat.component.chart.prototype.get_options_chart_ = function() { }, 'spacing': this.get_options_chart_spacing_(), // For consistent left spacing on charts with no y-axis values + 'marginTop': this.get_options_chart_marginTop_(), 'marginLeft': this.get_options_chart_marginLeft_(), 'marginRight': this.get_options_chart_marginRight_(), 'marginBottom': this.get_options_chart_marginBottom_(), @@ -230,6 +231,15 @@ beestat.component.chart.prototype.get_options_chart_ = function() { }; }; +/** + * Get the top margin for the chart. + * + * @return {number} The top margin for the chart. + */ +beestat.component.chart.prototype.get_options_chart_marginTop_ = function() { + return undefined; +}; + /** * Get the left margin for the chart. * diff --git a/js/component/chart/time_to_detail.js b/js/component/chart/time_to_detail.js new file mode 100644 index 0000000..ffb27f6 --- /dev/null +++ b/js/component/chart/time_to_detail.js @@ -0,0 +1,313 @@ +/** + * Runtime thermostat detail temperature chart. + * + * @param {object} data The chart data. + */ +beestat.component.chart.time_to_detail = function(data) { + this.data_ = data; + + beestat.component.chart.apply(this, arguments); +}; +beestat.extend(beestat.component.chart.time_to_detail, beestat.component.chart); + +/** + * Override for get_options_xAxis_. + * + * @return {object} The xAxis options. + */ +beestat.component.chart.time_to_detail.prototype.get_options_xAxis_ = function() { + return { + 'categories': this.data_.x, + 'lineColor': beestat.style.color.bluegray.light, + 'tickLength': 0, + 'labels': { + 'style': { + 'color': beestat.style.color.gray.base, + 'font-size': '12px' + }, + 'formatter': this.get_options_xAxis_labels_formatter_() + }, + 'crosshair': this.get_options_xAxis_crosshair_(), + 'plotLines': [ + { + 'color': beestat.series.outdoor_temperature.color, + 'dashStyle': 'ShortDash', + 'width': 1, + 'label': { + 'style': { + 'color': beestat.series.outdoor_temperature.color + }, + 'useHTML': true, + 'text': beestat.time( + this.data_.metadata.chart.setpoint_reached_m.diff(moment(), 'second'), + ) + ' (' + this.data_.metadata.chart.setpoint_reached_m.format('h:mm a') + ')' + }, + 'value': this.data_.metadata.chart.setpoint_reached_m.diff(moment(), 'minute'), + 'zIndex': 2 + } + ] + }; +}; + +/** + * Override for get_options_xAxis_labels_formatter_. + * + * @return {Function} xAxis labels formatter. + */ +beestat.component.chart.time_to_detail.prototype.get_options_xAxis_labels_formatter_ = function() { + var current_day; + var current_time; + + return function() { + var day = this.value.format('ddd'); + + var time = this.value.clone(); + var minutes = time.minutes(); + var rounded_minutes = Math.round(minutes / 5) * 5; + time.minutes(rounded_minutes).seconds(0); + time = time.format('h:mm'); + + var label_parts = []; + if (day !== current_day) { + label_parts.push(day); + } + if (time !== current_time) { + label_parts.push(time); + } + current_day = day; + current_time = time; + + return label_parts.join(' '); + }; +}; + +/** + * Override for get_options_series_. + * + * @return {Array} All of the series to display on the chart. + */ +beestat.component.chart.time_to_detail.prototype.get_options_series_ = function() { + var self = this; + var series = []; + + // Indoor/Outdoor Temperature + [ + 'indoor_temperature', + 'outdoor_temperature', + ].forEach(function(series_code) { + if (self.data_.metadata.series[series_code].active === true) { + series.push({ + 'name': series_code, + 'data': self.data_.series[series_code], + 'color': beestat.series[series_code].color, + 'yAxis': (series_code === 'indoor_temperature') ? 0 : 1, + 'type': 'spline', + 'dashStyle': (series_code === 'indoor_temperature') ? 'Solid' : 'ShortDash', + 'lineWidth': (series_code === 'indoor_temperature') ? 2 : 1 + }); + } + }); + + // Setpoint Heat/Cool + [ + 'setpoint_heat', + 'setpoint_cool' + ].forEach(function(series_code) { + if (self.data_.metadata.series[series_code].active === true) { + series.push({ + 'name': series_code, + 'data': self.data_.series[series_code], + 'color': beestat.series[series_code].color, + 'yAxis': 0, + 'type': 'line', + 'lineWidth': 1, + 'step': 'right', + 'className': 'crisp_edges' + }); + } + }); + + return series; +}; + +/** + * Override for get_options_yAxis_. + * + * @return {Array} The y-axis options. + */ +beestat.component.chart.time_to_detail.prototype.get_options_yAxis_ = function() { + return [ + // Indoor Temperature + { + 'gridLineColor': beestat.style.color.bluegray.light, + 'gridLineDashStyle': 'longdash', + 'allowDecimals': false, + 'title': {'text': null}, + 'labels': { + 'style': { + 'color': beestat.style.color.gray.base, + 'fontSize': '11px' + }, + 'formatter': function() { + return this.value + beestat.setting('units.temperature'); + } + } + }, + // Outdoor Temperature + { + 'gridLineColor': beestat.style.color.bluegray.light, + 'opposite': true, + 'gridLineDashStyle': 'longdash', + 'allowDecimals': false, + 'title': {'text': null}, + 'labels': { + 'style': { + 'color': beestat.style.color.gray.base, + 'fontSize': '11px' + }, + 'formatter': function() { + return this.value + beestat.setting('units.temperature'); + } + } + } + ]; +}; + +/** + * Override for get_options_tooltip_formatter_. + * + * @return {Function} The tooltip formatter. + */ +beestat.component.chart.time_to_detail.prototype.get_options_tooltip_formatter_ = function() { + var self = this; + + return function() { + var points = []; + var x = this.x; + + var sections = []; + var groups = { + 'data': [], + 'delta': [] + }; + + // Add some other stuff. + [ + 'indoor_temperature', + 'outdoor_temperature', + 'setpoint_heat', + 'setpoint_cool', + 'indoor_cool_1_delta', + 'indoor_cool_2_delta', + 'indoor_heat_1_delta', + 'indoor_heat_2_delta', + 'indoor_auxiliary_heat_1_delta', + // 'indoor_auxiliary_heat_2_delta' + ].forEach(function(series_code) { + if ( + self.data_.metadata.series[series_code].active === true + ) { + points.push({ + 'series_code': series_code, + 'value': self.data_.metadata.series[series_code].data[x.valueOf()], + 'color': beestat.series[series_code].color + }); + } + }); + + points.forEach(function(point) { + var label; + var value; + var color; + var group; + + if ( + point.series_code.includes('temperature') === true || + point.series_code.includes('setpoint') === true + ) { + group = 'data'; + label = beestat.series[point.series_code].name; + color = beestat.series[point.series_code].color; + value = point.value; + + value = beestat.temperature({ + 'temperature': value, + 'input_temperature_unit': beestat.setting('units.temperature'), + 'units': true + }); + } else if(point.series_code.includes('delta') === true) { + group = 'delta'; + label = beestat.series[point.series_code].name; + color = beestat.series[point.series_code].color; + value = point.value; + + value = beestat.temperature({ + 'temperature': point.value, + 'units': true, + 'input_temperature_unit': beestat.setting('units.temperature'), + 'delta': true, + 'type': 'string' + }) + ' / h'; + + if (point.value.toFixed(1) > 0) { + value = '+' + value; + } + } + else { + return; + } + + groups[group].push({ + 'label': label, + 'value': value, + 'color': color + }); + }); + + sections.push(groups.data); + sections.push(groups.delta); + + var title = this.x.format('ddd, MMM D @ h:mma'); + + return self.tooltip_formatter_helper_( + title, + sections + ); + }; +}; + +/** + * Get the height of the chart. + * + * @return {number} The height of the chart. + */ +beestat.component.chart.time_to_detail.prototype.get_options_chart_height_ = function() { + return 350; +}; + +/** + * Get the top margin for the chart. + * + * @return {number} The top margin for the chart. + */ +beestat.component.chart.time_to_detail.prototype.get_options_chart_marginTop_ = function() { + return 20; +}; + +/** + * Get the left margin for the chart. + * + * @return {number} The left margin for the chart. + */ +beestat.component.chart.time_to_detail.prototype.get_options_chart_marginLeft_ = function() { + return 45; +}; + +/** + * Get the right margin for the chart. + * + * @return {number} The right margin for the chart. + */ +beestat.component.chart.time_to_detail.prototype.get_options_chart_marginRight_ = function() { + return 45; +}; diff --git a/js/component/modal/time_to_detail.js b/js/component/modal/time_to_detail.js new file mode 100644 index 0000000..788fd46 --- /dev/null +++ b/js/component/modal/time_to_detail.js @@ -0,0 +1,44 @@ +/** + * Current time_to_detail. + */ +beestat.component.modal.time_to_detail = function() { + var self = this; + + beestat.dispatcher.addEventListener( + 'cache.thermostat', + function() { + self.rerender(); + } + ); + + beestat.component.modal.apply(this, arguments); +}; +beestat.extend(beestat.component.modal.time_to_detail, beestat.component.modal); + +/** + * Decorate + * + * @param {rocket.Elements} parent + */ +beestat.component.modal.time_to_detail.prototype.decorate_contents_ = function(parent) { + new beestat.component.chart.time_to_detail( + beestat.time_to_detail.get_data(beestat.setting('thermostat_id')) + ).render(parent); +}; + +/** + * Get the title of the chart. + * + * @return {string} + */ +beestat.component.modal.time_to_detail.prototype.get_title_ = function() { + const thermostat = beestat.cache.thermostat[beestat.setting('thermostat_id')]; + const operating_mode = beestat.thermostat.get_operating_mode( + thermostat.thermostat_id + ); + + // Convert "heat_1" etc to "heat" + const simplified_operating_mode = operating_mode.replace(/[_\d]|auxiliary/g, ''); + + return 'Time to ' + simplified_operating_mode; +}; diff --git a/js/js.php b/js/js.php index 0312ee5..1ed202b 100755 --- a/js/js.php +++ b/js/js.php @@ -56,6 +56,7 @@ if($setting->get('environment') === 'dev' || $setting->get('environment') === 'd echo '' . PHP_EOL; echo '' . PHP_EOL; echo '' . PHP_EOL; + echo '' . PHP_EOL; // Layer echo '' . PHP_EOL; @@ -112,6 +113,7 @@ if($setting->get('environment') === 'dev' || $setting->get('environment') === 'd echo '' . PHP_EOL; echo '' . PHP_EOL; echo '' . PHP_EOL; + echo '' . PHP_EOL; echo '' . PHP_EOL; echo '' . PHP_EOL; echo '' . PHP_EOL; @@ -144,6 +146,7 @@ if($setting->get('environment') === 'dev' || $setting->get('environment') === 'd echo '' . PHP_EOL; echo '' . PHP_EOL; echo '' . PHP_EOL; + echo '' . PHP_EOL; echo '' . PHP_EOL; echo '' . PHP_EOL; echo '' . PHP_EOL;