updating configuration

This commit is contained in:
Your Name
2026-08-10 19:42:19 +00:00
parent f07dc7b8c9
commit e872659b52
66 changed files with 3169 additions and 5107 deletions
@@ -0,0 +1,2 @@
Ȩê~Ùþé®üRÓtk5�ÈA-�Û½
¿¿S²}$P
@@ -0,0 +1,16 @@
{
"dashboard_view": "cards",
"theme": "system",
"navigator_visible": true,
"device_editor_layout": "both",
"secrets_editor_layout": "visual",
"table_page_size": 25,
"table_column_visibility": {},
"table_sort_column": null,
"table_sort_direction": null,
"experience_level": "expert",
"remote_compute_only": false,
"hide_device_builder": false,
"version_history_enabled": true,
"onboarding_completed_version": 0
}
+528
View File
@@ -0,0 +1,528 @@
{
"_board_id_user_set_migrated": true,
"_dashboard_identity": {
"dashboard_id": "0KZiUm7NlelQU9rd7sQkY0vnPCPsAAwX"
},
"_firmware_jobs": [
{
"job_id": "3882d84e20a0",
"configuration": "svetlana_bedside_lamp.yaml",
"job_type": "upload",
"status": "completed",
"created_at": "2026-08-10T19:36:39.124524+00:00",
"started_at": "2026-08-10T19:36:45.031124+00:00",
"completed_at": "2026-08-10T19:36:59.800293+00:00",
"compile_started_at": null,
"compile_ended_at": null,
"exit_code": 0,
"error": null,
"port": "OTA",
"flash_bootloader": false,
"is_deferred_install": false,
"new_name": "",
"depends_on": "c3079a6dedd8",
"dependency_released": true,
"progress": 100,
"remote_peer": "",
"remote_job_id": "",
"remote_peer_label": "",
"device_name": "",
"device_friendly_name": "",
"source": "local",
"source_pin_sha256": "",
"source_label": "",
"source_esphome_version": "",
"target_esphome_version": "",
"failure_reason": ""
},
{
"job_id": "c3079a6dedd8",
"configuration": "svetlana_bedside_lamp.yaml",
"job_type": "compile",
"status": "completed",
"created_at": "2026-08-10T19:36:39.124438+00:00",
"started_at": "2026-08-10T19:36:39.128040+00:00",
"completed_at": "2026-08-10T19:36:45.029189+00:00",
"compile_started_at": "2026-08-10T19:36:43.919682+00:00",
"compile_ended_at": "2026-08-10T19:36:44.893943+00:00",
"exit_code": 0,
"error": null,
"port": "",
"flash_bootloader": false,
"is_deferred_install": false,
"new_name": "",
"depends_on": "",
"dependency_released": false,
"progress": 55,
"remote_peer": "",
"remote_job_id": "",
"remote_peer_label": "",
"device_name": "",
"device_friendly_name": "",
"source": "local",
"source_pin_sha256": "",
"source_label": "",
"source_esphome_version": "",
"target_esphome_version": "",
"failure_reason": ""
},
{
"job_id": "fcd4c51be9e0",
"configuration": "jkbms-monitor.yml",
"job_type": "upload",
"status": "cancelled",
"created_at": "2026-08-10T19:33:27.879713+00:00",
"started_at": null,
"completed_at": "2026-08-10T19:33:29.542990+00:00",
"compile_started_at": null,
"compile_ended_at": null,
"exit_code": null,
"error": "prerequisite job did not complete successfully",
"port": "OTA",
"flash_bootloader": false,
"is_deferred_install": false,
"new_name": "",
"depends_on": "0d1c14adcd37",
"dependency_released": false,
"progress": null,
"remote_peer": "",
"remote_job_id": "",
"remote_peer_label": "",
"device_name": "",
"device_friendly_name": "",
"source": "local",
"source_pin_sha256": "",
"source_label": "",
"source_esphome_version": "",
"target_esphome_version": "",
"failure_reason": ""
},
{
"job_id": "0d1c14adcd37",
"configuration": "jkbms-monitor.yml",
"job_type": "compile",
"status": "failed",
"created_at": "2026-08-10T19:33:27.879625+00:00",
"started_at": "2026-08-10T19:33:27.883196+00:00",
"completed_at": "2026-08-10T19:33:29.542874+00:00",
"compile_started_at": null,
"compile_ended_at": null,
"exit_code": 2,
"error": null,
"port": "",
"flash_bootloader": false,
"is_deferred_install": false,
"new_name": "",
"depends_on": "",
"dependency_released": false,
"progress": null,
"remote_peer": "",
"remote_job_id": "",
"remote_peer_label": "",
"device_name": "",
"device_friendly_name": "",
"source": "local",
"source_pin_sha256": "",
"source_label": "",
"source_esphome_version": "",
"target_esphome_version": "",
"failure_reason": ""
},
{
"job_id": "d0161b963d9c",
"configuration": "ac_mainroom.yaml",
"job_type": "upload",
"status": "completed",
"created_at": "2026-08-10T19:30:21.667492+00:00",
"started_at": "2026-08-10T19:31:36.315770+00:00",
"completed_at": "2026-08-10T19:31:48.047345+00:00",
"compile_started_at": null,
"compile_ended_at": null,
"exit_code": 0,
"error": null,
"port": "OTA",
"flash_bootloader": false,
"is_deferred_install": false,
"new_name": "",
"depends_on": "8c84a4bf3d18",
"dependency_released": true,
"progress": 100,
"remote_peer": "",
"remote_job_id": "",
"remote_peer_label": "",
"device_name": "",
"device_friendly_name": "",
"source": "local",
"source_pin_sha256": "",
"source_label": "",
"source_esphome_version": "",
"target_esphome_version": "",
"failure_reason": ""
},
{
"job_id": "8c84a4bf3d18",
"configuration": "ac_mainroom.yaml",
"job_type": "compile",
"status": "completed",
"created_at": "2026-08-10T19:30:21.667406+00:00",
"started_at": "2026-08-10T19:30:21.670887+00:00",
"completed_at": "2026-08-10T19:31:36.315107+00:00",
"compile_started_at": "2026-08-10T19:30:26.426848+00:00",
"compile_ended_at": "2026-08-10T19:31:35.083336+00:00",
"exit_code": 0,
"error": null,
"port": "",
"flash_bootloader": false,
"is_deferred_install": false,
"new_name": "",
"depends_on": "",
"dependency_released": false,
"progress": null,
"remote_peer": "",
"remote_job_id": "",
"remote_peer_label": "",
"device_name": "",
"device_friendly_name": "",
"source": "local",
"source_pin_sha256": "",
"source_label": "",
"source_esphome_version": "",
"target_esphome_version": "",
"failure_reason": ""
},
{
"job_id": "c616adde2ce9",
"configuration": "ac_kitchen.yaml",
"job_type": "upload",
"status": "completed",
"created_at": "2026-08-10T19:27:40.554431+00:00",
"started_at": "2026-08-10T19:28:55.819303+00:00",
"completed_at": "2026-08-10T19:29:04.946454+00:00",
"compile_started_at": null,
"compile_ended_at": null,
"exit_code": 0,
"error": null,
"port": "OTA",
"flash_bootloader": false,
"is_deferred_install": false,
"new_name": "",
"depends_on": "addc80e89300",
"dependency_released": true,
"progress": 100,
"remote_peer": "",
"remote_job_id": "",
"remote_peer_label": "",
"device_name": "",
"device_friendly_name": "",
"source": "local",
"source_pin_sha256": "",
"source_label": "",
"source_esphome_version": "",
"target_esphome_version": "",
"failure_reason": ""
},
{
"job_id": "addc80e89300",
"configuration": "ac_kitchen.yaml",
"job_type": "compile",
"status": "completed",
"created_at": "2026-08-10T19:27:40.554338+00:00",
"started_at": "2026-08-10T19:27:40.558134+00:00",
"completed_at": "2026-08-10T19:28:55.818581+00:00",
"compile_started_at": "2026-08-10T19:27:45.454257+00:00",
"compile_ended_at": "2026-08-10T19:28:54.558070+00:00",
"exit_code": 0,
"error": null,
"port": "",
"flash_bootloader": false,
"is_deferred_install": false,
"new_name": "",
"depends_on": "",
"dependency_released": false,
"progress": null,
"remote_peer": "",
"remote_job_id": "",
"remote_peer_label": "",
"device_name": "",
"device_friendly_name": "",
"source": "local",
"source_pin_sha256": "",
"source_label": "",
"source_esphome_version": "",
"target_esphome_version": "",
"failure_reason": ""
},
{
"job_id": "41e7d03c814f",
"configuration": "danil_bedside_lamp.yaml",
"job_type": "upload",
"status": "completed",
"created_at": "2026-08-10T19:23:51.467405+00:00",
"started_at": "2026-08-10T19:23:57.776187+00:00",
"completed_at": "2026-08-10T19:24:11.242268+00:00",
"compile_started_at": null,
"compile_ended_at": null,
"exit_code": 0,
"error": null,
"port": "OTA",
"flash_bootloader": false,
"is_deferred_install": false,
"new_name": "",
"depends_on": "18f12d06f212",
"dependency_released": true,
"progress": 100,
"remote_peer": "",
"remote_job_id": "",
"remote_peer_label": "",
"device_name": "",
"device_friendly_name": "",
"source": "local",
"source_pin_sha256": "",
"source_label": "",
"source_esphome_version": "",
"target_esphome_version": "",
"failure_reason": ""
},
{
"job_id": "18f12d06f212",
"configuration": "danil_bedside_lamp.yaml",
"job_type": "compile",
"status": "completed",
"created_at": "2026-08-10T19:23:51.467360+00:00",
"started_at": "2026-08-10T19:23:51.469053+00:00",
"completed_at": "2026-08-10T19:23:57.774440+00:00",
"compile_started_at": "2026-08-10T19:23:56.625355+00:00",
"compile_ended_at": "2026-08-10T19:23:57.647086+00:00",
"exit_code": 0,
"error": null,
"port": "",
"flash_bootloader": false,
"is_deferred_install": false,
"new_name": "",
"depends_on": "",
"dependency_released": false,
"progress": 55,
"remote_peer": "",
"remote_job_id": "",
"remote_peer_label": "",
"device_name": "",
"device_friendly_name": "",
"source": "local",
"source_pin_sha256": "",
"source_label": "",
"source_esphome_version": "",
"target_esphome_version": "",
"failure_reason": ""
},
{
"job_id": "e4b09f0c27ff",
"configuration": "ac_childroom.yaml",
"job_type": "upload",
"status": "completed",
"created_at": "2026-08-10T19:04:24.871708+00:00",
"started_at": "2026-08-10T19:05:40.872001+00:00",
"completed_at": "2026-08-10T19:05:49.216717+00:00",
"compile_started_at": null,
"compile_ended_at": null,
"exit_code": 0,
"error": null,
"port": "OTA",
"flash_bootloader": false,
"is_deferred_install": false,
"new_name": "",
"depends_on": "37490cc5a49a",
"dependency_released": true,
"progress": 100,
"remote_peer": "",
"remote_job_id": "",
"remote_peer_label": "",
"device_name": "",
"device_friendly_name": "",
"source": "local",
"source_pin_sha256": "",
"source_label": "",
"source_esphome_version": "",
"target_esphome_version": "",
"failure_reason": ""
},
{
"job_id": "37490cc5a49a",
"configuration": "ac_childroom.yaml",
"job_type": "compile",
"status": "completed",
"created_at": "2026-08-10T19:04:24.871671+00:00",
"started_at": "2026-08-10T19:04:24.872470+00:00",
"completed_at": "2026-08-10T19:05:40.870855+00:00",
"compile_started_at": "2026-08-10T19:04:30.508438+00:00",
"compile_ended_at": "2026-08-10T19:05:39.688311+00:00",
"exit_code": 0,
"error": null,
"port": "",
"flash_bootloader": false,
"is_deferred_install": false,
"new_name": "",
"depends_on": "",
"dependency_released": false,
"progress": null,
"remote_peer": "",
"remote_job_id": "",
"remote_peer_label": "",
"device_name": "",
"device_friendly_name": "",
"source": "local",
"source_pin_sha256": "",
"source_label": "",
"source_esphome_version": "",
"target_esphome_version": "",
"failure_reason": ""
},
{
"job_id": "464e761cadaa",
"configuration": "ac_bedroom.yaml",
"job_type": "upload",
"status": "completed",
"created_at": "2026-08-10T19:00:05.400293+00:00",
"started_at": "2026-08-10T19:01:52.249356+00:00",
"completed_at": "2026-08-10T19:02:01.871248+00:00",
"compile_started_at": null,
"compile_ended_at": null,
"exit_code": 0,
"error": null,
"port": "OTA",
"flash_bootloader": false,
"is_deferred_install": false,
"new_name": "",
"depends_on": "6412d321d3e5",
"dependency_released": true,
"progress": 100,
"remote_peer": "",
"remote_job_id": "",
"remote_peer_label": "",
"device_name": "",
"device_friendly_name": "",
"source": "local",
"source_pin_sha256": "",
"source_label": "",
"source_esphome_version": "",
"target_esphome_version": "",
"failure_reason": ""
},
{
"job_id": "6412d321d3e5",
"configuration": "ac_bedroom.yaml",
"job_type": "compile",
"status": "completed",
"created_at": "2026-08-10T19:00:05.400199+00:00",
"started_at": "2026-08-10T19:00:05.403331+00:00",
"completed_at": "2026-08-10T19:01:52.248561+00:00",
"compile_started_at": "2026-08-10T19:00:40.614649+00:00",
"compile_ended_at": "2026-08-10T19:01:51.053257+00:00",
"exit_code": 0,
"error": null,
"port": "",
"flash_bootloader": false,
"is_deferred_install": false,
"new_name": "",
"depends_on": "",
"dependency_released": false,
"progress": null,
"remote_peer": "",
"remote_job_id": "",
"remote_peer_label": "",
"device_name": "",
"device_friendly_name": "",
"source": "local",
"source_pin_sha256": "",
"source_label": "",
"source_esphome_version": "",
"target_esphome_version": "",
"failure_reason": ""
},
{
"job_id": "804036526918",
"configuration": "senseair.yaml",
"job_type": "compile",
"status": "failed",
"created_at": "2026-08-10T18:59:46.036866+00:00",
"started_at": "2026-08-10T18:59:46.037246+00:00",
"completed_at": "2026-08-10T18:59:46.308635+00:00",
"compile_started_at": null,
"compile_ended_at": null,
"exit_code": 2,
"error": null,
"port": "",
"flash_bootloader": false,
"is_deferred_install": false,
"new_name": "",
"depends_on": "",
"dependency_released": false,
"progress": null,
"remote_peer": "",
"remote_job_id": "",
"remote_peer_label": "",
"device_name": "",
"device_friendly_name": "",
"source": "local",
"source_pin_sha256": "",
"source_label": "",
"source_esphome_version": "",
"target_esphome_version": "",
"failure_reason": ""
},
{
"job_id": "a732134af43e",
"configuration": "radar_esphome.yaml",
"job_type": "compile",
"status": "failed",
"created_at": "2026-08-10T18:59:45.486258+00:00",
"started_at": "2026-08-10T18:59:45.487419+00:00",
"completed_at": "2026-08-10T18:59:45.976409+00:00",
"compile_started_at": null,
"compile_ended_at": null,
"exit_code": 2,
"error": null,
"port": "",
"flash_bootloader": false,
"is_deferred_install": false,
"new_name": "",
"depends_on": "",
"dependency_released": false,
"progress": null,
"remote_peer": "",
"remote_job_id": "",
"remote_peer_label": "",
"device_name": "",
"device_friendly_name": "",
"source": "local",
"source_pin_sha256": "",
"source_label": "",
"source_esphome_version": "",
"target_esphome_version": "",
"failure_reason": ""
}
],
"ac_bedroom.yaml": {
"mac_address": "50:02:91:C3:82:8C"
},
"ac_childroom.yaml": {
"mac_address": "84:F3:EB:DC:DE:94"
},
"ac_kitchen.yaml": {
"mac_address": "84:F3:EB:DC:DE:F6"
},
"ac_mainroom.yaml": {
"mac_address": "A0:20:A6:16:7B:1B"
},
"coffee-philips-5400.yaml": {
"mac_address": "EC:64:C9:90:E3:D8"
},
"danil_bedside_lamp.yaml": {
"mac_address": "EC:4D:3E:94:FC:A2"
},
"jkbms-monitor.yml": {
"mac_address": "AC:A7:04:B8:CD:70"
},
"svetlana_bedside_lamp.yaml": {
"mac_address": "B4:60:ED:36:FC:8B"
}
}
+133 -5
View File
@@ -1,12 +1,12 @@
sensor: []
substitutions:
board_name: Coffee Philips 5400
device_name: coffee-philips-5400
node_name: coffee-philips-5400
esphome:
name: ${device_name}
friendly_name: ${board_name}
comment: ${board_name}
name: ${node_name}
friendly_name: coffee-philips-5400
comment: ESP32 Coffee Philips 5400
esp32:
board: esp32dev
@@ -33,7 +33,8 @@ ota:
wifi:
ssid: "MonsterFNV"
password: "7065485qwe"
use_address: ${device_name}.lan
fast_connect: off
reboot_timeout: 15min
#Если не будет связи с WiFi, то поднимется точка доступа
ap:
@@ -172,7 +173,111 @@ text_sensor:
icon: mdi:alert-circle-outline
update_interval: never
select:
- platform: template
name: "Тип напитка"
id: coffee_type
options:
- "Эспрессо"
- "Американо"
- "Капучино"
- "Латте"
- "Кофе с молоком"
- "Молочная пена"
- "Горячая вода"
initial_option: "Эспрессо"
optimistic: true
- platform: template
name: "Крепость"
id: coffee_strength
options:
- "Мягкий"
- "Обычный"
- "Крепкий"
- "Молотый"
initial_option: "Обычный"
optimistic: true
- platform: template
name: "Чашки"
id: coffee_cups
options:
- "1"
- "2"
- "3"
- "4"
initial_option: "1"
optimistic: true
number:
- platform: template
name: "Объем кофе (мл)"
id: coffee_volume
min_value: 20
max_value: 250
step: 10
initial_value: 100
unit_of_measurement: "ml"
optimistic: true
- platform: template
name: "Объем молока (мл)"
id: milk_volume
min_value: 0
max_value: 250
step: 10
initial_value: 0
unit_of_measurement: "ml"
optimistic: true
#Тестовый стенд: произвольный hex-пакет(ы) для отправки в материнскую плату.
#Можно указывать несколько пакетов через разделитель ";".
text:
- platform: template
name: "Test Hex"
id: test_hex
mode: TEXT
optimistic: true
max_length: 255
initial_value: "AA AA AA FE 00 00 C8 87 1B 40 55"
button:
- platform: template
name: "SEND Test"
on_press:
then:
- lambda: |-
{
std::string str = id(test_hex).state;
std::vector<std::string> pkts;
size_t pos = 0;
while (pos <= str.size()) {
size_t sep = str.find(';', pos);
if (sep == std::string::npos) sep = str.size();
std::string part = str.substr(pos, sep - pos);
while (!part.empty() && std::isspace(part.front())) part.erase(part.begin());
while (!part.empty() && std::isspace(part.back())) part.pop_back();
if (!part.empty()) pkts.push_back(part);
pos = sep + 1;
}
id(saeco_series_6000_component)->send_packets_to_mainboard(pkts, 50);
}
- platform: template
name: "SEND Test RAW"
on_press:
then:
- lambda: |-
id(saeco_series_6000_component)->send_raw_to_mainboard(id(test_hex).state, 300);
- platform: template
name: "Включение (FE 00 01)"
on_press:
then:
- lambda: |-
id(saeco_series_6000_component)->send_packets_to_mainboard(
std::vector<std::string>{
"AA AA AA FE 00 01 5E B7 1C 37 55"
}, 50);
- platform: template
name: "Американо"
on_press:
@@ -184,6 +289,29 @@ button:
"AA AA AA 90 0F 0A 00 02 00 03 02 00 B4 00 00 00 67 48 E4 4B 55",
"AA AA AA 91 10 01 03 56 9A 8C B8 55"
}, 50);
- platform: template
name: "Выключение"
on_press:
then:
- lambda: |-
id(saeco_series_6000_component)->send_packets_to_mainboard(
std::vector<std::string>{
"AA AA AA FE 00 00 C8 87 1B 40 55"
}, 50);
- platform: template
name: "Приготовить напиток"
icon: mdi:coffee
on_press:
- lambda: |-
{
uint8_t type = id(coffee_type).index_of(id(coffee_type).state).value_or(0); // 0..6
uint8_t bean = id(coffee_strength).index_of(id(coffee_strength).state).value_or(0); // 0..3
uint8_t cups = id(coffee_cups).index_of(id(coffee_cups).state).value_or(0) + 1; // 1..4
uint16_t vol = (uint16_t) id(coffee_volume).state;
uint16_t milk = (uint16_t) id(milk_volume).state;
id(saeco_series_6000_component)->coffee_build(type, bean, cups, vol, milk);
}
#[20:45:34][I][saeco_series_6000:079]: Packet from Display: AA AA AA 93 0A 01 01 57 15 3A ED 55
#[20:45:34][I][saeco_series_6000:079]: Packet from Display: AA AA AA 90 0B 0A 05 03 00 02 00 00 64 00 00 00 39 BF 5B 74 55
@@ -0,0 +1,6 @@
# Добавляем файл генерации рецептов
src_files(
saeco_series_6000.cpp
recipe.cpp
crc.cpp
)
@@ -1,98 +1,98 @@
DEPENDENCIES = ["uart", "sensor"]
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import uart, sensor, button
from esphome.const import CONF_ID, CONF_DEBUG
import esphome.automation as automation
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"
CONF_PACKETS = "packets"
CONF_SAECO_ID = "saeco_id"
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.Required(cv.CONF_ID): 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))
def validate_packet(value):
if isinstance(value, str):
return [int(x, 16) for x in value.split()]
return [cv.hex_uint8_t(x) for x in value]
SEND_PACKETS_ACTION_SCHEMA = cv.Schema({
cv.Required(CONF_ID): cv.use_id(SaecoSeries6000),
cv.Required(CONF_PACKETS): [cv.All(validate_packet)],
cv.Optional("delay_ms", default=10): cv.positive_int,
})
SendPacketsAction = saeco_series_6000_ns.class_("SendPacketsAction", automation.Action)
@automation.register_action(
"saeco_series_6000.send_packets",
SendPacketsAction,
SEND_PACKETS_ACTION_SCHEMA,
)
def send_packets_to_mainboard_action_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id)
parent = yield cg.get_variable(config[CONF_ID])
var.set_parent(parent)
var.set_packets(config[CONF_PACKETS])
var.set_delay_ms(config.get("delay_ms", 10))
return var
SaecoSendPacketsButton = saeco_series_6000_ns.class_("SaecoSendPacketsButton", button.Button, cg.Component)
SAECO_SEND_PACKETS_BUTTON_SCHEMA = button.BUTTON_SCHEMA.extend({
cv.GenerateID(): cv.declare_id(SaecoSendPacketsButton),
cv.Required(CONF_ID): cv.use_id(SaecoSeries6000),
cv.Required(CONF_PACKETS): [cv.All(validate_packet)],
cv.Optional("delay_ms", default=10): cv.positive_int,
}).extend(cv.COMPONENT_SCHEMA)
async def saeco_send_packets_button_to_code(config):
parent = await cg.get_variable(config[CONF_SAECO_ID])
var = cg.new_Pvariable(config["id"])
await cg.register_component(var, config)
await button.register_button(var, config)
cg.add(var.set_parent(parent))
cg.add(var.set_packets(config[CONF_PACKETS]))
cg.add(var.set_delay_ms(config.get("delay_ms", 10)))
return var
DEPENDENCIES = ["uart", "sensor"]
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import uart, sensor, button
from esphome.const import CONF_ID, CONF_DEBUG
import esphome.automation as automation
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"
CONF_PACKETS = "packets"
CONF_SAECO_ID = "saeco_id"
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.Required(cv.CONF_ID): 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))
def validate_packet(value):
if isinstance(value, str):
return [int(x, 16) for x in value.split()]
return [cv.hex_uint8_t(x) for x in value]
SEND_PACKETS_ACTION_SCHEMA = cv.Schema({
cv.Required(CONF_ID): cv.use_id(SaecoSeries6000),
cv.Required(CONF_PACKETS): [cv.All(validate_packet)],
cv.Optional("delay_ms", default=10): cv.positive_int,
})
SendPacketsAction = saeco_series_6000_ns.class_("SendPacketsAction", automation.Action)
@automation.register_action(
"saeco_series_6000.send_packets",
SendPacketsAction,
SEND_PACKETS_ACTION_SCHEMA,
)
def send_packets_to_mainboard_action_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id)
parent = yield cg.get_variable(config[CONF_ID])
var.set_parent(parent)
var.set_packets(config[CONF_PACKETS])
var.set_delay_ms(config.get("delay_ms", 10))
return var
SaecoSendPacketsButton = saeco_series_6000_ns.class_("SaecoSendPacketsButton", button.Button, cg.Component)
SAECO_SEND_PACKETS_BUTTON_SCHEMA = button._BUTTON_SCHEMA.extend({
cv.GenerateID(): cv.declare_id(SaecoSendPacketsButton),
cv.Required(CONF_ID): cv.use_id(SaecoSeries6000),
cv.Required(CONF_PACKETS): [cv.All(validate_packet)],
cv.Optional("delay_ms", default=10): cv.positive_int,
}).extend(cv.COMPONENT_SCHEMA)
async def saeco_send_packets_button_to_code(config):
parent = await cg.get_variable(config[CONF_SAECO_ID])
var = cg.new_Pvariable(config["id"])
await cg.register_component(var, config)
await button.register_button(var, config)
cg.add(var.set_parent(parent))
cg.add(var.set_packets(config[CONF_PACKETS]))
cg.add(var.set_delay_ms(config.get("delay_ms", 10)))
return var
button.register_button("saeco_series_6000", saeco_send_packets_button_to_code)
@@ -0,0 +1,318 @@
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: INFO #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:
id: saeco_series_6000_component
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
select:
- platform: template
name: "Тип напитка"
id: coffee_type
options:
- "Эспрессо"
- "Американо"
- "Капучино"
- "Латте"
- "Кофе с молоком"
- "Молочная пена"
- "Горячая вода"
initial_option: "Эспрессо"
optimistic: true
- platform: template
name: "Крепость"
id: coffee_strength
options:
- "Мягкий"
- "Обычный"
- "Крепкий"
- "Молотый"
initial_option: "Обычный"
optimistic: true
- platform: template
name: "Чашки"
id: coffee_cups
options:
- "1"
- "2"
- "3"
- "4"
initial_option: "1"
optimistic: true
number:
- platform: template
name: "Объем кофе (мл)"
id: coffee_volume
min_value: 20
max_value: 250
step: 10
initial_value: 100
unit_of_measurement: "ml"
optimistic: true
- platform: template
name: "Объем молока (мл)"
id: milk_volume
min_value: 0
max_value: 250
step: 10
initial_value: 0
unit_of_measurement: "ml"
optimistic: true
#Тестовый стенд: произвольный hex-пакет(ы) для отправки в материнскую плату.
#Можно указывать несколько пакетов через разделитель ";".
text:
- platform: template
name: "Test Hex"
id: test_hex
mode: TEXT
optimistic: true
max_length: 255
initial_value: "AA AA AA FE 00 00 C8 87 1B 40 55"
button:
- platform: template
name: "SEND Test"
on_press:
then:
- lambda: |-
{
std::string str = id(test_hex).state;
std::vector<std::string> pkts;
size_t pos = 0;
while (pos <= str.size()) {
size_t sep = str.find(';', pos);
if (sep == std::string::npos) sep = str.size();
std::string part = str.substr(pos, sep - pos);
while (!part.empty() && std::isspace(part.front())) part.erase(part.begin());
while (!part.empty() && std::isspace(part.back())) part.pop_back();
if (!part.empty()) pkts.push_back(part);
pos = sep + 1;
}
id(saeco_series_6000_component)->send_packets_to_mainboard(pkts, 50);
}
- platform: template
name: "SEND Test RAW"
on_press:
then:
- lambda: |-
id(saeco_series_6000_component)->send_raw_to_mainboard(id(test_hex).state, 50);
- platform: template
name: "Включение (FE 00 01)"
on_press:
then:
- lambda: |-
id(saeco_series_6000_component)->send_packets_to_mainboard(
std::vector<std::string>{
"AA AA AA FE 00 01 5E B7 1C 37 55"
}, 50);
- platform: template
name: "Американо"
on_press:
then:
- lambda: |-
id(saeco_series_6000_component)->send_packets_to_mainboard(
std::vector<std::string>{
"AA AA AA 93 0E 01 01 8B BD 33 EA 55",
"AA AA AA 90 0F 0A 00 02 00 03 02 00 B4 00 00 00 67 48 E4 4B 55",
"AA AA AA 91 10 01 03 56 9A 8C B8 55"
}, 50);
- platform: template
name: "Выключение"
on_press:
then:
- lambda: |-
id(saeco_series_6000_component)->send_packets_to_mainboard(
std::vector<std::string>{
"AA AA AA FE 00 00 C8 87 1B 40 55"
}, 50);
- platform: template
name: "Приготовить напиток"
icon: mdi:coffee
on_press:
- lambda: |-
{
uint8_t type = id(coffee_type).index_of(id(coffee_type).state).value_or(0); // 0..6
uint8_t bean = id(coffee_strength).index_of(id(coffee_strength).state).value_or(0); // 0..3
uint8_t cups = id(coffee_cups).index_of(id(coffee_cups).state).value_or(0) + 1; // 1..4
uint16_t vol = (uint16_t) id(coffee_volume).state;
uint16_t milk = (uint16_t) id(milk_volume).state;
id(saeco_series_6000_component)->coffee_build(type, bean, cups, vol, milk);
}
#[20:45:34][I][saeco_series_6000:079]: Packet from Display: AA AA AA 93 0A 01 01 57 15 3A ED 55
#[20:45:34][I][saeco_series_6000:079]: Packet from Display: AA AA AA 90 0B 0A 05 03 00 02 00 00 64 00 00 00 39 BF 5B 74 55
#[20:45:35][I][saeco_series_6000:079]: Packet from Display: AA AA AA 91 0C 01 03 42 C0 B0 AD 55"
@@ -0,0 +1,43 @@
#include "saeco_series_6000.h"
#include <vector>
namespace esphome {
namespace saeco_series_6000 {
uint32_t SaecoSeries6000::calc_crc(const std::vector<uint8_t>& data, size_t start, size_t end) {
uint32_t crc = 0xFFFFFFFF;
const uint32_t POLY = 0x04C11DB7;
if (start >= end) return 0;
// start
uint8_t val = data[start];
((uint8_t*)(&crc))[3] ^= BitReverse[val];
for (uint8_t j = 0; j < 8; j++) {
if (crc & 0x80000000) {
crc = (crc << 1) ^ POLY;
} else {
crc <<= 1;
}
}
// остальные байты
for (size_t i = start + 1; i < end; i++) {
uint8_t v = data[i];
((uint8_t*)(&crc))[3] ^= BitReverse[v];
for (uint8_t j = 0; j < 8; j++) {
if (crc & 0x80000000) {
crc = (crc << 1) ^ POLY;
} else {
crc <<= 1;
}
}
}
uint32_t t32 = 0;
((uint8_t*)(&t32))[0] = BitReverse[((uint8_t*)(&crc))[3]];
((uint8_t*)(&t32))[1] = BitReverse[((uint8_t*)(&crc))[2]];
((uint8_t*)(&t32))[2] = BitReverse[((uint8_t*)(&crc))[1]];
((uint8_t*)(&t32))[3] = BitReverse[((uint8_t*)(&crc))[0]];
crc = t32 ^ 0xFFFFFFFF;
return crc;
}
} // namespace saeco_series_6000
} // namespace esphome
@@ -0,0 +1,59 @@
#include "saeco_series_6000.h"
#include <vector>
namespace esphome {
namespace saeco_series_6000 {
std::vector<uint8_t> SaecoSeries6000::build_coffee_recipe(uint8_t type, uint8_t bean, uint8_t cups, uint16_t vol, uint16_t milk) {
static const uint8_t CoffeePattern[7][4] = {
{0,1,1,2}, // Espresso
{1,2,3,2}, // Americano
{2,2,1,2}, // Cappuccino
{3,2,2,2}, // Latte
{4,0,0,1}, // Coffee with milk
{5,1,0,1}, // Milk foam
{6,0,0,1} // Hot water
};
uint8_t rec[10] = {0};
if (type >= 7) type = 0;
rec[0] = CoffeePattern[type][0];
if (rec[0] == 5 || rec[0] == 4) {
rec[1] = 3; // всегда молотый для молока и milk foam
} else {
rec[1] = bean;
}
if (cups > CoffeePattern[type][3]) {
rec[2] = CoffeePattern[type][3];
} else {
rec[2] = cups;
}
rec[3] = CoffeePattern[type][1];
rec[4] = CoffeePattern[type][2];
if (rec[0] == 1) { // Americano
rec[6] = 40;
rec[7] = 0;
rec[8] = (uint8_t)vol;
rec[9] = vol / 0x100;
rec[5] = 0;
} else {
rec[6] = (uint8_t)vol;
rec[7] = vol / 0x100;
if (rec[0] == 0 || rec[0] == 1 || rec[0] == 4 || rec[0] == 5) {
if (rec[0] == 5) {
rec[5] = 2;
} else {
rec[5] = 0;
}
rec[8] = 0;
rec[9] = 0;
} else {
rec[5] = 2;
rec[8] = (uint8_t)milk;
rec[9] = milk / 0x100;
}
}
return std::vector<uint8_t>(rec, rec + 10);
}
} // namespace saeco_series_6000
} // namespace esphome
@@ -1,343 +1,382 @@
#include "saeco_series_6000.h"
#include "esphome/core/log.h"
#include <vector>
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::update_display_counter_from_packet(const uint8_t* buffer, uint16_t size) {
if (size > 4 && buffer[3] != 0xFF) {
last_display_counter_ = buffer[4];
}
}
void SaecoSeries6000::parse_display_packet(const uint8_t* buffer, uint16_t size) {
if (debug_) {
std::vector<char> 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]);
}
if (size > 4 && buffer[3] != 0xFF){
ESP_LOGI(TAG, "Packet from Display: %s", hex_buffer.data());
}
}
update_display_counter_from_packet(buffer, size);
// Здесь больше не парсим B0-пакеты, только debug
}
// Разбор пакета B0 (системные статусы кофемашины)
// buffer[6], buffer[7], buffer[9] содержат коды состояния
void SaecoSeries6000::parse_b0_packet(const uint8_t* buffer, uint16_t size) {
if (size < 10) return;
if (buffer[6] == 0x0E) {
if (buffer[9] == 0x00) {
pub_status(20); // 20: "Опорож. контейнер для коф. гущи"
} else {
if ((buffer[9] & 0x40) != 0) {
pub_status(22); // 22: "Воды нет"
} else {
pub_status(17); // 17: "Вода есть"
}
if ((buffer[9] & 0x80) != 0) {
pub_status(23); // 23: "Извлечен контейнер для жмыха"
} else {
pub_status(18); // 18: "Вставлен контейнер для жмыха"
}
}
} else if (buffer[6] == 0x06) {
pub_status(21); // 21: "Готово/Выберите напиток"
} else if (buffer[6] == 0x0C) {
if (buffer[7] == 0x01) {
pub_status(8); // 8: "Наслаждайтесь"
} else if (buffer[7] == 0x02) {
pub_status(15); // 15: "Что-то (07 0C 02)"
}
} else if (buffer[6] == 0x07) {
if (buffer[7] == 0x0E) {
pub_status(9); // 9: "Нагрев воды"
} else if (buffer[7] == 0x0D) {
pub_status(10); // 10: "Перемалываем зерна"
} else if (buffer[7] == 0x10) {
pub_status(3); // 3: "Наливаем молоко"
} else if (buffer[7] == 0x11) {
pub_status(4); // 4: "Наливаем кофе"
} else if (buffer[7] == 0x12) {
pub_status(5); // 5: "Предварительное дозирование"
} else if (buffer[7] == 0x13) {
pub_status(6); // 6: "Создание пара для молока"
} else if (buffer[7] == 0x14) {
pub_status(7); // 7: "Заварочный узел в положение заваривания"
} else if (buffer[7] == 0x15) {
pub_status(15); // 15: "Наслаждайтесь"
}
} else if (buffer[6] == 0x08) {
if (buffer[7] == 0x0E) {
pub_status(14); // 14: "Нагревание"
} else if (buffer[7] == 0x02) {
pub_status(11); // 11: "Промывка"
} else if (buffer[7] == 0x14) {
pub_status(13); // 13: "Что-то (07 08 14)"
} else if (buffer[7] == 0x05) {
pub_status(12); // 12: "Зерна закончились"
} else if (buffer[7] == 0x16) {
pub_status(24); // 24: "Удаление накипи стадия 1"
} else if (buffer[7] == 0x18) {
pub_status(25); // 25: "Удаление накипи стадия 2"
}
} else if (buffer[7] == 0x00) {
if (buffer[6] == 0x01) {
pub_status(1); // 1: "Что-то (07 01 00)"
} else if (buffer[6] == 0x05) {
pub_status(2); // 2: "Выключено"
}
}
}
// Разбор пакета B5 (коды ошибок кофемашины)
// buffer[10], buffer[11] содержат коды ошибок
void SaecoSeries6000::parse_b5_packet(const uint8_t* buffer, uint16_t size) {
if (size <= 12) return;
if (buffer[10] == 0x00) {
if (buffer[11] == 0x00) pub_status(30); // 30: "Ошибка 0x00"
else if (buffer[11] == 0x0B) pub_status(31); // 31: "Ошибка 0x0B"
else if (buffer[11] == 0xE6) pub_status(32); // 32: "Ошибка 0xE6"
else if (buffer[11] == 0x80) pub_status(33); // 33: "Ошибка 0x80"
else if (buffer[11] == 0xCB) pub_status(34); // 34: "Ошибка 0xCB"
else if (buffer[11] == 0xFF) pub_status(35); // 35: "Ошибка 0xFF"
else if (buffer[11] == 0xA0) pub_status(36); // 36: "Ошибка 0xA0"
} else if (buffer[10] == 0x01) {
pub_status(37); // 37: "Статус2 0x01"
}
}
void SaecoSeries6000::parse_mainboard_packet(const uint8_t* buffer, uint16_t size) {
last_activity_time = millis(); // сбрасываем таймер активности при любом пакете
if (debug_) {
// std::vector<char> 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) {
parse_b0_packet(buffer, size);
} else if (size > 12 && buffer[3] == 0xB5) {
parse_b5_packet(buffer, size);
}
}
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_));
}
void SaecoSeries6000::send_packet_to_mainboard(const std::vector<uint8_t> &data) {
ESP_LOGI(TAG, "Send to Mainboard pressed.");
for (auto b : data) {
uart_mainboard_->write_byte(b);
}
}
const uint8_t SaecoSeries6000::BitReverse[256] = {
0x00,0x80,0x40,0xC0,0x20,0xA0,0x60,0xE0,0x10,0x90,0x50,0xD0,0x30,0xB0,0x70,0xF0,
0x08,0x88,0x48,0xC8,0x28,0xA8,0x68,0xE8,0x18,0x98,0x58,0xD8,0x38,0xB8,0x78,0xF8,
0x04,0x84,0x44,0xC4,0x24,0xA4,0x64,0xE4,0x14,0x94,0x54,0xD4,0x34,0xB4,0x74,0xF4,
0x0C,0x8C,0x4C,0xCC,0x2C,0xAC,0x6C,0xEC,0x1C,0x9C,0x5C,0xDC,0x3C,0xBC,0x7C,0xFC,
0x02,0x82,0x42,0xC2,0x22,0xA2,0x62,0xE2,0x12,0x92,0x52,0xD2,0x32,0xB2,0x72,0xF2,
0x0A,0x8A,0x4A,0xCA,0x2A,0xAA,0x6A,0xEA,0x1A,0x9A,0x5A,0xDA,0x3A,0xBA,0x7A,0xFA,
0x06,0x86,0x46,0xC6,0x26,0xA6,0x66,0xE6,0x16,0x96,0x56,0xD6,0x36,0xB6,0x76,0xF6,
0x0E,0x8E,0x4E,0xCE,0x2E,0xAE,0x6E,0xEE,0x1E,0x9E,0x5E,0xDE,0x3E,0xBE,0x7E,0xFE,
0x01,0x81,0x41,0xC1,0x21,0xA1,0x61,0xE1,0x11,0x91,0x51,0xD1,0x31,0xB1,0x71,0xF1,
0x09,0x89,0x49,0xC9,0x29,0xA9,0x69,0xE9,0x19,0x99,0x59,0xD9,0x39,0xB9,0x79,0xF9,
0x05,0x85,0x45,0xC5,0x25,0xA5,0x65,0xE5,0x15,0x95,0x55,0xD5,0x35,0xB5,0x75,0xF5,
0x0D,0x8D,0x4D,0xCD,0x2D,0xAD,0x6D,0xED,0x1D,0x9D,0x5D,0xDD,0x3D,0xBD,0x7D,0xFD,
0x03,0x83,0x43,0xC3,0x23,0xA3,0x63,0xE3,0x13,0x93,0x53,0xD3,0x33,0xB3,0x73,0xF3,
0x0B,0x8B,0x4B,0xCB,0x2B,0xAB,0x6B,0xEB,0x1B,0x9B,0x5B,0xDB,0x3B,0xBB,0x7B,0xFB,
0x07,0x87,0x47,0xC7,0x27,0xA7,0x67,0xE7,0x17,0x97,0x57,0xD7,0x37,0xB7,0x77,0xF7,
0x0F,0x8F,0x4F,0xCF,0x2F,0xAF,0x6F,0xEF,0x1F,0x9F,0x5F,0xDF,0x3F,0xBF,0x7F,0xFF
};
uint32_t SaecoSeries6000::calc_crc(const std::vector<uint8_t>& data, size_t start, size_t end) {
uint32_t crc = 0xFFFFFFFF;
const uint32_t POLY = 0x04C11DB7;
if (start >= end) return 0;
// start
uint8_t val = data[start];
((uint8_t*)(&crc))[3] ^= BitReverse[val];
for (uint8_t j = 0; j < 8; j++) {
if (crc & 0x80000000) {
crc = (crc << 1) ^ POLY;
} else {
crc <<= 1;
}
}
// остальные байты
for (size_t i = start + 1; i < end; i++) {
uint8_t v = data[i];
((uint8_t*)(&crc))[3] ^= BitReverse[v];
for (uint8_t j = 0; j < 8; j++) {
if (crc & 0x80000000) {
crc = (crc << 1) ^ POLY;
} else {
crc <<= 1;
}
}
}
uint32_t t32 = 0;
((uint8_t*)(&t32))[0] = BitReverse[((uint8_t*)(&crc))[3]];
((uint8_t*)(&t32))[1] = BitReverse[((uint8_t*)(&crc))[2]];
((uint8_t*)(&t32))[2] = BitReverse[((uint8_t*)(&crc))[1]];
((uint8_t*)(&t32))[3] = BitReverse[((uint8_t*)(&crc))[0]];
crc = t32 ^ 0xFFFFFFFF;
return crc;
}
std::vector<uint8_t> SaecoSeries6000::parse_hex_string_to_bytes(const std::string& hex_string) {
std::vector<uint8_t> pkt;
size_t pos = 0;
while (pos < hex_string.size()) {
while (pos < hex_string.size() && isspace(hex_string[pos])) ++pos;
if (pos >= hex_string.size()) break;
char* endptr = nullptr;
uint8_t byte = static_cast<uint8_t>(std::strtoul(hex_string.c_str() + pos, &endptr, 16));
pkt.push_back(byte);
if (endptr == nullptr || endptr == hex_string.c_str() + pos) break;
pos = endptr - hex_string.c_str();
}
return pkt;
}
void SaecoSeries6000::send_packets_to_mainboard(const std::string& hex_packet, uint32_t delay_ms) {
std::vector<std::vector<uint8_t>> pkts = {parse_hex_string_to_bytes(hex_packet)};
send_packets_to_mainboard(pkts, delay_ms);
}
void SaecoSeries6000::send_packets_to_mainboard(const std::vector<std::string>& hex_packets, uint32_t delay_ms) {
std::vector<std::vector<uint8_t>> pkts;
for (const auto& s : hex_packets) {
pkts.push_back(parse_hex_string_to_bytes(s));
}
send_packets_to_mainboard(pkts, delay_ms);
}
void SaecoSeries6000::send_packets_to_mainboard(const std::vector<std::vector<uint8_t>> &packets, uint32_t delay_ms) {
uint8_t counter = (last_display_counter_ + 1) & 0xFF;
for (size_t i = 0; i < packets.size(); ++i) {
std::vector<uint8_t> pkt = packets[i];
// Заменяем байт счетчика (4-й байт)
if (pkt.size() > 4) {
pkt[4] = counter;
}
// Пересчёт CRC: ищем преамбулу 0xAA 0xAA 0xAA, считаем CRC по данным между преамбулой и последними 5 байтами
if (pkt.size() > 8) {
size_t preamble = 0;
if (pkt[0] == 0xAA && pkt[1] == 0xAA && pkt[2] == 0xAA) {
preamble = 3;
}
size_t crc_start = preamble;
size_t crc_end = pkt.size() - 5; // не включаем 4 CRC и 0x55
uint32_t crc = calc_crc(pkt, crc_start, crc_end);
// Заменяем последние 4 байта перед 0x55 на новую CRC
pkt[pkt.size()-2] = (crc >> 24) & 0xFF;
pkt[pkt.size()-3] = (crc >> 16) & 0xFF;
pkt[pkt.size()-4] = (crc >> 8) & 0xFF;
pkt[pkt.size()-5] = crc & 0xFF;
// pkt[pkt.size()-5] = (crc >> 24) & 0xFF;
// pkt[pkt.size()-4] = (crc >> 16) & 0xFF;
// pkt[pkt.size()-3] = (crc >> 8) & 0xFF;
// pkt[pkt.size()-2] = (crc) & 0xFF;
}
// Логируем пакет перед отправкой
std::string log_str;
char buf[8];
for (auto b : pkt) {
snprintf(buf, sizeof(buf), "%02X ", b);
log_str += buf;
}
ESP_LOGI(TAG, "Send to Mainboard: %s", log_str.c_str());
send_packet_to_mainboard(pkt);
counter = (counter + 1) & 0xFF;
delay(delay_ms);
}
last_display_counter_ = (counter - 1) & 0xFF;
}
// Реализация SendPacketsAction полностью в header (saeco_series_6000.h)
} // namespace saeco_series_6000
#include "saeco_series_6000.h"
#include "esphome/core/log.h"
#include <vector>
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);
if (display_pending_len_ < DISPLAY_BUFFER_SIZE) {
display_pending_[display_pending_len_++] = byte;
}
process_display_byte(byte); // Process the "sniffed" byte
if (byte == 0x55) {
// Пакет дисплея собран полностью — пробрасываем в mainboard,
// кроме idle FE 00 00, если активен режим подавления
bool idle = suppress_display_idle_ && is_idle_packet(display_pending_, display_pending_len_);
if (!idle) {
uart_mainboard_->write_array(display_pending_, display_pending_len_);
} else {
ESP_LOGI(TAG, "Suppressed idle packet from Display");
}
display_pending_len_ = 0;
}
}
// 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::update_display_counter_from_packet(const uint8_t* buffer, uint16_t size) {
if (size > 4 && buffer[3] != 0xFF) {
last_display_counter_ = buffer[4];
}
}
void SaecoSeries6000::parse_display_packet(const uint8_t* buffer, uint16_t size) {
if (debug_) {
std::vector<char> 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]);
}
if (size > 4 && buffer[3] != 0xFF){
ESP_LOGI(TAG, "Packet from Display: %s", hex_buffer.data());
}
}
update_display_counter_from_packet(buffer, size);
// Здесь больше не парсим B0-пакеты, только debug
}
// Разбор пакета B0 (системные статусы кофемашины)
// buffer[6], buffer[7], buffer[9] содержат коды состояния
void SaecoSeries6000::parse_b0_packet(const uint8_t* buffer, uint16_t size) {
if (size < 10) return;
if (buffer[6] == 0x08) {
// Любой статус 0x08 - рабочий режим mainboard: дисплей не должен слать idle FE 00 00
suppress_display_idle_ = true;
}
if (buffer[6] == 0x0E) {
if (buffer[9] == 0x00) {
pub_status(20); // 20: "Опорож. контейнер для коф. гущи"
} else {
if ((buffer[9] & 0x40) != 0) {
pub_status(22); // 22: "Воды нет"
} else {
pub_status(17); // 17: "Вода есть"
}
if ((buffer[9] & 0x80) != 0) {
pub_status(23); // 23: "Извлечен контейнер для жмыха"
} else {
pub_status(18); // 18: "Вставлен контейнер для жмыха"
}
}
} else if (buffer[6] == 0x06) {
pub_status(21); // 21: "Готово/Выберите напиток"
} else if (buffer[6] == 0x0C) {
if (buffer[7] == 0x01) {
pub_status(8); // 8: "Наслаждайтесь"
} else if (buffer[7] == 0x02) {
pub_status(15); // 15: "Что-то (07 0C 02)"
}
} else if (buffer[6] == 0x07) {
if (buffer[7] == 0x0E) {
pub_status(9); // 9: "Нагрев воды"
} else if (buffer[7] == 0x0D) {
pub_status(10); // 10: "Перемалываем зерна"
} else if (buffer[7] == 0x10) {
pub_status(3); // 3: "Наливаем молоко"
} else if (buffer[7] == 0x11) {
pub_status(4); // 4: "Наливаем кофе"
} else if (buffer[7] == 0x12) {
pub_status(5); // 5: "Предварительное дозирование"
} else if (buffer[7] == 0x13) {
pub_status(6); // 6: "Создание пара для молока"
} else if (buffer[7] == 0x14) {
pub_status(7); // 7: "Заварочный узел в положение заваривания"
} else if (buffer[7] == 0x15) {
pub_status(15); // 15: "Наслаждайтесь"
}
} else if (buffer[6] == 0x08) {
if (buffer[7] == 0x0E) {
pub_status(14); // 14: "Нагревание"
} else if (buffer[7] == 0x02) {
pub_status(11); // 11: "Промывка"
} else if (buffer[7] == 0x14) {
pub_status(13); // 13: "Что-то (07 08 14)"
} else if (buffer[7] == 0x05) {
pub_status(12); // 12: "Зерна закончились"
} else if (buffer[7] == 0x16) {
pub_status(24); // 24: "Удаление накипи стадия 1"
} else if (buffer[7] == 0x18) {
pub_status(25); // 25: "Удаление накипи стадия 2"
}
} else if (buffer[7] == 0x00) {
if (buffer[6] == 0x01) {
pub_status(1); // 1: "Что-то (07 01 00)"
} else if (buffer[6] == 0x05) {
pub_status(2); // 2: "Выключено"
}
}
}
// Разбор пакета B5 (коды ошибок кофемашины)
// buffer[10], buffer[11] содержат коды ошибок
void SaecoSeries6000::parse_b5_packet(const uint8_t* buffer, uint16_t size) {
if (size <= 12) return;
if (buffer[10] == 0x00) {
if (buffer[11] == 0x00) pub_status(30); // 30: "Ошибка 0x00"
else if (buffer[11] == 0x0B) pub_status(31); // 31: "Ошибка 0x0B"
else if (buffer[11] == 0xE6) pub_status(32); // 32: "Ошибка 0xE6"
else if (buffer[11] == 0x80) pub_status(33); // 33: "Ошибка 0x80"
else if (buffer[11] == 0xCB) pub_status(34); // 34: "Ошибка 0xCB"
else if (buffer[11] == 0xFF) pub_status(35); // 35: "Ошибка 0xFF"
else if (buffer[11] == 0xA0) pub_status(36); // 36: "Ошибка 0xA0"
} else if (buffer[10] == 0x01) {
pub_status(37); // 37: "Статус2 0x01"
}
}
void SaecoSeries6000::parse_mainboard_packet(const uint8_t* buffer, uint16_t size) {
last_activity_time = millis(); // сбрасываем таймер активности при любом пакете
if (is_mainboard_idle(buffer, size)) {
suppress_display_idle_ = false; // mainboard вернулся в выключенный режим
}
if (debug_) {
std::vector<char> 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_LOGI(TAG, "Packet from Mainboard: %s", hex_buffer.data());
}
// Парсим B0
if (size > 4 && buffer[3] == 0xB0) {
parse_b0_packet(buffer, size);
} else if (size > 12 && buffer[3] == 0xB5) {
parse_b5_packet(buffer, size);
}
}
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_));
}
void SaecoSeries6000::send_packet_to_mainboard(const std::vector<uint8_t> &data) {
ESP_LOGI(TAG, "Send to Mainboard pressed.");
for (auto b : data) {
uart_mainboard_->write_byte(b);
}
}
const uint8_t SaecoSeries6000::BitReverse[256] = {
0x00,0x80,0x40,0xC0,0x20,0xA0,0x60,0xE0,0x10,0x90,0x50,0xD0,0x30,0xB0,0x70,0xF0,
0x08,0x88,0x48,0xC8,0x28,0xA8,0x68,0xE8,0x18,0x98,0x58,0xD8,0x38,0xB8,0x78,0xF8,
0x04,0x84,0x44,0xC4,0x24,0xA4,0x64,0xE4,0x14,0x94,0x54,0xD4,0x34,0xB4,0x74,0xF4,
0x0C,0x8C,0x4C,0xCC,0x2C,0xAC,0x6C,0xEC,0x1C,0x9C,0x5C,0xDC,0x3C,0xBC,0x7C,0xFC,
0x02,0x82,0x42,0xC2,0x22,0xA2,0x62,0xE2,0x12,0x92,0x52,0xD2,0x32,0xB2,0x72,0xF2,
0x0A,0x8A,0x4A,0xCA,0x2A,0xAA,0x6A,0xEA,0x1A,0x9A,0x5A,0xDA,0x3A,0xBA,0x7A,0xFA,
0x06,0x86,0x46,0xC6,0x26,0xA6,0x66,0xE6,0x16,0x96,0x56,0xD6,0x36,0xB6,0x76,0xF6,
0x0E,0x8E,0x4E,0xCE,0x2E,0xAE,0x6E,0xEE,0x1E,0x9E,0x5E,0xDE,0x3E,0xBE,0x7E,0xFE,
0x01,0x81,0x41,0xC1,0x21,0xA1,0x61,0xE1,0x11,0x91,0x51,0xD1,0x31,0xB1,0x71,0xF1,
0x09,0x89,0x49,0xC9,0x29,0xA9,0x69,0xE9,0x19,0x99,0x59,0xD9,0x39,0xB9,0x79,0xF9,
0x05,0x85,0x45,0xC5,0x25,0xA5,0x65,0xE5,0x15,0x95,0x55,0xD5,0x35,0xB5,0x75,0xF5,
0x0D,0x8D,0x4D,0xCD,0x2D,0xAD,0x6D,0xED,0x1D,0x9D,0x5D,0xDD,0x3D,0xBD,0x7D,0xFD,
0x03,0x83,0x43,0xC3,0x23,0xA3,0x63,0xE3,0x13,0x93,0x53,0xD3,0x33,0xB3,0x73,0xF3,
0x0B,0x8B,0x4B,0xCB,0x2B,0xAB,0x6B,0xEB,0x1B,0x9B,0x5B,0xDB,0x3B,0xBB,0x7B,0xFB,
0x07,0x87,0x47,0xC7,0x27,0xA7,0x67,0xE7,0x17,0x97,0x57,0xD7,0x37,0xB7,0x77,0xF7,
0x0F,0x8F,0x4F,0xCF,0x2F,0xAF,0x6F,0xEF,0x1F,0x9F,0x5F,0xDF,0x3F,0xBF,0x7F,0xFF
};
std::vector<uint8_t> SaecoSeries6000::parse_hex_string_to_bytes(const std::string& hex_string) {
std::vector<uint8_t> pkt;
size_t pos = 0;
while (pos < hex_string.size()) {
while (pos < hex_string.size() && isspace(hex_string[pos])) ++pos;
if (pos >= hex_string.size()) break;
char* endptr = nullptr;
uint8_t byte = static_cast<uint8_t>(std::strtoul(hex_string.c_str() + pos, &endptr, 16));
pkt.push_back(byte);
if (endptr == nullptr || endptr == hex_string.c_str() + pos) break;
pos = endptr - hex_string.c_str();
}
return pkt;
}
void SaecoSeries6000::send_packets_to_mainboard(const std::string& hex_packet, uint32_t delay_ms) {
std::vector<std::vector<uint8_t>> pkts = {parse_hex_string_to_bytes(hex_packet)};
send_packets_to_mainboard(pkts, delay_ms);
}
void SaecoSeries6000::send_raw_to_mainboard(const std::string& hex_packet, uint32_t delay_ms) {
std::vector<std::string> pkts;
size_t pos = 0;
while (pos <= hex_packet.size()) {
size_t sep = hex_packet.find(';', pos);
if (sep == std::string::npos) sep = hex_packet.size();
std::string part = hex_packet.substr(pos, sep - pos);
while (!part.empty() && std::isspace(part.front())) part.erase(part.begin());
while (!part.empty() && std::isspace(part.back())) part.pop_back();
if (!part.empty()) pkts.push_back(part);
pos = sep + 1;
}
for (const auto& p : pkts) {
std::vector<uint8_t> pkt = parse_hex_string_to_bytes(p);
if (pkt.empty()) continue;
std::string log_str;
char buf[8];
for (auto b : pkt) {
snprintf(buf, sizeof(buf), "%02X ", b);
log_str += buf;
}
ESP_LOGI(TAG, "Send RAW to Mainboard: %s", log_str.c_str());
if (is_power_off_packet(pkt.data(), pkt.size())) {
suppress_display_idle_ = false; // программное выключение: idle снова пропускаем
}
send_packet_to_mainboard(pkt);
delay(delay_ms);
}
}
void SaecoSeries6000::send_packets_to_mainboard(const std::vector<std::string>& hex_packets, uint32_t delay_ms) {
std::vector<std::vector<uint8_t>> pkts;
for (const auto& s : hex_packets) {
pkts.push_back(parse_hex_string_to_bytes(s));
}
send_packets_to_mainboard(pkts, delay_ms);
}
void SaecoSeries6000::send_packets_to_mainboard(const std::vector<std::vector<uint8_t>> &packets, uint32_t delay_ms) {
uint8_t counter = (last_display_counter_ + 1) & 0xFF;
for (size_t i = 0; i < packets.size(); ++i) {
std::vector<uint8_t> pkt = packets[i];
// Заменяем байт счетчика (4-й байт)
if (pkt.size() > 4) {
pkt[4] = counter;
}
// Пересчёт CRC: ищем преамбулу 0xAA 0xAA 0xAA, считаем CRC по данным между преамбулой и последними 5 байтами
if (pkt.size() > 8) {
size_t preamble = 0;
if (pkt[0] == 0xAA && pkt[1] == 0xAA && pkt[2] == 0xAA) {
preamble = 3;
}
size_t crc_start = preamble;
size_t crc_end = pkt.size() - 5; // не включаем 4 CRC и 0x55
uint32_t crc = calc_crc(pkt, crc_start, crc_end);
// Заменяем последние 4 байта перед 0x55 на новую CRC
pkt[pkt.size()-2] = (crc >> 24) & 0xFF;
pkt[pkt.size()-3] = (crc >> 16) & 0xFF;
pkt[pkt.size()-4] = (crc >> 8) & 0xFF;
pkt[pkt.size()-5] = crc & 0xFF;
// pkt[pkt.size()-5] = (crc >> 24) & 0xFF;
// pkt[pkt.size()-4] = (crc >> 16) & 0xFF;
// pkt[pkt.size()-3] = (crc >> 8) & 0xFF;
// pkt[pkt.size()-2] = (crc) & 0xFF;
}
// Логируем пакет перед отправкой
std::string log_str;
char buf[8];
for (auto b : pkt) {
snprintf(buf, sizeof(buf), "%02X ", b);
log_str += buf;
}
ESP_LOGI(TAG, "Send to Mainboard: %s", log_str.c_str());
if (is_power_off_packet(pkt.data(), pkt.size())) {
suppress_display_idle_ = false; // программное выключение: idle снова пропускаем
}
send_packet_to_mainboard(pkt);
counter = (counter + 1) & 0xFF;
delay(delay_ms);
}
last_display_counter_ = (counter - 1) & 0xFF;
}
// Реализация SendPacketsAction полностью в header (saeco_series_6000.h)
void SaecoSeries6000::coffee_build(uint8_t type, uint8_t bean, uint8_t cups, uint16_t vol, uint16_t milk) {
// Формируем пакет для отправки (AA AA AA 90 ... CRC CRC CRC CRC 55)
std::vector<uint8_t> pkt = {0xAA, 0xAA, 0xAA, 0x90};
std::vector<uint8_t> rec = build_coffee_recipe(type, bean, cups, vol, milk);
pkt.insert(pkt.end(), rec.begin(), rec.end());
// Добавляем 4 байта CRC (заполним позже) и 0x55
pkt.resize(pkt.size() + 4 + 1, 0);
pkt[pkt.size() - 1] = 0x55;
// Пересчёт CRC
size_t preamble = 3;
size_t crc_start = preamble;
size_t crc_end = pkt.size() - 5;
uint32_t crc = calc_crc(pkt, crc_start, crc_end);
pkt[pkt.size()-2] = (crc >> 24) & 0xFF;
pkt[pkt.size()-3] = (crc >> 16) & 0xFF;
pkt[pkt.size()-4] = (crc >> 8) & 0xFF;
pkt[pkt.size()-5] = crc & 0xFF;
// Отправляем пакет
send_packets_to_mainboard(std::vector<std::vector<uint8_t>>{pkt});
}
} // namespace saeco_series_6000
} // namespace esphome
@@ -1,81 +1,124 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/uart/uart.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/core/automation.h"
#include "esphome/components/button/button.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;
void send_packet_to_mainboard(const std::vector<uint8_t> &data);
void send_packets_to_mainboard(const std::vector<std::vector<uint8_t>> &packets, uint32_t delay_ms = 10);
void send_packets_to_mainboard(const std::vector<std::string>& hex_packets, uint32_t delay_ms = 10);
void send_packets_to_mainboard(const std::string& hex_packet, uint32_t delay_ms = 10);
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};
uint8_t last_display_counter_{0};
void update_display_counter_from_packet(const uint8_t* buffer, uint16_t size);
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 parse_b0_packet(const uint8_t* buffer, uint16_t size);
void parse_b5_packet(const uint8_t* buffer, uint16_t size);
void pub_status(uint8_t status);
static const uint8_t BitReverse[256];
uint32_t calc_crc(const std::vector<uint8_t>& data, size_t start, size_t end);
std::vector<uint8_t> parse_hex_string_to_bytes(const std::string& hex_string);
};
class SaecoSendPacketsButton : public esphome::button::Button, public esphome::Component {
public:
void set_parent(SaecoSeries6000 *parent) { parent_ = parent; }
void set_packets(const std::vector<std::vector<uint8_t>> &packets) { packets_ = packets; }
void set_delay_ms(uint32_t delay_ms) { delay_ms_ = delay_ms; }
void press_action() override {
if (parent_ != nullptr) {
parent_->send_packets_to_mainboard(packets_, delay_ms_);
}
}
private:
SaecoSeries6000 *parent_{nullptr};
std::vector<std::vector<uint8_t>> packets_;
uint32_t delay_ms_{10};
};
} // namespace saeco_series_6000
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/uart/uart.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/core/automation.h"
#include "esphome/components/button/button.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;
void send_packet_to_mainboard(const std::vector<uint8_t> &data);
void send_packets_to_mainboard(const std::vector<std::vector<uint8_t>> &packets, uint32_t delay_ms = 10);
void send_packets_to_mainboard(const std::vector<std::string>& hex_packets, uint32_t delay_ms = 10);
void send_packets_to_mainboard(const std::string& hex_packet, uint32_t delay_ms = 10);
// Отправка пакета(ов) без пересчёта CRC и счётчика (точный реплей захваченных пакетов)
void send_raw_to_mainboard(const std::string& hex_packet, uint32_t delay_ms = 10);
// Типы напитков
enum CoffeeType {
ESPRESSO = 0,
AMERICANO = 1,
CAPPUCCINO = 2,
LATTE = 3,
COFFEE_WITH_MILK = 4,
MILK_FOAM = 5,
HOT_WATER = 6
};
// Крепость/помол
enum CoffeeStrength {
MILD = 0,
NORMAL = 1,
STRONG = 2,
GROUND = 3
};
// Построение и отправка пакета приготовления кофе
void coffee_build(uint8_t type, uint8_t bean, uint8_t cups, uint16_t vol, uint16_t milk);
// Генерация рецепта (только формирование данных)
static std::vector<uint8_t> build_coffee_recipe(uint8_t type, uint8_t bean, uint8_t cups, uint16_t vol, uint16_t milk);
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};
uint8_t last_display_counter_{0};
void update_display_counter_from_packet(const uint8_t* buffer, uint16_t size);
// Подавление idle-пакета FE 00 00 от дисплея (когда дисплей не знает о программном включении)
bool suppress_display_idle_{false};
bool is_idle_packet(const uint8_t* buffer, uint16_t size) const {
return size > 5 && buffer[3] == 0xFE && buffer[4] == 0x00 && buffer[5] == 0x00;
}
bool is_mainboard_idle(const uint8_t* buffer, uint16_t size) const {
return size > 6 && buffer[3] == 0xFF && buffer[4] == 0x00 && buffer[5] == 0x01 && buffer[6] == 0x00;
}
bool is_power_button_packet(const uint8_t* buffer, uint16_t size) const {
return size > 5 && buffer[3] == 0x93;
}
bool is_power_off_packet(const uint8_t* buffer, uint16_t size) const {
return size > 5 && buffer[3] == 0xFE && buffer[4] != 0x00;
}
uint8_t display_pending_[DISPLAY_BUFFER_SIZE];
uint16_t display_pending_len_{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 parse_b0_packet(const uint8_t* buffer, uint16_t size);
void parse_b5_packet(const uint8_t* buffer, uint16_t size);
void pub_status(uint8_t status);
static const uint8_t BitReverse[256];
// CRC-32 для пакетов кофемашины
static uint32_t calc_crc(const std::vector<uint8_t>& data, size_t start, size_t end);
std::vector<uint8_t> parse_hex_string_to_bytes(const std::string& hex_string);
};
class SaecoSendPacketsButton : public esphome::button::Button, public esphome::Component {
public:
void set_parent(SaecoSeries6000 *parent) { parent_ = parent; }
void set_packets(const std::vector<std::vector<uint8_t>> &packets) { packets_ = packets; }
void set_delay_ms(uint32_t delay_ms) { delay_ms_ = delay_ms; }
void press_action() override {
if (parent_ != nullptr) {
parent_->send_packets_to_mainboard(packets_, delay_ms_);
}
}
private:
SaecoSeries6000 *parent_{nullptr};
std::vector<std::vector<uint8_t>> packets_;
uint32_t delay_ms_{10};
};
} // namespace saeco_series_6000
} // namespace esphome
@@ -0,0 +1,6 @@
# Добавляем файл генерации рецептов
src_files(
saeco_series_6000.cpp
recipe.cpp
crc.cpp
)
@@ -0,0 +1,98 @@
DEPENDENCIES = ["uart", "sensor"]
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import uart, sensor, button
from esphome.const import CONF_ID, CONF_DEBUG
import esphome.automation as automation
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"
CONF_PACKETS = "packets"
CONF_SAECO_ID = "saeco_id"
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.Required(cv.CONF_ID): 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))
def validate_packet(value):
if isinstance(value, str):
return [int(x, 16) for x in value.split()]
return [cv.hex_uint8_t(x) for x in value]
SEND_PACKETS_ACTION_SCHEMA = cv.Schema({
cv.Required(CONF_ID): cv.use_id(SaecoSeries6000),
cv.Required(CONF_PACKETS): [cv.All(validate_packet)],
cv.Optional("delay_ms", default=10): cv.positive_int,
})
SendPacketsAction = saeco_series_6000_ns.class_("SendPacketsAction", automation.Action)
@automation.register_action(
"saeco_series_6000.send_packets",
SendPacketsAction,
SEND_PACKETS_ACTION_SCHEMA,
)
def send_packets_to_mainboard_action_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id)
parent = yield cg.get_variable(config[CONF_ID])
var.set_parent(parent)
var.set_packets(config[CONF_PACKETS])
var.set_delay_ms(config.get("delay_ms", 10))
return var
SaecoSendPacketsButton = saeco_series_6000_ns.class_("SaecoSendPacketsButton", button.Button, cg.Component)
SAECO_SEND_PACKETS_BUTTON_SCHEMA = button._BUTTON_SCHEMA.extend({
cv.GenerateID(): cv.declare_id(SaecoSendPacketsButton),
cv.Required(CONF_ID): cv.use_id(SaecoSeries6000),
cv.Required(CONF_PACKETS): [cv.All(validate_packet)],
cv.Optional("delay_ms", default=10): cv.positive_int,
}).extend(cv.COMPONENT_SCHEMA)
async def saeco_send_packets_button_to_code(config):
parent = await cg.get_variable(config[CONF_SAECO_ID])
var = cg.new_Pvariable(config["id"])
await cg.register_component(var, config)
await button.register_button(var, config)
cg.add(var.set_parent(parent))
cg.add(var.set_packets(config[CONF_PACKETS]))
cg.add(var.set_delay_ms(config.get("delay_ms", 10)))
return var
button.register_button("saeco_series_6000", saeco_send_packets_button_to_code)
@@ -0,0 +1,43 @@
#include "saeco_series_6000.h"
#include <vector>
namespace esphome {
namespace saeco_series_6000 {
uint32_t SaecoSeries6000::calc_crc(const std::vector<uint8_t>& data, size_t start, size_t end) {
uint32_t crc = 0xFFFFFFFF;
const uint32_t POLY = 0x04C11DB7;
if (start >= end) return 0;
// start
uint8_t val = data[start];
((uint8_t*)(&crc))[3] ^= BitReverse[val];
for (uint8_t j = 0; j < 8; j++) {
if (crc & 0x80000000) {
crc = (crc << 1) ^ POLY;
} else {
crc <<= 1;
}
}
// остальные байты
for (size_t i = start + 1; i < end; i++) {
uint8_t v = data[i];
((uint8_t*)(&crc))[3] ^= BitReverse[v];
for (uint8_t j = 0; j < 8; j++) {
if (crc & 0x80000000) {
crc = (crc << 1) ^ POLY;
} else {
crc <<= 1;
}
}
}
uint32_t t32 = 0;
((uint8_t*)(&t32))[0] = BitReverse[((uint8_t*)(&crc))[3]];
((uint8_t*)(&t32))[1] = BitReverse[((uint8_t*)(&crc))[2]];
((uint8_t*)(&t32))[2] = BitReverse[((uint8_t*)(&crc))[1]];
((uint8_t*)(&t32))[3] = BitReverse[((uint8_t*)(&crc))[0]];
crc = t32 ^ 0xFFFFFFFF;
return crc;
}
} // namespace saeco_series_6000
} // namespace esphome
@@ -0,0 +1,59 @@
#include "saeco_series_6000.h"
#include <vector>
namespace esphome {
namespace saeco_series_6000 {
std::vector<uint8_t> SaecoSeries6000::build_coffee_recipe(uint8_t type, uint8_t bean, uint8_t cups, uint16_t vol, uint16_t milk) {
static const uint8_t CoffeePattern[7][4] = {
{0,1,1,2}, // Espresso
{1,2,3,2}, // Americano
{2,2,1,2}, // Cappuccino
{3,2,2,2}, // Latte
{4,0,0,1}, // Coffee with milk
{5,1,0,1}, // Milk foam
{6,0,0,1} // Hot water
};
uint8_t rec[10] = {0};
if (type >= 7) type = 0;
rec[0] = CoffeePattern[type][0];
if (rec[0] == 5 || rec[0] == 4) {
rec[1] = 3; // всегда молотый для молока и milk foam
} else {
rec[1] = bean;
}
if (cups > CoffeePattern[type][3]) {
rec[2] = CoffeePattern[type][3];
} else {
rec[2] = cups;
}
rec[3] = CoffeePattern[type][1];
rec[4] = CoffeePattern[type][2];
if (rec[0] == 1) { // Americano
rec[6] = 40;
rec[7] = 0;
rec[8] = (uint8_t)vol;
rec[9] = vol / 0x100;
rec[5] = 0;
} else {
rec[6] = (uint8_t)vol;
rec[7] = vol / 0x100;
if (rec[0] == 0 || rec[0] == 1 || rec[0] == 4 || rec[0] == 5) {
if (rec[0] == 5) {
rec[5] = 2;
} else {
rec[5] = 0;
}
rec[8] = 0;
rec[9] = 0;
} else {
rec[5] = 2;
rec[8] = (uint8_t)milk;
rec[9] = milk / 0x100;
}
}
return std::vector<uint8_t>(rec, rec + 10);
}
} // namespace saeco_series_6000
} // namespace esphome
@@ -0,0 +1,344 @@
#include "saeco_series_6000.h"
#include "esphome/core/log.h"
#include <vector>
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::update_display_counter_from_packet(const uint8_t* buffer, uint16_t size) {
if (size > 4 && buffer[3] != 0xFF) {
last_display_counter_ = buffer[4];
}
}
void SaecoSeries6000::parse_display_packet(const uint8_t* buffer, uint16_t size) {
if (debug_) {
std::vector<char> 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]);
}
if (size > 4 && buffer[3] != 0xFF){
ESP_LOGI(TAG, "Packet from Display: %s", hex_buffer.data());
}
}
update_display_counter_from_packet(buffer, size);
// Здесь больше не парсим B0-пакеты, только debug
}
// Разбор пакета B0 (системные статусы кофемашины)
// buffer[6], buffer[7], buffer[9] содержат коды состояния
void SaecoSeries6000::parse_b0_packet(const uint8_t* buffer, uint16_t size) {
if (size < 10) return;
if (buffer[6] == 0x0E) {
if (buffer[9] == 0x00) {
pub_status(20); // 20: "Опорож. контейнер для коф. гущи"
} else {
if ((buffer[9] & 0x40) != 0) {
pub_status(22); // 22: "Воды нет"
} else {
pub_status(17); // 17: "Вода есть"
}
if ((buffer[9] & 0x80) != 0) {
pub_status(23); // 23: "Извлечен контейнер для жмыха"
} else {
pub_status(18); // 18: "Вставлен контейнер для жмыха"
}
}
} else if (buffer[6] == 0x06) {
pub_status(21); // 21: "Готово/Выберите напиток"
} else if (buffer[6] == 0x0C) {
if (buffer[7] == 0x01) {
pub_status(8); // 8: "Наслаждайтесь"
} else if (buffer[7] == 0x02) {
pub_status(15); // 15: "Что-то (07 0C 02)"
}
} else if (buffer[6] == 0x07) {
if (buffer[7] == 0x0E) {
pub_status(9); // 9: "Нагрев воды"
} else if (buffer[7] == 0x0D) {
pub_status(10); // 10: "Перемалываем зерна"
} else if (buffer[7] == 0x10) {
pub_status(3); // 3: "Наливаем молоко"
} else if (buffer[7] == 0x11) {
pub_status(4); // 4: "Наливаем кофе"
} else if (buffer[7] == 0x12) {
pub_status(5); // 5: "Предварительное дозирование"
} else if (buffer[7] == 0x13) {
pub_status(6); // 6: "Создание пара для молока"
} else if (buffer[7] == 0x14) {
pub_status(7); // 7: "Заварочный узел в положение заваривания"
} else if (buffer[7] == 0x15) {
pub_status(15); // 15: "Наслаждайтесь"
}
} else if (buffer[6] == 0x08) {
if (buffer[7] == 0x0E) {
pub_status(14); // 14: "Нагревание"
} else if (buffer[7] == 0x02) {
pub_status(11); // 11: "Промывка"
} else if (buffer[7] == 0x14) {
pub_status(13); // 13: "Что-то (07 08 14)"
} else if (buffer[7] == 0x05) {
pub_status(12); // 12: "Зерна закончились"
} else if (buffer[7] == 0x16) {
pub_status(24); // 24: "Удаление накипи стадия 1"
} else if (buffer[7] == 0x18) {
pub_status(25); // 25: "Удаление накипи стадия 2"
}
} else if (buffer[7] == 0x00) {
if (buffer[6] == 0x01) {
pub_status(1); // 1: "Что-то (07 01 00)"
} else if (buffer[6] == 0x05) {
pub_status(2); // 2: "Выключено"
}
}
}
// Разбор пакета B5 (коды ошибок кофемашины)
// buffer[10], buffer[11] содержат коды ошибок
void SaecoSeries6000::parse_b5_packet(const uint8_t* buffer, uint16_t size) {
if (size <= 12) return;
if (buffer[10] == 0x00) {
if (buffer[11] == 0x00) pub_status(30); // 30: "Ошибка 0x00"
else if (buffer[11] == 0x0B) pub_status(31); // 31: "Ошибка 0x0B"
else if (buffer[11] == 0xE6) pub_status(32); // 32: "Ошибка 0xE6"
else if (buffer[11] == 0x80) pub_status(33); // 33: "Ошибка 0x80"
else if (buffer[11] == 0xCB) pub_status(34); // 34: "Ошибка 0xCB"
else if (buffer[11] == 0xFF) pub_status(35); // 35: "Ошибка 0xFF"
else if (buffer[11] == 0xA0) pub_status(36); // 36: "Ошибка 0xA0"
} else if (buffer[10] == 0x01) {
pub_status(37); // 37: "Статус2 0x01"
}
}
void SaecoSeries6000::parse_mainboard_packet(const uint8_t* buffer, uint16_t size) {
last_activity_time = millis(); // сбрасываем таймер активности при любом пакете
if (debug_) {
// std::vector<char> 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) {
parse_b0_packet(buffer, size);
} else if (size > 12 && buffer[3] == 0xB5) {
parse_b5_packet(buffer, size);
}
}
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_));
}
void SaecoSeries6000::send_packet_to_mainboard(const std::vector<uint8_t> &data) {
ESP_LOGI(TAG, "Send to Mainboard pressed.");
for (auto b : data) {
uart_mainboard_->write_byte(b);
}
}
const uint8_t SaecoSeries6000::BitReverse[256] = {
0x00,0x80,0x40,0xC0,0x20,0xA0,0x60,0xE0,0x10,0x90,0x50,0xD0,0x30,0xB0,0x70,0xF0,
0x08,0x88,0x48,0xC8,0x28,0xA8,0x68,0xE8,0x18,0x98,0x58,0xD8,0x38,0xB8,0x78,0xF8,
0x04,0x84,0x44,0xC4,0x24,0xA4,0x64,0xE4,0x14,0x94,0x54,0xD4,0x34,0xB4,0x74,0xF4,
0x0C,0x8C,0x4C,0xCC,0x2C,0xAC,0x6C,0xEC,0x1C,0x9C,0x5C,0xDC,0x3C,0xBC,0x7C,0xFC,
0x02,0x82,0x42,0xC2,0x22,0xA2,0x62,0xE2,0x12,0x92,0x52,0xD2,0x32,0xB2,0x72,0xF2,
0x0A,0x8A,0x4A,0xCA,0x2A,0xAA,0x6A,0xEA,0x1A,0x9A,0x5A,0xDA,0x3A,0xBA,0x7A,0xFA,
0x06,0x86,0x46,0xC6,0x26,0xA6,0x66,0xE6,0x16,0x96,0x56,0xD6,0x36,0xB6,0x76,0xF6,
0x0E,0x8E,0x4E,0xCE,0x2E,0xAE,0x6E,0xEE,0x1E,0x9E,0x5E,0xDE,0x3E,0xBE,0x7E,0xFE,
0x01,0x81,0x41,0xC1,0x21,0xA1,0x61,0xE1,0x11,0x91,0x51,0xD1,0x31,0xB1,0x71,0xF1,
0x09,0x89,0x49,0xC9,0x29,0xA9,0x69,0xE9,0x19,0x99,0x59,0xD9,0x39,0xB9,0x79,0xF9,
0x05,0x85,0x45,0xC5,0x25,0xA5,0x65,0xE5,0x15,0x95,0x55,0xD5,0x35,0xB5,0x75,0xF5,
0x0D,0x8D,0x4D,0xCD,0x2D,0xAD,0x6D,0xED,0x1D,0x9D,0x5D,0xDD,0x3D,0xBD,0x7D,0xFD,
0x03,0x83,0x43,0xC3,0x23,0xA3,0x63,0xE3,0x13,0x93,0x53,0xD3,0x33,0xB3,0x73,0xF3,
0x0B,0x8B,0x4B,0xCB,0x2B,0xAB,0x6B,0xEB,0x1B,0x9B,0x5B,0xDB,0x3B,0xBB,0x7B,0xFB,
0x07,0x87,0x47,0xC7,0x27,0xA7,0x67,0xE7,0x17,0x97,0x57,0xD7,0x37,0xB7,0x77,0xF7,
0x0F,0x8F,0x4F,0xCF,0x2F,0xAF,0x6F,0xEF,0x1F,0x9F,0x5F,0xDF,0x3F,0xBF,0x7F,0xFF
};
std::vector<uint8_t> SaecoSeries6000::parse_hex_string_to_bytes(const std::string& hex_string) {
std::vector<uint8_t> pkt;
size_t pos = 0;
while (pos < hex_string.size()) {
while (pos < hex_string.size() && isspace(hex_string[pos])) ++pos;
if (pos >= hex_string.size()) break;
char* endptr = nullptr;
uint8_t byte = static_cast<uint8_t>(std::strtoul(hex_string.c_str() + pos, &endptr, 16));
pkt.push_back(byte);
if (endptr == nullptr || endptr == hex_string.c_str() + pos) break;
pos = endptr - hex_string.c_str();
}
return pkt;
}
void SaecoSeries6000::send_packets_to_mainboard(const std::string& hex_packet, uint32_t delay_ms) {
std::vector<std::vector<uint8_t>> pkts = {parse_hex_string_to_bytes(hex_packet)};
send_packets_to_mainboard(pkts, delay_ms);
}
void SaecoSeries6000::send_raw_to_mainboard(const std::string& hex_packet, uint32_t delay_ms) {
std::vector<uint8_t> pkt = parse_hex_string_to_bytes(hex_packet);
if (pkt.empty()) return;
std::string log_str;
char buf[8];
for (auto b : pkt) {
snprintf(buf, sizeof(buf), "%02X ", b);
log_str += buf;
}
ESP_LOGI(TAG, "Send RAW to Mainboard: %s", log_str.c_str());
send_packet_to_mainboard(pkt);
delay(delay_ms);
}
void SaecoSeries6000::send_packets_to_mainboard(const std::vector<std::string>& hex_packets, uint32_t delay_ms) {
std::vector<std::vector<uint8_t>> pkts;
for (const auto& s : hex_packets) {
pkts.push_back(parse_hex_string_to_bytes(s));
}
send_packets_to_mainboard(pkts, delay_ms);
}
void SaecoSeries6000::send_packets_to_mainboard(const std::vector<std::vector<uint8_t>> &packets, uint32_t delay_ms) {
uint8_t counter = (last_display_counter_ + 1) & 0xFF;
for (size_t i = 0; i < packets.size(); ++i) {
std::vector<uint8_t> pkt = packets[i];
// Заменяем байт счетчика (4-й байт)
if (pkt.size() > 4) {
pkt[4] = counter;
}
// Пересчёт CRC: ищем преамбулу 0xAA 0xAA 0xAA, считаем CRC по данным между преамбулой и последними 5 байтами
if (pkt.size() > 8) {
size_t preamble = 0;
if (pkt[0] == 0xAA && pkt[1] == 0xAA && pkt[2] == 0xAA) {
preamble = 3;
}
size_t crc_start = preamble;
size_t crc_end = pkt.size() - 5; // не включаем 4 CRC и 0x55
uint32_t crc = calc_crc(pkt, crc_start, crc_end);
// Заменяем последние 4 байта перед 0x55 на новую CRC
pkt[pkt.size()-2] = (crc >> 24) & 0xFF;
pkt[pkt.size()-3] = (crc >> 16) & 0xFF;
pkt[pkt.size()-4] = (crc >> 8) & 0xFF;
pkt[pkt.size()-5] = crc & 0xFF;
// pkt[pkt.size()-5] = (crc >> 24) & 0xFF;
// pkt[pkt.size()-4] = (crc >> 16) & 0xFF;
// pkt[pkt.size()-3] = (crc >> 8) & 0xFF;
// pkt[pkt.size()-2] = (crc) & 0xFF;
}
// Логируем пакет перед отправкой
std::string log_str;
char buf[8];
for (auto b : pkt) {
snprintf(buf, sizeof(buf), "%02X ", b);
log_str += buf;
}
ESP_LOGI(TAG, "Send to Mainboard: %s", log_str.c_str());
send_packet_to_mainboard(pkt);
counter = (counter + 1) & 0xFF;
delay(delay_ms);
}
last_display_counter_ = (counter - 1) & 0xFF;
}
// Реализация SendPacketsAction полностью в header (saeco_series_6000.h)
void SaecoSeries6000::coffee_build(uint8_t type, uint8_t bean, uint8_t cups, uint16_t vol, uint16_t milk) {
// Формируем пакет для отправки (AA AA AA 90 ... CRC CRC CRC CRC 55)
std::vector<uint8_t> pkt = {0xAA, 0xAA, 0xAA, 0x90};
std::vector<uint8_t> rec = build_coffee_recipe(type, bean, cups, vol, milk);
pkt.insert(pkt.end(), rec.begin(), rec.end());
// Добавляем 4 байта CRC (заполним позже) и 0x55
pkt.resize(pkt.size() + 4 + 1, 0);
pkt[pkt.size() - 1] = 0x55;
// Пересчёт CRC
size_t preamble = 3;
size_t crc_start = preamble;
size_t crc_end = pkt.size() - 5;
uint32_t crc = calc_crc(pkt, crc_start, crc_end);
pkt[pkt.size()-2] = (crc >> 24) & 0xFF;
pkt[pkt.size()-3] = (crc >> 16) & 0xFF;
pkt[pkt.size()-4] = (crc >> 8) & 0xFF;
pkt[pkt.size()-5] = crc & 0xFF;
// Отправляем пакет
send_packets_to_mainboard(std::vector<std::vector<uint8_t>>{pkt});
}
} // namespace saeco_series_6000
} // namespace esphome
@@ -0,0 +1,106 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/uart/uart.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/core/automation.h"
#include "esphome/components/button/button.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;
void send_packet_to_mainboard(const std::vector<uint8_t> &data);
void send_packets_to_mainboard(const std::vector<std::vector<uint8_t>> &packets, uint32_t delay_ms = 10);
void send_packets_to_mainboard(const std::vector<std::string>& hex_packets, uint32_t delay_ms = 10);
void send_packets_to_mainboard(const std::string& hex_packet, uint32_t delay_ms = 10);
// Отправка пакета(ов) без пересчёта CRC и счётчика (точный реплей захваченных пакетов)
void send_raw_to_mainboard(const std::string& hex_packet, uint32_t delay_ms = 10);
// Типы напитков
enum CoffeeType {
ESPRESSO = 0,
AMERICANO = 1,
CAPPUCCINO = 2,
LATTE = 3,
COFFEE_WITH_MILK = 4,
MILK_FOAM = 5,
HOT_WATER = 6
};
// Крепость/помол
enum CoffeeStrength {
MILD = 0,
NORMAL = 1,
STRONG = 2,
GROUND = 3
};
// Построение и отправка пакета приготовления кофе
void coffee_build(uint8_t type, uint8_t bean, uint8_t cups, uint16_t vol, uint16_t milk);
// Генерация рецепта (только формирование данных)
static std::vector<uint8_t> build_coffee_recipe(uint8_t type, uint8_t bean, uint8_t cups, uint16_t vol, uint16_t milk);
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};
uint8_t last_display_counter_{0};
void update_display_counter_from_packet(const uint8_t* buffer, uint16_t size);
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 parse_b0_packet(const uint8_t* buffer, uint16_t size);
void parse_b5_packet(const uint8_t* buffer, uint16_t size);
void pub_status(uint8_t status);
static const uint8_t BitReverse[256];
// CRC-32 для пакетов кофемашины
static uint32_t calc_crc(const std::vector<uint8_t>& data, size_t start, size_t end);
std::vector<uint8_t> parse_hex_string_to_bytes(const std::string& hex_string);
};
class SaecoSendPacketsButton : public esphome::button::Button, public esphome::Component {
public:
void set_parent(SaecoSeries6000 *parent) { parent_ = parent; }
void set_packets(const std::vector<std::vector<uint8_t>> &packets) { packets_ = packets; }
void set_delay_ms(uint32_t delay_ms) { delay_ms_ = delay_ms; }
void press_action() override {
if (parent_ != nullptr) {
parent_->send_packets_to_mainboard(packets_, delay_ms_);
}
}
private:
SaecoSeries6000 *parent_{nullptr};
std::vector<std::vector<uint8_t>> packets_;
uint32_t delay_ms_{10};
};
} // namespace saeco_series_6000
} // namespace esphome