Skip to content

Commit

Permalink
add number entity to support vedirect configuration for numeric regis…
Browse files Browse the repository at this point in the history
…ters (m3_vedirect)
  • Loading branch information
krahabb committed Nov 14, 2024
1 parent cf37f27 commit d402f3c
Show file tree
Hide file tree
Showing 12 changed files with 226 additions and 34 deletions.
16 changes: 11 additions & 5 deletions esphome/components/m3_vedirect/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,16 @@
from . import ve_reg

CODEOWNERS = ["@krahabb"]
DEPENDENCIES = ["binary_sensor", "select", "sensor", "switch", "text_sensor", "uart"]
AUTO_LOAD = ["binary_sensor", "select", "sensor", "switch", "text_sensor"]
DEPENDENCIES = [
"binary_sensor",
"number",
"select",
"sensor",
"switch",
"text_sensor",
"uart",
]
AUTO_LOAD = ["binary_sensor", "number", "select", "sensor", "switch", "text_sensor"]
MULTI_CONF = True


Expand Down Expand Up @@ -171,9 +179,7 @@ def _entity_base_validator(config):
)


def vedirect_platform_schema(
platform_entities: dict[str, cv.Schema],
):
def vedirect_platform_schema(platform_entities: dict[str, cv.Schema]):
return VEDIRECT_PLATFORM_SCHEMA.extend(
{cv.Optional(type): schema for type, schema in platform_entities.items()}
)
Expand Down
13 changes: 7 additions & 6 deletions esphome/components/m3_vedirect/entity.cpp
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
#include "entity.h"
#include "esphome/core/application.h"
#include "esphome/core/log.h"
#include "esphome/components/api/api_server.h"

#include "manager.h"

namespace esphome {
namespace m3_vedirect {} // namespace m3_vedirect
namespace m3_vedirect {

const char *NumericEntity::UNIT_TO_DEVICE_CLASS[REG_DEF::UNIT::UNIT_COUNT] = {
nullptr, "current", "voltage", "apparent_power", "power", nullptr, "energy", "battery", "duration", "temperature",
};

} // namespace m3_vedirect
} // namespace esphome
19 changes: 13 additions & 6 deletions esphome/components/m3_vedirect/entity.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,21 @@ class Entity : public HexRegister {
virtual void dynamic_register_(){};
};

/// @brief Specialization for entities that can write configuration data to
/// the VEDirect interface
class ConfigEntity : public Entity {
/// @brief Mixin style specialization for entities that can write configuration data to
/// the VEDirect interface (Number, Select, Switch)
class ConfigEntity {
public:
Manager *const manager;
ConfigEntity(Manager *manager, parse_hex_func_t parse_hex_func = parse_hex_empty_,
parse_text_func_t parse_text_func = parse_text_empty_)
: Entity(parse_hex_func, parse_text_func), manager(manager) {}
ConfigEntity(Manager *manager) : manager(manager) {}
};

/// @brief Mixin style specialization for Number and Sensor entities.
class NumericEntity {
public:
static const char *UNIT_TO_DEVICE_CLASS[REG_DEF::UNIT::UNIT_COUNT];

protected:
float hex_scale_{1.};
};

} // namespace m3_vedirect
Expand Down
4 changes: 2 additions & 2 deletions esphome/components/m3_vedirect/manager.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include "manager.h"
#include "esphome/core/log.h"
#include "binary_sensor/binary_sensor.h"
#include "number/number.h"
#include "select/select.h"
#include "sensor/sensor.h"
#include "switch/switch.h"
Expand Down Expand Up @@ -264,8 +265,7 @@ HexRegister *Manager::build_hex_register_(register_id_t register_id) {
if (reg_def->access == REG_DEF::ACCESS::READ_ONLY) {
hexregister = this->dynamic_build_entity_<Sensor>(reg_def->label, reg_def->label);
} else {
// TODO: build a number entity
hexregister = this->dynamic_build_entity_<Sensor>(reg_def->label, reg_def->label);
hexregister = this->dynamic_build_entity_<Number>(reg_def->label, reg_def->label);
}
break;
case REG_DEF::CLASS::BOOLEAN:
Expand Down
53 changes: 53 additions & 0 deletions esphome/components/m3_vedirect/number/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from esphome.components import number
import esphome.config_validation as cv
import esphome.const as ec

from .. import (
CONF_VEDIRECT_ENTITIES,
m3_vedirect_ns,
new_vedirect_entity,
ve_reg,
vedirect_entity_schema,
vedirect_platform_schema,
vedirect_platform_to_code,
)

VEDirectNumber = m3_vedirect_ns.class_("Number", number.Number)
VEDIRECT_NUMBER_SCHEMA = (
number.number_schema(VEDirectNumber)
.extend(vedirect_entity_schema((ve_reg.CLASS.NUMERIC,), False))
.extend(
{
cv.Required(ec.CONF_MIN_VALUE): cv.float_,
cv.Required(ec.CONF_MAX_VALUE): cv.float_,
cv.Required(ec.CONF_STEP): cv.positive_float,
}
)
)

PLATFORM_ENTITIES = {
CONF_VEDIRECT_ENTITIES: cv.ensure_list(VEDIRECT_NUMBER_SCHEMA),
}

CONFIG_SCHEMA = vedirect_platform_schema(PLATFORM_ENTITIES)


async def new_vedirect_number(config, manager):
var = await new_vedirect_entity(config, manager)
await number.register_number(
var,
config,
min_value=config[ec.CONF_MIN_VALUE],
max_value=config[ec.CONF_MAX_VALUE],
step=config[ec.CONF_STEP],
)
return var


async def to_code(config: dict):
await vedirect_platform_to_code(
config,
PLATFORM_ENTITIES,
new_vedirect_number,
number.new_number,
)
90 changes: 90 additions & 0 deletions esphome/components/m3_vedirect/number/number.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#include "number.h"
#include "esphome/core/application.h"
#include "esphome/components/api/api_server.h"
#include "../manager.h"

namespace esphome {
namespace m3_vedirect {

void Number::dynamic_register_() {
App.register_number(this);
if (api::global_api_server)
add_on_state_callback([this](float state) { api::global_api_server->on_number_update(this, state); });
}

void Number::link_disconnected_() { this->publish_state(NAN); }

void Number::init_reg_def_() {
auto reg_def = this->reg_def_;
// Whatever the CLASS, number will just extract any meaningful numeric value
// from the HEX payload eventually scaling by hex_scale
this->hex_scale_ = REG_DEF::SCALE_TO_SCALE[reg_def->scale];
this->traits.set_unit_of_measurement(REG_DEF::UNITS[reg_def->unit]);
this->traits.set_device_class(UNIT_TO_DEVICE_CLASS[reg_def->unit]);
this->traits.set_step(this->hex_scale_);

switch (reg_def->unit) {
case REG_DEF::UNIT::CELSIUS:
// special treatment for 'temperature' registers which are expected to carry un16 kelvin degrees
this->parse_hex_ = parse_hex_temperature_;
break;
default:
this->parse_hex_ = DATA_TYPE_TO_PARSE_HEX_FUNC_[reg_def->data_type];
}
}

void Number::parse_hex_default_(HexRegister *hex_register, const RxHexFrame *hex_frame) {
Number *number = static_cast<Number *>(hex_register);
float value;
switch (hex_frame->data_size()) {
case 1:
value = hex_frame->data_t<uint8_t>() * number->hex_scale_;
break;
case 2:
// it might be signed though
value = hex_frame->data_t<uint16_t>() * number->hex_scale_;
break;
case 4:
value = hex_frame->data_t<uint32_t>() * number->hex_scale_;
break;
default:
value = NAN;
}
if (number->state != value) {
number->publish_state(value);
}
}

void Number::parse_hex_temperature_(HexRegister *hex_register, const RxHexFrame *hex_frame) {
Number *number = static_cast<Number *>(hex_register);
// hoping the operands are int-promoted and the result is an int
float value = (hex_frame->data_t<uint16_t>() - 27316) * number->hex_scale_;
if (number->state != value) {
number->publish_state(value);
}
}

template<typename T> void Number::parse_hex_t_(HexRegister *hex_register, const RxHexFrame *hex_frame) {
static_assert(RxHexFrame::ALLOCATED_DATA_SIZE >= 4, "HexFrame storage might lead to access overflow");
Number *number = static_cast<Number *>(hex_register);
float value = hex_frame->data_t<T>() * number->hex_scale_;
if (number->state != value) {
number->publish_state(value);
}
}

const Number::parse_hex_func_t Number::DATA_TYPE_TO_PARSE_HEX_FUNC_[REG_DEF::DATA_TYPE::_COUNT] = {
Number::parse_hex_default_, Number::parse_hex_t_<uint8_t>, Number::parse_hex_t_<uint16_t>,
Number::parse_hex_t_<uint32_t>, Number::parse_hex_t_<int8_t>, Number::parse_hex_t_<int16_t>,
Number::parse_hex_t_<int32_t>,
};

void Number::control(float value) {
// Assuming 'value' is not out of range of the underlying data type, this code
// should work for both signed/unsigned quantities
int native_value = value / this->hex_scale_;
this->manager->send_register_set(this->reg_def_->register_id, &native_value, this->reg_def_->data_type);
};

} // namespace m3_vedirect
} // namespace esphome
31 changes: 31 additions & 0 deletions esphome/components/m3_vedirect/number/number.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#pragma once
#include "esphome/components/number/number.h"

#include "../entity.h"

namespace esphome {
namespace m3_vedirect {

class Number final : public ConfigEntity, public NumericEntity, public Entity, public esphome::number::Number {
public:
Number(Manager *manager) : ConfigEntity(manager), Entity(parse_hex_default_, parse_text_empty_) {}

protected:
friend class Manager;
void dynamic_register_() override;
void link_disconnected_() override;

void init_reg_def_() override;

static void parse_hex_default_(HexRegister *hex_register, const RxHexFrame *hex_frame);
static void parse_hex_temperature_(HexRegister *hex_register, const RxHexFrame *hex_frame);
template<typename T> static void parse_hex_t_(HexRegister *hex_register, const RxHexFrame *hex_frame);
static const parse_hex_func_t DATA_TYPE_TO_PARSE_HEX_FUNC_[];

// interface esphome::number::Number

void control(float value) override;
};

} // namespace m3_vedirect
} // namespace esphome
4 changes: 2 additions & 2 deletions esphome/components/m3_vedirect/select/select.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
namespace esphome {
namespace m3_vedirect {

class Select final : public ConfigEntity, public esphome::select::Select {
class Select final : public ConfigEntity, public Entity, public esphome::select::Select {
public:
Select(Manager *manager) : ConfigEntity(manager, parse_hex_default_, parse_text_default_) {}
Select(Manager *manager) : ConfigEntity(manager), Entity(parse_hex_default_, parse_text_default_) {}

protected:
friend class Manager;
Expand Down
8 changes: 2 additions & 6 deletions esphome/components/m3_vedirect/sensor/sensor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@
namespace esphome {
namespace m3_vedirect {

const char *Sensor::UNIT_TO_DEVICE_CLASS[REG_DEF::UNIT::UNIT_COUNT] = {
nullptr, "current", "voltage", "apparent_power", "power", nullptr, "energy", "battery", "duration", "temperature",
};
const sensor::StateClass Sensor::UNIT_TO_STATE_CLASS[REG_DEF::UNIT::UNIT_COUNT] = {
sensor::StateClass::STATE_CLASS_NONE,
sensor::StateClass::STATE_CLASS_MEASUREMENT,
Expand Down Expand Up @@ -41,13 +38,12 @@ void Sensor::init_reg_def_() {
auto reg_def = this->reg_def_;
// Whatever the CLASS, sensor will just extract any meaningful numeric value
// from the HEX payload eventually scaling by hex_scale

this->set_unit_of_measurement(REG_DEF::UNITS[reg_def->unit]);
this->set_device_class(UNIT_TO_DEVICE_CLASS[reg_def->unit]);
this->set_state_class(UNIT_TO_STATE_CLASS[reg_def->unit]);
this->set_accuracy_decimals(SCALE_TO_DIGITS[reg_def->scale]);
this->set_hex_scale(REG_DEF::SCALE_TO_SCALE[reg_def->scale]);
this->set_text_scale(REG_DEF::SCALE_TO_SCALE[reg_def_->text_scale]);
this->hex_scale_ = REG_DEF::SCALE_TO_SCALE[reg_def->scale];
this->text_scale_ = REG_DEF::SCALE_TO_SCALE[reg_def_->text_scale];

switch (reg_def->unit) {
case REG_DEF::UNIT::CELSIUS:
Expand Down
9 changes: 4 additions & 5 deletions esphome/components/m3_vedirect/sensor/sensor.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,19 @@
namespace esphome {
namespace m3_vedirect {

class Sensor final : public Entity, public esphome::sensor::Sensor {
class Sensor final : public NumericEntity, public Entity, public esphome::sensor::Sensor {
public:
// configuration symbols for numeric sensors
static const char *UNIT_TO_DEVICE_CLASS[REG_DEF::UNIT::UNIT_COUNT];
static const sensor::StateClass UNIT_TO_STATE_CLASS[REG_DEF::UNIT::UNIT_COUNT];
static const uint8_t SCALE_TO_DIGITS[REG_DEF::SCALE::SCALE_COUNT];

Sensor(Manager *Manager) : Entity(parse_hex_default_, parse_text_default_) {}
void set_hex_scale(float scale) { this->hex_scale_ = scale; }
void set_text_scale(float scale) { this->text_scale_ = scale; }
// REMOVE void set_hex_scale(float scale) { this->hex_scale_ = scale; }
// REMOVE void set_text_scale(float scale) { this->text_scale_ = scale; }

protected:
friend class Manager;
float hex_scale_{1.};

float text_scale_{1.};

void dynamic_register_() override;
Expand Down
4 changes: 2 additions & 2 deletions esphome/components/m3_vedirect/switch/switch.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
namespace esphome {
namespace m3_vedirect {

class Switch final : public ConfigEntity, public esphome::switch_::Switch {
class Switch final : public ConfigEntity, public Entity, public esphome::switch_::Switch {
public:
Switch(Manager *manager) : ConfigEntity(manager, parse_hex_default_, parse_text_default_) {}
Switch(Manager *manager) : ConfigEntity(manager), Entity(parse_hex_default_, parse_text_default_) {}

void set_mask(uint32_t mask) { this->mask_ = mask; }

Expand Down
9 changes: 9 additions & 0 deletions esphome/components/m3_vedirect/ve_reg.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,18 +133,27 @@ class TYPE(MockEnum):
PRODUCT_ID = enum.auto()
SERIAL_NUMBER = enum.auto()
MODEL_NAME = enum.auto()
CAPABILITIES = enum.auto()
CAPABILITIES_BLE = enum.auto()
DEVICE_MODE = enum.auto()
DEVICE_STATE = enum.auto()
DEVICE_OFF_REASON = enum.auto()
DEVICE_OFF_REASON_2 = enum.auto()
AC_OUT_VOLTAGE_SETPOINT = enum.auto()
WARNING_REASON = enum.auto()
ALARM_REASON = enum.auto()
ALARM_LOW_VOLTAGE_SET = enum.auto()
ALARM_LOW_VOLTAGE_CLEAR = enum.auto()
RELAY_CONTROL = enum.auto()
RELAY_MODE = enum.auto()
TTG = enum.auto()
SOC = enum.auto()
AC_OUT_VOLTAGE = enum.auto()
AC_OUT_CURRENT = enum.auto()
AC_OUT_APPARENT_POWER = enum.auto()
SHUTDOWN_LOW_VOLTAGE_SET = enum.auto()
VOLTAGE_RANGE_MIN = enum.auto()
VOLTAGE_RANGE_MAX = enum.auto()
DC_CHANNEL1_VOLTAGE = enum.auto()
DC_CHANNEL1_POWER = enum.auto()
DC_CHANNEL1_CURRENT = enum.auto()
Expand Down

0 comments on commit d402f3c

Please sign in to comment.