mirror of
https://github.com/esphome/esphome.git
synced 2024-11-25 00:18:11 +01:00
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:
parent
fff5ba03c2
commit
945ed5d3bd
8 changed files with 806 additions and 0 deletions
|
@ -54,6 +54,7 @@ esphome/components/fingerprint_grow/* @OnFreund @loongyh
|
|||
esphome/components/globals/* @esphome/core
|
||||
esphome/components/gpio/* @esphome/core
|
||||
esphome/components/gps/* @coogle
|
||||
esphome/components/graph/* @synco
|
||||
esphome/components/havells_solar/* @sourabhjaiswal
|
||||
esphome/components/hbridge/fan/* @WeekendWarrior
|
||||
esphome/components/hbridge/light/* @DotNetDann
|
||||
|
|
|
@ -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,
|
||||
int *width, int *height) {
|
||||
int x_offset, baseline;
|
||||
|
|
|
@ -9,6 +9,10 @@
|
|||
#include "esphome/components/time/real_time_clock.h"
|
||||
#endif
|
||||
|
||||
#ifdef USE_GRAPH
|
||||
#include "esphome/components/graph/graph.h"
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
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);
|
||||
|
||||
#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.
|
||||
*
|
||||
* @param x The x coordinate to place the string at, can be 0 if only interested in dimensions.
|
||||
|
|
216
esphome/components/graph/__init__.py
Normal file
216
esphome/components/graph/__init__.py
Normal 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")
|
361
esphome/components/graph/graph.cpp
Normal file
361
esphome/components/graph/graph.cpp
Normal 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
|
178
esphome/components/graph/graph.h
Normal file
178
esphome/components/graph/graph.h
Normal 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
|
|
@ -90,6 +90,7 @@ CONF_BIT_DEPTH = "bit_depth"
|
|||
CONF_BLUE = "blue"
|
||||
CONF_BOARD = "board"
|
||||
CONF_BOARD_FLASH_MODE = "board_flash_mode"
|
||||
CONF_BORDER = "border"
|
||||
CONF_BRANCH = "branch"
|
||||
CONF_BRIGHTNESS = "brightness"
|
||||
CONF_BROKER = "broker"
|
||||
|
@ -327,6 +328,7 @@ CONF_LAMBDA = "lambda"
|
|||
CONF_LAST_CONFIDENCE = "last_confidence"
|
||||
CONF_LAST_FINGER_ID = "last_finger_id"
|
||||
CONF_LATITUDE = "latitude"
|
||||
CONF_LEGEND = "legend"
|
||||
CONF_LENGTH = "length"
|
||||
CONF_LEVEL = "level"
|
||||
CONF_LG = "lg"
|
||||
|
@ -334,6 +336,8 @@ CONF_LIBRARIES = "libraries"
|
|||
CONF_LIGHT = "light"
|
||||
CONF_LIGHTNING_ENERGY = "lightning_energy"
|
||||
CONF_LIGHTNING_THRESHOLD = "lightning_threshold"
|
||||
CONF_LINE_THICKNESS = "line_thickness"
|
||||
CONF_LINE_TYPE = "line_type"
|
||||
CONF_LOADED_INTEGRATIONS = "loaded_integrations"
|
||||
CONF_LOCAL = "local"
|
||||
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_LEVEL = "max_level"
|
||||
CONF_MAX_POWER = "max_power"
|
||||
CONF_MAX_RANGE = "max_range"
|
||||
CONF_MAX_REFRESH_RATE = "max_refresh_rate"
|
||||
CONF_MAX_SPEED = "max_speed"
|
||||
CONF_MAX_TEMPERATURE = "max_temperature"
|
||||
|
@ -376,6 +381,7 @@ CONF_MIN_IDLE_TIME = "min_idle_time"
|
|||
CONF_MIN_LENGTH = "min_length"
|
||||
CONF_MIN_LEVEL = "min_level"
|
||||
CONF_MIN_POWER = "min_power"
|
||||
CONF_MIN_RANGE = "min_range"
|
||||
CONF_MIN_TEMPERATURE = "min_temperature"
|
||||
CONF_MIN_VALUE = "min_value"
|
||||
CONF_MINUTE = "minute"
|
||||
|
@ -393,6 +399,7 @@ CONF_MQTT_ID = "mqtt_id"
|
|||
CONF_MULTIPLEXER = "multiplexer"
|
||||
CONF_MULTIPLY = "multiply"
|
||||
CONF_NAME = "name"
|
||||
CONF_NAME_FONT = "name_font"
|
||||
CONF_NBITS = "nbits"
|
||||
CONF_NEC = "nec"
|
||||
CONF_NETWORKS = "networks"
|
||||
|
@ -584,6 +591,9 @@ CONF_SERVICES = "services"
|
|||
CONF_SET_POINT_MINIMUM_DIFFERENTIAL = "set_point_minimum_differential"
|
||||
CONF_SETUP_MODE = "setup_mode"
|
||||
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_VOLTAGE = "shunt_voltage"
|
||||
CONF_SHUTDOWN_MESSAGE = "shutdown_message"
|
||||
|
@ -659,6 +669,7 @@ CONF_TOLERANCE = "tolerance"
|
|||
CONF_TOPIC = "topic"
|
||||
CONF_TOPIC_PREFIX = "topic_prefix"
|
||||
CONF_TOTAL = "total"
|
||||
CONF_TRACES = "traces"
|
||||
CONF_TRANSITION_LENGTH = "transition_length"
|
||||
CONF_TRIGGER_ID = "trigger_id"
|
||||
CONF_TRIGGER_PIN = "trigger_pin"
|
||||
|
@ -681,6 +692,7 @@ CONF_USE_ADDRESS = "use_address"
|
|||
CONF_USERNAME = "username"
|
||||
CONF_UUID = "uuid"
|
||||
CONF_VALUE = "value"
|
||||
CONF_VALUE_FONT = "value_font"
|
||||
CONF_VARIABLES = "variables"
|
||||
CONF_VARIANT = "variant"
|
||||
CONF_VERSION = "version"
|
||||
|
@ -704,6 +716,8 @@ CONF_WILL_MESSAGE = "will_message"
|
|||
CONF_WIND_DIRECTION_DEGREES = "wind_direction_degrees"
|
||||
CONF_WIND_SPEED = "wind_speed"
|
||||
CONF_WINDOW_SIZE = "window_size"
|
||||
CONF_X_GRID = "x_grid"
|
||||
CONF_Y_GRID = "y_grid"
|
||||
CONF_ZERO = "zero"
|
||||
|
||||
ENV_NOGITIGNORE = "ESPHOME_NOGITIGNORE"
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
#define USE_DEEP_SLEEP
|
||||
#define USE_ESP8266_PREFERENCES_FLASH
|
||||
#define USE_FAN
|
||||
#define USE_GRAPH
|
||||
#define USE_HOMEASSISTANT_TIME
|
||||
#define USE_HTTP_REQUEST_ESP8266_HTTPS
|
||||
#define USE_I2C_MULTIPLEXER
|
||||
|
|
Loading…
Reference in a new issue