mirror of
https://github.com/esphome/esphome.git
synced 2025-03-25 03:58:23 +00:00
Co-authored-by: Martin <25747549+martgras@users.noreply.github.com> Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
524 lines
15 KiB
C++
524 lines
15 KiB
C++
#include "esphome/core/defines.h"
|
|
#include "esphome/core/helpers.h"
|
|
|
|
#include "shelly_dimmer.h"
|
|
#ifdef USE_SHD_FIRMWARE_DATA
|
|
#include "stm32flash.h"
|
|
#endif
|
|
|
|
#ifndef USE_ESP_IDF
|
|
#include <HardwareSerial.h>
|
|
#endif
|
|
|
|
#include <algorithm>
|
|
#include <cstring>
|
|
#include <memory>
|
|
#include <numeric>
|
|
|
|
namespace {
|
|
|
|
constexpr char TAG[] = "shelly_dimmer";
|
|
|
|
constexpr uint8_t SHELLY_DIMMER_ACK_TIMEOUT = 200; // ms
|
|
constexpr uint8_t SHELLY_DIMMER_MAX_RETRIES = 3;
|
|
constexpr uint16_t SHELLY_DIMMER_MAX_BRIGHTNESS = 1000; // 100%
|
|
|
|
// Protocol framing.
|
|
constexpr uint8_t SHELLY_DIMMER_PROTO_START_BYTE = 0x01;
|
|
constexpr uint8_t SHELLY_DIMMER_PROTO_END_BYTE = 0x04;
|
|
|
|
// Supported commands.
|
|
constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_SWITCH = 0x01;
|
|
constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_POLL = 0x10;
|
|
constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_VERSION = 0x11;
|
|
constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_SETTINGS = 0x20;
|
|
|
|
// Command payload sizes.
|
|
constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_SWITCH_SIZE = 2;
|
|
constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_SETTINGS_SIZE = 10;
|
|
constexpr uint8_t SHELLY_DIMMER_PROTO_MAX_FRAME_SIZE = 4 + 72 + 3;
|
|
|
|
// STM Firmware
|
|
#ifdef USE_SHD_FIRMWARE_DATA
|
|
constexpr uint8_t STM_FIRMWARE[] PROGMEM = USE_SHD_FIRMWARE_DATA;
|
|
constexpr uint32_t STM_FIRMWARE_SIZE_IN_BYTES = sizeof(STM_FIRMWARE);
|
|
#endif
|
|
|
|
// Scaling Constants
|
|
constexpr float POWER_SCALING_FACTOR = 880373;
|
|
constexpr float VOLTAGE_SCALING_FACTOR = 347800;
|
|
constexpr float CURRENT_SCALING_FACTOR = 1448;
|
|
|
|
// Esentially std::size() for pre c++17
|
|
template<typename T, size_t N> constexpr size_t size(const T (&/*unused*/)[N]) noexcept { return N; }
|
|
|
|
} // Anonymous namespace
|
|
|
|
namespace esphome {
|
|
namespace shelly_dimmer {
|
|
|
|
/// Computes a crappy checksum as defined by the Shelly Dimmer protocol.
|
|
uint16_t shelly_dimmer_checksum(const uint8_t *buf, int len) {
|
|
return std::accumulate<decltype(buf), uint16_t>(buf, buf + len, 0);
|
|
}
|
|
|
|
void ShellyDimmer::setup() {
|
|
this->pin_nrst_->setup();
|
|
this->pin_boot0_->setup();
|
|
|
|
ESP_LOGI(TAG, "Initializing Shelly Dimmer...");
|
|
|
|
// Reset the STM32 and check the firmware version.
|
|
for (int i = 0; i < 2; i++) {
|
|
this->reset_normal_boot_();
|
|
this->send_command_(SHELLY_DIMMER_PROTO_CMD_VERSION, nullptr, 0);
|
|
ESP_LOGI(TAG, "STM32 current firmware version: %d.%d, desired version: %d.%d", this->version_major_,
|
|
this->version_minor_, USE_SHD_FIRMWARE_MAJOR_VERSION, USE_SHD_FIRMWARE_MINOR_VERSION);
|
|
if (this->version_major_ != USE_SHD_FIRMWARE_MAJOR_VERSION ||
|
|
this->version_minor_ != USE_SHD_FIRMWARE_MINOR_VERSION) {
|
|
#ifdef USE_SHD_FIRMWARE_DATA
|
|
// Update firmware if needed.
|
|
ESP_LOGW(TAG, "Unsupported STM32 firmware version, flashing");
|
|
if (i > 0) {
|
|
// Upgrade was already performed but the reported version is still not right.
|
|
ESP_LOGE(TAG, "STM32 firmware upgrade already performed, but version is still incorrect");
|
|
this->mark_failed();
|
|
return;
|
|
}
|
|
|
|
if (!this->upgrade_firmware_()) {
|
|
ESP_LOGW(TAG, "Failed to upgrade firmware");
|
|
this->mark_failed();
|
|
return;
|
|
}
|
|
|
|
// Firmware upgrade completed, do the checks again.
|
|
continue;
|
|
#else
|
|
ESP_LOGW(TAG, "Firmware version mismatch, put 'update: true' in the yaml to flash an update.");
|
|
this->mark_failed();
|
|
return;
|
|
#endif
|
|
}
|
|
break;
|
|
}
|
|
|
|
this->send_settings_();
|
|
// Do an immediate poll to refresh current state.
|
|
this->send_command_(SHELLY_DIMMER_PROTO_CMD_POLL, nullptr, 0);
|
|
|
|
this->ready_ = true;
|
|
}
|
|
|
|
void ShellyDimmer::update() { this->send_command_(SHELLY_DIMMER_PROTO_CMD_POLL, nullptr, 0); }
|
|
|
|
void ShellyDimmer::dump_config() {
|
|
ESP_LOGCONFIG(TAG, "ShellyDimmer:");
|
|
LOG_PIN(" NRST Pin: ", this->pin_nrst_);
|
|
LOG_PIN(" BOOT0 Pin: ", this->pin_boot0_);
|
|
|
|
ESP_LOGCONFIG(TAG, " Leading Edge: %s", YESNO(this->leading_edge_));
|
|
ESP_LOGCONFIG(TAG, " Warmup Brightness: %d", this->warmup_brightness_);
|
|
// ESP_LOGCONFIG(TAG, " Warmup Time: %d", this->warmup_time_);
|
|
// ESP_LOGCONFIG(TAG, " Fade Rate: %d", this->fade_rate_);
|
|
ESP_LOGCONFIG(TAG, " Minimum Brightness: %d", this->min_brightness_);
|
|
ESP_LOGCONFIG(TAG, " Maximum Brightness: %d", this->max_brightness_);
|
|
|
|
LOG_UPDATE_INTERVAL(this);
|
|
|
|
ESP_LOGCONFIG(TAG, " STM32 current firmware version: %d.%d ", this->version_major_, this->version_minor_);
|
|
ESP_LOGCONFIG(TAG, " STM32 required firmware version: %d.%d", USE_SHD_FIRMWARE_MAJOR_VERSION,
|
|
USE_SHD_FIRMWARE_MINOR_VERSION);
|
|
|
|
if (this->version_major_ != USE_SHD_FIRMWARE_MAJOR_VERSION ||
|
|
this->version_minor_ != USE_SHD_FIRMWARE_MINOR_VERSION) {
|
|
ESP_LOGE(TAG, " Firmware version mismatch, put 'update: true' in the yaml to flash an update.");
|
|
}
|
|
}
|
|
|
|
void ShellyDimmer::write_state(light::LightState *state) {
|
|
if (!this->ready_) {
|
|
return;
|
|
}
|
|
|
|
float brightness;
|
|
state->current_values_as_brightness(&brightness);
|
|
|
|
const uint16_t brightness_int = this->convert_brightness_(brightness);
|
|
if (brightness_int == this->brightness_) {
|
|
ESP_LOGV(TAG, "Not sending unchanged value");
|
|
return;
|
|
}
|
|
ESP_LOGD(TAG, "Brightness update: %d (raw: %f)", brightness_int, brightness);
|
|
|
|
this->send_brightness_(brightness_int);
|
|
}
|
|
#ifdef USE_SHD_FIRMWARE_DATA
|
|
bool ShellyDimmer::upgrade_firmware_() {
|
|
ESP_LOGW(TAG, "Starting STM32 firmware upgrade");
|
|
this->reset_dfu_boot_();
|
|
|
|
// Cleanup with RAII
|
|
auto stm32 = stm32_init(this, STREAM_SERIAL, 1);
|
|
|
|
if (!stm32) {
|
|
ESP_LOGW(TAG, "Failed to initialize STM32");
|
|
return false;
|
|
}
|
|
|
|
// Erase STM32 flash.
|
|
if (stm32_erase_memory(stm32, 0, STM32_MASS_ERASE) != STM32_ERR_OK) {
|
|
ESP_LOGW(TAG, "Failed to erase STM32 flash memory");
|
|
return false;
|
|
}
|
|
|
|
static constexpr uint32_t BUFFER_SIZE = 256;
|
|
|
|
// Copy the STM32 firmware over in 256-byte chunks. Note that the firmware is stored
|
|
// in flash memory so all accesses need to be 4-byte aligned.
|
|
uint8_t buffer[BUFFER_SIZE];
|
|
const uint8_t *p = STM_FIRMWARE;
|
|
uint32_t offset = 0;
|
|
uint32_t addr = stm32->dev->fl_start;
|
|
const uint32_t end = addr + STM_FIRMWARE_SIZE_IN_BYTES;
|
|
|
|
while (addr < end && offset < STM_FIRMWARE_SIZE_IN_BYTES) {
|
|
const uint32_t left_of_buffer = std::min(end - addr, BUFFER_SIZE);
|
|
const uint32_t len = std::min(left_of_buffer, STM_FIRMWARE_SIZE_IN_BYTES - offset);
|
|
|
|
if (len == 0) {
|
|
break;
|
|
}
|
|
|
|
std::memcpy(buffer, p, BUFFER_SIZE);
|
|
p += BUFFER_SIZE;
|
|
|
|
if (stm32_write_memory(stm32, addr, buffer, len) != STM32_ERR_OK) {
|
|
ESP_LOGW(TAG, "Failed to write to STM32 flash memory");
|
|
return false;
|
|
}
|
|
|
|
addr += len;
|
|
offset += len;
|
|
}
|
|
|
|
ESP_LOGI(TAG, "STM32 firmware upgrade successful");
|
|
|
|
return true;
|
|
}
|
|
#endif
|
|
|
|
uint16_t ShellyDimmer::convert_brightness_(float brightness) {
|
|
// Special case for zero as only zero means turn off completely.
|
|
if (brightness == 0.0) {
|
|
return 0;
|
|
}
|
|
|
|
return remap<uint16_t, float>(brightness, 0.0f, 1.0f, this->min_brightness_, this->max_brightness_);
|
|
}
|
|
|
|
void ShellyDimmer::send_brightness_(uint16_t brightness) {
|
|
const uint8_t payload[] = {
|
|
// Brightness (%) * 10.
|
|
static_cast<uint8_t>(brightness & 0xff),
|
|
static_cast<uint8_t>(brightness >> 8),
|
|
};
|
|
static_assert(size(payload) == SHELLY_DIMMER_PROTO_CMD_SWITCH_SIZE, "Invalid payload size");
|
|
|
|
this->send_command_(SHELLY_DIMMER_PROTO_CMD_SWITCH, payload, SHELLY_DIMMER_PROTO_CMD_SWITCH_SIZE);
|
|
|
|
this->brightness_ = brightness;
|
|
}
|
|
|
|
void ShellyDimmer::send_settings_() {
|
|
const uint16_t fade_rate = std::min(uint16_t{100}, this->fade_rate_);
|
|
|
|
float brightness = 0.0;
|
|
if (this->state_ != nullptr) {
|
|
this->state_->current_values_as_brightness(&brightness);
|
|
}
|
|
const uint16_t brightness_int = this->convert_brightness_(brightness);
|
|
ESP_LOGD(TAG, "Brightness update: %d (raw: %f)", brightness_int, brightness);
|
|
|
|
const uint8_t payload[] = {
|
|
// Brightness (%) * 10.
|
|
static_cast<uint8_t>(brightness_int & 0xff),
|
|
static_cast<uint8_t>(brightness_int >> 8),
|
|
// Leading / trailing edge [0x01 = leading, 0x02 = trailing].
|
|
this->leading_edge_ ? uint8_t{0x01} : uint8_t{0x02},
|
|
0x00,
|
|
// Fade rate.
|
|
static_cast<uint8_t>(fade_rate & 0xff),
|
|
static_cast<uint8_t>(fade_rate >> 8),
|
|
// Warmup brightness.
|
|
static_cast<uint8_t>(this->warmup_brightness_ & 0xff),
|
|
static_cast<uint8_t>(this->warmup_brightness_ >> 8),
|
|
// Warmup time.
|
|
static_cast<uint8_t>(this->warmup_time_ & 0xff),
|
|
static_cast<uint8_t>(this->warmup_time_ >> 8),
|
|
};
|
|
static_assert(size(payload) == SHELLY_DIMMER_PROTO_CMD_SETTINGS_SIZE, "Invalid payload size");
|
|
|
|
this->send_command_(SHELLY_DIMMER_PROTO_CMD_SETTINGS, payload, SHELLY_DIMMER_PROTO_CMD_SETTINGS_SIZE);
|
|
|
|
// Also send brightness separately as it is ignored above.
|
|
this->send_brightness_(brightness_int);
|
|
}
|
|
|
|
bool ShellyDimmer::send_command_(uint8_t cmd, const uint8_t *const payload, uint8_t len) {
|
|
ESP_LOGD(TAG, "Sending command: 0x%02x (%d bytes) payload 0x%s", cmd, len, format_hex(payload, len).c_str());
|
|
|
|
// Prepare a command frame.
|
|
uint8_t frame[SHELLY_DIMMER_PROTO_MAX_FRAME_SIZE];
|
|
const size_t frame_len = this->frame_command_(frame, cmd, payload, len);
|
|
|
|
// Write the frame and wait for acknowledgement.
|
|
int retries = SHELLY_DIMMER_MAX_RETRIES;
|
|
while (retries--) {
|
|
this->write_array(frame, frame_len);
|
|
this->flush();
|
|
|
|
ESP_LOGD(TAG, "Command sent, waiting for reply");
|
|
const uint32_t tx_time = millis();
|
|
while (millis() - tx_time < SHELLY_DIMMER_ACK_TIMEOUT) {
|
|
if (this->read_frame_()) {
|
|
return true;
|
|
}
|
|
delay(1);
|
|
}
|
|
ESP_LOGW(TAG, "Timeout while waiting for reply");
|
|
}
|
|
ESP_LOGW(TAG, "Failed to send command");
|
|
return false;
|
|
}
|
|
|
|
size_t ShellyDimmer::frame_command_(uint8_t *data, uint8_t cmd, const uint8_t *const payload, size_t len) {
|
|
size_t pos = 0;
|
|
|
|
// Generate a frame.
|
|
data[0] = SHELLY_DIMMER_PROTO_START_BYTE;
|
|
data[1] = ++this->seq_;
|
|
data[2] = cmd;
|
|
data[3] = len;
|
|
pos += 4;
|
|
|
|
if (payload != nullptr) {
|
|
std::memcpy(data + 4, payload, len);
|
|
pos += len;
|
|
}
|
|
|
|
// Calculate checksum for the payload.
|
|
const uint16_t csum = shelly_dimmer_checksum(data + 1, 3 + len);
|
|
data[pos++] = static_cast<uint8_t>(csum >> 8);
|
|
data[pos++] = static_cast<uint8_t>(csum & 0xff);
|
|
data[pos++] = SHELLY_DIMMER_PROTO_END_BYTE;
|
|
return pos;
|
|
}
|
|
|
|
int ShellyDimmer::handle_byte_(uint8_t c) {
|
|
const uint8_t pos = this->buffer_pos_;
|
|
|
|
if (pos == 0) {
|
|
// Must be start byte.
|
|
return c == SHELLY_DIMMER_PROTO_START_BYTE ? 1 : -1;
|
|
} else if (pos < 4) {
|
|
// Header.
|
|
return 1;
|
|
}
|
|
|
|
// Decode payload length from header.
|
|
const uint8_t payload_len = this->buffer_[3];
|
|
if ((4 + payload_len + 3) > SHELLY_DIMMER_BUFFER_SIZE) {
|
|
return -1;
|
|
}
|
|
|
|
if (pos < 4 + payload_len + 1) {
|
|
// Payload.
|
|
return 1;
|
|
}
|
|
|
|
if (pos == 4 + payload_len + 1) {
|
|
// Verify checksum.
|
|
const uint16_t csum = (this->buffer_[pos - 1] << 8 | c);
|
|
const uint16_t csum_verify = shelly_dimmer_checksum(&this->buffer_[1], 3 + payload_len);
|
|
if (csum != csum_verify) {
|
|
return -1;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
if (pos == 4 + payload_len + 2) {
|
|
// Must be end byte.
|
|
return c == SHELLY_DIMMER_PROTO_END_BYTE ? 0 : -1;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
bool ShellyDimmer::read_frame_() {
|
|
while (this->available()) {
|
|
const uint8_t c = this->read();
|
|
this->buffer_[this->buffer_pos_] = c;
|
|
|
|
ESP_LOGV(TAG, "Read byte: 0x%02x (pos %d)", c, this->buffer_pos_);
|
|
|
|
switch (this->handle_byte_(c)) {
|
|
case 0: {
|
|
// Frame successfully received.
|
|
this->handle_frame_();
|
|
this->buffer_pos_ = 0;
|
|
return true;
|
|
}
|
|
case -1: {
|
|
// Failure.
|
|
this->buffer_pos_ = 0;
|
|
break;
|
|
}
|
|
case 1: {
|
|
// Need more data.
|
|
this->buffer_pos_++;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool ShellyDimmer::handle_frame_() {
|
|
const uint8_t seq = this->buffer_[1];
|
|
const uint8_t cmd = this->buffer_[2];
|
|
const uint8_t payload_len = this->buffer_[3];
|
|
|
|
ESP_LOGD(TAG, "Got frame: 0x%02x", cmd);
|
|
|
|
// Compare with expected identifier as the frame is always a response to
|
|
// our previously sent command.
|
|
if (seq != this->seq_) {
|
|
return false;
|
|
}
|
|
|
|
const uint8_t *payload = &this->buffer_[4];
|
|
|
|
// Handle response.
|
|
switch (cmd) {
|
|
case SHELLY_DIMMER_PROTO_CMD_POLL: {
|
|
if (payload_len < 16) {
|
|
return false;
|
|
}
|
|
|
|
const uint8_t hw_version = payload[0];
|
|
// payload[1] is unused.
|
|
const uint16_t brightness = encode_uint16(payload[3], payload[2]);
|
|
|
|
const uint32_t power_raw = encode_uint32(payload[7], payload[6], payload[5], payload[4]);
|
|
|
|
const uint32_t voltage_raw = encode_uint32(payload[11], payload[10], payload[9], payload[8]);
|
|
|
|
const uint32_t current_raw = encode_uint32(payload[15], payload[14], payload[13], payload[12]);
|
|
|
|
const uint16_t fade_rate = payload[16];
|
|
|
|
float power = 0;
|
|
if (power_raw > 0) {
|
|
power = POWER_SCALING_FACTOR / static_cast<float>(power_raw);
|
|
}
|
|
|
|
float voltage = 0;
|
|
if (voltage_raw > 0) {
|
|
voltage = VOLTAGE_SCALING_FACTOR / static_cast<float>(voltage_raw);
|
|
}
|
|
|
|
float current = 0;
|
|
if (current_raw > 0) {
|
|
current = CURRENT_SCALING_FACTOR / static_cast<float>(current_raw);
|
|
}
|
|
|
|
ESP_LOGI(TAG, "Got dimmer data:");
|
|
ESP_LOGI(TAG, " HW version: %d", hw_version);
|
|
ESP_LOGI(TAG, " Brightness: %d", brightness);
|
|
ESP_LOGI(TAG, " Fade rate: %d", fade_rate);
|
|
ESP_LOGI(TAG, " Power: %f W", power);
|
|
ESP_LOGI(TAG, " Voltage: %f V", voltage);
|
|
ESP_LOGI(TAG, " Current: %f A", current);
|
|
|
|
// Update sensors.
|
|
if (this->power_sensor_ != nullptr) {
|
|
this->power_sensor_->publish_state(power);
|
|
}
|
|
if (this->voltage_sensor_ != nullptr) {
|
|
this->voltage_sensor_->publish_state(voltage);
|
|
}
|
|
if (this->current_sensor_ != nullptr) {
|
|
this->current_sensor_->publish_state(current);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
case SHELLY_DIMMER_PROTO_CMD_VERSION: {
|
|
if (payload_len < 2) {
|
|
return false;
|
|
}
|
|
|
|
this->version_minor_ = payload[0];
|
|
this->version_major_ = payload[1];
|
|
return true;
|
|
}
|
|
case SHELLY_DIMMER_PROTO_CMD_SWITCH:
|
|
case SHELLY_DIMMER_PROTO_CMD_SETTINGS: {
|
|
return !(payload_len < 1 || payload[0] != 0x01);
|
|
}
|
|
default: {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
void ShellyDimmer::reset_(bool boot0) {
|
|
ESP_LOGD(TAG, "Reset STM32, boot0=%d", boot0);
|
|
|
|
this->pin_boot0_->digital_write(boot0);
|
|
this->pin_nrst_->digital_write(false);
|
|
|
|
// Wait 50ms for the STM32 to reset.
|
|
delay(50); // NOLINT
|
|
|
|
// Clear receive buffer.
|
|
while (this->available()) {
|
|
this->read();
|
|
}
|
|
|
|
this->pin_nrst_->digital_write(true);
|
|
// Wait 50ms for the STM32 to boot.
|
|
delay(50); // NOLINT
|
|
|
|
ESP_LOGD(TAG, "Reset STM32 done");
|
|
}
|
|
|
|
void ShellyDimmer::reset_normal_boot_() {
|
|
// set NONE parity in normal mode
|
|
|
|
#ifndef USE_ESP_IDF // workaround for reconfiguring the uart
|
|
Serial.end();
|
|
Serial.begin(115200, SERIAL_8N1);
|
|
Serial.flush();
|
|
#endif
|
|
|
|
this->flush();
|
|
this->reset_(false);
|
|
}
|
|
|
|
void ShellyDimmer::reset_dfu_boot_() {
|
|
// set EVEN parity in bootloader mode
|
|
|
|
#ifndef USE_ESP_IDF // workaround for reconfiguring the uart
|
|
Serial.end();
|
|
Serial.begin(115200, SERIAL_8E1);
|
|
Serial.flush();
|
|
#endif
|
|
|
|
this->flush();
|
|
this->reset_(true);
|
|
}
|
|
|
|
} // namespace shelly_dimmer
|
|
} // namespace esphome
|