diff --git a/esphome/components/m3_vedirect/__init__.py b/esphome/components/m3_vedirect/__init__.py index 0fb2dd014635..97d8f5ce574f 100644 --- a/esphome/components/m3_vedirect/__init__.py +++ b/esphome/components/m3_vedirect/__init__.py @@ -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 @@ -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()} ) diff --git a/esphome/components/m3_vedirect/entity.cpp b/esphome/components/m3_vedirect/entity.cpp index c0cf3a904438..4605c4802f37 100644 --- a/esphome/components/m3_vedirect/entity.cpp +++ b/esphome/components/m3_vedirect/entity.cpp @@ -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 diff --git a/esphome/components/m3_vedirect/entity.h b/esphome/components/m3_vedirect/entity.h index 981b54d1e38b..a85c8b779f5d 100644 --- a/esphome/components/m3_vedirect/entity.h +++ b/esphome/components/m3_vedirect/entity.h @@ -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 diff --git a/esphome/components/m3_vedirect/manager.cpp b/esphome/components/m3_vedirect/manager.cpp index fc792a2d92ef..61aee69afad6 100644 --- a/esphome/components/m3_vedirect/manager.cpp +++ b/esphome/components/m3_vedirect/manager.cpp @@ -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" @@ -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_(reg_def->label, reg_def->label); } else { - // TODO: build a number entity - hexregister = this->dynamic_build_entity_(reg_def->label, reg_def->label); + hexregister = this->dynamic_build_entity_(reg_def->label, reg_def->label); } break; case REG_DEF::CLASS::BOOLEAN: diff --git a/esphome/components/m3_vedirect/number/__init__.py b/esphome/components/m3_vedirect/number/__init__.py new file mode 100644 index 000000000000..86d4278f234b --- /dev/null +++ b/esphome/components/m3_vedirect/number/__init__.py @@ -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, + ) diff --git a/esphome/components/m3_vedirect/number/number.cpp b/esphome/components/m3_vedirect/number/number.cpp new file mode 100644 index 000000000000..81147fed4965 --- /dev/null +++ b/esphome/components/m3_vedirect/number/number.cpp @@ -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(hex_register); + float value; + switch (hex_frame->data_size()) { + case 1: + value = hex_frame->data_t() * number->hex_scale_; + break; + case 2: + // it might be signed though + value = hex_frame->data_t() * number->hex_scale_; + break; + case 4: + value = hex_frame->data_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(hex_register); + // hoping the operands are int-promoted and the result is an int + float value = (hex_frame->data_t() - 27316) * number->hex_scale_; + if (number->state != value) { + number->publish_state(value); + } +} + +template 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(hex_register); + float value = hex_frame->data_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_, Number::parse_hex_t_, + Number::parse_hex_t_, Number::parse_hex_t_, Number::parse_hex_t_, + Number::parse_hex_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 diff --git a/esphome/components/m3_vedirect/number/number.h b/esphome/components/m3_vedirect/number/number.h new file mode 100644 index 000000000000..1ba3617d47b2 --- /dev/null +++ b/esphome/components/m3_vedirect/number/number.h @@ -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 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 diff --git a/esphome/components/m3_vedirect/select/select.h b/esphome/components/m3_vedirect/select/select.h index 6d9d138ba16b..070479dd5090 100644 --- a/esphome/components/m3_vedirect/select/select.h +++ b/esphome/components/m3_vedirect/select/select.h @@ -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; diff --git a/esphome/components/m3_vedirect/sensor/sensor.cpp b/esphome/components/m3_vedirect/sensor/sensor.cpp index f55da3fa1a85..64f3b6b8133e 100644 --- a/esphome/components/m3_vedirect/sensor/sensor.cpp +++ b/esphome/components/m3_vedirect/sensor/sensor.cpp @@ -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, @@ -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: diff --git a/esphome/components/m3_vedirect/sensor/sensor.h b/esphome/components/m3_vedirect/sensor/sensor.h index 77621feedf77..5a1a029cca45 100644 --- a/esphome/components/m3_vedirect/sensor/sensor.h +++ b/esphome/components/m3_vedirect/sensor/sensor.h @@ -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; diff --git a/esphome/components/m3_vedirect/switch/switch.h b/esphome/components/m3_vedirect/switch/switch.h index 21b534f7c1e7..61a119e90521 100644 --- a/esphome/components/m3_vedirect/switch/switch.h +++ b/esphome/components/m3_vedirect/switch/switch.h @@ -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; } diff --git a/esphome/components/m3_vedirect/ve_reg.py b/esphome/components/m3_vedirect/ve_reg.py index 12a857157d30..f012d7e89b66 100644 --- a/esphome/components/m3_vedirect/ve_reg.py +++ b/esphome/components/m3_vedirect/ve_reg.py @@ -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()