From 7c7cd6a198f489fbdf40676b0167867674d301e3 Mon Sep 17 00:00:00 2001 From: kislovdm Date: Mon, 21 Jul 2025 19:54:32 +0300 Subject: [PATCH] first commit --- .gitignore | 5 + coffee-philips-5400.yaml | 175 +++++++++++++++++ components/saeco_series_6000/__init__.py | 48 +++++ .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 3488 bytes .../saeco_series_6000/saeco_series_6000.cpp | 185 ++++++++++++++++++ .../saeco_series_6000/saeco_series_6000.h | 51 +++++ 6 files changed, 464 insertions(+) create mode 100644 .gitignore create mode 100644 coffee-philips-5400.yaml create mode 100644 components/saeco_series_6000/__init__.py create mode 100644 components/saeco_series_6000/__pycache__/__init__.cpython-311.pyc create mode 100644 components/saeco_series_6000/saeco_series_6000.cpp create mode 100644 components/saeco_series_6000/saeco_series_6000.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d8b4157 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +# Gitignore settings for ESPHome +# This is an example and may include too much for your use-case. +# You can modify this file to suit your needs. +/.esphome/ +/secrets.yaml diff --git a/coffee-philips-5400.yaml b/coffee-philips-5400.yaml new file mode 100644 index 0000000..5bb56a3 --- /dev/null +++ b/coffee-philips-5400.yaml @@ -0,0 +1,175 @@ +sensor: [] +substitutions: + board_name: Coffee Philips 5400 + node_name: coffee-philips-5400 + +esphome: + name: ${node_name} + friendly_name: coffee-philips-5400 + comment: ESP32 Coffee Philips 5400 + +esp32: + board: esp32dev + framework: + type: arduino + +# Enable logging +#Журналирование +logger: + level: DEBUG #NONE + baud_rate: 0 + +# Enable Home Assistant API +api: + +# web_server: +# port: 80 + +ota: + platform: esphome + password: "S/6VwLrSJ+vRfPKWYzbg+iasPO6h3sXm3jY9Ko1YCOo=" + +#Учетные данные Wi-Fi для подключения платы к домашней сети +wifi: + ssid: "MonsterFNV" + password: "7065485qwe" + fast_connect: off + reboot_timeout: 15min + +#Если не будет связи с WiFi, то поднимется точка доступа + ap: + ssid: ESP SmartCoffeePhilips + password: ap_esp_password + +uart: +#UART подключение к панели управления. Считываем данные которые приходят от материнской плате в панель управления + - id: uart_display + rx_pin: GPIO16 #Считываем данные с панели управления + tx_pin: GPIO17 #Посылаем данные в панель управления + baud_rate: 115200 + stop_bits: 1 + data_bits: 8 + parity: NONE + rx_buffer_size: 256 + + - id: uart_mainboard + rx_pin: GPIO14 #Считываем данные с материнской платы + tx_pin: GPIO5 #Посылаем данные в материнскую плату + baud_rate: 115200 + stop_bits: 1 + data_bits: 8 + parity: NONE + rx_buffer_size: 256 + +external_components: + - source: + type: local + path: ./components + +saeco_series_6000: + uart_display: uart_display + uart_mainboard: uart_mainboard + debug: true + + #Сенсор в который компонент публикует статус в цифровом виде + state_sensor: + name: Work State + id: idControlSensor + internal: True #Скрыть - True \показать - False + on_value: + then: + - lambda: |- + //Статус состояния кофемашины, включена или выключена + if(x==2){ + id(idPowerStatus).publish_state(false); + } else if(id(idPowerStatus).state==false){ + id(idPowerStatus).publish_state(true); + } + + //AA:AA:AA:B0 "Системные сенсоры" + if(x==21){ + id(idStatus).publish_state("Выберите напиток"); + id(idWater).publish_state("Вода есть"); + id(idPallet).publish_state("Вставлен"); + id(idGrainTray).publish_state("Зерна есть"); + id(idCoffeeGroundsContainer).publish_state("Пустой"); + } + + //AA:AA:AA:B0 "Статусы" + else if(x==1){id(idStatus).publish_state("Что-то (07 01 00)");} + else if(x==2){id(idStatus).publish_state("Выключено");} + else if(x==3){id(idStatus).publish_state("Наливаем молоко");} + else if(x==4){id(idStatus).publish_state("Наливаем кофе");} + else if(x==5){id(idStatus).publish_state("Предварительное дозирование");} + else if(x==6){id(idStatus).publish_state("Создание пара для молока");} + else if(x==7){id(idStatus).publish_state("Заварочный узел в положение заваривания");} + else if(x==8){id(idStatus).publish_state("Наслаждайтесь");} + else if(x==9){id(idStatus).publish_state("Нагрев воды");} + else if(x==10){id(idStatus).publish_state("Перемалываем зерна");} + else if(x==11){id(idStatus).publish_state("Промывка");} + else if(x==12){id(idGrainTray).publish_state("Зерна закончились");} + else if(x==13){id(idStatus).publish_state("Что-то (07 08 14)");} + else if(x==14){id(idStatus).publish_state("Нагревание");} + else if(x==15){id(idStatus).publish_state("Наслаждайтесь");} + else if(x==16){id(idStatus).publish_state("Что-то (07 0C 02)");} + else if(x==17){id(idWater).publish_state("Вода есть");} + else if(x==18){id(idPallet).publish_state("Вставлен");} + else if(x==20){id(idCoffeeGroundsContainer).publish_state("Опорож. контейнер для коф. гущи");} + else if(x==22){id(idWater).publish_state("Воды нет");} + else if(x==23){id(idPallet).publish_state("Извлечен");} + else if(x==24){id(idPallet).publish_state("Очистка от накипи. Шаг 1");} + else if(x==25){id(idPallet).publish_state("Очистка от накипи. Шаг 2");} + + + //AA:AA:AA:B5 "Error Code" + else if(x==30){id(idErrorCode).publish_state("00");} + else if(x==31){id(idErrorCode).publish_state("0B");} + else if(x==32){id(idErrorCode).publish_state("E6");} + else if(x==33){id(idErrorCode).publish_state("80");} + else if(x==34){id(idErrorCode).publish_state("CB");} + else if(x==35){id(idErrorCode).publish_state("FF");} + else if(x==36){id(idErrorCode).publish_state("A0");} + +##################################################################################### +################################## Бинарный сенсор ################################## +#Статус кофемашины: включена или выключена +binary_sensor: + - platform: template + name: "Power Status" + id: idPowerStatus + icon: mdi:power + +##################################################################################### +################################### Текстовый сенсор ################################ + +text_sensor: + - platform: template + name: "Status" + id: idStatus + icon: mdi:coffee-to-go + update_interval: never + - platform: template + name: "Water" + id: idWater + icon: mdi:cup-water + update_interval: never + - platform: template + name: "Coffee Pallet" + id: idPallet + icon: mdi:spirit-level + update_interval: never + - platform: template + name: "Grain Tray" + id: idGrainTray + icon: mdi:grain + update_interval: never + - platform: template + name: "Coffee Grounds Container" + id: idCoffeeGroundsContainer + icon: mdi:train-car-centerbeam-full + update_interval: never + - platform: template + name: "Error Code" + id: idErrorCode + icon: mdi:alert-circle-outline + update_interval: never \ No newline at end of file diff --git a/components/saeco_series_6000/__init__.py b/components/saeco_series_6000/__init__.py new file mode 100644 index 0000000..85acddf --- /dev/null +++ b/components/saeco_series_6000/__init__.py @@ -0,0 +1,48 @@ +DEPENDENCIES = ["uart", "sensor"] + +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import uart, sensor +from esphome.const import CONF_ID, CONF_DEBUG + +CONF_STATE_SENSOR = "state_sensor" +CONF_B0_STATUS_SENSOR = "b0_status_sensor" +CONF_B5_STATUS_SENSOR = "b5_status_sensor" +CONF_BA_STATUS_SENSOR = "ba_status_sensor" +CONF_S90_STATUS_SENSOR = "s90_status_sensor" +CONF_S91_STATUS_SENSOR = "s91_status_sensor" +CONF_S93_STATUS_SENSOR = "s93_status_sensor" +CONF_WATER_SENSOR = "water_sensor" +CONF_COFFEE_GROUNDS_CONTAINER_SENSOR = "coffee_grounds_container_sensor" +CONF_PALLET_SENSOR = "pallet_sensor" +CONF_ERROR_CODE_SENSOR = "error_code_sensor" +CONF_GRAIN_TRAY_SENSOR = "grain_tray_sensor" +CONF_STATUS_TEXT_SENSOR = "status_text_sensor" + +saeco_series_6000_ns = cg.esphome_ns.namespace('saeco_series_6000') +SaecoSeries6000 = saeco_series_6000_ns.class_('SaecoSeries6000', cg.Component) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(SaecoSeries6000), + cv.Required('uart_display'): cv.use_id(uart.UARTComponent), + cv.Required('uart_mainboard'): cv.use_id(uart.UARTComponent), + cv.Optional(CONF_DEBUG, default=False): cv.boolean, + cv.Optional(CONF_STATE_SENSOR): sensor.sensor_schema(), +}).extend(cv.COMPONENT_SCHEMA) + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + + uart_display = yield cg.get_variable(config['uart_display']) + cg.add(var.set_uart_display(uart_display)) + + uart_mainboard = yield cg.get_variable(config['uart_mainboard']) + cg.add(var.set_uart_mainboard(uart_mainboard)) + + if config[CONF_DEBUG]: + cg.add(var.set_debug(True)) + + if CONF_STATE_SENSOR in config: + sens = yield sensor.new_sensor(config[CONF_STATE_SENSOR]) + cg.add(var.set_status_sensor(sens)) \ No newline at end of file diff --git a/components/saeco_series_6000/__pycache__/__init__.cpython-311.pyc b/components/saeco_series_6000/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f7395f9389a9196346b31f63bc164a6157f19617 GIT binary patch literal 3488 zcma(TOK;oAc_h(#%X-AJqBw~nS&b{(QXRV+Y=SnnEXz)I6Umj7ZA*AT(3*)NLl2jv zvkhPuZjnP!w5SgOHisN^*rqPBhn{-wp+A5$SVR~gfPo@FdvlQj1@_b#N)$~$wjGUT zzW4Xd%s1cg4?dq8kak%6#be|E;O`XCo39Uicx(mWO8^11)T}IkR%mNjnpW0k>A_5M zh_!1l({MDMth4E2T^c^ra5p`yr|D(A1h=z3=wOGTll4Ov8-Q*$2t8~Fdf5xm$Bw{Z zHVpmjC=9R>7-TQP5F16djZ1nsoiQCcKEn&IEdabB|6XG+qsyHuI(B@9BgEQOo${Kj z=?(e!8XL0$WZ$@^;~Ey%vDXB6qa#4$C3FFg*xXy*)Gq_2ei<}$Z(cv@X3@f8& zaQ|N`*L4rb3nO1&G_RYic|zxFksH#!Q$AfA-_>Q!J5nDq%XU(_-0_+Cn$&T#JUa!W z&j33OZDo+UgqbZ~kg#1uEwL^9ld2R_R)Lv*QgbIr-YQbTpdL;C7LsR5YwM`_1Iuf~? zGcz;TU#6t8M$rgfx}O}Zi<=GpDIV6aCNaL=<^{N%vS0^9kNB;IR2|$Rvdu%}Pc!ht z9@+U%frV56=@@@w0B-|{A#(i|)d65W;~k4hn;xXrwk_MhoRgn{FD(|cuYvt-QiIO( zK=pR4CNFdieJ>rm4j-Slt>!4r*tbMs{njXK=SD$l?CG{`Gy64T7C?45U}zbhg^6?h zxD0s%g8##hG-AkZgC{`vM2Z-c4X+N9c#cUL@tEV*&VxBOLj85ojcW5YW42+&$QuI6 z+CRkTuDR(;BqY$eZ?^M!J;q!lRarh)Y9P>_n7 z3Q=kZ+etxqRIgzhiNaV1W~h!x=CH*H(r02C7Qp8WJ}jx0`4!UtYaS=JdmC zsghn4>ad2^25S@7PSt}oj;puo631mWpJIp9)~bv^D>QxI#B1ap|AYK?!8^BCbzgdU z^Y;_4<`wsp>Ym!oX|!0!*7;0vPpj@}nd$fU(Xm&t{n0-StEqV;7q!py47tjx?FI()y~P#w`DU>QojbGm7i z&hht-i!YaDM}qWs;W0IQz4PR6nR#_$K?!Hoa8~xy{+#KXc&!CWK#7{D$Xg@ zIVJ1;Xb2FKj>HGDQ6QDZ-lhC%zOa-pX4V}?G_I9j{i8xT77~;hDJYCMdQ`OK7-p5B`v$PgcRtf2o7thxH!YnY@kIA(t>479Q)4AgEaz4+kl!^}vOJ$DGD%pDl zLn@?6t!D4v&sTKKp7VUER4fsNCHFPHdOLI85nZ+H;`+I z*wx~j1m&v;kDR(ORKQ+s@S@0JS57Y*LYVHeyH>n;z9niDAHAppE7)1wq&dJhuw%X5ZXmveeZ5k_i9OSH^1P!KsY$uwgVkao zUmz>U-OWG9Vjo@Fy_G(y-Qhn1fqqm8^vmoU=MS1~xYa;+gdY)6L)2h z?1qyv2${H-GBSu9Mxrlv-bOM9k&OJkMJ19`BRLt2bVJwQhSCS2w49m$OX#ncl~6$q z6?XlHf#{y~g{#X1cNd>M_+{}~aqsiJ(u>bK^GbM54bOEMKVf{jH>@x*m5FttozPd& z{qL(&KUCs()cBn~Uk+STn7GQsJGVNsU)|cTsOfoSd_f&w&<(gdd6}71m`RnH++Y0q zlYwGfVG=5nkdq&Gmeu6PO7gavynW=ck6Hf(>_OYV>F^&fVBEjD`8=VzuPC;ws_p8L S4_Jne0)ET%5jZ_)=lWm8Vrv5c literal 0 HcmV?d00001 diff --git a/components/saeco_series_6000/saeco_series_6000.cpp b/components/saeco_series_6000/saeco_series_6000.cpp new file mode 100644 index 0000000..a05bcf0 --- /dev/null +++ b/components/saeco_series_6000/saeco_series_6000.cpp @@ -0,0 +1,185 @@ +#include "saeco_series_6000.h" +#include "esphome/core/log.h" +#include + +namespace esphome { +namespace saeco_series_6000 { + +static const char *TAG = "saeco_series_6000"; +static uint32_t last_activity_time = 0; + +void SaecoSeries6000::loop() { + uint8_t byte; + + // Bridge from uart_display (display) to uart_mainboard (mainboard) + while (uart_display_->available()) { + uart_display_->read_byte(&byte); + uart_mainboard_->write_byte(byte); // Mirror byte immediately + process_display_byte(byte); // Process the "sniffed" byte + } + + // Bridge from uart_mainboard (mainboard) to uart_display (display) + while (uart_mainboard_->available()) { + uart_mainboard_->read_byte(&byte); + uart_display_->write_byte(byte); // Mirror byte immediately + process_mainboard_byte(byte); // Process the "sniffed" byte + } + // Контроль активности UART (выключение через 15 сек) + static uint32_t last_check = 0; + uint32_t now = millis(); + if (now - last_check > 1000) { // проверяем каждую секунду + last_check = now; + if (now - last_activity_time > 15000) { + pub_status(2); // публикуем статус "выключено" + last_activity_time = now; // чтобы не публиковать повторно + } + } +} + +void SaecoSeries6000::process_display_byte(uint8_t byte) { + // Если получили первый 0xAA — начинаем новый пакет + if (byte == 0xAA && display_buffer_pos_ == 0) { + display_buffer_[display_buffer_pos_++] = byte; + return; + } + // Если уже начали собирать пакет + if (display_buffer_pos_ > 0 && display_buffer_pos_ < DISPLAY_BUFFER_SIZE) { + display_buffer_[display_buffer_pos_++] = byte; + if (byte == 0x55) { + parse_display_packet(display_buffer_, display_buffer_pos_); + display_buffer_pos_ = 0; + } + } +} + +void SaecoSeries6000::process_mainboard_byte(uint8_t byte) { + // Если получили первый 0xAA — начинаем новый пакет + if (byte == 0xAA && mainboard_buffer_pos_ == 0) { + mainboard_buffer_[mainboard_buffer_pos_++] = byte; + return; + } + // Если уже начали собирать пакет + if (mainboard_buffer_pos_ > 0 && mainboard_buffer_pos_ < MAINBOARD_BUFFER_SIZE) { + mainboard_buffer_[mainboard_buffer_pos_++] = byte; + if (byte == 0x55) { + parse_mainboard_packet(mainboard_buffer_, mainboard_buffer_pos_); + mainboard_buffer_pos_ = 0; + } + } +} + +void SaecoSeries6000::parse_display_packet(const uint8_t* buffer, uint16_t size) { + if (debug_) { + ESP_LOGD(TAG, "parse_display_packet: size=%u", size); + } + // Здесь больше не парсим B0-пакеты, только debug +} + +void SaecoSeries6000::parse_mainboard_packet(const uint8_t* buffer, uint16_t size) { + last_activity_time = millis(); // сбрасываем таймер активности при любом пакете + if (debug_) { + std::vector hex_buffer; + hex_buffer.resize(size * 3 + 1); + for (uint16_t i = 0; i < size; i++) { + sprintf(hex_buffer.data() + i * 3, "%02X ", buffer[i]); + } + ESP_LOGD(TAG, "Packet from Mainboard: %s", hex_buffer.data()); + ESP_LOGD(TAG, "parse_mainboard_packet: size=%u", size); + } + // Парсим B0 + if (size > 4 && buffer[3] == 0xB0) { + // Логика из philips_series_5400.cpp + if (size < 12) return; + if (buffer[6] == 0x0E) { + if (buffer[9] == 0x00) { + pub_status(20); // "Опорож. контейнер для коф. гущи" + } else { + if ((buffer[9] & 0x40) != 0) { + pub_status(22); // "Воды нет" + } else { + pub_status(17); // "Вода есть" + } + if ((buffer[9] & 0x80) != 0) { + pub_status(23); // "Извлечен" + } else { + pub_status(18); // "Вставлен" + } + } + } else if (buffer[6] == 0x06) { + pub_status(21); // "Вода есть","Вставлен","Зерна есть","Выберите напиток","Пустой" + } else if (buffer[6] == 0x0C) { + if (buffer[7] == 0x01) { + pub_status(8); // "Наслаждайтесь" + } else if (buffer[7] == 0x02) { + pub_status(15); // "Что-то (07 0C 02)" + } + } else if (buffer[6] == 0x07) { + if (buffer[7] == 0x0E) { + pub_status(9); // "Нагрев воды" + } else if (buffer[7] == 0x0D) { + pub_status(10); // "Перемалываем зерна" + } else if (buffer[7] == 0x10) { + pub_status(3); // "Наливаем молоко" + } else if (buffer[7] == 0x11) { + pub_status(4); // "Наливаем кофе" + } else if (buffer[7] == 0x12) { + pub_status(5); // "Предварительное дозирование" + } else if (buffer[7] == 0x13) { + pub_status(6); // "Создание пара для молока" + } else if (buffer[7] == 0x14) { + pub_status(7); // "Заварочный узел в положение заваривания" + } else if (buffer[7] == 0x15) { + pub_status(15); // "Наслаждайтесь" + } + } else if (buffer[6] == 0x08) { + if (buffer[7] == 0x0E) { + pub_status(14); // "Нагревание" + } else if (buffer[7] == 0x02) { + pub_status(11); // "Промывка" + } else if (buffer[7] == 0x14) { + pub_status(13); // "Что-то (07 08 14)" + } else if (buffer[7] == 0x05) { + pub_status(12); // "Зерна закончились" + } else if (buffer[7] == 0x16) { + pub_status(24); // "Удаление накипи стадия 1" + } else if (buffer[7] == 0x18) { + pub_status(25); // "Удаление накипи стадия 2" + } + } else if (buffer[7] == 0x00) { + if (buffer[6] == 0x01) { + pub_status(1); // "Что-то (07 01 00)" + } else if (buffer[6] == 0x05) { + pub_status(2); // "Выключено" + } + } + } else if (size > 12 && buffer[3] == 0xB5) { + // Парсим B5 (Error Code) + if (buffer[10] == 0x00) { + if (buffer[11] == 0x00) pub_status(30); + else if (buffer[11] == 0x0B) pub_status(31); + else if (buffer[11] == 0xE6) pub_status(32); + else if (buffer[11] == 0x80) pub_status(33); + else if (buffer[11] == 0xCB) pub_status(34); + else if (buffer[11] == 0xFF) pub_status(35); + else if (buffer[11] == 0xA0) pub_status(36); + } else if (buffer[10] == 0x01) { + pub_status(37); // "Статус2 0x01" + } + } +} + +void SaecoSeries6000::pub_status(uint8_t status) { + if (status_sensor_ != nullptr) { + status_sensor_->publish_state(status); + } +} + +void SaecoSeries6000::dump_config() { + ESP_LOGCONFIG(TAG, "Saeco Series 6000 UART Bridge"); + ESP_LOGCONFIG(TAG, " UART Display (uart_display): %p", uart_display_); + ESP_LOGCONFIG(TAG, " UART Mainboard (uart_mainboard): %p", uart_mainboard_); + ESP_LOGCONFIG(TAG, " Debug mode: %s", ONOFF(this->debug_)); +} + +} // namespace saeco_series_6000 +} // namespace esphome \ No newline at end of file diff --git a/components/saeco_series_6000/saeco_series_6000.h b/components/saeco_series_6000/saeco_series_6000.h new file mode 100644 index 0000000..a1bd489 --- /dev/null +++ b/components/saeco_series_6000/saeco_series_6000.h @@ -0,0 +1,51 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/uart/uart.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace saeco_series_6000 { + +// Buffer sizes for packet sniffing +constexpr size_t DISPLAY_BUFFER_SIZE = 128; +constexpr size_t MAINBOARD_BUFFER_SIZE = 128; + +class SaecoSeries6000 : public Component { + public: + void set_uart_display(uart::UARTComponent *uart_display) { uart_display_ = uart_display; } + void set_uart_mainboard(uart::UARTComponent *uart_mainboard) { uart_mainboard_ = uart_mainboard; } + void set_debug(bool debug) { debug_ = debug; } + void set_status_sensor(sensor::Sensor *sensor) { status_sensor_ = sensor; } + void loop() override; + void dump_config() override; + + protected: + uart::UARTComponent *uart_display_; + uart::UARTComponent *uart_mainboard_; + bool debug_{false}; + sensor::Sensor *status_sensor_{nullptr}; + + private: + uint8_t display_buffer_[DISPLAY_BUFFER_SIZE]; + uint16_t display_buffer_pos_{0}; + + uint8_t mainboard_buffer_[MAINBOARD_BUFFER_SIZE]; + uint16_t mainboard_buffer_pos_{0}; + + uint8_t display_last_bytes_[3] = {0, 0, 0}; + uint8_t mainboard_last_bytes_[3] = {0, 0, 0}; + + uint8_t display_sync_count_{0}; + uint8_t mainboard_sync_count_{0}; + + void process_display_byte(uint8_t byte); + void process_mainboard_byte(uint8_t byte); + + void parse_display_packet(const uint8_t* buffer, uint16_t size); + void parse_mainboard_packet(const uint8_t* buffer, uint16_t size); + void pub_status(uint8_t status); +}; + +} // namespace saeco_series_6000 +} // namespace esphome \ No newline at end of file