Added graphing component (#2109)

Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
Co-authored-by: Oxan van Leeuwen <oxan@oxanvanleeuwen.nl>
Co-authored-by: Synco Reynders <synco@deviceware.co.nz>
Co-authored-by: Otto winter <otto@otto-winter.com>
This commit is contained in:
synco 2021-09-20 19:29:47 +12:00 committed by GitHub
parent fff5ba03c2
commit 945ed5d3bd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 806 additions and 0 deletions

View file

@ -54,6 +54,7 @@ esphome/components/fingerprint_grow/* @OnFreund @loongyh
esphome/components/globals/* @esphome/core esphome/components/globals/* @esphome/core
esphome/components/gpio/* @esphome/core esphome/components/gpio/* @esphome/core
esphome/components/gps/* @coogle esphome/components/gps/* @coogle
esphome/components/graph/* @synco
esphome/components/havells_solar/* @sourabhjaiswal esphome/components/havells_solar/* @sourabhjaiswal
esphome/components/hbridge/fan/* @WeekendWarrior esphome/components/hbridge/fan/* @WeekendWarrior
esphome/components/hbridge/light/* @DotNetDann esphome/components/hbridge/light/* @DotNetDann

View file

@ -233,6 +233,13 @@ void DisplayBuffer::image(int x, int y, Image *image, Color color_on, Color colo
} }
} }
#ifdef USE_GRAPH
void DisplayBuffer::graph(int x, int y, graph::Graph *graph, Color color_on) { graph->draw(this, x, y, color_on); }
void DisplayBuffer::legend(int x, int y, graph::Graph *graph, Color color_on) {
graph->draw_legend(this, x, y, color_on);
}
#endif // USE_GRAPH
void DisplayBuffer::get_text_bounds(int x, int y, const char *text, Font *font, TextAlign align, int *x1, int *y1, void DisplayBuffer::get_text_bounds(int x, int y, const char *text, Font *font, TextAlign align, int *x1, int *y1,
int *width, int *height) { int *width, int *height) {
int x_offset, baseline; int x_offset, baseline;

View file

@ -9,6 +9,10 @@
#include "esphome/components/time/real_time_clock.h" #include "esphome/components/time/real_time_clock.h"
#endif #endif
#ifdef USE_GRAPH
#include "esphome/components/graph/graph.h"
#endif
namespace esphome { namespace esphome {
namespace display { namespace display {
@ -273,6 +277,30 @@ class DisplayBuffer {
*/ */
void image(int x, int y, Image *image, Color color_on = COLOR_ON, Color color_off = COLOR_OFF); void image(int x, int y, Image *image, Color color_on = COLOR_ON, Color color_off = COLOR_OFF);
#ifdef USE_GRAPH
/** Draw the `graph` with the top-left corner at [x,y] to the screen.
*
* @param x The x coordinate of the upper left corner.
* @param y The y coordinate of the upper left corner.
* @param graph The graph id to draw
* @param color_on The color to replace in binary images for the on bits.
*/
void graph(int x, int y, graph::Graph *graph, Color color_on = COLOR_ON);
/** Draw the `legend` for graph with the top-left corner at [x,y] to the screen.
*
* @param x The x coordinate of the upper left corner.
* @param y The y coordinate of the upper left corner.
* @param graph The graph id for which the legend applies to
* @param graph The graph id for which the legend applies to
* @param graph The graph id for which the legend applies to
* @param name_font The font used for the trace name
* @param value_font The font used for the trace value and units
* @param color_on The color of the border
*/
void legend(int x, int y, graph::Graph *graph, Color color_on = COLOR_ON);
#endif // USE_GRAPH
/** Get the text bounds of the given string. /** Get the text bounds of the given string.
* *
* @param x The x coordinate to place the string at, can be 0 if only interested in dimensions. * @param x The x coordinate to place the string at, can be 0 if only interested in dimensions.

View file

@ -0,0 +1,216 @@
from esphome.components.font import Font
from esphome.components import sensor, color
import esphome.config_validation as cv
import esphome.codegen as cg
from esphome.const import (
CONF_COLOR,
CONF_DIRECTION,
CONF_DURATION,
CONF_ID,
CONF_LEGEND,
CONF_NAME,
CONF_NAME_FONT,
CONF_SHOW_LINES,
CONF_SHOW_UNITS,
CONF_SHOW_VALUES,
CONF_VALUE_FONT,
CONF_WIDTH,
CONF_SENSOR,
CONF_HEIGHT,
CONF_MIN_VALUE,
CONF_MAX_VALUE,
CONF_MIN_RANGE,
CONF_MAX_RANGE,
CONF_LINE_THICKNESS,
CONF_LINE_TYPE,
CONF_X_GRID,
CONF_Y_GRID,
CONF_BORDER,
CONF_TRACES,
)
CODEOWNERS = ["@synco"]
DEPENDENCIES = ["display", "sensor"]
MULTI_CONF = True
graph_ns = cg.esphome_ns.namespace("graph")
Graph_ = graph_ns.class_("Graph", cg.Component)
GraphTrace = graph_ns.class_("GraphTrace")
GraphLegend = graph_ns.class_("GraphLegend")
LineType = graph_ns.enum("LineType")
LINE_TYPE = {
"SOLID": LineType.LINE_TYPE_SOLID,
"DOTTED": LineType.LINE_TYPE_DOTTED,
"DASHED": LineType.LINE_TYPE_DASHED,
}
DirectionType = graph_ns.enum("DirectionType")
DIRECTION_TYPE = {
"AUTO": DirectionType.DIRECTION_TYPE_AUTO,
"HORIZONTAL": DirectionType.DIRECTION_TYPE_HORIZONTAL,
"VERTICAL": DirectionType.DIRECTION_TYPE_VERTICAL,
}
ValuePositionType = graph_ns.enum("ValuePositionType")
VALUE_POSITION_TYPE = {
"NONE": ValuePositionType.VALUE_POSITION_TYPE_NONE,
"AUTO": ValuePositionType.VALUE_POSITION_TYPE_AUTO,
"BESIDE": ValuePositionType.VALUE_POSITION_TYPE_BESIDE,
"BELOW": ValuePositionType.VALUE_POSITION_TYPE_BELOW,
}
GRAPH_TRACE_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(GraphTrace),
cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor),
cv.Optional(CONF_NAME): cv.string,
cv.Optional(CONF_LINE_THICKNESS): cv.positive_int,
cv.Optional(CONF_LINE_TYPE): cv.enum(LINE_TYPE, upper=True),
cv.Optional(CONF_COLOR): cv.use_id(color.ColorStruct),
}
)
GRAPH_LEGEND_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_ID): cv.declare_id(GraphLegend),
cv.Required(CONF_NAME_FONT): cv.use_id(Font),
cv.Optional(CONF_VALUE_FONT): cv.use_id(Font),
cv.Optional(CONF_WIDTH): cv.positive_not_null_int,
cv.Optional(CONF_HEIGHT): cv.positive_not_null_int,
cv.Optional(CONF_BORDER): cv.boolean,
cv.Optional(CONF_SHOW_LINES): cv.boolean,
cv.Optional(CONF_SHOW_VALUES): cv.enum(VALUE_POSITION_TYPE, upper=True),
cv.Optional(CONF_SHOW_UNITS): cv.boolean,
cv.Optional(CONF_DIRECTION): cv.enum(DIRECTION_TYPE, upper=True),
}
)
GRAPH_SCHEMA = cv.Schema(
{
cv.Required(CONF_ID): cv.declare_id(Graph_),
cv.Required(CONF_DURATION): cv.positive_time_period_seconds,
cv.Required(CONF_WIDTH): cv.positive_not_null_int,
cv.Required(CONF_HEIGHT): cv.positive_not_null_int,
cv.Optional(CONF_X_GRID): cv.positive_time_period_seconds,
cv.Optional(CONF_Y_GRID): cv.float_range(min=0, min_included=False),
cv.Optional(CONF_BORDER): cv.boolean,
# Single trace options in base
cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor),
cv.Optional(CONF_LINE_THICKNESS): cv.positive_int,
cv.Optional(CONF_LINE_TYPE): cv.enum(LINE_TYPE, upper=True),
cv.Optional(CONF_COLOR): cv.use_id(color.ColorStruct),
# Axis specific options (Future feature may be to add second Y-axis)
cv.Optional(CONF_MIN_VALUE): cv.float_,
cv.Optional(CONF_MAX_VALUE): cv.float_,
cv.Optional(CONF_MIN_RANGE): cv.float_range(min=0, min_included=False),
cv.Optional(CONF_MAX_RANGE): cv.float_range(min=0, min_included=False),
cv.Optional(CONF_TRACES): cv.ensure_list(GRAPH_TRACE_SCHEMA),
cv.Optional(CONF_LEGEND): cv.ensure_list(GRAPH_LEGEND_SCHEMA),
}
)
def _relocate_fields_to_subfolder(config, subfolder, subschema):
fields = [k.schema for k in subschema.schema.keys()]
fields.remove(CONF_ID)
if subfolder in config:
# Ensure no ambigious fields in base of config
for f in fields:
if f in config:
raise cv.Invalid(
"You cannot use the '"
+ str(f)
+ "' field when already using 'traces:'. "
"Please move it into 'traces:' entry."
)
else:
# Copy over all fields to subfolder:
trace = {}
for f in fields:
if f in config:
trace[f] = config.pop(f)
config[subfolder] = cv.ensure_list(subschema)(trace)
return config
def _relocate_trace(config):
return _relocate_fields_to_subfolder(config, CONF_TRACES, GRAPH_TRACE_SCHEMA)
CONFIG_SCHEMA = cv.All(
GRAPH_SCHEMA,
_relocate_trace,
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
cg.add(var.set_duration(config[CONF_DURATION]))
cg.add(var.set_width(config[CONF_WIDTH]))
cg.add(var.set_height(config[CONF_HEIGHT]))
await cg.register_component(var, config)
# Graph options
if CONF_X_GRID in config:
cg.add(var.set_grid_x(config[CONF_X_GRID]))
if CONF_Y_GRID in config:
cg.add(var.set_grid_y(config[CONF_Y_GRID]))
if CONF_BORDER in config:
cg.add(var.set_border(config[CONF_BORDER]))
# Axis related options
if CONF_MIN_VALUE in config:
cg.add(var.set_min_value(config[CONF_MIN_VALUE]))
if CONF_MAX_VALUE in config:
cg.add(var.set_max_value(config[CONF_MAX_VALUE]))
if CONF_MIN_RANGE in config:
cg.add(var.set_min_range(config[CONF_MIN_RANGE]))
if CONF_MAX_RANGE in config:
cg.add(var.set_max_range(config[CONF_MAX_RANGE]))
# Trace options
for trace in config[CONF_TRACES]:
tr = cg.new_Pvariable(trace[CONF_ID], GraphTrace())
sens = await cg.get_variable(trace[CONF_SENSOR])
cg.add(tr.set_sensor(sens))
if CONF_NAME in trace:
cg.add(tr.set_name(trace[CONF_NAME]))
else:
cg.add(tr.set_name(trace[CONF_SENSOR].id))
if CONF_LINE_THICKNESS in trace:
cg.add(tr.set_line_thickness(trace[CONF_LINE_THICKNESS]))
if CONF_LINE_TYPE in trace:
cg.add(tr.set_line_type(trace[CONF_LINE_TYPE]))
if CONF_COLOR in trace:
c = await cg.get_variable(trace[CONF_COLOR])
cg.add(tr.set_line_color(c))
cg.add(var.add_trace(tr))
# Add legend
if CONF_LEGEND in config:
lgd = config[CONF_LEGEND][0]
legend = cg.new_Pvariable(lgd[CONF_ID], GraphLegend())
if CONF_NAME_FONT in lgd:
font = await cg.get_variable(lgd[CONF_NAME_FONT])
cg.add(legend.set_name_font(font))
if CONF_VALUE_FONT in lgd:
font = await cg.get_variable(lgd[CONF_VALUE_FONT])
cg.add(legend.set_value_font(font))
if CONF_WIDTH in lgd:
cg.add(legend.set_width(lgd[CONF_WIDTH]))
if CONF_HEIGHT in lgd:
cg.add(legend.set_height(lgd[CONF_HEIGHT]))
if CONF_BORDER in lgd:
cg.add(legend.set_border(lgd[CONF_BORDER]))
if CONF_SHOW_LINES in lgd:
cg.add(legend.set_lines(lgd[CONF_SHOW_LINES]))
if CONF_SHOW_VALUES in lgd:
cg.add(legend.set_values(lgd[CONF_SHOW_VALUES]))
if CONF_SHOW_UNITS in lgd:
cg.add(legend.set_units(lgd[CONF_SHOW_UNITS]))
if CONF_DIRECTION in lgd:
cg.add(legend.set_direction(lgd[CONF_DIRECTION]))
cg.add(var.add_legend(legend))
cg.add_define("USE_GRAPH")

View file

@ -0,0 +1,361 @@
#include "graph.h"
#include "esphome/components/display/display_buffer.h"
#include "esphome/core/color.h"
#include "esphome/core/log.h"
#include "esphome/core/esphal.h"
#include <algorithm>
#include <sstream>
#include <iostream> // std::cout, std::fixed
#include <iomanip>
namespace esphome {
namespace graph {
using namespace display;
static const char *const TAG = "graph";
static const char *const TAGL = "graphlegend";
void HistoryData::init(int length) {
this->length_ = length;
this->samples_.resize(length, NAN);
this->last_sample_ = millis();
}
void HistoryData::take_sample(float data) {
uint32_t tm = millis();
uint32_t dt = tm - last_sample_;
last_sample_ = tm;
// Step data based on time
this->period_ += dt;
while (this->period_ >= this->update_time_) {
this->samples_[this->count_] = data;
this->period_ -= this->update_time_;
this->count_ = (this->count_ + 1) % this->length_;
ESP_LOGV(TAG, "Updating trace with value: %f", data);
}
if (!isnan(data)) {
// Recalc recent max/min
this->recent_min_ = data;
this->recent_max_ = data;
for (int i = 0; i < this->length_; i++) {
if (!isnan(this->samples_[i])) {
if (this->recent_max_ < this->samples_[i])
this->recent_max_ = this->samples_[i];
if (this->recent_min_ > this->samples_[i])
this->recent_min_ = this->samples_[i];
}
}
}
}
void GraphTrace::init(Graph *g) {
ESP_LOGI(TAG, "Init trace for sensor %s", this->get_name().c_str());
this->data_.init(g->get_width());
sensor_->add_on_state_callback([this](float state) { this->data_.take_sample(state); });
this->data_.set_update_time_ms(g->get_duration() * 1000 / g->get_width());
}
void Graph::draw(DisplayBuffer *buff, uint16_t x_offset, uint16_t y_offset, Color color) {
/// Plot border
if (this->border_) {
buff->horizontal_line(x_offset, y_offset, this->width_, color);
buff->horizontal_line(x_offset, y_offset + this->height_ - 1, this->width_, color);
buff->vertical_line(x_offset, y_offset, this->height_, color);
buff->vertical_line(x_offset + this->width_ - 1, y_offset, this->height_, color);
}
/// Determine best y-axis scale and range
float ymin = NAN;
float ymax = NAN;
for (auto *trace : traces_) {
float mx = trace->get_tracedata()->get_recent_max();
float mn = trace->get_tracedata()->get_recent_min();
if (isnan(ymax) || (ymax < mx))
ymax = mx;
if (isnan(ymin) || (ymin > mn))
ymin = mn;
}
// Adjust if manually overridden
if (!isnan(this->min_value_))
ymin = this->min_value_;
if (!isnan(this->max_value_))
ymax = this->max_value_;
float yrange = ymax - ymin;
if (yrange > this->max_range_) {
// Look back in trace data to best-fit into local range
float mx = NAN;
float mn = NAN;
for (int16_t i = 0; i < this->width_; i++) {
for (auto *trace : traces_) {
float v = trace->get_tracedata()->get_value(i);
if (!isnan(v)) {
if ((v - mn) > this->max_range_)
break;
if ((mx - v) > this->max_range_)
break;
if (isnan(mx) || (v > mx))
mx = v;
if (isnan(mn) || (v < mn))
mn = v;
}
}
}
yrange = this->max_range_;
if (!isnan(mn)) {
ymin = mn;
ymax = ymin + this->max_range_;
}
ESP_LOGV(TAG, "Graphing at max_range. Using local min %f, max %f", mn, mx);
}
float y_per_div = this->min_range_;
if (!isnan(this->gridspacing_y_)) {
y_per_div = this->gridspacing_y_;
}
// Restrict drawing too many gridlines
if (yrange > 10 * y_per_div) {
while (yrange > 10 * y_per_div) {
y_per_div *= 2;
}
ESP_LOGW(TAG, "Graphing reducing y-scale to prevent too many gridlines");
}
// Adjust limits to nice y_per_div boundaries
int yn = int(ymin / y_per_div);
int ym = int(ymax / y_per_div) + int(1 * (fmodf(ymax, y_per_div) != 0));
ymin = yn * y_per_div;
ymax = ym * y_per_div;
yrange = ymax - ymin;
/// Draw grid
if (!isnan(this->gridspacing_y_)) {
for (int y = yn; y <= ym; y++) {
int16_t py = (int16_t) roundf((this->height_ - 1) * (1.0 - (float) (y - yn) / (ym - yn)));
for (int x = 0; x < this->width_; x += 2) {
buff->draw_pixel_at(x_offset + x, y_offset + py, color);
}
}
}
if (!isnan(this->gridspacing_x_) && (this->gridspacing_x_ > 0)) {
int n = this->duration_ / this->gridspacing_x_;
// Restrict drawing too many gridlines
if (n > 20) {
while (n > 20) {
n /= 2;
}
ESP_LOGW(TAG, "Graphing reducing x-scale to prevent too many gridlines");
}
for (int i = 0; i <= n; i++) {
for (int y = 0; y < this->height_; y += 2) {
buff->draw_pixel_at(x_offset + i * (this->width_ - 1) / n, y_offset + y, color);
}
}
}
/// Draw traces
ESP_LOGV(TAG, "Updating graph. ymin %f, ymax %f", ymin, ymax);
for (auto *trace : traces_) {
Color c = trace->get_line_color();
uint16_t thick = trace->get_line_thickness();
for (int16_t i = 0; i < this->width_; i++) {
float v = (trace->get_tracedata()->get_value(i) - ymin) / yrange;
if (!isnan(v) && (thick > 0)) {
int16_t x = this->width_ - 1 - i;
uint8_t b = (i % (thick * LineType::PATTERN_LENGTH)) / thick;
if (((uint8_t) trace->get_line_type() & (1 << b)) == (1 << b)) {
int16_t y = (int16_t) roundf((this->height_ - 1) * (1.0 - v)) - thick / 2;
for (int16_t t = 0; t < thick; t++) {
buff->draw_pixel_at(x_offset + x, y_offset + y + t, c);
}
}
}
}
}
}
/// Determine the best coordinates of drawing text + lines
void GraphLegend::init(Graph *g) {
parent_ = g;
// Determine maximum expected text and value width / height
int txtw = 0, txtos = 0, txtbl = 0, txth = 0;
int valw = 0, valos = 0, valbl = 0, valh = 0;
int lt = 0;
for (auto *trace : g->traces_) {
std::string txtstr = trace->get_name();
int fw, fos, fbl, fh;
this->font_label->measure(txtstr.c_str(), &fw, &fos, &fbl, &fh);
if (fw > txtw)
txtw = fw;
if (fh > txth)
txth = fh;
if (trace->get_line_thickness() > lt)
lt = trace->get_line_thickness();
ESP_LOGI(TAGL, " %s %d %d", txtstr.c_str(), fw, fh);
if (this->values_ != VALUE_POSITION_TYPE_NONE) {
std::stringstream ss;
ss << std::fixed << std::setprecision(trace->sensor_->get_accuracy_decimals()) << trace->sensor_->get_state();
std::string valstr = ss.str();
if (this->units_) {
valstr += trace->sensor_->get_unit_of_measurement();
}
this->font_value->measure(valstr.c_str(), &fw, &fos, &fbl, &fh);
if (fw > valw)
valw = fw;
if (fh > valh)
valh = fh;
ESP_LOGI(TAGL, " %s %d %d", valstr.c_str(), fw, fh);
}
}
// Add extra margin
txtw *= 1.2;
valw *= 1.2;
uint8_t n = g->traces_.size();
uint16_t w = this->width_;
uint16_t h = this->height_;
DirectionType dir = this->direction_;
ValuePositionType valpos = this->values_;
if (!this->font_value) {
valpos = VALUE_POSITION_TYPE_NONE;
}
// Line sample always goes below text for compactness
this->yl = txth + (txth / 4) + lt / 2;
if (dir == DIRECTION_TYPE_AUTO) {
dir = DIRECTION_TYPE_HORIZONTAL; // as default
if (h > 0) {
dir = DIRECTION_TYPE_VERTICAL;
}
}
if (valpos == VALUE_POSITION_TYPE_AUTO) {
// TODO: do something smarter?? - fit to w and h?
valpos = VALUE_POSITION_TYPE_BELOW;
}
if (valpos == VALUE_POSITION_TYPE_BELOW) {
this->yv = txth + (txth / 4);
if (this->lines_)
this->yv += txth / 4 + lt;
} else if (valpos == VALUE_POSITION_TYPE_BESIDE) {
this->xv = (txtw + valw) / 2;
}
// If width or height is specified we divide evenly within, else we do tight-fit
if (w == 0) {
this->x0 = txtw / 2;
this->xs = txtw;
if (valpos == VALUE_POSITION_TYPE_BELOW) {
this->xs = std::max(txtw, valw);
;
this->x0 = this->xs / 2;
} else if (valpos == VALUE_POSITION_TYPE_BESIDE) {
this->xs = txtw + valw;
}
if (dir == DIRECTION_TYPE_VERTICAL) {
this->width_ = this->xs;
} else {
this->width_ = this->xs * n;
}
} else {
this->xs = w / n;
this->x0 = this->xs / 2;
}
if (h == 0) {
this->ys = txth;
if (valpos == VALUE_POSITION_TYPE_BELOW) {
this->ys = txth + txth / 2 + valh;
if (this->lines_) {
this->ys += lt;
}
} else if (valpos == VALUE_POSITION_TYPE_BESIDE) {
if (this->lines_) {
this->ys = std::max(txth + txth / 4 + lt + txth / 4, valh + valh / 4);
} else {
this->ys = std::max(txth + txth / 4, valh + valh / 4);
}
this->height_ = this->ys * n;
}
if (dir == DIRECTION_TYPE_HORIZONTAL) {
this->height_ = this->ys;
} else {
this->height_ = this->ys * n;
}
} else {
this->ys = h / n;
}
if (dir == DIRECTION_TYPE_HORIZONTAL) {
this->ys = 0;
} else {
this->xs = 0;
}
}
void Graph::draw_legend(display::DisplayBuffer *buff, uint16_t x_offset, uint16_t y_offset, Color color) {
if (!legend_)
return;
/// Plot border
if (this->border_) {
int w = legend_->width_;
int h = legend_->height_;
buff->horizontal_line(x_offset, y_offset, w, color);
buff->horizontal_line(x_offset, y_offset + h - 1, w, color);
buff->vertical_line(x_offset, y_offset, h, color);
buff->vertical_line(x_offset + w - 1, y_offset, h, color);
}
int x = x_offset + legend_->x0;
int y = y_offset;
for (auto *trace : traces_) {
std::string txtstr = trace->get_name();
ESP_LOGV(TAG, " %s", txtstr.c_str());
buff->printf(x, y, legend_->font_label, trace->get_line_color(), TextAlign::TOP_CENTER, "%s", txtstr.c_str());
if (legend_->lines_) {
uint16_t thick = trace->get_line_thickness();
for (int16_t i = 0; i < legend_->x0 * 4 / 3; i++) {
uint8_t b = (i % (thick * LineType::PATTERN_LENGTH)) / thick;
if (((uint8_t) trace->get_line_type() & (1 << b)) == (1 << b)) {
buff->vertical_line(x - legend_->x0 * 2 / 3 + i, y + legend_->yl - thick / 2, thick, trace->get_line_color());
}
}
}
if (legend_->values_ != VALUE_POSITION_TYPE_NONE) {
int xv = x + legend_->xv;
int yv = y + legend_->yv;
std::stringstream ss;
ss << std::fixed << std::setprecision(trace->sensor_->get_accuracy_decimals()) << trace->sensor_->get_state();
std::string valstr = ss.str();
if (legend_->units_) {
valstr += trace->sensor_->get_unit_of_measurement();
}
buff->printf(xv, yv, legend_->font_value, trace->get_line_color(), TextAlign::TOP_CENTER, "%s", valstr.c_str());
ESP_LOGV(TAG, " value: %s", valstr.c_str());
}
x += legend_->xs;
y += legend_->ys;
}
}
void Graph::setup() {
for (auto *trace : traces_) {
trace->init(this);
}
}
void Graph::dump_config() {
for (auto *trace : traces_) {
ESP_LOGCONFIG(TAG, "Graph for sensor %s", trace->get_name().c_str());
}
}
} // namespace graph
} // namespace esphome

View file

@ -0,0 +1,178 @@
#pragma once
#include <cstdint>
#include "esphome/core/color.h"
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
namespace esphome {
// forward declare DisplayBuffer
namespace display {
class DisplayBuffer;
class Font;
} // namespace display
namespace graph {
class Graph;
const Color COLOR_ON(255, 255, 255, 255);
/// Bit pattern defines the line-type
enum LineType {
LINE_TYPE_SOLID = 0b1111,
LINE_TYPE_DOTTED = 0b0101,
LINE_TYPE_DASHED = 0b1110,
// Following defines number of bits used to define line pattern
PATTERN_LENGTH = 4
};
enum DirectionType {
DIRECTION_TYPE_AUTO,
DIRECTION_TYPE_HORIZONTAL,
DIRECTION_TYPE_VERTICAL,
};
enum ValuePositionType {
VALUE_POSITION_TYPE_NONE,
VALUE_POSITION_TYPE_AUTO,
VALUE_POSITION_TYPE_BESIDE,
VALUE_POSITION_TYPE_BELOW
};
class GraphLegend {
public:
void init(Graph *g);
void set_name_font(display::Font *font) { this->font_label = font; }
void set_value_font(display::Font *font) { this->font_value = font; }
void set_width(uint32_t width) { this->width_ = width; }
void set_height(uint32_t height) { this->height_ = height; }
void set_border(bool val) { this->border_ = val; }
void set_lines(bool val) { this->lines_ = val; }
void set_values(ValuePositionType val) { this->values_ = val; }
void set_units(bool val) { this->units_ = val; }
void set_direction(DirectionType val) { this->direction_ = val; }
protected:
uint32_t width_{0};
uint32_t height_{0};
bool border_{true};
bool lines_{true};
ValuePositionType values_{VALUE_POSITION_TYPE_AUTO};
bool units_{true};
DirectionType direction_{DIRECTION_TYPE_AUTO};
display::Font *font_label{nullptr};
display::Font *font_value{nullptr};
// Calculated values
Graph *parent_{nullptr};
// (x0) (xs,ys) (xs,ys)
// <x_offset,y_offset> ------> LABEL1 -------> LABEL2 -------> ...
// | \(xv,yv) \ .
// | \ \-> VALUE1+units
// (0,yl)| \-> VALUE1+units
// v (top_center)
// LINE_SAMPLE
int x0{0}; // X-offset to centre of label text
int xs{0}; // X spacing between labels
int ys{0}; // Y spacing between labels
int yl{0}; // Y spacing from label to line sample
int xv{0}; // X distance between label to value text
int yv{0}; // Y distance between label to value text
friend Graph;
};
class HistoryData {
public:
void init(int length);
~HistoryData();
void set_update_time_ms(uint32_t update_time_ms) { update_time_ = update_time_ms; }
void take_sample(float data);
int get_length() const { return length_; }
float get_value(int idx) const { return samples_[(count_ + length_ - 1 - idx) % length_]; }
float get_recent_max() const { return recent_max_; }
float get_recent_min() const { return recent_min_; }
protected:
uint32_t last_sample_;
uint32_t period_{0}; /// in ms
uint32_t update_time_{0}; /// in ms
int length_;
int count_{0};
float recent_min_{NAN};
float recent_max_{NAN};
std::vector<float> samples_;
};
class GraphTrace {
public:
void init(Graph *g);
void set_name(std::string name) { name_ = name; }
void set_sensor(sensor::Sensor *sensor) { sensor_ = sensor; }
uint8_t get_line_thickness() { return this->line_thickness_; }
void set_line_thickness(uint8_t val) { this->line_thickness_ = val; }
enum LineType get_line_type() { return this->line_type_; }
void set_line_type(enum LineType val) { this->line_type_ = val; }
Color get_line_color() { return this->line_color_; }
void set_line_color(Color val) { this->line_color_ = val; }
const std::string get_name(void) { return name_; }
const HistoryData *get_tracedata() { return &data_; }
protected:
sensor::Sensor *sensor_{nullptr};
std::string name_{""};
uint8_t line_thickness_{3};
enum LineType line_type_ { LINE_TYPE_SOLID };
Color line_color_{COLOR_ON};
HistoryData data_;
friend Graph;
friend GraphLegend;
};
class Graph : public Component {
public:
void draw(display::DisplayBuffer *buff, uint16_t x_offset, uint16_t y_offset, Color color);
void draw_legend(display::DisplayBuffer *buff, uint16_t x_offset, uint16_t y_offset, Color color);
void setup() override;
float get_setup_priority() const override { return setup_priority::PROCESSOR; }
void dump_config() override;
void set_duration(uint32_t duration) { duration_ = duration; }
void set_width(uint32_t width) { width_ = width; }
void set_height(uint32_t height) { height_ = height; }
void set_min_value(float val) { this->min_value_ = val; }
void set_max_value(float val) { this->max_value_ = val; }
void set_min_range(float val) { this->min_range_ = val; }
void set_max_range(float val) { this->max_range_ = val; }
void set_grid_x(float val) { this->gridspacing_x_ = val; }
void set_grid_y(float val) { this->gridspacing_y_ = val; }
void set_border(bool val) { this->border_ = val; }
void add_trace(GraphTrace *trace) { traces_.push_back(trace); }
void add_legend(GraphLegend *legend) {
this->legend_ = legend;
legend->init(this);
}
uint32_t get_duration() { return duration_; }
uint32_t get_width() { return width_; }
uint32_t get_height() { return height_; }
protected:
uint32_t duration_; /// in seconds
uint32_t width_; /// in pixels
uint32_t height_; /// in pixels
float min_value_{NAN};
float max_value_{NAN};
float min_range_{1.0};
float max_range_{NAN};
float gridspacing_x_{NAN};
float gridspacing_y_{NAN};
bool border_{true};
std::vector<GraphTrace *> traces_;
GraphLegend *legend_{nullptr};
friend GraphLegend;
};
} // namespace graph
} // namespace esphome

View file

@ -90,6 +90,7 @@ CONF_BIT_DEPTH = "bit_depth"
CONF_BLUE = "blue" CONF_BLUE = "blue"
CONF_BOARD = "board" CONF_BOARD = "board"
CONF_BOARD_FLASH_MODE = "board_flash_mode" CONF_BOARD_FLASH_MODE = "board_flash_mode"
CONF_BORDER = "border"
CONF_BRANCH = "branch" CONF_BRANCH = "branch"
CONF_BRIGHTNESS = "brightness" CONF_BRIGHTNESS = "brightness"
CONF_BROKER = "broker" CONF_BROKER = "broker"
@ -327,6 +328,7 @@ CONF_LAMBDA = "lambda"
CONF_LAST_CONFIDENCE = "last_confidence" CONF_LAST_CONFIDENCE = "last_confidence"
CONF_LAST_FINGER_ID = "last_finger_id" CONF_LAST_FINGER_ID = "last_finger_id"
CONF_LATITUDE = "latitude" CONF_LATITUDE = "latitude"
CONF_LEGEND = "legend"
CONF_LENGTH = "length" CONF_LENGTH = "length"
CONF_LEVEL = "level" CONF_LEVEL = "level"
CONF_LG = "lg" CONF_LG = "lg"
@ -334,6 +336,8 @@ CONF_LIBRARIES = "libraries"
CONF_LIGHT = "light" CONF_LIGHT = "light"
CONF_LIGHTNING_ENERGY = "lightning_energy" CONF_LIGHTNING_ENERGY = "lightning_energy"
CONF_LIGHTNING_THRESHOLD = "lightning_threshold" CONF_LIGHTNING_THRESHOLD = "lightning_threshold"
CONF_LINE_THICKNESS = "line_thickness"
CONF_LINE_TYPE = "line_type"
CONF_LOADED_INTEGRATIONS = "loaded_integrations" CONF_LOADED_INTEGRATIONS = "loaded_integrations"
CONF_LOCAL = "local" CONF_LOCAL = "local"
CONF_LOG_TOPIC = "log_topic" CONF_LOG_TOPIC = "log_topic"
@ -355,6 +359,7 @@ CONF_MAX_HEATING_RUN_TIME = "max_heating_run_time"
CONF_MAX_LENGTH = "max_length" CONF_MAX_LENGTH = "max_length"
CONF_MAX_LEVEL = "max_level" CONF_MAX_LEVEL = "max_level"
CONF_MAX_POWER = "max_power" CONF_MAX_POWER = "max_power"
CONF_MAX_RANGE = "max_range"
CONF_MAX_REFRESH_RATE = "max_refresh_rate" CONF_MAX_REFRESH_RATE = "max_refresh_rate"
CONF_MAX_SPEED = "max_speed" CONF_MAX_SPEED = "max_speed"
CONF_MAX_TEMPERATURE = "max_temperature" CONF_MAX_TEMPERATURE = "max_temperature"
@ -376,6 +381,7 @@ CONF_MIN_IDLE_TIME = "min_idle_time"
CONF_MIN_LENGTH = "min_length" CONF_MIN_LENGTH = "min_length"
CONF_MIN_LEVEL = "min_level" CONF_MIN_LEVEL = "min_level"
CONF_MIN_POWER = "min_power" CONF_MIN_POWER = "min_power"
CONF_MIN_RANGE = "min_range"
CONF_MIN_TEMPERATURE = "min_temperature" CONF_MIN_TEMPERATURE = "min_temperature"
CONF_MIN_VALUE = "min_value" CONF_MIN_VALUE = "min_value"
CONF_MINUTE = "minute" CONF_MINUTE = "minute"
@ -393,6 +399,7 @@ CONF_MQTT_ID = "mqtt_id"
CONF_MULTIPLEXER = "multiplexer" CONF_MULTIPLEXER = "multiplexer"
CONF_MULTIPLY = "multiply" CONF_MULTIPLY = "multiply"
CONF_NAME = "name" CONF_NAME = "name"
CONF_NAME_FONT = "name_font"
CONF_NBITS = "nbits" CONF_NBITS = "nbits"
CONF_NEC = "nec" CONF_NEC = "nec"
CONF_NETWORKS = "networks" CONF_NETWORKS = "networks"
@ -584,6 +591,9 @@ CONF_SERVICES = "services"
CONF_SET_POINT_MINIMUM_DIFFERENTIAL = "set_point_minimum_differential" CONF_SET_POINT_MINIMUM_DIFFERENTIAL = "set_point_minimum_differential"
CONF_SETUP_MODE = "setup_mode" CONF_SETUP_MODE = "setup_mode"
CONF_SETUP_PRIORITY = "setup_priority" CONF_SETUP_PRIORITY = "setup_priority"
CONF_SHOW_LINES = "show_lines"
CONF_SHOW_UNITS = "show_units"
CONF_SHOW_VALUES = "show_values"
CONF_SHUNT_RESISTANCE = "shunt_resistance" CONF_SHUNT_RESISTANCE = "shunt_resistance"
CONF_SHUNT_VOLTAGE = "shunt_voltage" CONF_SHUNT_VOLTAGE = "shunt_voltage"
CONF_SHUTDOWN_MESSAGE = "shutdown_message" CONF_SHUTDOWN_MESSAGE = "shutdown_message"
@ -659,6 +669,7 @@ CONF_TOLERANCE = "tolerance"
CONF_TOPIC = "topic" CONF_TOPIC = "topic"
CONF_TOPIC_PREFIX = "topic_prefix" CONF_TOPIC_PREFIX = "topic_prefix"
CONF_TOTAL = "total" CONF_TOTAL = "total"
CONF_TRACES = "traces"
CONF_TRANSITION_LENGTH = "transition_length" CONF_TRANSITION_LENGTH = "transition_length"
CONF_TRIGGER_ID = "trigger_id" CONF_TRIGGER_ID = "trigger_id"
CONF_TRIGGER_PIN = "trigger_pin" CONF_TRIGGER_PIN = "trigger_pin"
@ -681,6 +692,7 @@ CONF_USE_ADDRESS = "use_address"
CONF_USERNAME = "username" CONF_USERNAME = "username"
CONF_UUID = "uuid" CONF_UUID = "uuid"
CONF_VALUE = "value" CONF_VALUE = "value"
CONF_VALUE_FONT = "value_font"
CONF_VARIABLES = "variables" CONF_VARIABLES = "variables"
CONF_VARIANT = "variant" CONF_VARIANT = "variant"
CONF_VERSION = "version" CONF_VERSION = "version"
@ -704,6 +716,8 @@ CONF_WILL_MESSAGE = "will_message"
CONF_WIND_DIRECTION_DEGREES = "wind_direction_degrees" CONF_WIND_DIRECTION_DEGREES = "wind_direction_degrees"
CONF_WIND_SPEED = "wind_speed" CONF_WIND_SPEED = "wind_speed"
CONF_WINDOW_SIZE = "window_size" CONF_WINDOW_SIZE = "window_size"
CONF_X_GRID = "x_grid"
CONF_Y_GRID = "y_grid"
CONF_ZERO = "zero" CONF_ZERO = "zero"
ENV_NOGITIGNORE = "ESPHOME_NOGITIGNORE" ENV_NOGITIGNORE = "ESPHOME_NOGITIGNORE"

View file

@ -19,6 +19,7 @@
#define USE_DEEP_SLEEP #define USE_DEEP_SLEEP
#define USE_ESP8266_PREFERENCES_FLASH #define USE_ESP8266_PREFERENCES_FLASH
#define USE_FAN #define USE_FAN
#define USE_GRAPH
#define USE_HOMEASSISTANT_TIME #define USE_HOMEASSISTANT_TIME
#define USE_HTTP_REQUEST_ESP8266_HTTPS #define USE_HTTP_REQUEST_ESP8266_HTTPS
#define USE_I2C_MULTIPLEXER #define USE_I2C_MULTIPLEXER