/** * Air Quality card. Shows a chart with comfort profiles, occupancy, and air * quality data. * * @param {number} thermostat_id The thermostat_id this card is displaying * data for */ beestat.component.card.air_quality_summary = function(thermostat_id) { var self = this; this.thermostat_id_ = thermostat_id; /* * When a setting is changed clear all of the data. Then rerender which will * trigger the loading state. Also do this when the cache changes. * * Debounce so that multiple setting changes don't re-trigger the same * event. This fires on the trailing edge so that all changes are accounted * for when rerendering. */ var change_function = beestat.debounce(function() { self.rerender(); }, 10); beestat.dispatcher.addEventListener( [ 'setting.air_quality_summary_range_type', 'setting.air_quality_summary_range_dynamic', 'cache.data.air_quality_summary' ], change_function ); beestat.component.card.apply(this, arguments); }; beestat.extend(beestat.component.card.air_quality_summary, beestat.component.card); beestat.component.card.air_quality_summary.prototype.rerender_on_resize_ = true; /** * Decorate * * @param {rocket.Elements} parent */ beestat.component.card.air_quality_summary.prototype.decorate_contents_ = function(parent) { var self = this; var container = $.createElement('div').style({ 'position': 'relative' }); parent.appendChild(container); var chart_container = $.createElement('div'); container.appendChild(chart_container); this.decorate_chart_(chart_container); var thermostat = beestat.cache.thermostat[this.thermostat_id_]; var required_begin; var required_end; if (beestat.setting('air_quality_summary_range_type') === 'dynamic') { required_begin = moment() .subtract( beestat.setting('air_quality_summary_range_dynamic'), 'day' ) .second(0); required_end = moment() .subtract(1, 'hour') .second(0); } else { required_begin = moment( beestat.setting('air_quality_summary_range_static_begin') + ' 00:00:00' ); required_end = moment( beestat.setting('air_quality_summary_range_static_end') + ' 23:59:59' ); } // Don't go before there's data. required_begin = moment.max( required_begin, moment(thermostat.data_begin) ); // Don't go after now. required_end = moment.min( required_end, moment().subtract(1, 'hour') ); required_end = moment.max( required_end, moment(thermostat.data_begin) ); /** * If the needed data exists in the database and the runtime_sensor * cache is empty, then query the data. If the needed data does not exist in * the database, check every 2 seconds until it does. */ if (beestat.thermostat.data_synced(this.thermostat_id_, required_begin, required_end) === true) { if (beestat.cache.data.air_quality_summary === undefined) { this.show_loading_('Fetching'); var value; var operator; if (beestat.setting('air_quality_summary_range_type') === 'dynamic') { value = required_begin.format(); operator = '>='; } else { value = [ required_begin.format(), required_end.format() ]; operator = 'between'; } var api_call = new beestat.api(); Object.values(beestat.cache.sensor).forEach(function(sensor) { if ( sensor.thermostat_id === self.thermostat_id_ && sensor.type === 'thermostat' ) { api_call.add_call( 'runtime_sensor', 'read', { 'attributes': { 'sensor_id': sensor.sensor_id, 'timestamp': { 'value': value, 'operator': operator } } }, 'runtime_sensor_' + sensor.sensor_id ); } }); api_call.set_callback(function(response) { var runtime_sensors = []; for (var alias in response) { var r = response[alias]; runtime_sensors = runtime_sensors.concat(r); } beestat.cache.set('data.air_quality_summary', runtime_sensors); }); api_call.send(); } else if (this.has_data_() === false) { chart_container.style('filter', 'blur(3px)'); var no_data = $.createElement('div'); no_data.style({ 'position': 'absolute', 'top': 0, 'left': 0, 'width': '100%', 'height': '100%', 'display': 'flex', 'flex-direction': 'column', 'justify-content': 'center', 'text-align': 'center' }); no_data.innerText('No data to display'); container.appendChild(no_data); } } else { this.show_loading_('Syncing'); window.setTimeout(function() { new beestat.api() .add_call( 'thermostat', 'read_id', { 'attributes': { 'inactive': 0 } }, 'thermostat' ) .set_callback(function(response) { beestat.cache.set('thermostat', response); self.rerender(); }) .send(); }, 2000); } }; /** * Decorate chart * * @param {rocket.Elements} parent */ beestat.component.card.air_quality_summary.prototype.decorate_chart_ = function(parent) { const gradient = beestat.style.generate_gradient( [ beestat.style.hex_to_rgb(beestat.style.color.green.base), beestat.style.hex_to_rgb(beestat.style.color.yellow.base), beestat.style.hex_to_rgb(beestat.style.color.red.base) ], 100 ); var grid_data = {}; for (var runtime_sensor_id in beestat.cache.data.air_quality_summary) { var runtime_sensor = beestat.cache.data.air_quality_summary[runtime_sensor_id]; var key = moment(runtime_sensor.timestamp).format('d_H'); if (grid_data[key] === undefined) { grid_data[key] = []; } if (runtime_sensor.air_quality !== null) { grid_data[key].push(runtime_sensor.air_quality); } } var table = $.createElement('table'); table.style({ 'table-layout': 'fixed', 'border-collapse': 'collapse', 'width': '100%' }); var tr; var td; tr = $.createElement('tr'); tr.appendChild($.createElement('td').style({'width': '50px'})); table.appendChild(tr); var days_of_week = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; var day; var hour; // Header row for (hour = 0; hour < 24; hour++) { var meridiem = hour >= 12 ? 'p' : 'a'; var new_hour = (hour % 12) || 12; tr.appendChild( $.createElement('td') .innerText(new_hour + (window.innerWidth > 700 ? meridiem : '')) .style({ 'text-align': 'center', 'font-size': beestat.style.font_size.small }) ); } for (day = 0; day < 7; day++) { tr = $.createElement('tr'); table.appendChild(tr); td = $.createElement('td') .innerHTML(days_of_week[day]); tr.appendChild(td); for (hour = 0; hour < 24; hour++) { var cell_value = grid_data[day + '_' + hour]; td = $.createElement('td'); var background = beestat.style.color.bluegray.light; if (cell_value !== undefined) { var average; if (grid_data[day + '_' + hour].length > 0) { average = grid_data[day + '_' + hour].reduce(function(a, b) { return a + b; }) / grid_data[day + '_' + hour].length; } else { average = 0; } td.setAttribute('title', Math.round(average)); // I am normalizing Air Quality between 0 and 100. const max_average = 100; if (average < 1) { background = beestat.style.color.bluegray.light; } else if (average <= max_average) { background = beestat.style.rgb_to_hex(gradient[ Math.floor(average / (max_average / gradient.length)) ]); } else { background = beestat.style.rgb_to_hex(gradient[gradient.length - 1]); } } td.style({ 'height': '20px', 'background': background }); tr.appendChild(td); } } parent.appendChild(table); }; /** * Decorate the menu * * @param {rocket.Elements} parent */ beestat.component.card.air_quality_summary.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('Past 1 Week') .set_icon('numeric_1_box') .set_callback(function() { if ( beestat.setting('air_quality_summary_range_dynamic') !== 7 || beestat.setting('air_quality_summary_range_type') !== 'dynamic' ) { beestat.cache.delete('data.air_quality_summary'); beestat.setting({ 'air_quality_summary_range_dynamic': 7, 'air_quality_summary_range_type': 'dynamic' }); } })); menu.add_menu_item(new beestat.component.menu_item() .set_text('Past 4 Weeks') .set_icon('numeric_4_box') .set_callback(function() { if ( beestat.setting('air_quality_summary_range_dynamic') !== 28 || beestat.setting('air_quality_summary_range_type') !== 'dynamic' ) { beestat.cache.delete('data.air_quality_summary'); beestat.setting({ 'air_quality_summary_range_dynamic': 28, 'air_quality_summary_range_type': 'dynamic' }); } })); 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/25a1a894a7f4432ead5831bef48770bd'); })); }; /** * Whether or not there is data to display on the chart. * * @return {boolean} Whether or not there is data to display on the chart. */ beestat.component.card.air_quality_summary.prototype.has_data_ = function() { return beestat.cache.data.air_quality_summary && beestat.cache.data.air_quality_summary.length > 0; }; /** * Get the title of the card. * * @return {string} Title */ beestat.component.card.air_quality_summary.prototype.get_title_ = function() { return 'Air Quality Summary'; }; /** * Get the subtitle of the card. * * @return {string} Subtitle */ beestat.component.card.air_quality_summary.prototype.get_subtitle_ = function() { if (beestat.setting('air_quality_summary_range_type') === 'dynamic') { var s = ((beestat.setting('air_quality_summary_range_dynamic') / 7) > 1) ? 's' : ''; return 'Past ' + (beestat.setting('air_quality_summary_range_dynamic') / 7) + ' week' + s; } var begin = moment(beestat.setting('air_quality_summary_range_static_begin')) .format('MMM D, YYYY'); var end = moment(beestat.setting('air_quality_summary_range_static_end')) .format('MMM D, YYYY'); return begin + ' to ' + end; };