From d2c7afeef062537449d29d08c3086a544aea64f8 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sun, 20 Oct 2019 19:24:20 +0200 Subject: [PATCH] Add PZEM004T/PZEMAC/PZEMDC Support (#587) * Add PZEM004T Support * Don't flush as much * Update pzem004t.cpp * Add generalized modbus * Add PZEMAC * Add PZEMDC * Fix file modes * Lint * Fix * Fix * Add check_uart_settings --- esphome/components/cse7766/cse7766.cpp | 1 + esphome/components/mhz19/mhz19.cpp | 1 + esphome/components/modbus/__init__.py | 43 ++++++++ esphome/components/modbus/modbus.cpp | 119 +++++++++++++++++++++++ esphome/components/modbus/modbus.h | 51 ++++++++++ esphome/components/pmsx003/pmsx003.cpp | 1 + esphome/components/pzem004t/__init__.py | 0 esphome/components/pzem004t/pzem004t.cpp | 103 ++++++++++++++++++++ esphome/components/pzem004t/pzem004t.h | 39 ++++++++ esphome/components/pzem004t/sensor.py | 37 +++++++ esphome/components/pzemac/__init__.py | 0 esphome/components/pzemac/pzemac.cpp | 62 ++++++++++++ esphome/components/pzemac/pzemac.h | 31 ++++++ esphome/components/pzemac/sensor.py | 47 +++++++++ esphome/components/pzemdc/__init__.py | 0 esphome/components/pzemdc/pzemdc.cpp | 52 ++++++++++ esphome/components/pzemdc/pzemdc.h | 31 ++++++ esphome/components/pzemdc/sensor.py | 36 +++++++ esphome/components/sds011/sds011.cpp | 1 + esphome/components/senseair/senseair.cpp | 1 + esphome/components/tuya/tuya.cpp | 1 + esphome/components/uart/__init__.py | 3 + esphome/components/uart/uart.cpp | 39 ++++++-- esphome/components/uart/uart.h | 19 +++- esphome/const.py | 2 + tests/test3.yaml | 25 +++++ 26 files changed, 738 insertions(+), 7 deletions(-) create mode 100644 esphome/components/modbus/__init__.py create mode 100644 esphome/components/modbus/modbus.cpp create mode 100644 esphome/components/modbus/modbus.h create mode 100644 esphome/components/pzem004t/__init__.py create mode 100644 esphome/components/pzem004t/pzem004t.cpp create mode 100644 esphome/components/pzem004t/pzem004t.h create mode 100644 esphome/components/pzem004t/sensor.py create mode 100644 esphome/components/pzemac/__init__.py create mode 100644 esphome/components/pzemac/pzemac.cpp create mode 100644 esphome/components/pzemac/pzemac.h create mode 100644 esphome/components/pzemac/sensor.py create mode 100644 esphome/components/pzemdc/__init__.py create mode 100644 esphome/components/pzemdc/pzemdc.cpp create mode 100644 esphome/components/pzemdc/pzemdc.h create mode 100644 esphome/components/pzemdc/sensor.py diff --git a/esphome/components/cse7766/cse7766.cpp b/esphome/components/cse7766/cse7766.cpp index 358453a63a..6c014138fd 100644 --- a/esphome/components/cse7766/cse7766.cpp +++ b/esphome/components/cse7766/cse7766.cpp @@ -172,6 +172,7 @@ void CSE7766Component::dump_config() { LOG_SENSOR(" ", "Voltage", this->voltage_sensor_); LOG_SENSOR(" ", "Current", this->current_sensor_); LOG_SENSOR(" ", "Power", this->power_sensor_); + this->check_uart_settings(4800); } } // namespace cse7766 diff --git a/esphome/components/mhz19/mhz19.cpp b/esphome/components/mhz19/mhz19.cpp index 36ccf70d84..8e28d04dea 100644 --- a/esphome/components/mhz19/mhz19.cpp +++ b/esphome/components/mhz19/mhz19.cpp @@ -94,6 +94,7 @@ void MHZ19Component::dump_config() { ESP_LOGCONFIG(TAG, "MH-Z19:"); LOG_SENSOR(" ", "CO2", this->co2_sensor_); LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + this->check_uart_settings(9600); if (this->abc_boot_logic_ == MHZ19_ABC_ENABLED) { ESP_LOGCONFIG(TAG, " Automatic baseline calibration enabled on boot"); diff --git a/esphome/components/modbus/__init__.py b/esphome/components/modbus/__init__.py new file mode 100644 index 0000000000..88105b7baf --- /dev/null +++ b/esphome/components/modbus/__init__.py @@ -0,0 +1,43 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import uart +from esphome.const import CONF_ID, CONF_ADDRESS +from esphome.core import coroutine + +DEPENDENCIES = ['uart'] + +modbus_ns = cg.esphome_ns.namespace('modbus') +Modbus = modbus_ns.class_('Modbus', cg.Component, uart.UARTDevice) +ModbusDevice = modbus_ns.class_('ModbusDevice') +MULTI_CONF = True + +CONF_MODBUS_ID = 'modbus_id' +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(Modbus), +}).extend(cv.COMPONENT_SCHEMA).extend(uart.UART_DEVICE_SCHEMA) + + +def to_code(config): + cg.add_global(modbus_ns.using) + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + + yield uart.register_uart_device(var, config) + + +def modbus_device_schema(default_address): + schema = { + cv.GenerateID(CONF_MODBUS_ID): cv.use_id(Modbus), + } + if default_address is None: + schema[cv.Required(CONF_ADDRESS)] = cv.hex_uint8_t + else: + schema[cv.Optional(CONF_ADDRESS, default=default_address)] = cv.hex_uint8_t + return cv.Schema(schema) + + +@coroutine +def register_modbus_device(var, config): + parent = yield cg.get_variable(config[CONF_MODBUS_ID]) + cg.add(var.set_parent(parent)) + cg.add(var.set_address(config[CONF_ADDRESS])) diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp new file mode 100644 index 0000000000..92505c6429 --- /dev/null +++ b/esphome/components/modbus/modbus.cpp @@ -0,0 +1,119 @@ +#include "modbus.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace modbus { + +static const char *TAG = "modbus"; + +void Modbus::loop() { + const uint32_t now = millis(); + if (now - this->last_modbus_byte_ > 50) { + this->rx_buffer_.clear(); + this->last_modbus_byte_ = now; + } + + while (this->available()) { + uint8_t byte; + this->read_byte(&byte); + if (this->parse_modbus_byte_(byte)) { + this->last_modbus_byte_ = now; + } else { + this->rx_buffer_.clear(); + } + } +} + +uint16_t crc16(uint8_t *data, uint8_t len) { + uint16_t crc = 0xFFFF; + while (len--) { + crc ^= *data++; + for (uint8_t i = 0; i < 8; i++) { + if ((crc & 0x01) != 0) { + crc >>= 1; + crc ^= 0xA001; + } else { + crc >>= 1; + } + } + } + return crc; +} + +bool Modbus::parse_modbus_byte_(uint8_t byte) { + size_t at = this->rx_buffer_.size(); + this->rx_buffer_.push_back(byte); + uint8_t *raw = &this->rx_buffer_[0]; + + // Byte 0: modbus address (match all) + if (at == 0) + return true; + uint8_t address = raw[0]; + + // Byte 1: Function (msb indicates error) + if (at == 1) + return (byte & 0x80) != 0x80; + + // Byte 2: Size (with modbus rtu function code 4/3) + // See also https://en.wikipedia.org/wiki/Modbus + if (at == 2) + return true; + + uint8_t data_len = raw[2]; + // Byte 3..3+data_len-1: Data + if (at < 3 + data_len) + return true; + + // Byte 3+data_len: CRC_LO (over all bytes) + if (at == 3 + data_len) + return true; + // Byte 3+len+1: CRC_HI (over all bytes) + uint16_t computed_crc = crc16(raw, 3 + data_len); + uint16_t remote_crc = uint16_t(raw[3 + data_len]) | (uint16_t(raw[3 + data_len]) << 8); + if (computed_crc != remote_crc) { + ESP_LOGW(TAG, "Modbus CRC Check failed! %02X!=%02X", computed_crc, remote_crc); + return false; + } + + std::vector data(this->rx_buffer_.begin() + 3, this->rx_buffer_.begin() + 3 + data_len); + + bool found = false; + for (auto *device : this->devices_) { + if (device->address_ == address) { + device->on_modbus_data(data); + found = true; + } + } + if (!found) { + ESP_LOGW(TAG, "Got Modbus frame from unknown address 0x%02X!", address); + } + + // return false to reset buffer + return false; +} + +void Modbus::dump_config() { + ESP_LOGCONFIG(TAG, "Modbus:"); + this->check_uart_settings(9600, 2); +} +float Modbus::get_setup_priority() const { + // After UART bus + return setup_priority::BUS - 1.0f; +} +void Modbus::send(uint8_t address, uint8_t function, uint16_t start_address, uint16_t register_count) { + uint8_t frame[8]; + frame[0] = address; + frame[1] = function; + frame[2] = start_address >> 8; + frame[3] = start_address >> 0; + frame[4] = register_count >> 8; + frame[5] = register_count >> 0; + auto crc = crc16(frame, 6); + frame[6] = crc >> 0; + frame[7] = crc >> 8; + + this->write_array(frame, 8); +} + +} // namespace modbus +} // namespace esphome diff --git a/esphome/components/modbus/modbus.h b/esphome/components/modbus/modbus.h new file mode 100644 index 0000000000..b75de147b1 --- /dev/null +++ b/esphome/components/modbus/modbus.h @@ -0,0 +1,51 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/uart/uart.h" + +namespace esphome { +namespace modbus { + +class ModbusDevice; + +class Modbus : public uart::UARTDevice, public Component { + public: + Modbus() = default; + + void loop() override; + + void dump_config() override; + + void register_device(ModbusDevice *device) { this->devices_.push_back(device); } + + float get_setup_priority() const override; + + void send(uint8_t address, uint8_t function, uint16_t start_address, uint16_t register_count); + + protected: + bool parse_modbus_byte_(uint8_t byte); + + std::vector rx_buffer_; + uint32_t last_modbus_byte_{0}; + std::vector devices_; +}; + +class ModbusDevice { + public: + void set_parent(Modbus *parent) { parent_ = parent; } + void set_address(uint8_t address) { address_ = address; } + virtual void on_modbus_data(const std::vector &data) = 0; + + void send(uint8_t function, uint16_t start_address, uint16_t register_count) { + this->parent_->send(this->address_, function, start_address, register_count); + } + + protected: + friend Modbus; + + Modbus *parent_; + uint8_t address_; +}; + +} // namespace modbus +} // namespace esphome diff --git a/esphome/components/pmsx003/pmsx003.cpp b/esphome/components/pmsx003/pmsx003.cpp index 548099a495..489442c637 100644 --- a/esphome/components/pmsx003/pmsx003.cpp +++ b/esphome/components/pmsx003/pmsx003.cpp @@ -169,6 +169,7 @@ void PMSX003Component::dump_config() { LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); LOG_SENSOR(" ", "Formaldehyde", this->formaldehyde_sensor_); + this->check_uart_settings(9600); } } // namespace pmsx003 diff --git a/esphome/components/pzem004t/__init__.py b/esphome/components/pzem004t/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/pzem004t/pzem004t.cpp b/esphome/components/pzem004t/pzem004t.cpp new file mode 100644 index 0000000000..3f07e74f9b --- /dev/null +++ b/esphome/components/pzem004t/pzem004t.cpp @@ -0,0 +1,103 @@ +#include "pzem004t.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pzem004t { + +static const char *TAG = "pzem004t"; + +void PZEM004T::loop() { + const uint32_t now = millis(); + if (now - this->last_read_ > 500 && this->available()) { + while (this->available()) + this->read(); + this->last_read_ = now; + } + + // PZEM004T packet size is 7 byte + while (this->available() >= 7) { + auto resp = *this->read_array<7>(); + // packet format: + // 0: packet type + // 1-5: data + // 6: checksum (sum of other bytes) + // see https://github.com/olehs/PZEM004T + uint8_t sum = 0; + for (int i = 0; i < 6; i++) + sum += resp[i]; + + if (sum != resp[6]) { + ESP_LOGV(TAG, "PZEM004T invalid checksum! 0x%02X != 0x%02X", sum, resp[6]); + continue; + } + + switch (resp[0]) { + case 0xA4: { // Set Module Address Response + this->write_state_(READ_VOLTAGE); + break; + } + case 0xA0: { // Voltage Response + uint16_t int_voltage = (uint16_t(resp[1]) << 8) | (uint16_t(resp[2]) << 0); + float voltage = int_voltage + (resp[3] / 10.0f); + if (this->voltage_sensor_ != nullptr) + this->voltage_sensor_->publish_state(voltage); + ESP_LOGD(TAG, "Got Voltage %.1f V", voltage); + this->write_state_(READ_CURRENT); + break; + } + case 0xA1: { // Current Response + uint16_t int_current = (uint16_t(resp[1]) << 8) | (uint16_t(resp[2]) << 0); + float current = int_current + (resp[3] / 100.0f); + if (this->current_sensor_ != nullptr) + this->current_sensor_->publish_state(current); + ESP_LOGD(TAG, "Got Current %.2f A", current); + this->write_state_(READ_POWER); + break; + } + case 0xA2: { // Active Power Response + uint16_t power = (uint16_t(resp[1]) << 8) | (uint16_t(resp[2]) << 0); + if (this->power_sensor_ != nullptr) + this->power_sensor_->publish_state(power); + ESP_LOGD(TAG, "Got Power %u W", power); + this->write_state_(DONE); + break; + } + + case 0xA3: // Energy Response + case 0xA5: // Set Power Alarm Response + case 0xB0: // Voltage Request + case 0xB1: // Current Request + case 0xB2: // Active Power Response + case 0xB3: // Energy Request + case 0xB4: // Set Module Address Request + case 0xB5: // Set Power Alarm Request + default: + break; + } + + this->last_read_ = now; + } +} +void PZEM004T::update() { this->write_state_(SET_ADDRESS); } +void PZEM004T::write_state_(PZEM004T::PZEM004TReadState state) { + if (state == DONE) { + this->read_state_ = state; + return; + } + std::array data{}; + data[0] = state; + data[1] = 192; + data[2] = 168; + data[3] = 1; + data[4] = 1; + data[5] = 0; + data[6] = 0; + for (int i = 0; i < 6; i++) + data[6] += data[i]; + + this->write_array(data); + this->read_state_ = state; +} + +} // namespace pzem004t +} // namespace esphome diff --git a/esphome/components/pzem004t/pzem004t.h b/esphome/components/pzem004t/pzem004t.h new file mode 100644 index 0000000000..7969efd78c --- /dev/null +++ b/esphome/components/pzem004t/pzem004t.h @@ -0,0 +1,39 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/uart/uart.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace pzem004t { + +class PZEM004T : public PollingComponent, public uart::UARTDevice { + public: + void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } + void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; } + void set_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_sensor; } + + void loop() override; + + void update() override; + + protected: + sensor::Sensor *voltage_sensor_; + sensor::Sensor *current_sensor_; + sensor::Sensor *power_sensor_; + + enum PZEM004TReadState { + SET_ADDRESS = 0xB4, + READ_VOLTAGE = 0xB0, + READ_CURRENT = 0xB1, + READ_POWER = 0xB2, + DONE = 0x00, + } read_state_{DONE}; + + void write_state_(PZEM004TReadState state); + + uint32_t last_read_{0}; +}; + +} // namespace pzem004t +} // namespace esphome diff --git a/esphome/components/pzem004t/sensor.py b/esphome/components/pzem004t/sensor.py new file mode 100644 index 0000000000..6e3628c5ec --- /dev/null +++ b/esphome/components/pzem004t/sensor.py @@ -0,0 +1,37 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, uart +from esphome.const import CONF_CURRENT, CONF_ID, CONF_POWER, CONF_VOLTAGE, \ + UNIT_VOLT, ICON_FLASH, UNIT_AMPERE, UNIT_WATT + +DEPENDENCIES = ['uart'] + +pzem004t_ns = cg.esphome_ns.namespace('pzem004t') +PZEM004T = pzem004t_ns.class_('PZEM004T', cg.PollingComponent, uart.UARTDevice) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(PZEM004T), + + cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(UNIT_VOLT, ICON_FLASH, 1), + cv.Optional(CONF_CURRENT): sensor.sensor_schema(UNIT_AMPERE, ICON_FLASH, 2), + cv.Optional(CONF_POWER): sensor.sensor_schema(UNIT_WATT, ICON_FLASH, 0), +}).extend(cv.polling_component_schema('60s')).extend(uart.UART_DEVICE_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield uart.register_uart_device(var, config) + + if CONF_VOLTAGE in config: + conf = config[CONF_VOLTAGE] + sens = yield sensor.new_sensor(conf) + cg.add(var.set_voltage_sensor(sens)) + if CONF_CURRENT in config: + conf = config[CONF_CURRENT] + sens = yield sensor.new_sensor(conf) + cg.add(var.set_current_sensor(sens)) + if CONF_POWER in config: + conf = config[CONF_POWER] + sens = yield sensor.new_sensor(conf) + cg.add(var.set_power_sensor(sens)) diff --git a/esphome/components/pzemac/__init__.py b/esphome/components/pzemac/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/pzemac/pzemac.cpp b/esphome/components/pzemac/pzemac.cpp new file mode 100644 index 0000000000..1c3957dc09 --- /dev/null +++ b/esphome/components/pzemac/pzemac.cpp @@ -0,0 +1,62 @@ +#include "pzemac.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pzemac { + +static const char *TAG = "pzemac"; + +static const uint8_t PZEM_CMD_READ_IN_REGISTERS = 0x04; +static const uint8_t PZEM_REGISTER_COUNT = 10; // 10x 16-bit registers + +void PZEMAC::on_modbus_data(const std::vector &data) { + if (data.size() < 20) { + ESP_LOGW(TAG, "Invalid size for PZEM AC!"); + return; + } + + // See https://github.com/esphome/feature-requests/issues/49#issuecomment-538636809 + // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 + // 01 04 14 08 D1 00 6C 00 00 00 F4 00 00 00 26 00 00 01 F4 00 64 00 00 51 34 + // Id Cc Sz Volt- Current---- Power------ Energy----- Frequ PFact Alarm Crc-- + + auto pzem_get_16bit = [&](size_t i) -> uint16_t { + return (uint16_t(data[i + 0]) << 8) | (uint16_t(data[i + 1]) << 0); + }; + auto pzem_get_32bit = [&](size_t i) -> uint32_t { + return (uint32_t(pzem_get_16bit(i + 2)) << 16) | (uint32_t(pzem_get_16bit(i + 0)) << 0); + }; + + uint16_t raw_voltage = pzem_get_16bit(0); + float voltage = raw_voltage / 10.0f; // max 6553.5 V + + uint32_t raw_current = pzem_get_32bit(2); + float current = raw_current / 1000.0f; // max 4294967.295 A + + uint32_t raw_active_power = pzem_get_32bit(6); + float active_power = raw_active_power / 10.0f; // max 429496729.5 W + + uint16_t raw_frequency = pzem_get_16bit(14); + float frequency = raw_frequency / 10.0f; + + uint16_t raw_power_factor = pzem_get_16bit(16); + float power_factor = raw_power_factor / 100.0f; + + ESP_LOGD(TAG, "PZEM AC: V=%.1f V, I=%.3f A, P=%.1f W, F=%.1f Hz, PF=%.2f", voltage, current, active_power, frequency, + power_factor); + if (this->voltage_sensor_ != nullptr) + this->voltage_sensor_->publish_state(voltage); + if (this->current_sensor_ != nullptr) + this->current_sensor_->publish_state(current); + if (this->power_sensor_ != nullptr) + this->power_sensor_->publish_state(active_power); + if (this->frequency_sensor_ != nullptr) + this->frequency_sensor_->publish_state(frequency); + if (this->power_factor_sensor_ != nullptr) + this->power_factor_sensor_->publish_state(power_factor); +} + +void PZEMAC::update() { this->send(PZEM_CMD_READ_IN_REGISTERS, 0, PZEM_REGISTER_COUNT); } + +} // namespace pzemac +} // namespace esphome diff --git a/esphome/components/pzemac/pzemac.h b/esphome/components/pzemac/pzemac.h new file mode 100644 index 0000000000..0ba742fafe --- /dev/null +++ b/esphome/components/pzemac/pzemac.h @@ -0,0 +1,31 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/modbus/modbus.h" + +namespace esphome { +namespace pzemac { + +class PZEMAC : public PollingComponent, public modbus::ModbusDevice { + public: + void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } + void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; } + void set_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_sensor; } + void set_frequency_sensor(sensor::Sensor *frequency_sensor) { frequency_sensor_ = frequency_sensor; } + void set_power_factor_sensor(sensor::Sensor *power_factor_sensor) { power_factor_sensor_ = power_factor_sensor; } + + void update() override; + + void on_modbus_data(const std::vector &data) override; + + protected: + sensor::Sensor *voltage_sensor_; + sensor::Sensor *current_sensor_; + sensor::Sensor *power_sensor_; + sensor::Sensor *frequency_sensor_; + sensor::Sensor *power_factor_sensor_; +}; + +} // namespace pzemac +} // namespace esphome diff --git a/esphome/components/pzemac/sensor.py b/esphome/components/pzemac/sensor.py new file mode 100644 index 0000000000..35d8069767 --- /dev/null +++ b/esphome/components/pzemac/sensor.py @@ -0,0 +1,47 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, modbus +from esphome.const import CONF_CURRENT, CONF_ID, CONF_POWER, CONF_VOLTAGE, \ + CONF_FREQUENCY, UNIT_VOLT, ICON_FLASH, UNIT_AMPERE, UNIT_WATT, UNIT_EMPTY, \ + ICON_POWER, CONF_POWER_FACTOR, ICON_CURRENT_AC + +AUTO_LOAD = ['modbus'] + +pzemac_ns = cg.esphome_ns.namespace('pzemac') +PZEMAC = pzemac_ns.class_('PZEMAC', cg.PollingComponent, modbus.ModbusDevice) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(PZEMAC), + cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(UNIT_VOLT, ICON_FLASH, 1), + cv.Optional(CONF_CURRENT): sensor.sensor_schema(UNIT_AMPERE, ICON_CURRENT_AC, 3), + cv.Optional(CONF_POWER): sensor.sensor_schema(UNIT_WATT, ICON_POWER, 1), + cv.Optional(CONF_FREQUENCY): sensor.sensor_schema(UNIT_EMPTY, ICON_CURRENT_AC, 1), + cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema(UNIT_EMPTY, ICON_FLASH, 2), +}).extend(cv.polling_component_schema('60s')).extend(modbus.modbus_device_schema(0x01)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield modbus.register_modbus_device(var, config) + + if CONF_VOLTAGE in config: + conf = config[CONF_VOLTAGE] + sens = yield sensor.new_sensor(conf) + cg.add(var.set_voltage_sensor(sens)) + if CONF_CURRENT in config: + conf = config[CONF_CURRENT] + sens = yield sensor.new_sensor(conf) + cg.add(var.set_current_sensor(sens)) + if CONF_POWER in config: + conf = config[CONF_POWER] + sens = yield sensor.new_sensor(conf) + cg.add(var.set_power_sensor(sens)) + if CONF_FREQUENCY in config: + conf = config[CONF_FREQUENCY] + sens = yield sensor.new_sensor(conf) + cg.add(var.set_frequency_sensor(sens)) + if CONF_POWER_FACTOR in config: + conf = config[CONF_POWER_FACTOR] + sens = yield sensor.new_sensor(conf) + cg.add(var.set_power_factor_sensor(sens)) diff --git a/esphome/components/pzemdc/__init__.py b/esphome/components/pzemdc/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/pzemdc/pzemdc.cpp b/esphome/components/pzemdc/pzemdc.cpp new file mode 100644 index 0000000000..c85d35106d --- /dev/null +++ b/esphome/components/pzemdc/pzemdc.cpp @@ -0,0 +1,52 @@ +#include "pzemdc.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pzemdc { + +static const char *TAG = "pzemdc"; + +static const uint8_t PZEM_CMD_READ_IN_REGISTERS = 0x04; +static const uint8_t PZEM_REGISTER_COUNT = 10; // 10x 16-bit registers + +void PZEMDC::on_modbus_data(const std::vector &data) { + if (data.size() < 16) { + ESP_LOGW(TAG, "Invalid size for PZEM DC!"); + return; + } + + // See https://github.com/esphome/feature-requests/issues/49#issuecomment-538636809 + // 0 1 2 3 4 5 6 7 = ModBus register + // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 = Buffer index + // 01 04 10 05 40 00 0A 00 0D 00 00 00 02 00 00 00 00 00 00 D6 29 + // Id Cc Sz Volt- Curre Power------ Energy----- HiAlm LoAlm Crc-- + + auto pzem_get_16bit = [&](size_t i) -> uint16_t { + return (uint16_t(data[i + 0]) << 8) | (uint16_t(data[i + 1]) << 0); + }; + auto pzem_get_32bit = [&](size_t i) -> uint32_t { + return (uint32_t(pzem_get_16bit(i + 2)) << 16) | (uint32_t(pzem_get_16bit(i + 0)) << 0); + }; + + uint16_t raw_voltage = pzem_get_16bit(0); + float voltage = raw_voltage / 100.0f; // max 655.35 V + + uint16_t raw_current = pzem_get_16bit(2); + float current = raw_current / 100.0f; // max 655.35 A + + uint32_t raw_power = pzem_get_32bit(4); + float power = raw_power / 10.0f; // max 429496729.5 W + + ESP_LOGD(TAG, "PZEM DC: V=%.1f V, I=%.3f A, P=%.1f W", voltage, current, power); + if (this->voltage_sensor_ != nullptr) + this->voltage_sensor_->publish_state(voltage); + if (this->current_sensor_ != nullptr) + this->current_sensor_->publish_state(current); + if (this->power_sensor_ != nullptr) + this->power_sensor_->publish_state(power); +} + +void PZEMDC::update() { this->send(PZEM_CMD_READ_IN_REGISTERS, 0, 8); } + +} // namespace pzemdc +} // namespace esphome diff --git a/esphome/components/pzemdc/pzemdc.h b/esphome/components/pzemdc/pzemdc.h new file mode 100644 index 0000000000..e19203dc21 --- /dev/null +++ b/esphome/components/pzemdc/pzemdc.h @@ -0,0 +1,31 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/modbus/modbus.h" + +namespace esphome { +namespace pzemdc { + +class PZEMDC : public PollingComponent, public modbus::ModbusDevice { + public: + void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } + void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; } + void set_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_sensor; } + void set_frequency_sensor(sensor::Sensor *frequency_sensor) { frequency_sensor_ = frequency_sensor; } + void set_powerfactor_sensor(sensor::Sensor *powerfactor_sensor) { power_factor_sensor_ = powerfactor_sensor; } + + void update() override; + + void on_modbus_data(const std::vector &data) override; + + protected: + sensor::Sensor *voltage_sensor_; + sensor::Sensor *current_sensor_; + sensor::Sensor *power_sensor_; + sensor::Sensor *frequency_sensor_; + sensor::Sensor *power_factor_sensor_; +}; + +} // namespace pzemdc +} // namespace esphome diff --git a/esphome/components/pzemdc/sensor.py b/esphome/components/pzemdc/sensor.py new file mode 100644 index 0000000000..8c6fd08868 --- /dev/null +++ b/esphome/components/pzemdc/sensor.py @@ -0,0 +1,36 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, modbus +from esphome.const import CONF_CURRENT, CONF_ID, CONF_POWER, CONF_VOLTAGE, \ + UNIT_VOLT, ICON_FLASH, UNIT_AMPERE, UNIT_WATT, ICON_POWER, ICON_CURRENT_AC + +AUTO_LOAD = ['modbus'] + +pzemdc_ns = cg.esphome_ns.namespace('pzemdc') +PZEMDC = pzemdc_ns.class_('PZEMDC', cg.PollingComponent, modbus.ModbusDevice) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(PZEMDC), + cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(UNIT_VOLT, ICON_FLASH, 1), + cv.Optional(CONF_CURRENT): sensor.sensor_schema(UNIT_AMPERE, ICON_CURRENT_AC, 3), + cv.Optional(CONF_POWER): sensor.sensor_schema(UNIT_WATT, ICON_POWER, 1), +}).extend(cv.polling_component_schema('60s')).extend(modbus.modbus_device_schema(0x01)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield modbus.register_modbus_device(var, config) + + if CONF_VOLTAGE in config: + conf = config[CONF_VOLTAGE] + sens = yield sensor.new_sensor(conf) + cg.add(var.set_voltage_sensor(sens)) + if CONF_CURRENT in config: + conf = config[CONF_CURRENT] + sens = yield sensor.new_sensor(conf) + cg.add(var.set_current_sensor(sens)) + if CONF_POWER in config: + conf = config[CONF_POWER] + sens = yield sensor.new_sensor(conf) + cg.add(var.set_power_sensor(sens)) diff --git a/esphome/components/sds011/sds011.cpp b/esphome/components/sds011/sds011.cpp index 6ca414c55d..1a5be0adc3 100644 --- a/esphome/components/sds011/sds011.cpp +++ b/esphome/components/sds011/sds011.cpp @@ -56,6 +56,7 @@ void SDS011Component::dump_config() { ESP_LOGCONFIG(TAG, " RX-only mode: %s", ONOFF(this->rx_mode_only_)); LOG_SENSOR(" ", "PM2.5", this->pm_2_5_sensor_); LOG_SENSOR(" ", "PM10.0", this->pm_10_0_sensor_); + this->check_uart_settings(9600); } void SDS011Component::loop() { diff --git a/esphome/components/senseair/senseair.cpp b/esphome/components/senseair/senseair.cpp index 96f456282f..8b41a441ad 100644 --- a/esphome/components/senseair/senseair.cpp +++ b/esphome/components/senseair/senseair.cpp @@ -73,6 +73,7 @@ bool SenseAirComponent::senseair_write_command_(const uint8_t *command, uint8_t void SenseAirComponent::dump_config() { ESP_LOGCONFIG(TAG, "SenseAir:"); LOG_SENSOR(" ", "CO2", this->co2_sensor_); + this->check_uart_settings(9600); } } // namespace senseair diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp index ae3e7eed43..4efcf08fe6 100644 --- a/esphome/components/tuya/tuya.cpp +++ b/esphome/components/tuya/tuya.cpp @@ -36,6 +36,7 @@ void Tuya::dump_config() { else ESP_LOGCONFIG(TAG, " Datapoint %d: unknown", info.id); } + this->check_uart_settings(9600); } bool Tuya::validate_message_() { diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index 110bd64c81..2511cf28b1 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -29,11 +29,13 @@ def validate_rx_pin(value): return value +CONF_STOP_BITS = 'stop_bits' CONFIG_SCHEMA = cv.All(cv.Schema({ cv.GenerateID(): cv.declare_id(UARTComponent), cv.Required(CONF_BAUD_RATE): cv.int_range(min=1), cv.Optional(CONF_TX_PIN): pins.output_pin, cv.Optional(CONF_RX_PIN): validate_rx_pin, + cv.Optional(CONF_STOP_BITS, default=1): cv.one_of(1, 2, int=True), }).extend(cv.COMPONENT_SCHEMA), cv.has_at_least_one_key(CONF_TX_PIN, CONF_RX_PIN)) @@ -48,6 +50,7 @@ def to_code(config): cg.add(var.set_tx_pin(config[CONF_TX_PIN])) if CONF_RX_PIN in config: cg.add(var.set_rx_pin(config[CONF_RX_PIN])) + cg.add(var.set_stop_bits(config[CONF_STOP_BITS])) # A schema to use for all UART devices, all UART integrations must extend this! diff --git a/esphome/components/uart/uart.cpp b/esphome/components/uart/uart.cpp index 83ae81490e..fd27a8f897 100644 --- a/esphome/components/uart/uart.cpp +++ b/esphome/components/uart/uart.cpp @@ -25,7 +25,10 @@ void UARTComponent::setup() { } int8_t tx = this->tx_pin_.has_value() ? *this->tx_pin_ : -1; int8_t rx = this->rx_pin_.has_value() ? *this->rx_pin_ : -1; - this->hw_serial_->begin(this->baud_rate_, SERIAL_8N1, rx, tx); + uint32_t config = SERIAL_8N1; + if (this->stop_bits_ == 2) + config = SERIAL_8N2; + this->hw_serial_->begin(this->baud_rate_, config, rx, tx); } void UARTComponent::dump_config() { @@ -37,6 +40,7 @@ void UARTComponent::dump_config() { ESP_LOGCONFIG(TAG, " RX Pin: GPIO%d", *this->rx_pin_); } ESP_LOGCONFIG(TAG, " Baud Rate: %u baud", this->baud_rate_); + ESP_LOGCONFIG(TAG, " Stop bits: %u", this->stop_bits_); } void UARTComponent::write_byte(uint8_t data) { @@ -102,21 +106,27 @@ void UARTComponent::setup() { // Use Arduino HardwareSerial UARTs if all used pins match the ones // preconfigured by the platform. For example if RX disabled but TX pin // is 1 we still want to use Serial. + uint32_t mode = UART_NB_BIT_8 | UART_PARITY_NONE; + if (this->stop_bits_ == 1) + mode |= UART_NB_STOP_BIT_1; + else + mode |= UART_NB_STOP_BIT_2; + SerialConfig config = static_cast(mode); if (this->tx_pin_.value_or(1) == 1 && this->rx_pin_.value_or(3) == 3) { this->hw_serial_ = &Serial; - this->hw_serial_->begin(this->baud_rate_); + this->hw_serial_->begin(this->baud_rate_, config); } else if (this->tx_pin_.value_or(15) == 15 && this->rx_pin_.value_or(13) == 13) { this->hw_serial_ = &Serial; - this->hw_serial_->begin(this->baud_rate_); + this->hw_serial_->begin(this->baud_rate_, config); this->hw_serial_->swap(); } else if (this->tx_pin_.value_or(2) == 2 && this->rx_pin_.value_or(8) == 8) { this->hw_serial_ = &Serial1; - this->hw_serial_->begin(this->baud_rate_); + this->hw_serial_->begin(this->baud_rate_, config); } else { this->sw_serial_ = new ESP8266SoftwareSerial(); int8_t tx = this->tx_pin_.has_value() ? *this->tx_pin_ : -1; int8_t rx = this->rx_pin_.has_value() ? *this->rx_pin_ : -1; - this->sw_serial_->setup(tx, rx, this->baud_rate_); + this->sw_serial_->setup(tx, rx, this->baud_rate_, this->stop_bits_); } } @@ -129,6 +139,7 @@ void UARTComponent::dump_config() { ESP_LOGCONFIG(TAG, " RX Pin: GPIO%d", *this->rx_pin_); } ESP_LOGCONFIG(TAG, " Baud Rate: %u baud", this->baud_rate_); + ESP_LOGCONFIG(TAG, " Stop bits: %u", this->stop_bits_); if (this->hw_serial_ != nullptr) { ESP_LOGCONFIG(TAG, " Using hardware serial interface."); } else { @@ -231,7 +242,7 @@ void UARTComponent::flush() { } } -void ESP8266SoftwareSerial::setup(int8_t tx_pin, int8_t rx_pin, uint32_t baud_rate) { +void ESP8266SoftwareSerial::setup(int8_t tx_pin, int8_t rx_pin, uint32_t baud_rate, uint8_t stop_bits) { this->bit_time_ = F_CPU / baud_rate; if (tx_pin != -1) { auto pin = GPIOPin(tx_pin, OUTPUT); @@ -246,6 +257,7 @@ void ESP8266SoftwareSerial::setup(int8_t tx_pin, int8_t rx_pin, uint32_t baud_ra this->rx_buffer_ = new uint8_t[this->rx_buffer_size_]; pin.attach_interrupt(ESP8266SoftwareSerial::gpio_intr, this, FALLING); } + this->stop_bits_ = stop_bits; } void ICACHE_RAM_ATTR ESP8266SoftwareSerial::gpio_intr(ESP8266SoftwareSerial *arg) { uint32_t wait = arg->bit_time_ + arg->bit_time_ / 3 - 500; @@ -262,6 +274,8 @@ void ICACHE_RAM_ATTR ESP8266SoftwareSerial::gpio_intr(ESP8266SoftwareSerial *arg rec |= arg->read_bit_(&wait, start) << 7; // Stop bit arg->wait_(&wait, start); + if (arg->stop_bits_ == 2) + arg->wait_(&wait, start); arg->rx_buffer_[arg->rx_in_pos_] = rec; arg->rx_in_pos_ = (arg->rx_in_pos_ + 1) % arg->rx_buffer_size_; @@ -289,6 +303,8 @@ void ICACHE_RAM_ATTR HOT ESP8266SoftwareSerial::write_byte(uint8_t data) { this->write_bit_(data & (1 << 7), &wait, start); // Stop bit this->write_bit_(true, &wait, start); + if (this->stop_bits_ == 2) + this->wait_(&wait, start); enable_interrupts(); } void ICACHE_RAM_ATTR ESP8266SoftwareSerial::wait_(uint32_t *wait, const uint32_t &start) { @@ -344,5 +360,16 @@ int UARTComponent::peek() { return data; } +void UARTDevice::check_uart_settings(uint32_t baud_rate, uint8_t stop_bits) { + if (this->parent_->baud_rate_ != baud_rate) { + ESP_LOGE(TAG, " Invalid baud_rate: Integration requested baud_rate %u but you have %u!", baud_rate, + this->parent_->baud_rate_); + } + if (this->parent_->stop_bits_ != stop_bits) { + ESP_LOGE(TAG, " Invalid stop bits: Integration requested stop_bits %u but you have %u!", stop_bits, + this->parent_->stop_bits_); + } +} + } // namespace uart } // namespace esphome diff --git a/esphome/components/uart/uart.h b/esphome/components/uart/uart.h index 3b347c1ff7..0e92fed0dc 100644 --- a/esphome/components/uart/uart.h +++ b/esphome/components/uart/uart.h @@ -10,7 +10,7 @@ namespace uart { #ifdef ARDUINO_ARCH_ESP8266 class ESP8266SoftwareSerial { public: - void setup(int8_t tx_pin, int8_t rx_pin, uint32_t baud_rate); + void setup(int8_t tx_pin, int8_t rx_pin, uint32_t baud_rate, uint8_t stop_bits); uint8_t read_byte(); uint8_t peek_byte(); @@ -33,6 +33,7 @@ class ESP8266SoftwareSerial { size_t rx_buffer_size_{512}; volatile size_t rx_in_pos_{0}; size_t rx_out_pos_{0}; + uint8_t stop_bits_; ISRInternalGPIOPin *tx_pin_{nullptr}; ISRInternalGPIOPin *rx_pin_{nullptr}; }; @@ -72,9 +73,11 @@ class UARTComponent : public Component, public Stream { void set_tx_pin(uint8_t tx_pin) { this->tx_pin_ = tx_pin; } void set_rx_pin(uint8_t rx_pin) { this->rx_pin_ = rx_pin; } + void set_stop_bits(uint8_t stop_bits) { this->stop_bits_ = stop_bits; } protected: bool check_read_timeout_(size_t len = 1); + friend class UARTDevice; HardwareSerial *hw_serial_{nullptr}; #ifdef ARDUINO_ARCH_ESP8266 @@ -83,6 +86,7 @@ class UARTComponent : public Component, public Stream { optional tx_pin_; optional rx_pin_; uint32_t baud_rate_; + uint8_t stop_bits_; }; #ifdef ARDUINO_ARCH_ESP32 @@ -100,6 +104,9 @@ class UARTDevice : public Stream { void write_array(const uint8_t *data, size_t len) { this->parent_->write_array(data, len); } void write_array(const std::vector &data) { this->parent_->write_array(data); } + template void write_array(const std::array &data) { + this->parent_->write_array(data.data(), data.size()); + } void write_str(const char *str) { this->parent_->write_str(str); } @@ -107,6 +114,13 @@ class UARTDevice : public Stream { bool peek_byte(uint8_t *data) { return this->parent_->peek_byte(data); } bool read_array(uint8_t *data, size_t len) { return this->parent_->read_array(data, len); } + template optional> read_array() { // NOLINT + std::array res; + if (!this->read_array(res.data(), N)) { + return {}; + } + return res; + } int available() override { return this->parent_->available(); } @@ -116,6 +130,9 @@ class UARTDevice : public Stream { int read() override { return this->parent_->read(); } int peek() override { return this->parent_->peek(); } + /// Check that the configuration of the UART bus matches the provided values and otherwise print a warning + void check_uart_settings(uint32_t baud_rate, uint8_t stop_bits = 1); + protected: UARTComponent *parent_{nullptr}; }; diff --git a/esphome/const.py b/esphome/const.py index b595bdf629..163b526b32 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -323,6 +323,7 @@ CONF_PM_2_5 = 'pm_2_5' CONF_PORT = 'port' CONF_POSITION = 'position' CONF_POWER = 'power' +CONF_POWER_FACTOR = 'power_factor' CONF_POWER_ON_VALUE = 'power_on_value' CONF_POWER_SAVE_MODE = 'power_save_mode' CONF_POWER_SUPPLY = 'power_supply' @@ -479,6 +480,7 @@ ICON_BRIEFCASE_DOWNLOAD = 'mdi:briefcase-download' ICON_BRIGHTNESS_5 = 'mdi:brightness-5' ICON_CHEMICAL_WEAPON = 'mdi:chemical-weapon' ICON_CHECK_CIRCLE_OUTLINE = 'mdi:check-circle-outline' +ICON_CURRENT_AC = 'mdi:current-ac' ICON_EMPTY = '' ICON_FLASH = 'mdi:flash' ICON_FLOWER = 'mdi:flower' diff --git a/tests/test3.yaml b/tests/test3.yaml index f3e530f07a..d1596c8f41 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -291,6 +291,31 @@ sensor: name: ADE7953 Active Power A active_power_b: name: ADE7953 Active Power B + - platform: pzem004t + voltage: + name: "PZEM00T Voltage" + current: + name: "PZEM004T Current" + power: + name: "PZEM004T Power" + - platform: pzemac + voltage: + name: "PZEMAC Voltage" + current: + name: "PZEMAC Current" + power: + name: "PZEMAC Power" + frequency: + name: "PZEMAC Frequency" + power_factor: + name: "PZEMAC Power Factor" + - platform: pzemdc + voltage: + name: "PZEMDC Voltage" + current: + name: "PZEMDC Current" + power: + name: "PZEMDC Power" time: - platform: homeassistant