From de10b356cfc7a26f7dca73bec25989cea4428e6f Mon Sep 17 00:00:00 2001 From: looping40 <36304961+looping40@users.noreply.github.com> Date: Mon, 1 May 2023 23:51:48 +0200 Subject: [PATCH] Max6956 support added (#3764) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/max6956/__init__.py | 148 +++++++++++++++ esphome/components/max6956/automation.h | 40 +++++ esphome/components/max6956/max6956.cpp | 170 ++++++++++++++++++ esphome/components/max6956/max6956.h | 94 ++++++++++ esphome/components/max6956/output/__init__.py | 28 +++ .../max6956/output/max6956_led_output.cpp | 26 +++ .../max6956/output/max6956_led_output.h | 28 +++ tests/test4.yaml | 14 ++ 9 files changed, 549 insertions(+) create mode 100644 esphome/components/max6956/__init__.py create mode 100644 esphome/components/max6956/automation.h create mode 100644 esphome/components/max6956/max6956.cpp create mode 100644 esphome/components/max6956/max6956.h create mode 100644 esphome/components/max6956/output/__init__.py create mode 100644 esphome/components/max6956/output/max6956_led_output.cpp create mode 100644 esphome/components/max6956/output/max6956_led_output.h diff --git a/CODEOWNERS b/CODEOWNERS index d71232ea07..3032e7dd88 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -140,6 +140,7 @@ esphome/components/ltr390/* @sjtrny esphome/components/matrix_keypad/* @ssieb esphome/components/max31865/* @DAVe3283 esphome/components/max44009/* @berfenger +esphome/components/max6956/* @looping40 esphome/components/max7219digit/* @rspaargaren esphome/components/max9611/* @mckaymatthew esphome/components/mcp23008/* @jesserockz diff --git a/esphome/components/max6956/__init__.py b/esphome/components/max6956/__init__.py new file mode 100644 index 0000000000..77e0d37e76 --- /dev/null +++ b/esphome/components/max6956/__init__.py @@ -0,0 +1,148 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins, automation +from esphome.components import i2c +from esphome.const import ( + CONF_ID, + CONF_NUMBER, + CONF_MODE, + CONF_INVERTED, + CONF_INPUT, + CONF_OUTPUT, + CONF_PULLUP, +) + +CODEOWNERS = ["@looping40"] + +DEPENDENCIES = ["i2c"] +MULTI_CONF = True + +CONF_BRIGHTNESS_MODE = "brightness_mode" +CONF_BRIGHTNESS_GLOBAL = "brightness_global" + + +max6956_ns = cg.esphome_ns.namespace("max6956") + +MAX6956 = max6956_ns.class_("MAX6956", cg.Component, i2c.I2CDevice) +MAX6956GPIOPin = max6956_ns.class_("MAX6956GPIOPin", cg.GPIOPin) + +# Actions +SetCurrentGlobalAction = max6956_ns.class_("SetCurrentGlobalAction", automation.Action) +SetCurrentModeAction = max6956_ns.class_("SetCurrentModeAction", automation.Action) + +MAX6956_CURRENTMODE = max6956_ns.enum("MAX6956CURRENTMODE") +CURRENT_MODES = { + "global": MAX6956_CURRENTMODE.GLOBAL, + "segment": MAX6956_CURRENTMODE.SEGMENT, +} + + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.Required(CONF_ID): cv.declare_id(MAX6956), + cv.Optional(CONF_BRIGHTNESS_GLOBAL, default="0"): cv.int_range( + min=0, max=15 + ), + cv.Optional(CONF_BRIGHTNESS_MODE, default="global"): cv.enum( + CURRENT_MODES, lower=True + ), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(i2c.i2c_device_schema(0x40)) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await i2c.register_i2c_device(var, config) + cg.add(var.set_brightness_mode(config[CONF_BRIGHTNESS_MODE])) + cg.add(var.set_brightness_global(config[CONF_BRIGHTNESS_GLOBAL])) + + +def validate_mode(value): + if not (value[CONF_INPUT] or value[CONF_OUTPUT]): + raise cv.Invalid("Mode must be either input or output") + if value[CONF_INPUT] and value[CONF_OUTPUT]: + raise cv.Invalid("Mode must be either input or output") + if value[CONF_PULLUP] and not value[CONF_INPUT]: + raise cv.Invalid("Pullup only available with input") + return value + + +CONF_MAX6956 = "max6956" + +MAX6956_PIN_SCHEMA = cv.All( + { + cv.GenerateID(): cv.declare_id(MAX6956GPIOPin), + cv.Required(CONF_MAX6956): cv.use_id(MAX6956), + cv.Required(CONF_NUMBER): cv.int_range(min=4, max=31), + cv.Optional(CONF_MODE, default={}): cv.All( + { + cv.Optional(CONF_INPUT, default=False): cv.boolean, + cv.Optional(CONF_PULLUP, default=False): cv.boolean, + cv.Optional(CONF_OUTPUT, default=False): cv.boolean, + }, + validate_mode, + ), + cv.Optional(CONF_INVERTED, default=False): cv.boolean, + } +) + + +@pins.PIN_SCHEMA_REGISTRY.register(CONF_MAX6956, MAX6956_PIN_SCHEMA) +async def max6956_pin_to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + parent = await cg.get_variable(config[CONF_MAX6956]) + + cg.add(var.set_parent(parent)) + + num = config[CONF_NUMBER] + cg.add(var.set_pin(num)) + cg.add(var.set_inverted(config[CONF_INVERTED])) + cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) + return var + + +@automation.register_action( + "max6956.set_brightness_global", + SetCurrentGlobalAction, + cv.maybe_simple_value( + { + cv.GenerateID(CONF_ID): cv.use_id(MAX6956), + cv.Required(CONF_BRIGHTNESS_GLOBAL): cv.templatable( + cv.int_range(min=0, max=15) + ), + }, + key=CONF_BRIGHTNESS_GLOBAL, + ), +) +async def max6956_set_brightness_global_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = await cg.templatable(config[CONF_BRIGHTNESS_GLOBAL], args, float) + cg.add(var.set_brightness_global(template_)) + return var + + +@automation.register_action( + "max6956.set_brightness_mode", + SetCurrentModeAction, + cv.maybe_simple_value( + { + cv.Required(CONF_ID): cv.use_id(MAX6956), + cv.Required(CONF_BRIGHTNESS_MODE): cv.templatable( + cv.enum(CURRENT_MODES, lower=True) + ), + }, + key=CONF_BRIGHTNESS_MODE, + ), +) +async def max6956_set_brightness_mode_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = await cg.templatable(config[CONF_BRIGHTNESS_MODE], args, float) + cg.add(var.set_brightness_mode(template_)) + return var diff --git a/esphome/components/max6956/automation.h b/esphome/components/max6956/automation.h new file mode 100644 index 0000000000..c0b491dc7f --- /dev/null +++ b/esphome/components/max6956/automation.h @@ -0,0 +1,40 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/max6956/max6956.h" + +namespace esphome { +namespace max6956 { + +template class SetCurrentGlobalAction : public Action { + public: + SetCurrentGlobalAction(MAX6956 *max6956) : max6956_(max6956) {} + + TEMPLATABLE_VALUE(uint8_t, brightness_global) + + void play(Ts... x) override { + this->max6956_->set_brightness_global(this->brightness_global_.value(x...)); + this->max6956_->write_brightness_global(); + } + + protected: + MAX6956 *max6956_; +}; + +template class SetCurrentModeAction : public Action { + public: + SetCurrentModeAction(MAX6956 *max6956) : max6956_(max6956) {} + + TEMPLATABLE_VALUE(max6956::MAX6956CURRENTMODE, brightness_mode) + + void play(Ts... x) override { + this->max6956_->set_brightness_mode(this->brightness_mode_.value(x...)); + this->max6956_->write_brightness_mode(); + } + + protected: + MAX6956 *max6956_; +}; +} // namespace max6956 +} // namespace esphome diff --git a/esphome/components/max6956/max6956.cpp b/esphome/components/max6956/max6956.cpp new file mode 100644 index 0000000000..c2d9ba0175 --- /dev/null +++ b/esphome/components/max6956/max6956.cpp @@ -0,0 +1,170 @@ +#include "max6956.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace max6956 { + +static const char *const TAG = "max6956"; + +/// Masks for MAX6956 Configuration register +const uint32_t MASK_TRANSITION_DETECTION = 0x80; +const uint32_t MASK_INDIVIDUAL_CURRENT = 0x40; +const uint32_t MASK_NORMAL_OPERATION = 0x01; + +const uint32_t MASK_1PORT_VALUE = 0x03; +const uint32_t MASK_PORT_CONFIG = 0x03; +const uint8_t MASK_CONFIG_CURRENT = 0x40; +const uint8_t MASK_CURRENT_PIN = 0x0F; + +/************************************** + * MAX6956 * + **************************************/ +void MAX6956::setup() { + ESP_LOGCONFIG(TAG, "Setting up MAX6956..."); + uint8_t configuration; + if (!this->read_reg_(MAX6956_CONFIGURATION, &configuration)) { + this->mark_failed(); + return; + } + + write_brightness_global(); + write_brightness_mode(); + + /** TO DO : read transition detection in yaml + TO DO : read indivdual current in yaml **/ + this->read_reg_(MAX6956_CONFIGURATION, &configuration); + ESP_LOGD(TAG, "Initial reg[0x%.2X]=0x%.2X", MAX6956_CONFIGURATION, configuration); + configuration = configuration | MASK_NORMAL_OPERATION; + this->write_reg_(MAX6956_CONFIGURATION, configuration); + + ESP_LOGCONFIG(TAG, "Enabling normal operation"); + ESP_LOGD(TAG, "setup reg[0x%.2X]=0x%.2X", MAX6956_CONFIGURATION, configuration); +} + +bool MAX6956::digital_read(uint8_t pin) { + uint8_t reg_addr = MAX6956_1PORT_VALUE_START + pin; + uint8_t value = 0; + this->read_reg_(reg_addr, &value); + return (value & MASK_1PORT_VALUE); +} + +void MAX6956::digital_write(uint8_t pin, bool value) { + uint8_t reg_addr = MAX6956_1PORT_VALUE_START + pin; + this->write_reg_(reg_addr, value); +} + +void MAX6956::pin_mode(uint8_t pin, gpio::Flags flags) { + uint8_t reg_addr = MAX6956_PORT_CONFIG_START + (pin - MAX6956_MIN) / 4; + uint8_t config = 0; + uint8_t shift = 2 * (pin % 4); + MAX6956GPIOMode mode = MAX6956_INPUT; + + if (flags == gpio::FLAG_INPUT) { + mode = MAX6956GPIOMode::MAX6956_INPUT; + } else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_PULLUP)) { + mode = MAX6956GPIOMode::MAX6956_INPUT_PULLUP; + } else if (flags == gpio::FLAG_OUTPUT) { + mode = MAX6956GPIOMode::MAX6956_OUTPUT; + } + + this->read_reg_(reg_addr, &config); + config &= ~(MASK_PORT_CONFIG << shift); + config |= (mode << shift); + this->write_reg_(reg_addr, config); +} + +void MAX6956::pin_mode(uint8_t pin, max6956::MAX6956GPIOFlag flags) { + uint8_t reg_addr = MAX6956_PORT_CONFIG_START + (pin - MAX6956_MIN) / 4; + uint8_t config = 0; + uint8_t shift = 2 * (pin % 4); + MAX6956GPIOMode mode = MAX6956GPIOMode::MAX6956_LED; + + if (flags == max6956::FLAG_LED) { + mode = MAX6956GPIOMode::MAX6956_LED; + } + + this->read_reg_(reg_addr, &config); + config &= ~(MASK_PORT_CONFIG << shift); + config |= (mode << shift); + this->write_reg_(reg_addr, config); +} + +void MAX6956::set_brightness_global(uint8_t current) { + if (current > 15) { + ESP_LOGE(TAG, "Global brightness out off range (%u)", current); + return; + } + global_brightness_ = current; +} + +void MAX6956::write_brightness_global() { this->write_reg_(MAX6956_GLOBAL_CURRENT, global_brightness_); } + +void MAX6956::set_brightness_mode(max6956::MAX6956CURRENTMODE brightness_mode) { brightness_mode_ = brightness_mode; }; + +void MAX6956::write_brightness_mode() { + uint8_t reg_addr = MAX6956_CONFIGURATION; + uint8_t config = 0; + + this->read_reg_(reg_addr, &config); + config &= ~MASK_CONFIG_CURRENT; + config |= brightness_mode_ << 6; + this->write_reg_(reg_addr, config); +} + +void MAX6956::set_pin_brightness(uint8_t pin, float brightness) { + uint8_t reg_addr = MAX6956_CURRENT_START + (pin - MAX6956_MIN) / 2; + uint8_t config = 0; + uint8_t shift = 4 * (pin % 2); + uint8_t bright = roundf(brightness * 15); + + if (prev_bright_[pin - MAX6956_MIN] == bright) + return; + + prev_bright_[pin - MAX6956_MIN] = bright; + + this->read_reg_(reg_addr, &config); + config &= ~(MASK_CURRENT_PIN << shift); + config |= (bright << shift); + this->write_reg_(reg_addr, config); +} + +bool MAX6956::read_reg_(uint8_t reg, uint8_t *value) { + if (this->is_failed()) + return false; + + return this->read_byte(reg, value); +} + +bool MAX6956::write_reg_(uint8_t reg, uint8_t value) { + if (this->is_failed()) + return false; + + return this->write_byte(reg, value); +} + +void MAX6956::dump_config() { + ESP_LOGCONFIG(TAG, "MAX6956"); + + if (brightness_mode_ == MAX6956CURRENTMODE::GLOBAL) { + ESP_LOGCONFIG(TAG, "current mode: global"); + ESP_LOGCONFIG(TAG, "global brightness: %u", global_brightness_); + } else { + ESP_LOGCONFIG(TAG, "current mode: segment"); + } +} + +/************************************** + * MAX6956GPIOPin * + **************************************/ +void MAX6956GPIOPin::setup() { pin_mode(flags_); } +void MAX6956GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } +bool MAX6956GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } +void MAX6956GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } +std::string MAX6956GPIOPin::dump_summary() const { + char buffer[32]; + snprintf(buffer, sizeof(buffer), "%u via Max6956", pin_); + return buffer; +} + +} // namespace max6956 +} // namespace esphome diff --git a/esphome/components/max6956/max6956.h b/esphome/components/max6956/max6956.h new file mode 100644 index 0000000000..141164ab30 --- /dev/null +++ b/esphome/components/max6956/max6956.h @@ -0,0 +1,94 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace max6956 { + +/// Modes for MAX6956 pins +enum MAX6956GPIOMode : uint8_t { + MAX6956_LED = 0x00, + MAX6956_OUTPUT = 0x01, + MAX6956_INPUT = 0x02, + MAX6956_INPUT_PULLUP = 0x03 +}; + +/// Range for MAX6956 pins +enum MAX6956GPIORange : uint8_t { + MAX6956_MIN = 4, + MAX6956_MAX = 31, +}; + +enum MAX6956GPIORegisters { + MAX6956_GLOBAL_CURRENT = 0x02, + MAX6956_CONFIGURATION = 0x04, + MAX6956_TRANSITION_DETECT_MASK = 0x06, + MAX6956_DISPLAY_TEST = 0x07, + MAX6956_PORT_CONFIG_START = 0x09, // Port Configuration P7, P6, P5, P4 + MAX6956_CURRENT_START = 0x12, // Current054 + MAX6956_1PORT_VALUE_START = 0x20, // Port 0 only (virtual port, no action) + MAX6956_8PORTS_VALUE_START = 0x44, // 8 ports 4–11 (data bits D0–D7) +}; + +enum MAX6956GPIOFlag { FLAG_LED = 0x20 }; + +enum MAX6956CURRENTMODE { GLOBAL = 0x00, SEGMENT = 0x01 }; + +class MAX6956 : public Component, public i2c::I2CDevice { + public: + MAX6956() = default; + + void setup() override; + + bool digital_read(uint8_t pin); + void digital_write(uint8_t pin, bool value); + void pin_mode(uint8_t pin, gpio::Flags flags); + void pin_mode(uint8_t pin, max6956::MAX6956GPIOFlag flags); + + float get_setup_priority() const override { return setup_priority::HARDWARE; } + + void set_brightness_global(uint8_t current); + void set_brightness_mode(max6956::MAX6956CURRENTMODE brightness_mode); + void set_pin_brightness(uint8_t pin, float brightness); + + void dump_config() override; + + void write_brightness_global(); + void write_brightness_mode(); + + protected: + // read a given register + bool read_reg_(uint8_t reg, uint8_t *value); + // write a value to a given register + bool write_reg_(uint8_t reg, uint8_t value); + max6956::MAX6956CURRENTMODE brightness_mode_; + uint8_t global_brightness_; + + private: + int8_t prev_bright_[28] = {0}; +}; + +class MAX6956GPIOPin : public GPIOPin { + public: + void setup() override; + void pin_mode(gpio::Flags flags) override; + bool digital_read() override; + void digital_write(bool value) override; + std::string dump_summary() const override; + + void set_parent(MAX6956 *parent) { parent_ = parent; } + void set_pin(uint8_t pin) { pin_ = pin; } + void set_inverted(bool inverted) { inverted_ = inverted; } + void set_flags(gpio::Flags flags) { flags_ = flags; } + + protected: + MAX6956 *parent_; + uint8_t pin_; + bool inverted_; + gpio::Flags flags_; +}; + +} // namespace max6956 +} // namespace esphome diff --git a/esphome/components/max6956/output/__init__.py b/esphome/components/max6956/output/__init__.py new file mode 100644 index 0000000000..1caf8c8a44 --- /dev/null +++ b/esphome/components/max6956/output/__init__.py @@ -0,0 +1,28 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import output +from esphome.const import CONF_PIN, CONF_ID +from .. import MAX6956, max6956_ns, CONF_MAX6956 + +DEPENDENCIES = ["max6956"] + +MAX6956LedChannel = max6956_ns.class_( + "MAX6956LedChannel", output.FloatOutput, cg.Component +) + +CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( + { + cv.Required(CONF_ID): cv.declare_id(MAX6956LedChannel), + cv.GenerateID(CONF_MAX6956): cv.use_id(MAX6956), + cv.Required(CONF_PIN): cv.int_range(min=4, max=31), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + parent = await cg.get_variable(config[CONF_MAX6956]) + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await output.register_output(var, config) + cg.add(var.set_pin(config[CONF_PIN])) + cg.add(var.set_parent(parent)) diff --git a/esphome/components/max6956/output/max6956_led_output.cpp b/esphome/components/max6956/output/max6956_led_output.cpp new file mode 100644 index 0000000000..5fa2dd9b34 --- /dev/null +++ b/esphome/components/max6956/output/max6956_led_output.cpp @@ -0,0 +1,26 @@ +#include "max6956_led_output.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace max6956 { + +static const char *const TAG = "max6956_led_channel"; + +void MAX6956LedChannel::write_state(float state) { this->parent_->set_pin_brightness(this->pin_, state); } + +void MAX6956LedChannel::write_state(bool state) { this->parent_->digital_write(this->pin_, state); } + +void MAX6956LedChannel::setup() { + this->parent_->pin_mode(this->pin_, max6956::FLAG_LED); + this->turn_off(); +} + +void MAX6956LedChannel::dump_config() { + ESP_LOGCONFIG(TAG, "MAX6956 current:"); + ESP_LOGCONFIG(TAG, " MAX6956 pin: %d", this->pin_); + LOG_FLOAT_OUTPUT(this); +} + +} // namespace max6956 +} // namespace esphome diff --git a/esphome/components/max6956/output/max6956_led_output.h b/esphome/components/max6956/output/max6956_led_output.h new file mode 100644 index 0000000000..b844a7ceee --- /dev/null +++ b/esphome/components/max6956/output/max6956_led_output.h @@ -0,0 +1,28 @@ +#pragma once + +#include "esphome/components/max6956/max6956.h" +#include "esphome/components/output/float_output.h" + +namespace esphome { +namespace max6956 { + +class MAX6956; + +class MAX6956LedChannel : public output::FloatOutput, public Component { + public: + void set_parent(MAX6956 *parent) { this->parent_ = parent; } + void set_pin(uint8_t pin) { pin_ = pin; } + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::HARDWARE; } + + protected: + void write_state(float state) override; + void write_state(bool state) override; + + MAX6956 *parent_; + uint8_t pin_; +}; + +} // namespace max6956 +} // namespace esphome diff --git a/tests/test4.yaml b/tests/test4.yaml index 7b8f139a43..04d6f4678e 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -374,6 +374,16 @@ binary_sensor: on_press: - logger.log: Touched + - platform: gpio + name: MaxIn Pin 4 + pin: + max6956: max6956_1 + number: 4 + mode: + input: true + pullup: true + inverted: false + climate: - platform: tuya id: tuya_climate @@ -716,3 +726,7 @@ voice_assistant: - logger.log: format: "Voice assistant error - code %s, message: %s" args: [code.c_str(), message.c_str()] + +max6956: + - id: max6956_1 + address: 0x40