diff --git a/CODEOWNERS b/CODEOWNERS index ca2c259624..68a8eb6d18 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -21,6 +21,7 @@ esphome/components/airthings_wave_base/* @jeromelaban @ncareau esphome/components/airthings_wave_mini/* @ncareau esphome/components/airthings_wave_plus/* @jeromelaban esphome/components/alarm_control_panel/* @grahambrown11 +esphome/components/alpha3/* @jan-hofmeier esphome/components/am43/* @buxtronix esphome/components/am43/cover/* @buxtronix esphome/components/am43/sensor/* @buxtronix diff --git a/esphome/components/alpha3/__init__.py b/esphome/components/alpha3/__init__.py new file mode 100644 index 0000000000..7cd320c80f --- /dev/null +++ b/esphome/components/alpha3/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@jan-hofmeier"] diff --git a/esphome/components/alpha3/alpha3.cpp b/esphome/components/alpha3/alpha3.cpp new file mode 100644 index 0000000000..17899c31cb --- /dev/null +++ b/esphome/components/alpha3/alpha3.cpp @@ -0,0 +1,189 @@ +#include "alpha3.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" +#include //gives ntohl + +#ifdef USE_ESP32 + +namespace esphome { +namespace alpha3 { + +static const char *const TAG = "alpha3"; + +void Alpha3::dump_config() { + ESP_LOGCONFIG(TAG, "ALPHA3"); + LOG_SENSOR(" ", "Flow", this->flow_sensor_); + LOG_SENSOR(" ", "Head", this->head_sensor_); + LOG_SENSOR(" ", "Power", this->power_sensor_); + LOG_SENSOR(" ", "Current", this->current_sensor_); + LOG_SENSOR(" ", "Speed", this->speed_sensor_); + LOG_SENSOR(" ", "Voltage", this->voltage_sensor_); +} + +void Alpha3::setup() {} + +void Alpha3::extract_publish_sensor_value_(const uint8_t *response, int16_t length, int16_t response_offset, + int16_t value_offset, sensor::Sensor *sensor, float factor) { + if (sensor == nullptr) + return; + // we need to handle cases where a value is split over two packets + const int16_t value_length = 4; // 32bit float + // offset inside current response packet + auto rel_offset = value_offset - response_offset; + if (rel_offset <= -value_length) + return; // aready passed the value completly + if (rel_offset >= length) + return; // value not in this packet + + auto start_offset = std::max(0, rel_offset); + auto end_offset = std::min((int16_t) (rel_offset + value_length), length); + auto copy_length = end_offset - start_offset; + auto buffer_offset = std::max(-rel_offset, 0); + std::memcpy(this->buffer_ + buffer_offset, response + start_offset, copy_length); + + if (rel_offset + value_length <= length) { + // we have the whole value + void *buffer = this->buffer_; // to prevent warnings when casting the pointer + *((int32_t *) buffer) = ntohl(*((int32_t *) buffer)); // values are big endian + float fvalue = *((float *) buffer); + sensor->publish_state(fvalue * factor); + } +} + +bool Alpha3::is_current_response_type_(const uint8_t *response_type) { + return !std::memcmp(this->response_type_, response_type, GENI_RESPONSE_TYPE_LENGTH); +} + +void Alpha3::handle_geni_response_(const uint8_t *response, uint16_t length) { + if (this->response_offset_ >= this->response_length_) { + ESP_LOGD(TAG, "[%s] GENI response begin", this->parent_->address_str().c_str()); + if (length < GENI_RESPONSE_HEADER_LENGTH) { + ESP_LOGW(TAG, "[%s] response to short", this->parent_->address_str().c_str()); + return; + } + if (response[0] != 36 || response[2] != 248 || response[3] != 231 || response[4] != 10) { + ESP_LOGW(TAG, "[%s] response bytes %d %d %d %d %d don't match GENI HEADER", this->parent_->address_str().c_str(), + response[0], response[1], response[2], response[3], response[4]); + return; + } + this->response_length_ = response[1] - GENI_RESPONSE_HEADER_LENGTH + 2; // maybe 2 byte checksum + this->response_offset_ = -GENI_RESPONSE_HEADER_LENGTH; + std::memcpy(this->response_type_, response + 5, GENI_RESPONSE_TYPE_LENGTH); + } + + auto extract_publish_sensor_value = [response, length, this](int16_t value_offset, sensor::Sensor *sensor, + float factor) { + this->extract_publish_sensor_value_(response, length, this->response_offset_, value_offset, sensor, factor); + }; + + if (this->is_current_response_type_(GENI_RESPONSE_TYPE_FLOW_HEAD)) { + ESP_LOGD(TAG, "[%s] FLOW HEAD Response", this->parent_->address_str().c_str()); + extract_publish_sensor_value(GENI_RESPONSE_FLOW_OFFSET, this->flow_sensor_, 3600.0F); + extract_publish_sensor_value(GENI_RESPONSE_HEAD_OFFSET, this->head_sensor_, .0001F); + } else if (this->is_current_response_type_(GENI_RESPONSE_TYPE_POWER)) { + ESP_LOGD(TAG, "[%s] POWER Response", this->parent_->address_str().c_str()); + extract_publish_sensor_value(GENI_RESPONSE_POWER_OFFSET, this->power_sensor_, 1.0F); + extract_publish_sensor_value(GENI_RESPONSE_CURRENT_OFFSET, this->current_sensor_, 1.0F); + extract_publish_sensor_value(GENI_RESPONSE_MOTOR_SPEED_OFFSET, this->speed_sensor_, 1.0F); + extract_publish_sensor_value(GENI_RESPONSE_VOLTAGE_AC_OFFSET, this->voltage_sensor_, 1.0F); + } else { + ESP_LOGW(TAG, "unkown GENI response Type %d %d %d %d %d %d %d %d", this->response_type_[0], this->response_type_[1], + this->response_type_[2], this->response_type_[3], this->response_type_[4], this->response_type_[5], + this->response_type_[6], this->response_type_[7]); + } + this->response_offset_ += length; +} + +void Alpha3::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { + switch (event) { + case ESP_GATTC_OPEN_EVT: { + this->response_offset_ = 0; + this->response_length_ = 0; + ESP_LOGI(TAG, "[%s] connection open", this->parent_->address_str().c_str()); + break; + } + case ESP_GATTC_CONNECT_EVT: { + if (std::memcmp(param->connect.remote_bda, this->parent_->get_remote_bda(), 6) != 0) + return; + auto ret = esp_ble_set_encryption(param->connect.remote_bda, ESP_BLE_SEC_ENCRYPT); + if (ret) { + ESP_LOGW(TAG, "esp_ble_set_encryption failed, status=%x", ret); + } + break; + } + case ESP_GATTC_DISCONNECT_EVT: { + this->node_state = espbt::ClientState::IDLE; + if (this->flow_sensor_ != nullptr) + this->flow_sensor_->publish_state(NAN); + if (this->head_sensor_ != nullptr) + this->head_sensor_->publish_state(NAN); + if (this->power_sensor_ != nullptr) + this->power_sensor_->publish_state(NAN); + if (this->current_sensor_ != nullptr) + this->current_sensor_->publish_state(NAN); + if (this->speed_sensor_ != nullptr) + this->speed_sensor_->publish_state(NAN); + if (this->speed_sensor_ != nullptr) + this->voltage_sensor_->publish_state(NAN); + break; + } + case ESP_GATTC_SEARCH_CMPL_EVT: { + auto *chr = this->parent_->get_characteristic(ALPHA3_GENI_SERVICE_UUID, ALPHA3_GENI_CHARACTERISTIC_UUID); + if (chr == nullptr) { + ESP_LOGE(TAG, "[%s] No GENI service found at device, not an Alpha3..?", this->parent_->address_str().c_str()); + break; + } + auto status = esp_ble_gattc_register_for_notify(this->parent_->get_gattc_if(), this->parent_->get_remote_bda(), + chr->handle); + if (status) { + ESP_LOGW(TAG, "esp_ble_gattc_register_for_notify failed, status=%d", status); + } + this->geni_handle_ = chr->handle; + break; + } + case ESP_GATTC_REG_FOR_NOTIFY_EVT: { + this->node_state = espbt::ClientState::ESTABLISHED; + this->update(); + break; + } + case ESP_GATTC_NOTIFY_EVT: { + if (param->notify.handle == this->geni_handle_) { + this->handle_geni_response_(param->notify.value, param->notify.value_len); + } + break; + } + default: + break; + } +} + +void Alpha3::send_request_(uint8_t *request, size_t len) { + auto status = + esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->geni_handle_, len, + request, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); + if (status) + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); +} + +void Alpha3::update() { + if (this->node_state != espbt::ClientState::ESTABLISHED) { + ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->parent_->address_str().c_str()); + return; + } + + if (this->flow_sensor_ != nullptr || this->head_sensor_ != nullptr) { + uint8_t geni_request_flow_head[] = {39, 7, 231, 248, 10, 3, 93, 1, 33, 82, 31}; + this->send_request_(geni_request_flow_head, sizeof(geni_request_flow_head)); + delay(25); // need to wait between requests + } + if (this->power_sensor_ != nullptr || this->current_sensor_ != nullptr || this->speed_sensor_ != nullptr || + this->voltage_sensor_ != nullptr) { + uint8_t geni_request_power[] = {39, 7, 231, 248, 10, 3, 87, 0, 69, 138, 205}; + this->send_request_(geni_request_power, sizeof(geni_request_power)); + delay(25); // need to wait between requests + } +} +} // namespace alpha3 +} // namespace esphome + +#endif diff --git a/esphome/components/alpha3/alpha3.h b/esphome/components/alpha3/alpha3.h new file mode 100644 index 0000000000..325c70a538 --- /dev/null +++ b/esphome/components/alpha3/alpha3.h @@ -0,0 +1,73 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/ble_client/ble_client.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/sensor/sensor.h" + +#ifdef USE_ESP32 + +#include + +namespace esphome { +namespace alpha3 { + +namespace espbt = esphome::esp32_ble_tracker; + +static const espbt::ESPBTUUID ALPHA3_GENI_SERVICE_UUID = espbt::ESPBTUUID::from_uint16(0xfe5d); +static const espbt::ESPBTUUID ALPHA3_GENI_CHARACTERISTIC_UUID = + espbt::ESPBTUUID::from_raw({static_cast(0xa9), 0x7b, static_cast(0xb8), static_cast(0x85), 0x0, + 0x1a, 0x28, static_cast(0xaa), 0x2a, 0x43, 0x6e, 0x3, static_cast(0xd1), + static_cast(0xff), static_cast(0x9c), static_cast(0x85)}); +static const int16_t GENI_RESPONSE_HEADER_LENGTH = 13; +static const size_t GENI_RESPONSE_TYPE_LENGTH = 8; + +static const uint8_t GENI_RESPONSE_TYPE_FLOW_HEAD[GENI_RESPONSE_TYPE_LENGTH] = {31, 0, 1, 48, 1, 0, 0, 24}; +static const int16_t GENI_RESPONSE_FLOW_OFFSET = 0; +static const int16_t GENI_RESPONSE_HEAD_OFFSET = 4; + +static const uint8_t GENI_RESPONSE_TYPE_POWER[GENI_RESPONSE_TYPE_LENGTH] = {44, 0, 1, 0, 1, 0, 0, 37}; +static const int16_t GENI_RESPONSE_VOLTAGE_AC_OFFSET = 0; +static const int16_t GENI_RESPONSE_VOLTAGE_DC_OFFSET = 4; +static const int16_t GENI_RESPONSE_CURRENT_OFFSET = 8; +static const int16_t GENI_RESPONSE_POWER_OFFSET = 12; +static const int16_t GENI_RESPONSE_MOTOR_POWER_OFFSET = 16; // not sure +static const int16_t GENI_RESPONSE_MOTOR_SPEED_OFFSET = 20; + +class Alpha3 : public esphome::ble_client::BLEClientNode, public PollingComponent { + public: + void setup() override; + void update() override; + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void set_flow_sensor(sensor::Sensor *sensor) { this->flow_sensor_ = sensor; } + void set_head_sensor(sensor::Sensor *sensor) { this->head_sensor_ = sensor; } + void set_power_sensor(sensor::Sensor *sensor) { this->power_sensor_ = sensor; } + void set_current_sensor(sensor::Sensor *sensor) { this->current_sensor_ = sensor; } + void set_speed_sensor(sensor::Sensor *sensor) { this->speed_sensor_ = sensor; } + void set_voltage_sensor(sensor::Sensor *sensor) { this->voltage_sensor_ = sensor; } + + protected: + sensor::Sensor *flow_sensor_{nullptr}; + sensor::Sensor *head_sensor_{nullptr}; + sensor::Sensor *power_sensor_{nullptr}; + sensor::Sensor *current_sensor_{nullptr}; + sensor::Sensor *speed_sensor_{nullptr}; + sensor::Sensor *voltage_sensor_{nullptr}; + uint16_t geni_handle_; + int16_t response_length_; + int16_t response_offset_; + uint8_t response_type_[GENI_RESPONSE_TYPE_LENGTH]; + uint8_t buffer_[4]; + void extract_publish_sensor_value_(const uint8_t *response, int16_t length, int16_t response_offset, + int16_t value_offset, sensor::Sensor *sensor, float factor); + void handle_geni_response_(const uint8_t *response, uint16_t length); + void send_request_(uint8_t *request, size_t len); + bool is_current_response_type_(const uint8_t *response_type); +}; +} // namespace alpha3 +} // namespace esphome + +#endif diff --git a/esphome/components/alpha3/sensor.py b/esphome/components/alpha3/sensor.py new file mode 100644 index 0000000000..ba4ca16a5a --- /dev/null +++ b/esphome/components/alpha3/sensor.py @@ -0,0 +1,85 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, ble_client +from esphome.const import ( + CONF_ID, + CONF_CURRENT, + CONF_FLOW, + CONF_HEAD, + CONF_POWER, + CONF_SPEED, + CONF_VOLTAGE, + UNIT_AMPERE, + UNIT_VOLT, + UNIT_WATT, + UNIT_METER, + UNIT_CUBIC_METER_PER_HOUR, + UNIT_REVOLUTIONS_PER_MINUTE, +) + +alpha3_ns = cg.esphome_ns.namespace("alpha3") +Alpha3 = alpha3_ns.class_("Alpha3", ble_client.BLEClientNode, cg.PollingComponent) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(Alpha3), + cv.Optional(CONF_FLOW): sensor.sensor_schema( + unit_of_measurement=UNIT_CUBIC_METER_PER_HOUR, + accuracy_decimals=2, + ), + cv.Optional(CONF_HEAD): sensor.sensor_schema( + unit_of_measurement=UNIT_METER, + accuracy_decimals=2, + ), + cv.Optional(CONF_POWER): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=2, + ), + cv.Optional(CONF_CURRENT): sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + ), + cv.Optional(CONF_SPEED): sensor.sensor_schema( + unit_of_measurement=UNIT_REVOLUTIONS_PER_MINUTE, + accuracy_decimals=2, + ), + cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=2, + ), + } + ) + .extend(ble_client.BLE_CLIENT_SCHEMA) + .extend(cv.polling_component_schema("15s")) +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await ble_client.register_ble_node(var, config) + + if CONF_FLOW in config: + sens = await sensor.new_sensor(config[CONF_FLOW]) + cg.add(var.set_flow_sensor(sens)) + + if CONF_HEAD in config: + sens = await sensor.new_sensor(config[CONF_HEAD]) + cg.add(var.set_head_sensor(sens)) + + if CONF_POWER in config: + sens = await sensor.new_sensor(config[CONF_POWER]) + cg.add(var.set_power_sensor(sens)) + + if CONF_CURRENT in config: + sens = await sensor.new_sensor(config[CONF_CURRENT]) + cg.add(var.set_current_sensor(sens)) + + if CONF_SPEED in config: + sens = await sensor.new_sensor(config[CONF_SPEED]) + cg.add(var.set_speed_sensor(sens)) + + if CONF_VOLTAGE in config: + sens = await sensor.new_sensor(config[CONF_VOLTAGE]) + cg.add(var.set_voltage_sensor(sens)) diff --git a/esphome/const.py b/esphome/const.py index 3145255e20..3442c392c7 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -262,6 +262,7 @@ CONF_FINGER_ID = "finger_id" CONF_FINGERPRINT_COUNT = "fingerprint_count" CONF_FLASH_LENGTH = "flash_length" CONF_FLASH_TRANSITION_LENGTH = "flash_transition_length" +CONF_FLOW = "flow" CONF_FLOW_CONTROL_PIN = "flow_control_pin" CONF_FOR = "for" CONF_FORCE_UPDATE = "force_update" @@ -286,6 +287,7 @@ CONF_GPIO = "gpio" CONF_GREEN = "green" CONF_GROUP = "group" CONF_HARDWARE_UART = "hardware_uart" +CONF_HEAD = "head" CONF_HEARTBEAT = "heartbeat" CONF_HEAT_ACTION = "heat_action" CONF_HEAT_DEADBAND = "heat_deadband" @@ -891,6 +893,7 @@ UNIT_CENTIMETER = "cm" UNIT_COUNT_DECILITRE = "/dL" UNIT_COUNTS_PER_CUBIC_METER = "#/m³" UNIT_CUBIC_METER = "m³" +UNIT_CUBIC_METER_PER_HOUR = "m³/h" UNIT_DECIBEL = "dB" UNIT_DECIBEL_MILLIWATT = "dBm" UNIT_DEGREE_PER_SECOND = "°/s" @@ -928,6 +931,7 @@ UNIT_PERCENT = "%" UNIT_PH = "pH" UNIT_PULSES = "pulses" UNIT_PULSES_PER_MINUTE = "pulses/min" +UNIT_REVOLUTIONS_PER_MINUTE = "RPM" UNIT_SECOND = "s" UNIT_STEPS = "steps" UNIT_VOLT = "V" diff --git a/tests/test1.yaml b/tests/test1.yaml index 21379e807c..9d279b5e40 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -325,6 +325,7 @@ ble_client: accept: True - mac_address: C4:4F:33:11:22:33 id: my_bedjet_ble_client + bedjet: - ble_client_id: my_bedjet_ble_client id: my_bedjet_client @@ -1269,6 +1270,16 @@ sensor: pressure: name: "MPL3115A2 Pressure" update_interval: 10s + - platform: alpha3 + ble_client_id: ble_foo + flow: + name: "Radiator Pump Flow" + head: + name: "Radiator Pump Head" + power: + name: "Radiator Pump Power" + speed: + name: "Radiator Pump Speed" - platform: ld2410 moving_distance: name: "Moving distance (cm)"