commit 7c7cd6a198f489fbdf40676b0167867674d301e3 Author: kislovdm Date: Mon Jul 21 19:54:32 2025 +0300 first commit 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 0000000..f7395f9 Binary files /dev/null and b/components/saeco_series_6000/__pycache__/__init__.cpython-311.pyc differ 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