updating configuration
@@ -23,7 +23,7 @@ cover_action:
|
|||||||
- cover.curtain_mainroom_bobbinet
|
- cover.curtain_mainroom_bobbinet
|
||||||
open_at_sunrise:
|
open_at_sunrise:
|
||||||
- cover.curtain_kitchen
|
- cover.curtain_kitchen
|
||||||
- cover.curtain_mainroom
|
#- cover.curtain_mainroom
|
||||||
cover_door_sensors:
|
cover_door_sensors:
|
||||||
cover.curtain_bedroom:
|
cover.curtain_bedroom:
|
||||||
- binary_sensor.door_bedroom_big_window_contact
|
- binary_sensor.door_bedroom_big_window_contact
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
light_control_hall:
|
|
||||||
module: lightcontrol
|
|
||||||
class: LightControl
|
|
||||||
sensor: binary_sensor.motion_sensor_hall
|
|
||||||
control_entity: switch.plug_light_hall
|
|
||||||
control_night_entity: light.gateway
|
|
||||||
timeout: 240
|
|
||||||
constraint: input_boolean.hall_light_control
|
|
||||||
ha_panel: alarm_control_panel.ha_alarm
|
|
||||||
vacuum: vacuum.xiaomi_vacuum_cleaner
|
|
||||||
global_dependencies:
|
|
||||||
- globals
|
|
||||||
|
|
||||||
light_control_kitchen:
|
|
||||||
module: lightcontrol
|
|
||||||
class: LightControl
|
|
||||||
sensor: group.kitchen_motion_sensors
|
|
||||||
control_entity: switch.light_kitchen
|
|
||||||
timeout: 600
|
|
||||||
constraint: input_boolean.kitchen_light_control
|
|
||||||
ha_panel: alarm_control_panel.ha_alarm
|
|
||||||
vacuum: vacuum.xiaomi_vacuum_cleaner
|
|
||||||
global_dependencies:
|
|
||||||
- globals
|
|
||||||
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
#################################
|
||||||
|
# CLOAKROOM LIGHT #
|
||||||
|
#################################
|
||||||
|
light_control_cloakroom:
|
||||||
|
module: lightcontrol
|
||||||
|
class: LightControl
|
||||||
|
sensor: binary_sensor.motion_cloakroom_occupancy
|
||||||
|
control_entity: light.light_cloakroom
|
||||||
|
timeout: 300
|
||||||
|
global_dependencies:
|
||||||
|
- globals
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import appdaemon.plugins.hass.hassapi as hass
|
||||||
|
import globals
|
||||||
|
|
||||||
|
#
|
||||||
|
# Turn on entry light when the lock is opened (coming home).
|
||||||
|
# Listens for 'door_opened' event, turns on control_entity and keeps it on
|
||||||
|
# for 'timeout' seconds after the last event.
|
||||||
|
#
|
||||||
|
# Args:
|
||||||
|
# control_entity = entity that will be turned on (example: light.light_hall_long)
|
||||||
|
# timeout = seconds to keep the light on after the last door opening
|
||||||
|
# events (optionally) = list of event names to listen to (default: ['door_opened'])
|
||||||
|
# constraint = you can define constraint (input_boolean), that will turn on and off this app.
|
||||||
|
#
|
||||||
|
# Release Notes
|
||||||
|
#
|
||||||
|
# Version 1.0:
|
||||||
|
# Initial Version
|
||||||
|
|
||||||
|
class LightControlEntry(hass.Hass):
|
||||||
|
timer = None
|
||||||
|
|
||||||
|
def initialize(self):
|
||||||
|
if not globals.check_args(self, ["control_entity", "timeout"]):
|
||||||
|
return
|
||||||
|
self.listen_event_handle_list = []
|
||||||
|
for event in self.args.get('events', ['door_opened']):
|
||||||
|
self.listen_event_handle_list.append(self.listen_event(self.event_trigger, event))
|
||||||
|
|
||||||
|
def event_trigger(self, event_name, data, kwargs):
|
||||||
|
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
|
||||||
|
return
|
||||||
|
#turn on control entity and keep it on for 'timeout' seconds after last event.
|
||||||
|
self.control_entity_on()
|
||||||
|
self.run_timer()
|
||||||
|
|
||||||
|
def terminate(self):
|
||||||
|
if self.listen_event_handle_list != None:
|
||||||
|
for listen_event_handle in self.listen_event_handle_list:
|
||||||
|
self.cancel_listen_event(listen_event_handle)
|
||||||
|
|
||||||
|
############ TIMER ########################
|
||||||
|
def run_timer(self):
|
||||||
|
if self.timer != None:
|
||||||
|
self.stop_timer()
|
||||||
|
self.timer = self.run_in(self.control_entity_off, self.args['timeout'])
|
||||||
|
|
||||||
|
def control_entity_on(self):
|
||||||
|
#if 'constraint' is off - we dont need to do anything
|
||||||
|
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
|
||||||
|
return
|
||||||
|
|
||||||
|
entity = self.args['control_entity']
|
||||||
|
state = self.get_state(entity)
|
||||||
|
|
||||||
|
self.turn_on(entity)
|
||||||
|
self.log("Light on ({}).".format(entity))
|
||||||
|
|
||||||
|
def control_entity_off(self, kwargs):
|
||||||
|
#if 'constraint' is off - we dont need to do anything
|
||||||
|
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
|
||||||
|
return
|
||||||
|
|
||||||
|
entity = self.args['control_entity']
|
||||||
|
state = self.get_state(entity)
|
||||||
|
self.turn_off(entity)
|
||||||
|
self.log("Light off ({}).".format(entity))
|
||||||
|
self.stop_timer()
|
||||||
|
|
||||||
|
def stop_timer(self):
|
||||||
|
if (self.timer == None):
|
||||||
|
return
|
||||||
|
self.log('stopping timer.')
|
||||||
|
self.cancel_timer(self.timer)
|
||||||
|
self.timer = None
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
#################################
|
||||||
|
# ENTRY LIGHT #
|
||||||
|
#################################
|
||||||
|
light_control_entry:
|
||||||
|
module: lightcontrol_entry
|
||||||
|
class: LightControlEntry
|
||||||
|
events:
|
||||||
|
- door_opened
|
||||||
|
control_entity: light.light_hall_small
|
||||||
|
timeout: 120
|
||||||
|
constraint: input_boolean.locker_light_control
|
||||||
|
global_dependencies:
|
||||||
|
- globals
|
||||||
@@ -34,9 +34,6 @@ class LightControlHall(hass.Hass):
|
|||||||
for sensor in self.args['motion_sensors']:
|
for sensor in self.args['motion_sensors']:
|
||||||
self.listen_event_handle_list.append(self.listen_state(self.motion_trigger, sensor))
|
self.listen_event_handle_list.append(self.listen_state(self.motion_trigger, sensor))
|
||||||
|
|
||||||
for event in self.args['events']:
|
|
||||||
self.listen_event(self.event_trigger, event)
|
|
||||||
|
|
||||||
|
|
||||||
def motion_trigger(self, entity, attribute, old, new, kwargs):
|
def motion_trigger(self, entity, attribute, old, new, kwargs):
|
||||||
#sensor is off
|
#sensor is off
|
||||||
@@ -59,15 +56,6 @@ class LightControlHall(hass.Hass):
|
|||||||
self.turn_on_by_door = True
|
self.turn_on_by_door = True
|
||||||
self.stop_timer()
|
self.stop_timer()
|
||||||
|
|
||||||
def event_trigger(self, event_name, data, kwargs):
|
|
||||||
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
|
|
||||||
return
|
|
||||||
#turnin on control entity and wait while sensor will be off, than run timer.
|
|
||||||
self.control_entity_on()
|
|
||||||
self.turn_on_by_door = True
|
|
||||||
self.stop_timer()
|
|
||||||
self.run_timer()
|
|
||||||
|
|
||||||
def terminate(self):
|
def terminate(self):
|
||||||
if self.listen_event_handle_list != None:
|
if self.listen_event_handle_list != None:
|
||||||
for listen_event_handle in self.listen_event_handle_list:
|
for listen_event_handle in self.listen_event_handle_list:
|
||||||
|
|||||||
@@ -9,10 +9,22 @@ light_control_hall:
|
|||||||
- binary_sensor.door_locker_hall_left_contact
|
- binary_sensor.door_locker_hall_left_contact
|
||||||
- binary_sensor.door_locker2_hall_left_contact
|
- binary_sensor.door_locker2_hall_left_contact
|
||||||
- binary_sensor.door_locker2_hall_right_contact
|
- binary_sensor.door_locker2_hall_right_contact
|
||||||
events:
|
|
||||||
- door_opened
|
|
||||||
control_entity: light.light_hall_long
|
control_entity: light.light_hall_long
|
||||||
timeout: 120
|
timeout: 120
|
||||||
constraint: input_boolean.locker_light_control
|
constraint: input_boolean.locker_light_control
|
||||||
|
global_dependencies:
|
||||||
|
- globals
|
||||||
|
|
||||||
|
light_control_hall_small:
|
||||||
|
module: lightcontrol_hall
|
||||||
|
class: LightControlHall
|
||||||
|
motion_sensors:
|
||||||
|
- binary_sensor.motion_hall_small_occupancy
|
||||||
|
door_sensors:
|
||||||
|
- binary_sensor.door_locker3_hall_left_contact
|
||||||
|
- binary_sensor.door_locker3_hall_right_contact
|
||||||
|
control_entity: light.light_hall_small
|
||||||
|
timeout: 120
|
||||||
|
constraint: input_boolean.locker_light_control
|
||||||
global_dependencies:
|
global_dependencies:
|
||||||
- globals
|
- globals
|
||||||
@@ -33,6 +33,8 @@ entities_action:
|
|||||||
- light.light_shower_shelf_light
|
- light.light_shower_shelf_light
|
||||||
# hall
|
# hall
|
||||||
- light.light_hall_long
|
- light.light_hall_long
|
||||||
|
- light.light_hall_small
|
||||||
|
- light.light_cloakroom
|
||||||
# childroom
|
# childroom
|
||||||
- light.light_childroom_main
|
- light.light_childroom_main
|
||||||
- light.light_childroom_spotlights
|
- light.light_childroom_spotlights
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
#################################
|
||||||
|
# HOME PRESENSE #
|
||||||
|
#################################
|
||||||
|
entities_action:
|
||||||
|
module: entities_action
|
||||||
|
class: EntitiesByPresence
|
||||||
|
away_entities:
|
||||||
|
# air conditioners
|
||||||
|
- climate.ac_bedroom
|
||||||
|
- climate.ac_childroom
|
||||||
|
- climate.ac_kitchen
|
||||||
|
- climate.ac_mainroom
|
||||||
|
|
||||||
|
# din rail lights
|
||||||
|
- light.light_mainroom_mainlamp
|
||||||
|
- light.light_mainroom_spotlights
|
||||||
|
- light.light_mainroom_pc_spotlight
|
||||||
|
# balcony
|
||||||
|
- light.light_balcony_spotlight
|
||||||
|
# bedroom
|
||||||
|
- light.light_bedroom_spotlights
|
||||||
|
- light.light_bedroom_nightlight_left
|
||||||
|
- light.light_bedroom_nightlight_right
|
||||||
|
- light.light_bedroom_pc_spotlight
|
||||||
|
- light.light_bedroom_mainlight
|
||||||
|
- light.light_bedroom_pc_nightlight
|
||||||
|
# bath
|
||||||
|
- light.light_bath
|
||||||
|
- light.light_bath_shelf
|
||||||
|
# shower
|
||||||
|
- light.light_shower_main_spotlights
|
||||||
|
- light.light_shower_shower_spotlights
|
||||||
|
- light.light_shower_shelf_light
|
||||||
|
# hall
|
||||||
|
- light.light_hall_long
|
||||||
|
# childroom
|
||||||
|
- light.light_childroom_main
|
||||||
|
- light.light_childroom_spotlights
|
||||||
|
- light.light_childroom_bed
|
||||||
|
# kitchen
|
||||||
|
- light.light_kitchen_spotlights
|
||||||
|
- light.light_kitchen_table_lamp
|
||||||
|
- light.light_kitchen_bar_lamp
|
||||||
|
- light.light_kitchen_set
|
||||||
|
|
||||||
|
# ventilation
|
||||||
|
- fan.ventilation_shower
|
||||||
|
- fan.ventilation_bath
|
||||||
|
|
||||||
|
# bedside lamps
|
||||||
|
- light.danil_bedside_lamp_rgbww_light
|
||||||
|
- light.svetlana_bedside_lamp_rgbww_light
|
||||||
|
|
||||||
|
# mirrors and heaters
|
||||||
|
- switch.shower_mirror
|
||||||
|
- switch.switch_mirror_shower_heater
|
||||||
|
- switch.light_bath_mirror
|
||||||
|
- switch.switch_mirror_bath_heater
|
||||||
|
|
||||||
|
# media
|
||||||
|
- media_player.bedroom_tv
|
||||||
|
- media_player.kitchen_tv
|
||||||
|
- media_player.mainroom_tv
|
||||||
|
|
||||||
|
# alice
|
||||||
|
- media_player.yandex_station_m00r46000m53db
|
||||||
|
- media_player.yandex_station_m00fhe2003seqr
|
||||||
|
- media_player.yandex_station_ly00000000000008674600007209d3f5
|
||||||
|
- media_player.yandex_station_ff98f029ab48468d28c557be
|
||||||
|
- media_player.yandex_station_u003jc100phn9k
|
||||||
|
return_entities:
|
||||||
|
# - light.light_kitchen_set
|
||||||
|
|
||||||
|
thremostats:
|
||||||
|
- climate.radiator_childroom
|
||||||
|
- climate.radiator_mainroom
|
||||||
|
- climate.radiator_kitchen
|
||||||
|
- climate.radiator_bedroom
|
||||||
|
- climate.radiator_bedroom_balcony
|
||||||
|
global_dependencies:
|
||||||
|
- globals
|
||||||
|
|
||||||
@@ -7,6 +7,17 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
- SUBDOMAIN="home-fnv"
|
- SUBDOMAIN="home-fnv"
|
||||||
restart: always
|
restart: always
|
||||||
|
healthcheck:
|
||||||
|
# нет HTTP-порта, сервис — вечный цикл run.sh с sleep 5m между обновлениями
|
||||||
|
test: ["CMD-SHELL", "pgrep -f run.sh || exit 1"]
|
||||||
|
interval: 1m
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
logging:
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
|
||||||
mosquitto:
|
mosquitto:
|
||||||
build: ./mosquitto/docker
|
build: ./mosquitto/docker
|
||||||
@@ -15,10 +26,16 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "1883:1883"
|
- "1883:1883"
|
||||||
restart: always
|
restart: always
|
||||||
|
healthcheck:
|
||||||
|
# auth включена (MQTT_USERNAME/PASSWORD из env_file), проверяем publish под теми же кредами
|
||||||
|
test: ["CMD-SHELL", "mosquitto_pub -h localhost -t healthcheck -m ping -u \"$$MQTT_USERNAME\" -P \"$$MQTT_PASSWORD\" -q 0"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 6
|
||||||
logging:
|
logging:
|
||||||
driver: json-file
|
driver: json-file
|
||||||
options:
|
options:
|
||||||
max-size: 50m
|
max-size: 50m
|
||||||
|
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:16.1
|
image: postgres:16.1
|
||||||
@@ -26,10 +43,15 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./postgres/settings/data:/var/lib/postgresql/data
|
- ./postgres/settings/data:/var/lib/postgresql/data
|
||||||
env_file: ./postgres/settings/postgres.env
|
env_file: ./postgres/settings/postgres.env
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 6
|
||||||
logging:
|
logging:
|
||||||
driver: json-file
|
driver: json-file
|
||||||
options:
|
options:
|
||||||
max-size: 50m
|
max-size: 50m
|
||||||
|
|
||||||
hass:
|
hass:
|
||||||
image: "homeassistant/home-assistant:stable" #2026.4.2
|
image: "homeassistant/home-assistant:stable" #2026.4.2
|
||||||
@@ -76,6 +98,12 @@ services:
|
|||||||
restart: always
|
restart: always
|
||||||
links:
|
links:
|
||||||
- "hass"
|
- "hass"
|
||||||
|
healthcheck:
|
||||||
|
# http: (dashboard) не включён в appdaemon.yaml -> HTTP-порта нет, проверяем процесс
|
||||||
|
test: ["CMD-SHELL", "pgrep -f 'appdaemon' || exit 1"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 6
|
||||||
logging:
|
logging:
|
||||||
driver: json-file
|
driver: json-file
|
||||||
options:
|
options:
|
||||||
@@ -95,6 +123,16 @@ services:
|
|||||||
restart: always
|
restart: always
|
||||||
volumes:
|
volumes:
|
||||||
- ./node-red/settings/data:/data
|
- ./node-red/settings/data:/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:1880/"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 6
|
||||||
|
logging:
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
|
||||||
esphome:
|
esphome:
|
||||||
image: esphome/esphome:latest #2025.6 #esphome-armv7
|
image: esphome/esphome:latest #2025.6 #esphome-armv7
|
||||||
@@ -107,6 +145,11 @@ services:
|
|||||||
#devices:
|
#devices:
|
||||||
# - "/dev/ttyUSB0:/dev/ttyUSB0"
|
# - "/dev/ttyUSB0:/dev/ttyUSB0"
|
||||||
#network_mode: host
|
#network_mode: host
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:6052/"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 6
|
||||||
logging:
|
logging:
|
||||||
driver: json-file
|
driver: json-file
|
||||||
options:
|
options:
|
||||||
@@ -161,6 +204,13 @@ services:
|
|||||||
- "80:80"
|
- "80:80"
|
||||||
- "443:443"
|
- "443:443"
|
||||||
restart: always
|
restart: always
|
||||||
|
healthcheck:
|
||||||
|
# без -f: бэкенд (hass) может честно вернуть не-2xx (напр. untrusted proxy),
|
||||||
|
# нас интересует только что nginx сам жив и отвечает на TCP/HTTP
|
||||||
|
test: ["CMD-SHELL", "curl -s -o /dev/null http://localhost/ || exit 1"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 6
|
||||||
logging:
|
logging:
|
||||||
driver: json-file
|
driver: json-file
|
||||||
options:
|
options:
|
||||||
@@ -181,6 +231,13 @@ services:
|
|||||||
- "8081:80"
|
- "8081:80"
|
||||||
#entrypoint: certbot
|
#entrypoint: certbot
|
||||||
#command: certonly --standalone --agree-tos --non-interactive --email admin@yandex5.ru -d gitea.yandex5.ru
|
#command: certonly --standalone --agree-tos --non-interactive --email admin@yandex5.ru -d gitea.yandex5.ru
|
||||||
|
healthcheck:
|
||||||
|
# standalone-сервер поднимается только на момент renew раз в сутки,
|
||||||
|
# 8081 в остальное время не слушает -> проверяем сам цикл update_certs.sh
|
||||||
|
test: ["CMD-SHELL", "pgrep -f update_certs.sh || exit 1"]
|
||||||
|
interval: 1m
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
logging:
|
logging:
|
||||||
driver: json-file
|
driver: json-file
|
||||||
options:
|
options:
|
||||||
@@ -217,6 +274,12 @@ services:
|
|||||||
- ./secrets/hass-fnv/settings/secrets.yaml:/config/secrets.yaml:ro
|
- ./secrets/hass-fnv/settings/secrets.yaml:/config/secrets.yaml:ro
|
||||||
env_file: ./matter-bridge/settings/matter-bridge.env
|
env_file: ./matter-bridge/settings/matter-bridge.env
|
||||||
network_mode: host
|
network_mode: host
|
||||||
|
healthcheck:
|
||||||
|
# в образе нет curl/wget, только nc — TCP-проверка порта веб-UI
|
||||||
|
test: ["CMD-SHELL", "nc -z -w2 localhost 8482 || exit 1"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 6
|
||||||
logging:
|
logging:
|
||||||
driver: json-file
|
driver: json-file
|
||||||
options:
|
options:
|
||||||
@@ -232,3 +295,14 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "8001:8000"
|
- "8001:8000"
|
||||||
env_file: ./keenetic/settings/keenetic.env
|
env_file: ./keenetic/settings/keenetic.env
|
||||||
|
healthcheck:
|
||||||
|
# в образе только python3 (нет curl/wget), используем встроенный /health FastAPI-роут
|
||||||
|
test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health', timeout=5)"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 6
|
||||||
|
logging:
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
max-size: "10m"
|
||||||
|
max-file: "3"
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
sensor: []
|
sensor: []
|
||||||
substitutions:
|
substitutions:
|
||||||
board_name: Coffee Philips 5400
|
board_name: Coffee Philips 5400
|
||||||
device_name: coffee-philips-5400
|
node_name: coffee-philips-5400
|
||||||
|
|
||||||
esphome:
|
esphome:
|
||||||
name: ${device_name}
|
name: ${node_name}
|
||||||
friendly_name: ${board_name}
|
friendly_name: coffee-philips-5400
|
||||||
comment: ${board_name}
|
comment: ESP32 Coffee Philips 5400
|
||||||
|
|
||||||
esp32:
|
esp32:
|
||||||
board: esp32dev
|
board: esp32dev
|
||||||
@@ -33,7 +33,8 @@ ota:
|
|||||||
wifi:
|
wifi:
|
||||||
ssid: "MonsterFNV"
|
ssid: "MonsterFNV"
|
||||||
password: "7065485qwe"
|
password: "7065485qwe"
|
||||||
use_address: ${device_name}.lan
|
fast_connect: off
|
||||||
|
reboot_timeout: 15min
|
||||||
|
|
||||||
#Если не будет связи с WiFi, то поднимется точка доступа
|
#Если не будет связи с WiFi, то поднимется точка доступа
|
||||||
ap:
|
ap:
|
||||||
@@ -172,7 +173,111 @@ text_sensor:
|
|||||||
icon: mdi:alert-circle-outline
|
icon: mdi:alert-circle-outline
|
||||||
update_interval: never
|
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:
|
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
|
- platform: template
|
||||||
name: "Американо"
|
name: "Американо"
|
||||||
on_press:
|
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 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"
|
"AA AA AA 91 10 01 03 56 9A 8C B8 55"
|
||||||
}, 50);
|
}, 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 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: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"]
|
DEPENDENCIES = ["uart", "sensor"]
|
||||||
|
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.components import uart, sensor, button
|
from esphome.components import uart, sensor, button
|
||||||
from esphome.const import CONF_ID, CONF_DEBUG
|
from esphome.const import CONF_ID, CONF_DEBUG
|
||||||
import esphome.automation as automation
|
import esphome.automation as automation
|
||||||
|
|
||||||
CONF_STATE_SENSOR = "state_sensor"
|
CONF_STATE_SENSOR = "state_sensor"
|
||||||
CONF_B0_STATUS_SENSOR = "b0_status_sensor"
|
CONF_B0_STATUS_SENSOR = "b0_status_sensor"
|
||||||
CONF_B5_STATUS_SENSOR = "b5_status_sensor"
|
CONF_B5_STATUS_SENSOR = "b5_status_sensor"
|
||||||
CONF_BA_STATUS_SENSOR = "ba_status_sensor"
|
CONF_BA_STATUS_SENSOR = "ba_status_sensor"
|
||||||
CONF_S90_STATUS_SENSOR = "s90_status_sensor"
|
CONF_S90_STATUS_SENSOR = "s90_status_sensor"
|
||||||
CONF_S91_STATUS_SENSOR = "s91_status_sensor"
|
CONF_S91_STATUS_SENSOR = "s91_status_sensor"
|
||||||
CONF_S93_STATUS_SENSOR = "s93_status_sensor"
|
CONF_S93_STATUS_SENSOR = "s93_status_sensor"
|
||||||
CONF_WATER_SENSOR = "water_sensor"
|
CONF_WATER_SENSOR = "water_sensor"
|
||||||
CONF_COFFEE_GROUNDS_CONTAINER_SENSOR = "coffee_grounds_container_sensor"
|
CONF_COFFEE_GROUNDS_CONTAINER_SENSOR = "coffee_grounds_container_sensor"
|
||||||
CONF_PALLET_SENSOR = "pallet_sensor"
|
CONF_PALLET_SENSOR = "pallet_sensor"
|
||||||
CONF_ERROR_CODE_SENSOR = "error_code_sensor"
|
CONF_ERROR_CODE_SENSOR = "error_code_sensor"
|
||||||
CONF_GRAIN_TRAY_SENSOR = "grain_tray_sensor"
|
CONF_GRAIN_TRAY_SENSOR = "grain_tray_sensor"
|
||||||
CONF_STATUS_TEXT_SENSOR = "status_text_sensor"
|
CONF_STATUS_TEXT_SENSOR = "status_text_sensor"
|
||||||
CONF_PACKETS = "packets"
|
CONF_PACKETS = "packets"
|
||||||
CONF_SAECO_ID = "saeco_id"
|
CONF_SAECO_ID = "saeco_id"
|
||||||
|
|
||||||
saeco_series_6000_ns = cg.esphome_ns.namespace('saeco_series_6000')
|
saeco_series_6000_ns = cg.esphome_ns.namespace('saeco_series_6000')
|
||||||
SaecoSeries6000 = saeco_series_6000_ns.class_('SaecoSeries6000', cg.Component)
|
SaecoSeries6000 = saeco_series_6000_ns.class_('SaecoSeries6000', cg.Component)
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.Schema({
|
CONFIG_SCHEMA = cv.Schema({
|
||||||
cv.Required(cv.CONF_ID): cv.declare_id(SaecoSeries6000),
|
cv.Required(cv.CONF_ID): cv.declare_id(SaecoSeries6000),
|
||||||
cv.Required('uart_display'): cv.use_id(uart.UARTComponent),
|
cv.Required('uart_display'): cv.use_id(uart.UARTComponent),
|
||||||
cv.Required('uart_mainboard'): 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_DEBUG, default=False): cv.boolean,
|
||||||
cv.Optional(CONF_STATE_SENSOR): sensor.sensor_schema(),
|
cv.Optional(CONF_STATE_SENSOR): sensor.sensor_schema(),
|
||||||
}).extend(cv.COMPONENT_SCHEMA)
|
}).extend(cv.COMPONENT_SCHEMA)
|
||||||
|
|
||||||
def to_code(config):
|
def to_code(config):
|
||||||
var = cg.new_Pvariable(config[CONF_ID])
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
yield cg.register_component(var, config)
|
yield cg.register_component(var, config)
|
||||||
|
|
||||||
uart_display = yield cg.get_variable(config['uart_display'])
|
uart_display = yield cg.get_variable(config['uart_display'])
|
||||||
cg.add(var.set_uart_display(uart_display))
|
cg.add(var.set_uart_display(uart_display))
|
||||||
|
|
||||||
uart_mainboard = yield cg.get_variable(config['uart_mainboard'])
|
uart_mainboard = yield cg.get_variable(config['uart_mainboard'])
|
||||||
cg.add(var.set_uart_mainboard(uart_mainboard))
|
cg.add(var.set_uart_mainboard(uart_mainboard))
|
||||||
|
|
||||||
if config[CONF_DEBUG]:
|
if config[CONF_DEBUG]:
|
||||||
cg.add(var.set_debug(True))
|
cg.add(var.set_debug(True))
|
||||||
|
|
||||||
if CONF_STATE_SENSOR in config:
|
if CONF_STATE_SENSOR in config:
|
||||||
sens = yield sensor.new_sensor(config[CONF_STATE_SENSOR])
|
sens = yield sensor.new_sensor(config[CONF_STATE_SENSOR])
|
||||||
cg.add(var.set_status_sensor(sens))
|
cg.add(var.set_status_sensor(sens))
|
||||||
|
|
||||||
def validate_packet(value):
|
def validate_packet(value):
|
||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
return [int(x, 16) for x in value.split()]
|
return [int(x, 16) for x in value.split()]
|
||||||
return [cv.hex_uint8_t(x) for x in value]
|
return [cv.hex_uint8_t(x) for x in value]
|
||||||
|
|
||||||
SEND_PACKETS_ACTION_SCHEMA = cv.Schema({
|
SEND_PACKETS_ACTION_SCHEMA = cv.Schema({
|
||||||
cv.Required(CONF_ID): cv.use_id(SaecoSeries6000),
|
cv.Required(CONF_ID): cv.use_id(SaecoSeries6000),
|
||||||
cv.Required(CONF_PACKETS): [cv.All(validate_packet)],
|
cv.Required(CONF_PACKETS): [cv.All(validate_packet)],
|
||||||
cv.Optional("delay_ms", default=10): cv.positive_int,
|
cv.Optional("delay_ms", default=10): cv.positive_int,
|
||||||
})
|
})
|
||||||
|
|
||||||
SendPacketsAction = saeco_series_6000_ns.class_("SendPacketsAction", automation.Action)
|
SendPacketsAction = saeco_series_6000_ns.class_("SendPacketsAction", automation.Action)
|
||||||
|
|
||||||
@automation.register_action(
|
@automation.register_action(
|
||||||
"saeco_series_6000.send_packets",
|
"saeco_series_6000.send_packets",
|
||||||
SendPacketsAction,
|
SendPacketsAction,
|
||||||
SEND_PACKETS_ACTION_SCHEMA,
|
SEND_PACKETS_ACTION_SCHEMA,
|
||||||
)
|
)
|
||||||
def send_packets_to_mainboard_action_to_code(config, action_id, template_arg, args):
|
def send_packets_to_mainboard_action_to_code(config, action_id, template_arg, args):
|
||||||
var = cg.new_Pvariable(action_id)
|
var = cg.new_Pvariable(action_id)
|
||||||
parent = yield cg.get_variable(config[CONF_ID])
|
parent = yield cg.get_variable(config[CONF_ID])
|
||||||
var.set_parent(parent)
|
var.set_parent(parent)
|
||||||
var.set_packets(config[CONF_PACKETS])
|
var.set_packets(config[CONF_PACKETS])
|
||||||
var.set_delay_ms(config.get("delay_ms", 10))
|
var.set_delay_ms(config.get("delay_ms", 10))
|
||||||
return var
|
return var
|
||||||
|
|
||||||
SaecoSendPacketsButton = saeco_series_6000_ns.class_("SaecoSendPacketsButton", button.Button, cg.Component)
|
SaecoSendPacketsButton = saeco_series_6000_ns.class_("SaecoSendPacketsButton", button.Button, cg.Component)
|
||||||
|
|
||||||
SAECO_SEND_PACKETS_BUTTON_SCHEMA = button.BUTTON_SCHEMA.extend({
|
SAECO_SEND_PACKETS_BUTTON_SCHEMA = button._BUTTON_SCHEMA.extend({
|
||||||
cv.GenerateID(): cv.declare_id(SaecoSendPacketsButton),
|
cv.GenerateID(): cv.declare_id(SaecoSendPacketsButton),
|
||||||
cv.Required(CONF_ID): cv.use_id(SaecoSeries6000),
|
cv.Required(CONF_ID): cv.use_id(SaecoSeries6000),
|
||||||
cv.Required(CONF_PACKETS): [cv.All(validate_packet)],
|
cv.Required(CONF_PACKETS): [cv.All(validate_packet)],
|
||||||
cv.Optional("delay_ms", default=10): cv.positive_int,
|
cv.Optional("delay_ms", default=10): cv.positive_int,
|
||||||
}).extend(cv.COMPONENT_SCHEMA)
|
}).extend(cv.COMPONENT_SCHEMA)
|
||||||
|
|
||||||
async def saeco_send_packets_button_to_code(config):
|
async def saeco_send_packets_button_to_code(config):
|
||||||
parent = await cg.get_variable(config[CONF_SAECO_ID])
|
parent = await cg.get_variable(config[CONF_SAECO_ID])
|
||||||
var = cg.new_Pvariable(config["id"])
|
var = cg.new_Pvariable(config["id"])
|
||||||
await cg.register_component(var, config)
|
await cg.register_component(var, config)
|
||||||
await button.register_button(var, config)
|
await button.register_button(var, config)
|
||||||
cg.add(var.set_parent(parent))
|
cg.add(var.set_parent(parent))
|
||||||
cg.add(var.set_packets(config[CONF_PACKETS]))
|
cg.add(var.set_packets(config[CONF_PACKETS]))
|
||||||
cg.add(var.set_delay_ms(config.get("delay_ms", 10)))
|
cg.add(var.set_delay_ms(config.get("delay_ms", 10)))
|
||||||
return var
|
return var
|
||||||
|
|
||||||
button.register_button("saeco_series_6000", saeco_send_packets_button_to_code)
|
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 "saeco_series_6000.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace saeco_series_6000 {
|
namespace saeco_series_6000 {
|
||||||
|
|
||||||
static const char *TAG = "saeco_series_6000";
|
static const char *TAG = "saeco_series_6000";
|
||||||
static uint32_t last_activity_time = 0;
|
static uint32_t last_activity_time = 0;
|
||||||
|
|
||||||
void SaecoSeries6000::loop() {
|
void SaecoSeries6000::loop() {
|
||||||
uint8_t byte;
|
uint8_t byte;
|
||||||
|
|
||||||
// Bridge from uart_display (display) to uart_mainboard (mainboard)
|
// Bridge from uart_display (display) to uart_mainboard (mainboard)
|
||||||
while (uart_display_->available()) {
|
while (uart_display_->available()) {
|
||||||
uart_display_->read_byte(&byte);
|
uart_display_->read_byte(&byte);
|
||||||
uart_mainboard_->write_byte(byte); // Mirror byte immediately
|
if (display_pending_len_ < DISPLAY_BUFFER_SIZE) {
|
||||||
process_display_byte(byte); // Process the "sniffed" byte
|
display_pending_[display_pending_len_++] = byte;
|
||||||
}
|
}
|
||||||
|
process_display_byte(byte); // Process the "sniffed" byte
|
||||||
// Bridge from uart_mainboard (mainboard) to uart_display (display)
|
if (byte == 0x55) {
|
||||||
while (uart_mainboard_->available()) {
|
// Пакет дисплея собран полностью — пробрасываем в mainboard,
|
||||||
uart_mainboard_->read_byte(&byte);
|
// кроме idle FE 00 00, если активен режим подавления
|
||||||
uart_display_->write_byte(byte); // Mirror byte immediately
|
bool idle = suppress_display_idle_ && is_idle_packet(display_pending_, display_pending_len_);
|
||||||
process_mainboard_byte(byte); // Process the "sniffed" byte
|
if (!idle) {
|
||||||
}
|
uart_mainboard_->write_array(display_pending_, display_pending_len_);
|
||||||
// Контроль активности UART (выключение через 15 сек)
|
} else {
|
||||||
static uint32_t last_check = 0;
|
ESP_LOGI(TAG, "Suppressed idle packet from Display");
|
||||||
uint32_t now = millis();
|
}
|
||||||
if (now - last_check > 1000) { // проверяем каждую секунду
|
display_pending_len_ = 0;
|
||||||
last_check = now;
|
}
|
||||||
if (now - last_activity_time > 15000) {
|
}
|
||||||
pub_status(2); // публикуем статус "выключено"
|
|
||||||
last_activity_time = now; // чтобы не публиковать повторно
|
// 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
|
||||||
void SaecoSeries6000::process_display_byte(uint8_t byte) {
|
}
|
||||||
// Если получили первый 0xAA — начинаем новый пакет
|
|
||||||
if (byte == 0xAA && display_buffer_pos_ == 0) {
|
// Контроль активности UART (выключение через 15 сек)
|
||||||
display_buffer_[display_buffer_pos_++] = byte;
|
static uint32_t last_check = 0;
|
||||||
return;
|
uint32_t now = millis();
|
||||||
}
|
if (now - last_check > 1000) { // проверяем каждую секунду
|
||||||
// Если уже начали собирать пакет
|
last_check = now;
|
||||||
if (display_buffer_pos_ > 0 && display_buffer_pos_ < DISPLAY_BUFFER_SIZE) {
|
if (now - last_activity_time > 15000) {
|
||||||
display_buffer_[display_buffer_pos_++] = byte;
|
pub_status(2); // публикуем статус "выключено"
|
||||||
if (byte == 0x55) {
|
last_activity_time = now; // чтобы не публиковать повторно
|
||||||
parse_display_packet(display_buffer_, display_buffer_pos_);
|
}
|
||||||
display_buffer_pos_ = 0;
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
void SaecoSeries6000::process_display_byte(uint8_t byte) {
|
||||||
|
// Если получили первый 0xAA — начинаем новый пакет
|
||||||
void SaecoSeries6000::process_mainboard_byte(uint8_t byte) {
|
if (byte == 0xAA && display_buffer_pos_ == 0) {
|
||||||
// Если получили первый 0xAA — начинаем новый пакет
|
display_buffer_[display_buffer_pos_++] = byte;
|
||||||
if (byte == 0xAA && mainboard_buffer_pos_ == 0) {
|
return;
|
||||||
mainboard_buffer_[mainboard_buffer_pos_++] = byte;
|
}
|
||||||
return;
|
// Если уже начали собирать пакет
|
||||||
}
|
if (display_buffer_pos_ > 0 && display_buffer_pos_ < DISPLAY_BUFFER_SIZE) {
|
||||||
// Если уже начали собирать пакет
|
display_buffer_[display_buffer_pos_++] = byte;
|
||||||
if (mainboard_buffer_pos_ > 0 && mainboard_buffer_pos_ < MAINBOARD_BUFFER_SIZE) {
|
if (byte == 0x55) {
|
||||||
mainboard_buffer_[mainboard_buffer_pos_++] = byte;
|
parse_display_packet(display_buffer_, display_buffer_pos_);
|
||||||
if (byte == 0x55) {
|
display_buffer_pos_ = 0;
|
||||||
parse_mainboard_packet(mainboard_buffer_, mainboard_buffer_pos_);
|
}
|
||||||
mainboard_buffer_pos_ = 0;
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
void SaecoSeries6000::process_mainboard_byte(uint8_t byte) {
|
||||||
|
// Если получили первый 0xAA — начинаем новый пакет
|
||||||
void SaecoSeries6000::update_display_counter_from_packet(const uint8_t* buffer, uint16_t size) {
|
if (byte == 0xAA && mainboard_buffer_pos_ == 0) {
|
||||||
if (size > 4 && buffer[3] != 0xFF) {
|
mainboard_buffer_[mainboard_buffer_pos_++] = byte;
|
||||||
last_display_counter_ = buffer[4];
|
return;
|
||||||
}
|
}
|
||||||
}
|
// Если уже начали собирать пакет
|
||||||
|
if (mainboard_buffer_pos_ > 0 && mainboard_buffer_pos_ < MAINBOARD_BUFFER_SIZE) {
|
||||||
void SaecoSeries6000::parse_display_packet(const uint8_t* buffer, uint16_t size) {
|
mainboard_buffer_[mainboard_buffer_pos_++] = byte;
|
||||||
if (debug_) {
|
if (byte == 0x55) {
|
||||||
std::vector<char> hex_buffer;
|
parse_mainboard_packet(mainboard_buffer_, mainboard_buffer_pos_);
|
||||||
hex_buffer.resize(size * 3 + 1);
|
mainboard_buffer_pos_ = 0;
|
||||||
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());
|
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];
|
||||||
update_display_counter_from_packet(buffer, size);
|
}
|
||||||
// Здесь больше не парсим B0-пакеты, только debug
|
}
|
||||||
}
|
|
||||||
|
void SaecoSeries6000::parse_display_packet(const uint8_t* buffer, uint16_t size) {
|
||||||
// Разбор пакета B0 (системные статусы кофемашины)
|
if (debug_) {
|
||||||
// buffer[6], buffer[7], buffer[9] содержат коды состояния
|
std::vector<char> hex_buffer;
|
||||||
void SaecoSeries6000::parse_b0_packet(const uint8_t* buffer, uint16_t size) {
|
hex_buffer.resize(size * 3 + 1);
|
||||||
if (size < 10) return;
|
for (uint16_t i = 0; i < size; i++) {
|
||||||
if (buffer[6] == 0x0E) {
|
sprintf(hex_buffer.data() + i * 3, "%02X ", buffer[i]);
|
||||||
if (buffer[9] == 0x00) {
|
}
|
||||||
pub_status(20); // 20: "Опорож. контейнер для коф. гущи"
|
if (size > 4 && buffer[3] != 0xFF){
|
||||||
} else {
|
ESP_LOGI(TAG, "Packet from Display: %s", hex_buffer.data());
|
||||||
if ((buffer[9] & 0x40) != 0) {
|
}
|
||||||
pub_status(22); // 22: "Воды нет"
|
}
|
||||||
} else {
|
update_display_counter_from_packet(buffer, size);
|
||||||
pub_status(17); // 17: "Вода есть"
|
// Здесь больше не парсим B0-пакеты, только debug
|
||||||
}
|
}
|
||||||
if ((buffer[9] & 0x80) != 0) {
|
|
||||||
pub_status(23); // 23: "Извлечен контейнер для жмыха"
|
// Разбор пакета B0 (системные статусы кофемашины)
|
||||||
} else {
|
// buffer[6], buffer[7], buffer[9] содержат коды состояния
|
||||||
pub_status(18); // 18: "Вставлен контейнер для жмыха"
|
void SaecoSeries6000::parse_b0_packet(const uint8_t* buffer, uint16_t size) {
|
||||||
}
|
if (size < 10) return;
|
||||||
}
|
if (buffer[6] == 0x08) {
|
||||||
} else if (buffer[6] == 0x06) {
|
// Любой статус 0x08 - рабочий режим mainboard: дисплей не должен слать idle FE 00 00
|
||||||
pub_status(21); // 21: "Готово/Выберите напиток"
|
suppress_display_idle_ = true;
|
||||||
} else if (buffer[6] == 0x0C) {
|
}
|
||||||
if (buffer[7] == 0x01) {
|
if (buffer[6] == 0x0E) {
|
||||||
pub_status(8); // 8: "Наслаждайтесь"
|
if (buffer[9] == 0x00) {
|
||||||
} else if (buffer[7] == 0x02) {
|
pub_status(20); // 20: "Опорож. контейнер для коф. гущи"
|
||||||
pub_status(15); // 15: "Что-то (07 0C 02)"
|
} else {
|
||||||
}
|
if ((buffer[9] & 0x40) != 0) {
|
||||||
} else if (buffer[6] == 0x07) {
|
pub_status(22); // 22: "Воды нет"
|
||||||
if (buffer[7] == 0x0E) {
|
} else {
|
||||||
pub_status(9); // 9: "Нагрев воды"
|
pub_status(17); // 17: "Вода есть"
|
||||||
} else if (buffer[7] == 0x0D) {
|
}
|
||||||
pub_status(10); // 10: "Перемалываем зерна"
|
if ((buffer[9] & 0x80) != 0) {
|
||||||
} else if (buffer[7] == 0x10) {
|
pub_status(23); // 23: "Извлечен контейнер для жмыха"
|
||||||
pub_status(3); // 3: "Наливаем молоко"
|
} else {
|
||||||
} else if (buffer[7] == 0x11) {
|
pub_status(18); // 18: "Вставлен контейнер для жмыха"
|
||||||
pub_status(4); // 4: "Наливаем кофе"
|
}
|
||||||
} else if (buffer[7] == 0x12) {
|
}
|
||||||
pub_status(5); // 5: "Предварительное дозирование"
|
} else if (buffer[6] == 0x06) {
|
||||||
} else if (buffer[7] == 0x13) {
|
pub_status(21); // 21: "Готово/Выберите напиток"
|
||||||
pub_status(6); // 6: "Создание пара для молока"
|
} else if (buffer[6] == 0x0C) {
|
||||||
} else if (buffer[7] == 0x14) {
|
if (buffer[7] == 0x01) {
|
||||||
pub_status(7); // 7: "Заварочный узел в положение заваривания"
|
pub_status(8); // 8: "Наслаждайтесь"
|
||||||
} else if (buffer[7] == 0x15) {
|
} else if (buffer[7] == 0x02) {
|
||||||
pub_status(15); // 15: "Наслаждайтесь"
|
pub_status(15); // 15: "Что-то (07 0C 02)"
|
||||||
}
|
}
|
||||||
} else if (buffer[6] == 0x08) {
|
} else if (buffer[6] == 0x07) {
|
||||||
if (buffer[7] == 0x0E) {
|
if (buffer[7] == 0x0E) {
|
||||||
pub_status(14); // 14: "Нагревание"
|
pub_status(9); // 9: "Нагрев воды"
|
||||||
} else if (buffer[7] == 0x02) {
|
} else if (buffer[7] == 0x0D) {
|
||||||
pub_status(11); // 11: "Промывка"
|
pub_status(10); // 10: "Перемалываем зерна"
|
||||||
} else if (buffer[7] == 0x14) {
|
} else if (buffer[7] == 0x10) {
|
||||||
pub_status(13); // 13: "Что-то (07 08 14)"
|
pub_status(3); // 3: "Наливаем молоко"
|
||||||
} else if (buffer[7] == 0x05) {
|
} else if (buffer[7] == 0x11) {
|
||||||
pub_status(12); // 12: "Зерна закончились"
|
pub_status(4); // 4: "Наливаем кофе"
|
||||||
} else if (buffer[7] == 0x16) {
|
} else if (buffer[7] == 0x12) {
|
||||||
pub_status(24); // 24: "Удаление накипи стадия 1"
|
pub_status(5); // 5: "Предварительное дозирование"
|
||||||
} else if (buffer[7] == 0x18) {
|
} else if (buffer[7] == 0x13) {
|
||||||
pub_status(25); // 25: "Удаление накипи стадия 2"
|
pub_status(6); // 6: "Создание пара для молока"
|
||||||
}
|
} else if (buffer[7] == 0x14) {
|
||||||
} else if (buffer[7] == 0x00) {
|
pub_status(7); // 7: "Заварочный узел в положение заваривания"
|
||||||
if (buffer[6] == 0x01) {
|
} else if (buffer[7] == 0x15) {
|
||||||
pub_status(1); // 1: "Что-то (07 01 00)"
|
pub_status(15); // 15: "Наслаждайтесь"
|
||||||
} else if (buffer[6] == 0x05) {
|
}
|
||||||
pub_status(2); // 2: "Выключено"
|
} else if (buffer[6] == 0x08) {
|
||||||
}
|
if (buffer[7] == 0x0E) {
|
||||||
}
|
pub_status(14); // 14: "Нагревание"
|
||||||
}
|
} else if (buffer[7] == 0x02) {
|
||||||
|
pub_status(11); // 11: "Промывка"
|
||||||
// Разбор пакета B5 (коды ошибок кофемашины)
|
} else if (buffer[7] == 0x14) {
|
||||||
// buffer[10], buffer[11] содержат коды ошибок
|
pub_status(13); // 13: "Что-то (07 08 14)"
|
||||||
void SaecoSeries6000::parse_b5_packet(const uint8_t* buffer, uint16_t size) {
|
} else if (buffer[7] == 0x05) {
|
||||||
if (size <= 12) return;
|
pub_status(12); // 12: "Зерна закончились"
|
||||||
if (buffer[10] == 0x00) {
|
} else if (buffer[7] == 0x16) {
|
||||||
if (buffer[11] == 0x00) pub_status(30); // 30: "Ошибка 0x00"
|
pub_status(24); // 24: "Удаление накипи стадия 1"
|
||||||
else if (buffer[11] == 0x0B) pub_status(31); // 31: "Ошибка 0x0B"
|
} else if (buffer[7] == 0x18) {
|
||||||
else if (buffer[11] == 0xE6) pub_status(32); // 32: "Ошибка 0xE6"
|
pub_status(25); // 25: "Удаление накипи стадия 2"
|
||||||
else if (buffer[11] == 0x80) pub_status(33); // 33: "Ошибка 0x80"
|
}
|
||||||
else if (buffer[11] == 0xCB) pub_status(34); // 34: "Ошибка 0xCB"
|
} else if (buffer[7] == 0x00) {
|
||||||
else if (buffer[11] == 0xFF) pub_status(35); // 35: "Ошибка 0xFF"
|
if (buffer[6] == 0x01) {
|
||||||
else if (buffer[11] == 0xA0) pub_status(36); // 36: "Ошибка 0xA0"
|
pub_status(1); // 1: "Что-то (07 01 00)"
|
||||||
} else if (buffer[10] == 0x01) {
|
} else if (buffer[6] == 0x05) {
|
||||||
pub_status(37); // 37: "Статус2 0x01"
|
pub_status(2); // 2: "Выключено"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
void SaecoSeries6000::parse_mainboard_packet(const uint8_t* buffer, uint16_t size) {
|
|
||||||
last_activity_time = millis(); // сбрасываем таймер активности при любом пакете
|
// Разбор пакета B5 (коды ошибок кофемашины)
|
||||||
if (debug_) {
|
// buffer[10], buffer[11] содержат коды ошибок
|
||||||
// std::vector<char> hex_buffer;
|
void SaecoSeries6000::parse_b5_packet(const uint8_t* buffer, uint16_t size) {
|
||||||
// hex_buffer.resize(size * 3 + 1);
|
if (size <= 12) return;
|
||||||
// for (uint16_t i = 0; i < size; i++) {
|
if (buffer[10] == 0x00) {
|
||||||
// sprintf(hex_buffer.data() + i * 3, "%02X ", buffer[i]);
|
if (buffer[11] == 0x00) pub_status(30); // 30: "Ошибка 0x00"
|
||||||
// }
|
else if (buffer[11] == 0x0B) pub_status(31); // 31: "Ошибка 0x0B"
|
||||||
//ESP_LOGD(TAG, "Packet from Mainboard: %s", hex_buffer.data());
|
else if (buffer[11] == 0xE6) pub_status(32); // 32: "Ошибка 0xE6"
|
||||||
//ESP_LOGD(TAG, "parse_mainboard_packet: size=%u", size);
|
else if (buffer[11] == 0x80) pub_status(33); // 33: "Ошибка 0x80"
|
||||||
}
|
else if (buffer[11] == 0xCB) pub_status(34); // 34: "Ошибка 0xCB"
|
||||||
// Парсим B0
|
else if (buffer[11] == 0xFF) pub_status(35); // 35: "Ошибка 0xFF"
|
||||||
if (size > 4 && buffer[3] == 0xB0) {
|
else if (buffer[11] == 0xA0) pub_status(36); // 36: "Ошибка 0xA0"
|
||||||
parse_b0_packet(buffer, size);
|
} else if (buffer[10] == 0x01) {
|
||||||
} else if (size > 12 && buffer[3] == 0xB5) {
|
pub_status(37); // 37: "Статус2 0x01"
|
||||||
parse_b5_packet(buffer, size);
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
void SaecoSeries6000::parse_mainboard_packet(const uint8_t* buffer, uint16_t size) {
|
||||||
void SaecoSeries6000::pub_status(uint8_t status) {
|
last_activity_time = millis(); // сбрасываем таймер активности при любом пакете
|
||||||
if (status_sensor_ != nullptr) {
|
if (is_mainboard_idle(buffer, size)) {
|
||||||
status_sensor_->publish_state(status);
|
suppress_display_idle_ = false; // mainboard вернулся в выключенный режим
|
||||||
}
|
}
|
||||||
}
|
if (debug_) {
|
||||||
|
std::vector<char> hex_buffer;
|
||||||
void SaecoSeries6000::dump_config() {
|
hex_buffer.resize(size * 3 + 1);
|
||||||
ESP_LOGCONFIG(TAG, "Saeco Series 6000 UART Bridge");
|
for (uint16_t i = 0; i < size; i++) {
|
||||||
ESP_LOGCONFIG(TAG, " UART Display (uart_display): %p", uart_display_);
|
sprintf(hex_buffer.data() + i * 3, "%02X ", buffer[i]);
|
||||||
ESP_LOGCONFIG(TAG, " UART Mainboard (uart_mainboard): %p", uart_mainboard_);
|
}
|
||||||
ESP_LOGCONFIG(TAG, " Debug mode: %s", ONOFF(this->debug_));
|
ESP_LOGI(TAG, "Packet from Mainboard: %s", hex_buffer.data());
|
||||||
}
|
}
|
||||||
|
// Парсим B0
|
||||||
void SaecoSeries6000::send_packet_to_mainboard(const std::vector<uint8_t> &data) {
|
if (size > 4 && buffer[3] == 0xB0) {
|
||||||
ESP_LOGI(TAG, "Send to Mainboard pressed.");
|
parse_b0_packet(buffer, size);
|
||||||
for (auto b : data) {
|
} else if (size > 12 && buffer[3] == 0xB5) {
|
||||||
uart_mainboard_->write_byte(b);
|
parse_b5_packet(buffer, size);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const uint8_t SaecoSeries6000::BitReverse[256] = {
|
void SaecoSeries6000::pub_status(uint8_t status) {
|
||||||
0x00,0x80,0x40,0xC0,0x20,0xA0,0x60,0xE0,0x10,0x90,0x50,0xD0,0x30,0xB0,0x70,0xF0,
|
if (status_sensor_ != nullptr) {
|
||||||
0x08,0x88,0x48,0xC8,0x28,0xA8,0x68,0xE8,0x18,0x98,0x58,0xD8,0x38,0xB8,0x78,0xF8,
|
status_sensor_->publish_state(status);
|
||||||
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,
|
void SaecoSeries6000::dump_config() {
|
||||||
0x06,0x86,0x46,0xC6,0x26,0xA6,0x66,0xE6,0x16,0x96,0x56,0xD6,0x36,0xB6,0x76,0xF6,
|
ESP_LOGCONFIG(TAG, "Saeco Series 6000 UART Bridge");
|
||||||
0x0E,0x8E,0x4E,0xCE,0x2E,0xAE,0x6E,0xEE,0x1E,0x9E,0x5E,0xDE,0x3E,0xBE,0x7E,0xFE,
|
ESP_LOGCONFIG(TAG, " UART Display (uart_display): %p", uart_display_);
|
||||||
0x01,0x81,0x41,0xC1,0x21,0xA1,0x61,0xE1,0x11,0x91,0x51,0xD1,0x31,0xB1,0x71,0xF1,
|
ESP_LOGCONFIG(TAG, " UART Mainboard (uart_mainboard): %p", uart_mainboard_);
|
||||||
0x09,0x89,0x49,0xC9,0x29,0xA9,0x69,0xE9,0x19,0x99,0x59,0xD9,0x39,0xB9,0x79,0xF9,
|
ESP_LOGCONFIG(TAG, " Debug mode: %s", ONOFF(this->debug_));
|
||||||
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,
|
void SaecoSeries6000::send_packet_to_mainboard(const std::vector<uint8_t> &data) {
|
||||||
0x0B,0x8B,0x4B,0xCB,0x2B,0xAB,0x6B,0xEB,0x1B,0x9B,0x5B,0xDB,0x3B,0xBB,0x7B,0xFB,
|
ESP_LOGI(TAG, "Send to Mainboard pressed.");
|
||||||
0x07,0x87,0x47,0xC7,0x27,0xA7,0x67,0xE7,0x17,0x97,0x57,0xD7,0x37,0xB7,0x77,0xF7,
|
for (auto b : data) {
|
||||||
0x0F,0x8F,0x4F,0xCF,0x2F,0xAF,0x6F,0xEF,0x1F,0x9F,0x5F,0xDF,0x3F,0xBF,0x7F,0xFF
|
uart_mainboard_->write_byte(b);
|
||||||
};
|
}
|
||||||
|
}
|
||||||
uint32_t SaecoSeries6000::calc_crc(const std::vector<uint8_t>& data, size_t start, size_t end) {
|
|
||||||
uint32_t crc = 0xFFFFFFFF;
|
const uint8_t SaecoSeries6000::BitReverse[256] = {
|
||||||
const uint32_t POLY = 0x04C11DB7;
|
0x00,0x80,0x40,0xC0,0x20,0xA0,0x60,0xE0,0x10,0x90,0x50,0xD0,0x30,0xB0,0x70,0xF0,
|
||||||
if (start >= end) return 0;
|
0x08,0x88,0x48,0xC8,0x28,0xA8,0x68,0xE8,0x18,0x98,0x58,0xD8,0x38,0xB8,0x78,0xF8,
|
||||||
// start
|
0x04,0x84,0x44,0xC4,0x24,0xA4,0x64,0xE4,0x14,0x94,0x54,0xD4,0x34,0xB4,0x74,0xF4,
|
||||||
uint8_t val = data[start];
|
0x0C,0x8C,0x4C,0xCC,0x2C,0xAC,0x6C,0xEC,0x1C,0x9C,0x5C,0xDC,0x3C,0xBC,0x7C,0xFC,
|
||||||
((uint8_t*)(&crc))[3] ^= BitReverse[val];
|
0x02,0x82,0x42,0xC2,0x22,0xA2,0x62,0xE2,0x12,0x92,0x52,0xD2,0x32,0xB2,0x72,0xF2,
|
||||||
for (uint8_t j = 0; j < 8; j++) {
|
0x0A,0x8A,0x4A,0xCA,0x2A,0xAA,0x6A,0xEA,0x1A,0x9A,0x5A,0xDA,0x3A,0xBA,0x7A,0xFA,
|
||||||
if (crc & 0x80000000) {
|
0x06,0x86,0x46,0xC6,0x26,0xA6,0x66,0xE6,0x16,0x96,0x56,0xD6,0x36,0xB6,0x76,0xF6,
|
||||||
crc = (crc << 1) ^ POLY;
|
0x0E,0x8E,0x4E,0xCE,0x2E,0xAE,0x6E,0xEE,0x1E,0x9E,0x5E,0xDE,0x3E,0xBE,0x7E,0xFE,
|
||||||
} else {
|
0x01,0x81,0x41,0xC1,0x21,0xA1,0x61,0xE1,0x11,0x91,0x51,0xD1,0x31,0xB1,0x71,0xF1,
|
||||||
crc <<= 1;
|
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,
|
||||||
for (size_t i = start + 1; i < end; i++) {
|
0x0B,0x8B,0x4B,0xCB,0x2B,0xAB,0x6B,0xEB,0x1B,0x9B,0x5B,0xDB,0x3B,0xBB,0x7B,0xFB,
|
||||||
uint8_t v = data[i];
|
0x07,0x87,0x47,0xC7,0x27,0xA7,0x67,0xE7,0x17,0x97,0x57,0xD7,0x37,0xB7,0x77,0xF7,
|
||||||
((uint8_t*)(&crc))[3] ^= BitReverse[v];
|
0x0F,0x8F,0x4F,0xCF,0x2F,0xAF,0x6F,0xEF,0x1F,0x9F,0x5F,0xDF,0x3F,0xBF,0x7F,0xFF
|
||||||
for (uint8_t j = 0; j < 8; j++) {
|
};
|
||||||
if (crc & 0x80000000) {
|
|
||||||
crc = (crc << 1) ^ POLY;
|
std::vector<uint8_t> SaecoSeries6000::parse_hex_string_to_bytes(const std::string& hex_string) {
|
||||||
} else {
|
std::vector<uint8_t> pkt;
|
||||||
crc <<= 1;
|
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;
|
||||||
uint32_t t32 = 0;
|
char* endptr = nullptr;
|
||||||
((uint8_t*)(&t32))[0] = BitReverse[((uint8_t*)(&crc))[3]];
|
uint8_t byte = static_cast<uint8_t>(std::strtoul(hex_string.c_str() + pos, &endptr, 16));
|
||||||
((uint8_t*)(&t32))[1] = BitReverse[((uint8_t*)(&crc))[2]];
|
pkt.push_back(byte);
|
||||||
((uint8_t*)(&t32))[2] = BitReverse[((uint8_t*)(&crc))[1]];
|
if (endptr == nullptr || endptr == hex_string.c_str() + pos) break;
|
||||||
((uint8_t*)(&t32))[3] = BitReverse[((uint8_t*)(&crc))[0]];
|
pos = endptr - hex_string.c_str();
|
||||||
crc = t32 ^ 0xFFFFFFFF;
|
}
|
||||||
return crc;
|
return pkt;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<uint8_t> SaecoSeries6000::parse_hex_string_to_bytes(const std::string& hex_string) {
|
void SaecoSeries6000::send_packets_to_mainboard(const std::string& hex_packet, uint32_t delay_ms) {
|
||||||
std::vector<uint8_t> pkt;
|
std::vector<std::vector<uint8_t>> pkts = {parse_hex_string_to_bytes(hex_packet)};
|
||||||
size_t pos = 0;
|
send_packets_to_mainboard(pkts, delay_ms);
|
||||||
while (pos < hex_string.size()) {
|
}
|
||||||
while (pos < hex_string.size() && isspace(hex_string[pos])) ++pos;
|
|
||||||
if (pos >= hex_string.size()) break;
|
void SaecoSeries6000::send_raw_to_mainboard(const std::string& hex_packet, uint32_t delay_ms) {
|
||||||
char* endptr = nullptr;
|
std::vector<std::string> pkts;
|
||||||
uint8_t byte = static_cast<uint8_t>(std::strtoul(hex_string.c_str() + pos, &endptr, 16));
|
size_t pos = 0;
|
||||||
pkt.push_back(byte);
|
while (pos <= hex_packet.size()) {
|
||||||
if (endptr == nullptr || endptr == hex_string.c_str() + pos) break;
|
size_t sep = hex_packet.find(';', pos);
|
||||||
pos = endptr - hex_string.c_str();
|
if (sep == std::string::npos) sep = hex_packet.size();
|
||||||
}
|
std::string part = hex_packet.substr(pos, sep - pos);
|
||||||
return pkt;
|
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);
|
||||||
void SaecoSeries6000::send_packets_to_mainboard(const std::string& hex_packet, uint32_t delay_ms) {
|
pos = sep + 1;
|
||||||
std::vector<std::vector<uint8_t>> pkts = {parse_hex_string_to_bytes(hex_packet)};
|
}
|
||||||
send_packets_to_mainboard(pkts, delay_ms);
|
for (const auto& p : pkts) {
|
||||||
}
|
std::vector<uint8_t> pkt = parse_hex_string_to_bytes(p);
|
||||||
|
if (pkt.empty()) continue;
|
||||||
void SaecoSeries6000::send_packets_to_mainboard(const std::vector<std::string>& hex_packets, uint32_t delay_ms) {
|
std::string log_str;
|
||||||
std::vector<std::vector<uint8_t>> pkts;
|
char buf[8];
|
||||||
for (const auto& s : hex_packets) {
|
for (auto b : pkt) {
|
||||||
pkts.push_back(parse_hex_string_to_bytes(s));
|
snprintf(buf, sizeof(buf), "%02X ", b);
|
||||||
}
|
log_str += buf;
|
||||||
send_packets_to_mainboard(pkts, delay_ms);
|
}
|
||||||
}
|
ESP_LOGI(TAG, "Send RAW to Mainboard: %s", log_str.c_str());
|
||||||
|
if (is_power_off_packet(pkt.data(), pkt.size())) {
|
||||||
void SaecoSeries6000::send_packets_to_mainboard(const std::vector<std::vector<uint8_t>> &packets, uint32_t delay_ms) {
|
suppress_display_idle_ = false; // программное выключение: idle снова пропускаем
|
||||||
uint8_t counter = (last_display_counter_ + 1) & 0xFF;
|
}
|
||||||
for (size_t i = 0; i < packets.size(); ++i) {
|
send_packet_to_mainboard(pkt);
|
||||||
std::vector<uint8_t> pkt = packets[i];
|
delay(delay_ms);
|
||||||
// Заменяем байт счетчика (4-й байт)
|
}
|
||||||
if (pkt.size() > 4) {
|
}
|
||||||
pkt[4] = counter;
|
|
||||||
}
|
void SaecoSeries6000::send_packets_to_mainboard(const std::vector<std::string>& hex_packets, uint32_t delay_ms) {
|
||||||
// Пересчёт CRC: ищем преамбулу 0xAA 0xAA 0xAA, считаем CRC по данным между преамбулой и последними 5 байтами
|
std::vector<std::vector<uint8_t>> pkts;
|
||||||
if (pkt.size() > 8) {
|
for (const auto& s : hex_packets) {
|
||||||
size_t preamble = 0;
|
pkts.push_back(parse_hex_string_to_bytes(s));
|
||||||
if (pkt[0] == 0xAA && pkt[1] == 0xAA && pkt[2] == 0xAA) {
|
}
|
||||||
preamble = 3;
|
send_packets_to_mainboard(pkts, delay_ms);
|
||||||
}
|
}
|
||||||
size_t crc_start = preamble;
|
|
||||||
size_t crc_end = pkt.size() - 5; // не включаем 4 CRC и 0x55
|
void SaecoSeries6000::send_packets_to_mainboard(const std::vector<std::vector<uint8_t>> &packets, uint32_t delay_ms) {
|
||||||
uint32_t crc = calc_crc(pkt, crc_start, crc_end);
|
uint8_t counter = (last_display_counter_ + 1) & 0xFF;
|
||||||
// Заменяем последние 4 байта перед 0x55 на новую CRC
|
for (size_t i = 0; i < packets.size(); ++i) {
|
||||||
pkt[pkt.size()-2] = (crc >> 24) & 0xFF;
|
std::vector<uint8_t> pkt = packets[i];
|
||||||
pkt[pkt.size()-3] = (crc >> 16) & 0xFF;
|
// Заменяем байт счетчика (4-й байт)
|
||||||
pkt[pkt.size()-4] = (crc >> 8) & 0xFF;
|
if (pkt.size() > 4) {
|
||||||
pkt[pkt.size()-5] = crc & 0xFF;
|
pkt[4] = counter;
|
||||||
// pkt[pkt.size()-5] = (crc >> 24) & 0xFF;
|
}
|
||||||
// pkt[pkt.size()-4] = (crc >> 16) & 0xFF;
|
// Пересчёт CRC: ищем преамбулу 0xAA 0xAA 0xAA, считаем CRC по данным между преамбулой и последними 5 байтами
|
||||||
// pkt[pkt.size()-3] = (crc >> 8) & 0xFF;
|
if (pkt.size() > 8) {
|
||||||
// pkt[pkt.size()-2] = (crc) & 0xFF;
|
size_t preamble = 0;
|
||||||
}
|
if (pkt[0] == 0xAA && pkt[1] == 0xAA && pkt[2] == 0xAA) {
|
||||||
// Логируем пакет перед отправкой
|
preamble = 3;
|
||||||
std::string log_str;
|
}
|
||||||
char buf[8];
|
size_t crc_start = preamble;
|
||||||
for (auto b : pkt) {
|
size_t crc_end = pkt.size() - 5; // не включаем 4 CRC и 0x55
|
||||||
snprintf(buf, sizeof(buf), "%02X ", b);
|
uint32_t crc = calc_crc(pkt, crc_start, crc_end);
|
||||||
log_str += buf;
|
// Заменяем последние 4 байта перед 0x55 на новую CRC
|
||||||
}
|
pkt[pkt.size()-2] = (crc >> 24) & 0xFF;
|
||||||
ESP_LOGI(TAG, "Send to Mainboard: %s", log_str.c_str());
|
pkt[pkt.size()-3] = (crc >> 16) & 0xFF;
|
||||||
send_packet_to_mainboard(pkt);
|
pkt[pkt.size()-4] = (crc >> 8) & 0xFF;
|
||||||
counter = (counter + 1) & 0xFF;
|
pkt[pkt.size()-5] = crc & 0xFF;
|
||||||
delay(delay_ms);
|
// pkt[pkt.size()-5] = (crc >> 24) & 0xFF;
|
||||||
}
|
// pkt[pkt.size()-4] = (crc >> 16) & 0xFF;
|
||||||
last_display_counter_ = (counter - 1) & 0xFF;
|
// pkt[pkt.size()-3] = (crc >> 8) & 0xFF;
|
||||||
}
|
// pkt[pkt.size()-2] = (crc) & 0xFF;
|
||||||
|
}
|
||||||
// Реализация SendPacketsAction полностью в header (saeco_series_6000.h)
|
// Логируем пакет перед отправкой
|
||||||
|
std::string log_str;
|
||||||
} // namespace saeco_series_6000
|
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
|
} // namespace esphome
|
||||||
@@ -1,81 +1,124 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "esphome/core/component.h"
|
#include "esphome/core/component.h"
|
||||||
#include "esphome/components/uart/uart.h"
|
#include "esphome/components/uart/uart.h"
|
||||||
#include "esphome/components/sensor/sensor.h"
|
#include "esphome/components/sensor/sensor.h"
|
||||||
#include "esphome/core/automation.h"
|
#include "esphome/core/automation.h"
|
||||||
#include "esphome/components/button/button.h"
|
#include "esphome/components/button/button.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace saeco_series_6000 {
|
namespace saeco_series_6000 {
|
||||||
|
|
||||||
// Buffer sizes for packet sniffing
|
// Buffer sizes for packet sniffing
|
||||||
constexpr size_t DISPLAY_BUFFER_SIZE = 128;
|
constexpr size_t DISPLAY_BUFFER_SIZE = 128;
|
||||||
constexpr size_t MAINBOARD_BUFFER_SIZE = 128;
|
constexpr size_t MAINBOARD_BUFFER_SIZE = 128;
|
||||||
|
|
||||||
class SaecoSeries6000 : public Component {
|
class SaecoSeries6000 : public Component {
|
||||||
public:
|
public:
|
||||||
void set_uart_display(uart::UARTComponent *uart_display) { uart_display_ = uart_display; }
|
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_uart_mainboard(uart::UARTComponent *uart_mainboard) { uart_mainboard_ = uart_mainboard; }
|
||||||
void set_debug(bool debug) { debug_ = debug; }
|
void set_debug(bool debug) { debug_ = debug; }
|
||||||
void set_status_sensor(sensor::Sensor *sensor) { status_sensor_ = sensor; }
|
void set_status_sensor(sensor::Sensor *sensor) { status_sensor_ = sensor; }
|
||||||
void loop() override;
|
void loop() override;
|
||||||
void dump_config() override;
|
void dump_config() override;
|
||||||
void send_packet_to_mainboard(const std::vector<uint8_t> &data);
|
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::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::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);
|
void send_packets_to_mainboard(const std::string& hex_packet, uint32_t delay_ms = 10);
|
||||||
|
// Отправка пакета(ов) без пересчёта CRC и счётчика (точный реплей захваченных пакетов)
|
||||||
protected:
|
void send_raw_to_mainboard(const std::string& hex_packet, uint32_t delay_ms = 10);
|
||||||
uart::UARTComponent *uart_display_;
|
|
||||||
uart::UARTComponent *uart_mainboard_;
|
// Типы напитков
|
||||||
bool debug_{false};
|
enum CoffeeType {
|
||||||
sensor::Sensor *status_sensor_{nullptr};
|
ESPRESSO = 0,
|
||||||
|
AMERICANO = 1,
|
||||||
private:
|
CAPPUCCINO = 2,
|
||||||
uint8_t display_buffer_[DISPLAY_BUFFER_SIZE];
|
LATTE = 3,
|
||||||
uint16_t display_buffer_pos_{0};
|
COFFEE_WITH_MILK = 4,
|
||||||
|
MILK_FOAM = 5,
|
||||||
uint8_t mainboard_buffer_[MAINBOARD_BUFFER_SIZE];
|
HOT_WATER = 6
|
||||||
uint16_t mainboard_buffer_pos_{0};
|
};
|
||||||
|
// Крепость/помол
|
||||||
uint8_t display_last_bytes_[3] = {0, 0, 0};
|
enum CoffeeStrength {
|
||||||
uint8_t mainboard_last_bytes_[3] = {0, 0, 0};
|
MILD = 0,
|
||||||
|
NORMAL = 1,
|
||||||
uint8_t display_sync_count_{0};
|
STRONG = 2,
|
||||||
uint8_t mainboard_sync_count_{0};
|
GROUND = 3
|
||||||
|
};
|
||||||
uint8_t last_display_counter_{0};
|
// Построение и отправка пакета приготовления кофе
|
||||||
void update_display_counter_from_packet(const uint8_t* buffer, uint16_t size);
|
void coffee_build(uint8_t type, uint8_t bean, uint8_t cups, uint16_t vol, uint16_t milk);
|
||||||
|
// Генерация рецепта (только формирование данных)
|
||||||
void process_display_byte(uint8_t byte);
|
static std::vector<uint8_t> build_coffee_recipe(uint8_t type, uint8_t bean, uint8_t cups, uint16_t vol, uint16_t milk);
|
||||||
void process_mainboard_byte(uint8_t byte);
|
|
||||||
|
protected:
|
||||||
void parse_display_packet(const uint8_t* buffer, uint16_t size);
|
uart::UARTComponent *uart_display_;
|
||||||
void parse_mainboard_packet(const uint8_t* buffer, uint16_t size);
|
uart::UARTComponent *uart_mainboard_;
|
||||||
void parse_b0_packet(const uint8_t* buffer, uint16_t size);
|
bool debug_{false};
|
||||||
void parse_b5_packet(const uint8_t* buffer, uint16_t size);
|
sensor::Sensor *status_sensor_{nullptr};
|
||||||
void pub_status(uint8_t status);
|
|
||||||
static const uint8_t BitReverse[256];
|
private:
|
||||||
uint32_t calc_crc(const std::vector<uint8_t>& data, size_t start, size_t end);
|
uint8_t display_buffer_[DISPLAY_BUFFER_SIZE];
|
||||||
std::vector<uint8_t> parse_hex_string_to_bytes(const std::string& hex_string);
|
uint16_t display_buffer_pos_{0};
|
||||||
};
|
|
||||||
|
uint8_t mainboard_buffer_[MAINBOARD_BUFFER_SIZE];
|
||||||
class SaecoSendPacketsButton : public esphome::button::Button, public esphome::Component {
|
uint16_t mainboard_buffer_pos_{0};
|
||||||
public:
|
|
||||||
void set_parent(SaecoSeries6000 *parent) { parent_ = parent; }
|
uint8_t display_last_bytes_[3] = {0, 0, 0};
|
||||||
void set_packets(const std::vector<std::vector<uint8_t>> &packets) { packets_ = packets; }
|
uint8_t mainboard_last_bytes_[3] = {0, 0, 0};
|
||||||
void set_delay_ms(uint32_t delay_ms) { delay_ms_ = delay_ms; }
|
|
||||||
void press_action() override {
|
uint8_t display_sync_count_{0};
|
||||||
if (parent_ != nullptr) {
|
uint8_t mainboard_sync_count_{0};
|
||||||
parent_->send_packets_to_mainboard(packets_, delay_ms_);
|
|
||||||
}
|
uint8_t last_display_counter_{0};
|
||||||
}
|
void update_display_counter_from_packet(const uint8_t* buffer, uint16_t size);
|
||||||
private:
|
|
||||||
SaecoSeries6000 *parent_{nullptr};
|
// Подавление idle-пакета FE 00 00 от дисплея (когда дисплей не знает о программном включении)
|
||||||
std::vector<std::vector<uint8_t>> packets_;
|
bool suppress_display_idle_{false};
|
||||||
uint32_t delay_ms_{10};
|
bool is_idle_packet(const uint8_t* buffer, uint16_t size) const {
|
||||||
};
|
return size > 5 && buffer[3] == 0xFE && buffer[4] == 0x00 && buffer[5] == 0x00;
|
||||||
|
}
|
||||||
} // namespace saeco_series_6000
|
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
|
} // 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
|
||||||
@@ -1 +1 @@
|
|||||||
2026.7.2
|
2026.8.0
|
||||||
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 24 KiB |
@@ -1 +1 @@
|
|||||||
{"pid": 71, "version": 1, "ha_version": "2026.7.2", "start_ts": 1784057808.5709116}
|
{"pid": 71, "version": 1, "ha_version": "2026.8.0", "start_ts": 1786366403.9299605}
|
||||||
@@ -24,8 +24,9 @@
|
|||||||
- 37e299b4ee90a2e39ae7e04328439ff4
|
- 37e299b4ee90a2e39ae7e04328439ff4
|
||||||
- 8883de9fc71bef0b663c61faa4fbefb4
|
- 8883de9fc71bef0b663c61faa4fbefb4
|
||||||
- f6dbda8398f290149bc8cb48a858452c
|
- f6dbda8398f290149bc8cb48a858452c
|
||||||
- service: notify.telegram
|
- service: telegram_bot.send_message
|
||||||
data:
|
data:
|
||||||
|
entity_id: notify.home_assistant_fonvizina_fonvizina_18_1001567589226
|
||||||
message: Звонок в дверь!
|
message: Звонок в дверь!
|
||||||
mode: single
|
mode: single
|
||||||
- id: '1694942205257'
|
- id: '1694942205257'
|
||||||
@@ -35,11 +36,11 @@
|
|||||||
path: gmlupatelli/unavailable_entities_notification.yaml
|
path: gmlupatelli/unavailable_entities_notification.yaml
|
||||||
input:
|
input:
|
||||||
actions:
|
actions:
|
||||||
- service: notify.telegram
|
- service: telegram_bot.send_message
|
||||||
data:
|
data:
|
||||||
|
entity_id: notify.home_assistant_fonvizina_fonvizina_18_1001567589226
|
||||||
message: 'У вас есть недоступные устройства: {{entities}}'
|
message: 'У вас есть недоступные устройства: {{entities}}'
|
||||||
data:
|
parse_mode: html
|
||||||
parse_mode: html
|
|
||||||
exclude:
|
exclude:
|
||||||
device_id:
|
device_id:
|
||||||
- fd4ed129390c68716ee6b5ca4ee6abe5
|
- fd4ed129390c68716ee6b5ca4ee6abe5
|
||||||
@@ -272,14 +273,14 @@
|
|||||||
include_persistent_notification: disabled_persistent_notification
|
include_persistent_notification: disabled_persistent_notification
|
||||||
include_custom_actions: enable_custom_actions
|
include_custom_actions: enable_custom_actions
|
||||||
custom_actions:
|
custom_actions:
|
||||||
- action: notify.telegram_monster
|
- action: telegram_bot.send_message
|
||||||
data:
|
data:
|
||||||
|
entity_id: notify.home_assistant_fonvizina_monster_123622180
|
||||||
message: '<b>Садится батарея в датчиках:</b>
|
message: '<b>Садится батарея в датчиках:</b>
|
||||||
|
|
||||||
|
|
||||||
{{all_sensors}}'
|
{{all_sensors}}'
|
||||||
data:
|
parse_mode: html
|
||||||
parse_mode: html
|
|
||||||
- id: '1767036452762'
|
- id: '1767036452762'
|
||||||
alias: Дверь в гостиную - закрыта
|
alias: Дверь в гостиную - закрыта
|
||||||
description: ''
|
description: ''
|
||||||
|
|||||||
@@ -6,14 +6,7 @@ homeassistant:
|
|||||||
external_url: "https://home-fnv.yandex5.ru"
|
external_url: "https://home-fnv.yandex5.ru"
|
||||||
internal_url: "http://192.168.1.6:8123"
|
internal_url: "http://192.168.1.6:8123"
|
||||||
|
|
||||||
http:
|
# http config moved to .storage/http (UI-managed) in HA 2026.x: trusted_proxies, use_x_forwarded_for etc.
|
||||||
use_x_forwarded_for: true
|
|
||||||
ip_ban_enabled: true
|
|
||||||
login_attempts_threshold: 15
|
|
||||||
trusted_proxies:
|
|
||||||
- 172.19.0.0/24
|
|
||||||
- 172.18.0.0/24
|
|
||||||
- 192.168.1.0/24
|
|
||||||
|
|
||||||
# Text to speech
|
# Text to speech
|
||||||
tts:
|
tts:
|
||||||
|
|||||||
@@ -34,7 +34,9 @@ yandex_smart_home:
|
|||||||
- light.light_childroom_bed
|
- light.light_childroom_bed
|
||||||
- light.light_childroom_main
|
- light.light_childroom_main
|
||||||
- light.light_childroom_spotlights
|
- light.light_childroom_spotlights
|
||||||
|
- light.light_cloakroom
|
||||||
- light.light_hall_long
|
- light.light_hall_long
|
||||||
|
- light.light_hall_small
|
||||||
- light.light_kitchen_bar_lamp
|
- light.light_kitchen_bar_lamp
|
||||||
- light.light_kitchen_set
|
- light.light_kitchen_set
|
||||||
- light.light_kitchen_spotlights
|
- light.light_kitchen_spotlights
|
||||||
@@ -60,4 +62,170 @@ yandex_smart_home:
|
|||||||
#exclude_entities:
|
#exclude_entities:
|
||||||
# - light.highlight
|
# - light.highlight
|
||||||
#exclude_entity_globs:
|
#exclude_entity_globs:
|
||||||
# - sensor.weather_*
|
# - sensor.weather_*
|
||||||
|
entity_config:
|
||||||
|
light.light_balcony_spotlight:
|
||||||
|
name: 'Светильники'
|
||||||
|
room: 'Балкон'
|
||||||
|
sensor.temperature_balcony_temperature:
|
||||||
|
room: 'Балкон'
|
||||||
|
fan.ventilation_bath:
|
||||||
|
name: 'Вытяжка'
|
||||||
|
room: 'Ванная'
|
||||||
|
light.light_bath:
|
||||||
|
room: 'Ванная'
|
||||||
|
light.light_bath_shelf:
|
||||||
|
name: 'Подсветка'
|
||||||
|
room: 'Ванная'
|
||||||
|
sensor.temperature_bath_temperature:
|
||||||
|
room: 'Ванная'
|
||||||
|
switch.light_bath_mirror:
|
||||||
|
name: 'Зеркало'
|
||||||
|
room: 'Ванная'
|
||||||
|
climate.ac_mainroom:
|
||||||
|
name: 'Кондиционер'
|
||||||
|
room: 'Гостиная'
|
||||||
|
climate.radiator_mainroom:
|
||||||
|
room: 'Гостиная'
|
||||||
|
cover.curtain_mainroom:
|
||||||
|
room: 'Гостиная'
|
||||||
|
cover.curtain_mainroom_bobbinet:
|
||||||
|
name: 'Тюль'
|
||||||
|
room: 'Гостиная'
|
||||||
|
light.light_mainroom_pc_spotlight:
|
||||||
|
name: 'Над столом'
|
||||||
|
room: 'Гостиная'
|
||||||
|
light.plug_yolka:
|
||||||
|
room: 'Гостиная'
|
||||||
|
media_player.mainroom_tv:
|
||||||
|
room: 'Гостиная'
|
||||||
|
sensor.temperature_mainroom_temperature:
|
||||||
|
room: 'Гостиная'
|
||||||
|
switch.light_mainroom_mainlamp:
|
||||||
|
name: 'Люстра'
|
||||||
|
room: 'Гостиная'
|
||||||
|
switch.light_mainroom_spotlights:
|
||||||
|
name: 'Светильники'
|
||||||
|
room: 'Гостиная'
|
||||||
|
climate.ac_childroom:
|
||||||
|
name: 'Кондиционер'
|
||||||
|
room: 'Детская'
|
||||||
|
climate.radiator_childroom:
|
||||||
|
room: 'Детская'
|
||||||
|
light.light_childroom_bed:
|
||||||
|
name: 'Кровать'
|
||||||
|
room: 'Детская'
|
||||||
|
light.light_childroom_main:
|
||||||
|
name: 'Люстра'
|
||||||
|
room: 'Детская'
|
||||||
|
light.light_childroom_spotlights:
|
||||||
|
name: 'Над Столом'
|
||||||
|
room: 'Детская'
|
||||||
|
sensor.temperature_childroom_temperature:
|
||||||
|
room: 'Детская'
|
||||||
|
script.away_mode:
|
||||||
|
name: 'Сценарий я ушел'
|
||||||
|
room: 'Дом'
|
||||||
|
switch.water_valve:
|
||||||
|
room: 'Дом'
|
||||||
|
fan.ventilation_shower:
|
||||||
|
name: 'Вытяжка'
|
||||||
|
room: 'Душевая'
|
||||||
|
light.light_shower_main_spotlights:
|
||||||
|
name: 'Светильники'
|
||||||
|
room: 'Душевая'
|
||||||
|
light.light_shower_shelf_light:
|
||||||
|
name: 'Подсветка'
|
||||||
|
room: 'Душевая'
|
||||||
|
light.light_shower_shower_spotlights:
|
||||||
|
name: 'Светильники Душ'
|
||||||
|
room: 'Душевая'
|
||||||
|
sensor.temperature_shower_temperature:
|
||||||
|
name: 'Температура'
|
||||||
|
room: 'Душевая'
|
||||||
|
switch.shower_mirror:
|
||||||
|
name: 'Зеркало'
|
||||||
|
room: 'Душевая'
|
||||||
|
light.light_cloakroom:
|
||||||
|
room: 'Кладовка'
|
||||||
|
light.light_hall_long:
|
||||||
|
name: 'Светильники'
|
||||||
|
room: 'Коридор'
|
||||||
|
light.light_hall_small:
|
||||||
|
name: 'Светильники Маленькие'
|
||||||
|
room: 'Коридор'
|
||||||
|
vacuum.dreame_p2027_6b1e_robot_cleaner:
|
||||||
|
room: 'Коридор'
|
||||||
|
climate.ac_kitchen:
|
||||||
|
name: 'Кондиционер'
|
||||||
|
room: 'Кухня'
|
||||||
|
climate.radiator_kitchen:
|
||||||
|
room: 'Кухня'
|
||||||
|
cover.curtain_kitchen:
|
||||||
|
room: 'Кухня'
|
||||||
|
cover.curtain_kitchen_bobbinet:
|
||||||
|
name: 'Тюль'
|
||||||
|
room: 'Кухня'
|
||||||
|
light.light_kitchen_bar_lamp:
|
||||||
|
room: 'Кухня'
|
||||||
|
light.light_kitchen_set:
|
||||||
|
name: 'Подсветка'
|
||||||
|
room: 'Кухня'
|
||||||
|
light.light_kitchen_spotlights:
|
||||||
|
name: 'Светильники'
|
||||||
|
room: 'Кухня'
|
||||||
|
light.light_kitchen_table_lamp:
|
||||||
|
name: 'Люстра'
|
||||||
|
room: 'Кухня'
|
||||||
|
media_player.kitchen_tv:
|
||||||
|
room: 'Кухня'
|
||||||
|
sensor.temperature_kitchen_temperature:
|
||||||
|
room: 'Кухня'
|
||||||
|
sensor.temperature_winecooler_temperature:
|
||||||
|
room: 'Кухня'
|
||||||
|
climate.ac_bedroom:
|
||||||
|
name: 'Кондиционер'
|
||||||
|
room: 'Спальня'
|
||||||
|
climate.radiator_bedroom:
|
||||||
|
room: 'Спальня'
|
||||||
|
climate.radiator_bedroom_balcony:
|
||||||
|
room: 'Спальня'
|
||||||
|
cover.curtain_bedroom:
|
||||||
|
room: 'Спальня'
|
||||||
|
cover.curtain_bedroom_balcony:
|
||||||
|
room: 'Спальня'
|
||||||
|
cover.curtain_bedroom_bobbinet_balcony:
|
||||||
|
name: 'Тюль'
|
||||||
|
room: 'Спальня'
|
||||||
|
fan.ventiliatsionnaia_sistema:
|
||||||
|
room: 'Спальня'
|
||||||
|
light.danil_bedside_lamp_rgbww_light:
|
||||||
|
name: 'Ночник Данил'
|
||||||
|
room: 'Спальня'
|
||||||
|
light.light_bedroom_mainlight:
|
||||||
|
name: 'Люстра'
|
||||||
|
room: 'Спальня'
|
||||||
|
light.light_bedroom_nightlight_left:
|
||||||
|
name: 'Бра левый'
|
||||||
|
room: 'Спальня'
|
||||||
|
light.light_bedroom_nightlight_right:
|
||||||
|
name: 'Бра правый'
|
||||||
|
room: 'Спальня'
|
||||||
|
light.light_bedroom_pc_nightlight:
|
||||||
|
name: 'Над столом бра'
|
||||||
|
room: 'Спальня'
|
||||||
|
light.light_bedroom_pc_spotlight:
|
||||||
|
name: 'Над столом'
|
||||||
|
room: 'Спальня'
|
||||||
|
light.light_bedroom_spotlights:
|
||||||
|
name: 'Светильники'
|
||||||
|
room: 'Спальня'
|
||||||
|
light.svetlana_bedside_lamp_rgbww_light:
|
||||||
|
name: 'Ночник Света'
|
||||||
|
room: 'Спальня'
|
||||||
|
media_player.bedroom_tv:
|
||||||
|
room: 'Спальня'
|
||||||
|
sensor.temperature_bedroom_temperature:
|
||||||
|
room: 'Спальня'
|
||||||
|
sensor.temperature_outside_bedroom_temperature:
|
||||||
|
room: 'Спальня'
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ yandex_smart_home:
|
|||||||
- switch.light_mainroom_mainlamp
|
- switch.light_mainroom_mainlamp
|
||||||
- switch.light_mainroom_spotlights
|
- switch.light_mainroom_spotlights
|
||||||
- switch.shower_mirror
|
- switch.shower_mirror
|
||||||
- switch.watercontrol_valves
|
- switch.water_valve
|
||||||
- vacuum.dreame_p2027_6b1e_robot_cleaner
|
- vacuum.dreame_p2027_6b1e_robot_cleaner
|
||||||
- light.plug_yolka
|
- light.plug_yolka
|
||||||
include_entity_globs:
|
include_entity_globs:
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
yandex_smart_home:
|
||||||
|
filter:
|
||||||
|
include_domains:
|
||||||
|
- cover
|
||||||
|
- fan
|
||||||
|
- vacuum
|
||||||
|
include_entities:
|
||||||
|
# радиаторы
|
||||||
|
- climate.radiator_bedroom
|
||||||
|
- climate.radiator_bedroom_balcony
|
||||||
|
- climate.radiator_childroom
|
||||||
|
- climate.radiator_kitchen
|
||||||
|
- climate.radiator_mainroom
|
||||||
|
# кондиционеры
|
||||||
|
- climate.ac_mainroom
|
||||||
|
- climate.ac_kitchen
|
||||||
|
- climate.ac_childroom
|
||||||
|
- climate.ac_bedroom
|
||||||
|
# вентиляторы
|
||||||
|
- fan.ventilation_bath
|
||||||
|
- fan.ventilation_shower
|
||||||
|
- fan.ventiliatsionnaia_sistema
|
||||||
|
# свет
|
||||||
|
- light.danil_bedside_lamp_rgbww_light
|
||||||
|
- light.light_balcony_spotlight
|
||||||
|
- light.light_bath
|
||||||
|
- light.light_bath_shelf
|
||||||
|
- light.light_bedroom_mainlight
|
||||||
|
- light.light_bedroom_nightlight_left
|
||||||
|
- light.light_bedroom_nightlight_right
|
||||||
|
- light.light_bedroom_pc_nightlight
|
||||||
|
- light.light_bedroom_pc_spotlight
|
||||||
|
- light.light_bedroom_spotlights
|
||||||
|
- light.light_childroom_bed
|
||||||
|
- light.light_childroom_main
|
||||||
|
- light.light_childroom_spotlights
|
||||||
|
- light.light_cloakroom
|
||||||
|
- light.light_hall_long
|
||||||
|
- light.light_hall_small
|
||||||
|
- light.light_kitchen_bar_lamp
|
||||||
|
- light.light_kitchen_set
|
||||||
|
- light.light_kitchen_spotlights
|
||||||
|
- light.light_kitchen_table_lamp
|
||||||
|
- light.light_mainroom_pc_spotlight
|
||||||
|
- light.light_shower_main_spotlights
|
||||||
|
- light.light_shower_shelf_light
|
||||||
|
- light.light_shower_shower_spotlights
|
||||||
|
- light.svetlana_bedside_lamp_rgbww_light
|
||||||
|
- media_player.bedroom_tv
|
||||||
|
- media_player.kitchen_tv
|
||||||
|
- media_player.mainroom_tv
|
||||||
|
- script.away_mode
|
||||||
|
- switch.light_bath_mirror
|
||||||
|
- switch.light_mainroom_mainlamp
|
||||||
|
- switch.light_mainroom_spotlights
|
||||||
|
- switch.shower_mirror
|
||||||
|
- switch.water_valve
|
||||||
|
- vacuum.dreame_p2027_6b1e_robot_cleaner
|
||||||
|
- light.plug_yolka
|
||||||
|
include_entity_globs:
|
||||||
|
- sensor.temperature_*_temperature
|
||||||
|
#exclude_entities:
|
||||||
|
# - light.highlight
|
||||||
|
#exclude_entity_globs:
|
||||||
|
# - sensor.weather_*
|
||||||
@@ -3,5 +3,5 @@ mqtt:
|
|||||||
- name: door_lock_mqtt_action
|
- name: door_lock_mqtt_action
|
||||||
unique_id: door_lock_mqtt_action
|
unique_id: door_lock_mqtt_action
|
||||||
state_topic: "z2m-tcp/lock_door"
|
state_topic: "z2m-tcp/lock_door"
|
||||||
value_template: "{{ value_json.action }}"
|
value_template: "{{ value_json.get('action', '') }}"
|
||||||
json_attributes_topic: "z2m-tcp/lock_door"
|
json_attributes_topic: "z2m-tcp/lock_door"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
recorder:
|
recorder:
|
||||||
db_url: !secret recorder_db_url
|
db_url: !secret recorder_db_url
|
||||||
purge_keep_days: 365
|
purge_keep_days: 180
|
||||||
include:
|
include:
|
||||||
domains:
|
domains:
|
||||||
- alarm_control_panel
|
- alarm_control_panel
|
||||||
|
|||||||
@@ -3,16 +3,24 @@ template:
|
|||||||
- name: heat_total
|
- name: heat_total
|
||||||
unit_of_measurement: gKal
|
unit_of_measurement: gKal
|
||||||
default_entity_id: sensor.heat_total
|
default_entity_id: sensor.heat_total
|
||||||
state: '{{ states(''sensor.saures_teploschetchik_fonvizina'') | float + 32.7600 | round(4) }}'
|
state: >-
|
||||||
|
{% set v = states('sensor.saures_teploschetchik_fonvizina') %}
|
||||||
|
{{ ((v | float + 32.7600) | round(4)) if v not in ['unavailable', 'unknown', ''] else 'unavailable' }}
|
||||||
- name: heat_hourly_cost
|
- name: heat_hourly_cost
|
||||||
unit_of_measurement: '₽'
|
unit_of_measurement: '₽'
|
||||||
state: "{{ states('sensor.heat_hourly') | float * 3035 | round(2) }}"
|
state: >-
|
||||||
|
{% set v = states('sensor.heat_hourly') %}
|
||||||
|
{{ ((v | float * 3035) | round(2)) if v not in ['unavailable', 'unknown', ''] else 'unavailable' }}
|
||||||
- name: heat_daily_cost
|
- name: heat_daily_cost
|
||||||
unit_of_measurement: '₽'
|
unit_of_measurement: '₽'
|
||||||
state: "{{ states('sensor.heat_daily') | float * 3035 | round(2) }}"
|
state: >-
|
||||||
|
{% set v = states('sensor.heat_daily') %}
|
||||||
|
{{ ((v | float * 3035) | round(2)) if v not in ['unavailable', 'unknown', ''] else 'unavailable' }}
|
||||||
- name: heat_monthly_cost
|
- name: heat_monthly_cost
|
||||||
unit_of_measurement: '₽'
|
unit_of_measurement: '₽'
|
||||||
state: "{{ states('sensor.heat_monthly') | float * 3035 | round(2) }}"
|
state: >-
|
||||||
|
{% set v = states('sensor.heat_monthly') %}
|
||||||
|
{{ ((v | float * 3035) | round(2)) if v not in ['unavailable', 'unknown', ''] else 'unavailable' }}
|
||||||
|
|
||||||
utility_meter:
|
utility_meter:
|
||||||
heat_hourly:
|
heat_hourly:
|
||||||
@@ -29,7 +37,7 @@ homeassistant:
|
|||||||
customize:
|
customize:
|
||||||
sensor.heat_hourly:
|
sensor.heat_hourly:
|
||||||
friendly_name: Тепло в час
|
friendly_name: Тепло в час
|
||||||
sensor.heat_hourly:
|
sensor.heat_daily:
|
||||||
friendly_name: Тепло в день
|
friendly_name: Тепло в день
|
||||||
sensor.heat_hourly:
|
sensor.heat_monthly:
|
||||||
friendly_name: Тепло в месяц
|
friendly_name: Тепло в месяц
|
||||||
|
|||||||
@@ -1,22 +1,35 @@
|
|||||||
template:
|
template:
|
||||||
- sensor:
|
- sensor:
|
||||||
- unit_of_measurement: kWh
|
- unit_of_measurement: kWh
|
||||||
state: "{{ states('sensor.power_total') | float - 7072.7 | round(2) }}"
|
state: >-
|
||||||
|
{% set v = states('sensor.power_total') %}
|
||||||
|
{{ ((v | float - 7072.7) | round(2)) if v not in ['unavailable', 'unknown', ''] else 'unavailable' }}
|
||||||
name: power_main_total
|
name: power_main_total
|
||||||
# 7072.7 - разница показаний.
|
# 7072.7 - разница показаний.
|
||||||
|
|
||||||
- unit_of_measurement: ₽
|
- unit_of_measurement: ₽
|
||||||
state: "{{ states('sensor.power_monthly_t1') | float * 8.23 | round(2) }}"
|
state: >-
|
||||||
|
{% set v = states('sensor.power_monthly_t1') %}
|
||||||
|
{{ ((v | float * 8.23) | round(2)) if v not in ['unavailable', 'unknown', ''] else 'unavailable' }}
|
||||||
name: power_monthly_cost_t1
|
name: power_monthly_cost_t1
|
||||||
|
|
||||||
- unit_of_measurement: ₽
|
- unit_of_measurement: ₽
|
||||||
state: "{{ states('sensor.power_monthly_t2') | float * 2.62 | round(2) }}"
|
state: >-
|
||||||
|
{% set v = states('sensor.power_monthly_t2') %}
|
||||||
|
{{ ((v | float * 2.62) | round(2)) if v not in ['unavailable', 'unknown', ''] else 'unavailable' }}
|
||||||
name: power_monthly_cost_t2
|
name: power_monthly_cost_t2
|
||||||
|
|
||||||
- unit_of_measurement: ₽
|
- unit_of_measurement: ₽
|
||||||
state: "{{ states('sensor.power_monthly_t3') | float * 5.66 | round(2) }}"
|
state: >-
|
||||||
|
{% set v = states('sensor.power_monthly_t3') %}
|
||||||
|
{{ ((v | float * 5.66) | round(2)) if v not in ['unavailable', 'unknown', ''] else 'unavailable' }}
|
||||||
name: power_monthly_cost_t3
|
name: power_monthly_cost_t3
|
||||||
|
|
||||||
- unit_of_measurement: ₽
|
- unit_of_measurement: ₽
|
||||||
state: "{{ (states('sensor.power_monthly_cost_t1') | float) + (states('sensor.power_monthly_cost_t2') | float) + (states('sensor.power_monthly_cost_t3') | float) | round(2) }}"
|
state: >-
|
||||||
|
{% set t1 = states('sensor.power_monthly_cost_t1') %}
|
||||||
|
{% set t2 = states('sensor.power_monthly_cost_t2') %}
|
||||||
|
{% set t3 = states('sensor.power_monthly_cost_t3') %}
|
||||||
|
{% set bad = ['unavailable', 'unknown', ''] %}
|
||||||
|
{{ ((t1 | float + t2 | float + t3 | float) | round(2)) if not (t1 in bad or t2 in bad or t3 in bad) else 'unavailable' }}
|
||||||
name: power_monthly_cost_total
|
name: power_monthly_cost_total
|
||||||
|
|||||||
@@ -11,12 +11,16 @@ template:
|
|||||||
device_class: water
|
device_class: water
|
||||||
state_class: total_increasing
|
state_class: total_increasing
|
||||||
unit_of_measurement: 'm³'
|
unit_of_measurement: 'm³'
|
||||||
state: "{{ states('sensor.counter_hot') | float + 88.97 | round(2) }}"
|
state: >-
|
||||||
|
{% set v = states('sensor.counter_hot') %}
|
||||||
|
{{ ((v | float + 88.97) | round(2)) if v not in ['unavailable', 'unknown', ''] else 'unavailable' }}
|
||||||
- name: counter_cold_outer
|
- name: counter_cold_outer
|
||||||
device_class: water
|
device_class: water
|
||||||
state_class: total_increasing
|
state_class: total_increasing
|
||||||
unit_of_measurement: 'm³'
|
unit_of_measurement: 'm³'
|
||||||
state: "{{ states('sensor.counter_cold') | float + 144.7 | round(2) }}"
|
state: >-
|
||||||
|
{% set v = states('sensor.counter_cold') %}
|
||||||
|
{{ ((v | float + 144.7) | round(2)) if v not in ['unavailable', 'unknown', ''] else 'unavailable' }}
|
||||||
|
|
||||||
binary_sensor:
|
binary_sensor:
|
||||||
- name: dryer_running
|
- name: dryer_running
|
||||||
|
|||||||
@@ -1,233 +0,0 @@
|
|||||||
version: '3'
|
|
||||||
services:
|
|
||||||
ddns:
|
|
||||||
build:
|
|
||||||
context: ./ddns/docker
|
|
||||||
env_file: ./ddns/settings/token.env
|
|
||||||
environment:
|
|
||||||
- SUBDOMAIN="home-fnv"
|
|
||||||
restart: always
|
|
||||||
|
|
||||||
mosquitto:
|
|
||||||
build: ./mosquitto/docker
|
|
||||||
container_name: mosquitto
|
|
||||||
env_file: ./mosquitto/settings/mqtt.env
|
|
||||||
ports:
|
|
||||||
- "1883:1883"
|
|
||||||
restart: always
|
|
||||||
logging:
|
|
||||||
driver: json-file
|
|
||||||
options:
|
|
||||||
max-size: 50m
|
|
||||||
|
|
||||||
postgres:
|
|
||||||
image: postgres:16.1
|
|
||||||
restart: always
|
|
||||||
volumes:
|
|
||||||
- ./postgres/settings/data:/var/lib/postgresql/data
|
|
||||||
env_file: ./postgres/settings/postgres.env
|
|
||||||
logging:
|
|
||||||
driver: json-file
|
|
||||||
options:
|
|
||||||
max-size: 50m
|
|
||||||
|
|
||||||
hass:
|
|
||||||
image: "homeassistant/home-assistant:stable" #2026.4.2
|
|
||||||
#image: ghcr.io/home-assistant/home-assistant:stable
|
|
||||||
container_name: hass
|
|
||||||
volumes:
|
|
||||||
- ./hass/settings:/config
|
|
||||||
- /dev/hass-wolpipe:/dev/hass-wolpipe
|
|
||||||
- ./secrets/hass-fnv/settings/secrets.yaml:/config/secrets.yaml:ro
|
|
||||||
- /etc/localtime:/etc/localtime:ro
|
|
||||||
ports:
|
|
||||||
- "8123:8123"
|
|
||||||
# - "29898:9898/udp"
|
|
||||||
# - "24321:4321/udp"
|
|
||||||
# - "251827:51827" #Homekit
|
|
||||||
devices:
|
|
||||||
- "/dev/ttyUSB0:/dev/ttyUSB0"
|
|
||||||
depends_on:
|
|
||||||
- mosquitto
|
|
||||||
- postgres
|
|
||||||
#network_mode: "host"
|
|
||||||
#privileged: true
|
|
||||||
restart: always
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:8123"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 6
|
|
||||||
logging:
|
|
||||||
driver: json-file
|
|
||||||
options:
|
|
||||||
max-size: 50m
|
|
||||||
|
|
||||||
appdaemon:
|
|
||||||
build:
|
|
||||||
context: ./appdaemon/docker
|
|
||||||
container_name: appdaemon
|
|
||||||
depends_on:
|
|
||||||
- hass
|
|
||||||
volumes:
|
|
||||||
- ./appdaemon/settings:/conf
|
|
||||||
- ./secrets/hass-fnv/settings/secrets.yaml:/conf/secrets.yaml:ro
|
|
||||||
- /etc/localtime:/etc/localtime:ro
|
|
||||||
restart: always
|
|
||||||
links:
|
|
||||||
- "hass"
|
|
||||||
logging:
|
|
||||||
driver: json-file
|
|
||||||
options:
|
|
||||||
max-size: "10m"
|
|
||||||
max-file: "5"
|
|
||||||
|
|
||||||
node-red:
|
|
||||||
build:
|
|
||||||
context: ./node-red/docker
|
|
||||||
#image: nodered/node-red:latest
|
|
||||||
environment:
|
|
||||||
- TZ=Europe/Moscow
|
|
||||||
ports:
|
|
||||||
- "1880:1880"
|
|
||||||
depends_on:
|
|
||||||
- hass
|
|
||||||
restart: always
|
|
||||||
volumes:
|
|
||||||
- ./node-red/settings/data:/data
|
|
||||||
|
|
||||||
esphome:
|
|
||||||
image: esphome/esphome:latest #2025.6 #esphome-armv7
|
|
||||||
container_name: esphome
|
|
||||||
ports:
|
|
||||||
- "6052:6052"
|
|
||||||
volumes:
|
|
||||||
- ./esphome/settings:/config
|
|
||||||
- ./secrets/hass-fnv/settings/secrets.yaml:/config/secrets.yaml:ro
|
|
||||||
#devices:
|
|
||||||
# - "/dev/ttyUSB0:/dev/ttyUSB0"
|
|
||||||
#network_mode: host
|
|
||||||
logging:
|
|
||||||
driver: json-file
|
|
||||||
options:
|
|
||||||
max-size: "10m"
|
|
||||||
max-file: "5"
|
|
||||||
|
|
||||||
zigbee2mqtt:
|
|
||||||
image: koenkk/zigbee2mqtt:latest #тут отваливается замок, поэтому не latest
|
|
||||||
container_name: zigbee2mqtt
|
|
||||||
ports:
|
|
||||||
- "8080:8080"
|
|
||||||
volumes:
|
|
||||||
- ./zigbee2mqtt/settings:/app/data
|
|
||||||
healthcheck:
|
|
||||||
# в образе нет curl (Alpine), используем wget --spider
|
|
||||||
test: ["CMD", "wget", "--spider", "-q", "http://localhost:8080"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 6
|
|
||||||
start_period: 60s
|
|
||||||
restart: always
|
|
||||||
logging:
|
|
||||||
driver: json-file
|
|
||||||
options:
|
|
||||||
max-size: 50m
|
|
||||||
|
|
||||||
neptun2mqtt:
|
|
||||||
build:
|
|
||||||
context: ./neptun2mqtt/docker
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
restart: always
|
|
||||||
container_name: neptun2mqtt
|
|
||||||
env_file: ./neptun2mqtt/settings/neptun.env
|
|
||||||
logging:
|
|
||||||
driver: json-file
|
|
||||||
options:
|
|
||||||
max-size: "10m"
|
|
||||||
max-file: "5"
|
|
||||||
|
|
||||||
nginx:
|
|
||||||
image: nginx
|
|
||||||
hostname: nginx
|
|
||||||
container_name: nginx
|
|
||||||
links:
|
|
||||||
- "hass"
|
|
||||||
- "certbot"
|
|
||||||
volumes:
|
|
||||||
- ./nginx/settings/site-confs:/etc/nginx/conf.d
|
|
||||||
- ./certbot/settings/data:/config/keys
|
|
||||||
ports:
|
|
||||||
- "80:80"
|
|
||||||
- "443:443"
|
|
||||||
restart: always
|
|
||||||
logging:
|
|
||||||
driver: json-file
|
|
||||||
options:
|
|
||||||
max-size: "10m"
|
|
||||||
max-file: "5"
|
|
||||||
|
|
||||||
certbot:
|
|
||||||
build:
|
|
||||||
context: ./certbot/docker
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
restart: always
|
|
||||||
container_name: certbot
|
|
||||||
volumes:
|
|
||||||
- ./certbot/docker/update_certs.sh:/update_certs.sh:ro
|
|
||||||
- ./certbot/settings/data:/etc/letsencrypt
|
|
||||||
- /etc/localtime:/etc/localtime:ro
|
|
||||||
ports:
|
|
||||||
- "8081:80"
|
|
||||||
#entrypoint: certbot
|
|
||||||
#command: certonly --standalone --agree-tos --non-interactive --email admin@yandex5.ru -d gitea.yandex5.ru
|
|
||||||
logging:
|
|
||||||
driver: json-file
|
|
||||||
options:
|
|
||||||
max-size: "10m"
|
|
||||||
max-file: "5"
|
|
||||||
|
|
||||||
#homeconnect2mqtt:
|
|
||||||
# build:
|
|
||||||
# context: ./homeconnect2mqtt/docker
|
|
||||||
# dockerfile: Dockerfile
|
|
||||||
# restart: always
|
|
||||||
# container_name: homeconnect2mqtt
|
|
||||||
# ports:
|
|
||||||
# - "5001:5000"
|
|
||||||
# env_file: ./homeconnect2mqtt/settings/homeconnect2mqtt.env
|
|
||||||
# volumes:
|
|
||||||
# - ./homeconnect2mqtt/settings/config.json:/app/config.json:ro
|
|
||||||
# logging:
|
|
||||||
# driver: json-file
|
|
||||||
# options:
|
|
||||||
# max-size: "10m"
|
|
||||||
# max-file: "5"
|
|
||||||
|
|
||||||
matter-bridge:
|
|
||||||
image: ghcr.io/t0bst4r/home-assistant-matter-hub:latest
|
|
||||||
container_name: matter-bridge
|
|
||||||
ports:
|
|
||||||
- "8482:8482"
|
|
||||||
- "5353:5353"
|
|
||||||
- "5540:5540"
|
|
||||||
- "5541:5541"
|
|
||||||
volumes:
|
|
||||||
- ./matter-bridge/settings/data:/data
|
|
||||||
- ./secrets/hass-fnv/settings/secrets.yaml:/config/secrets.yaml:ro
|
|
||||||
env_file: ./matter-bridge/settings/matter-bridge.env
|
|
||||||
network_mode: host
|
|
||||||
logging:
|
|
||||||
driver: json-file
|
|
||||||
options:
|
|
||||||
max-size: "10m"
|
|
||||||
max-file: "5"
|
|
||||||
|
|
||||||
keenetic:
|
|
||||||
build:
|
|
||||||
context: ./keenetic/docker
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
container_name: keenetic
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- "8001:8000"
|
|
||||||
env_file: ./keenetic/settings/keenetic.env
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
new_script:
|
|
||||||
alias: Сценарий Я Ушёл
|
|
||||||
sequence:
|
|
||||||
- service: switch.turn_off
|
|
||||||
data: {}
|
|
||||||
target:
|
|
||||||
entity_id: switch.watercontrol_valves
|
|
||||||
- service: climate.set_preset_mode
|
|
||||||
data:
|
|
||||||
preset_mode: away
|
|
||||||
target:
|
|
||||||
area_id:
|
|
||||||
- gostinaia
|
|
||||||
- kukhnia
|
|
||||||
- spalnia
|
|
||||||
- detskaia
|
|
||||||
- service: media_player.turn_off
|
|
||||||
data: {}
|
|
||||||
target:
|
|
||||||
device_id:
|
|
||||||
- 368a8521d684580dd39b88502808b3bc
|
|
||||||
- 07a253973c89fdb7e7b66174bf5a1275
|
|
||||||
- b60210b82dc048bffc5a59f683de2f0a
|
|
||||||
- service: cover.close_cover
|
|
||||||
data: {}
|
|
||||||
target:
|
|
||||||
area_id:
|
|
||||||
- gostinaia
|
|
||||||
- kukhnia
|
|
||||||
- spalnia
|
|
||||||
mode: single
|
|
||||||
icon: mdi:shield-lock
|
|
||||||
'1675839976060':
|
|
||||||
alias: Вскипяти чайник
|
|
||||||
sequence:
|
|
||||||
- service: input_select.set_options
|
|
||||||
data:
|
|
||||||
options: Boil Water
|
|
||||||
target:
|
|
||||||
entity_id: select.chunmi_a1_35e9_cook_mode
|
|
||||||
mode: single
|
|
||||||
'1676313705945':
|
|
||||||
alias: Сценарий Я Дома
|
|
||||||
sequence:
|
|
||||||
- service: switch.turn_on
|
|
||||||
data: {}
|
|
||||||
target:
|
|
||||||
entity_id: switch.watercontrol_valves
|
|
||||||
- service: climate.set_preset_mode
|
|
||||||
data:
|
|
||||||
preset_mode: manual
|
|
||||||
target:
|
|
||||||
area_id:
|
|
||||||
- gostinaia
|
|
||||||
- kukhnia
|
|
||||||
- spalnia
|
|
||||||
- detskaia
|
|
||||||
- if:
|
|
||||||
- condition: sun
|
|
||||||
before: sunset
|
|
||||||
after: sunrise
|
|
||||||
then:
|
|
||||||
- service: cover.open_cover
|
|
||||||
data: {}
|
|
||||||
target:
|
|
||||||
area_id:
|
|
||||||
- gostinaia
|
|
||||||
- kukhnia
|
|
||||||
- spalnia
|
|
||||||
mode: single
|
|
||||||
icon: mdi:shield-lock
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
#################################
|
|
||||||
# HOME PRESENSE #
|
|
||||||
#################################
|
|
||||||
water_action:
|
|
||||||
module: water_action
|
|
||||||
class: WaterValveControl
|
|
||||||
water_valve: switch.watercontrol_valves
|
|
||||||
notify: telegram
|
|
||||||
constraint: input_boolean.water_control
|
|
||||||
water_devices:
|
|
||||||
- binary_sensor.washer_running # стиралка
|
|
||||||
- binary_sensor.bosch_dishwasher_running # посудомойка
|
|
||||||
global_dependencies:
|
|
||||||
- globals
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
import appdaemon.plugins.hass.hassapi as hass
|
|
||||||
import globals
|
|
||||||
|
|
||||||
#
|
|
||||||
# Water leak detector
|
|
||||||
# close water valve and send a report
|
|
||||||
#
|
|
||||||
# Args:
|
|
||||||
#
|
|
||||||
# sensor = water leak sensor group
|
|
||||||
# valve = water valve - app will close it, when leak is detected
|
|
||||||
# notify = notify name to send notification
|
|
||||||
#
|
|
||||||
# None
|
|
||||||
#
|
|
||||||
# Release Notes
|
|
||||||
#
|
|
||||||
# Version 1.0:
|
|
||||||
# Initial Version
|
|
||||||
|
|
||||||
class WaterLeak(hass.Hass):
|
|
||||||
ringtone = 2
|
|
||||||
gw_mac = ''
|
|
||||||
sensors = []
|
|
||||||
def initialize(self):
|
|
||||||
if 'sensor' not in self.args or 'valve' not in self.args or 'notify' not in self.args:
|
|
||||||
self.error("Please provide sensor, valve and notify in config!")
|
|
||||||
return
|
|
||||||
self.sensors = self.get_water_leak_sensors()
|
|
||||||
self.log("leak sensors: {}".format(self.sensors))
|
|
||||||
for sensor in self.sensors:
|
|
||||||
self.listen_state(self.water_leak, sensor)
|
|
||||||
|
|
||||||
def get_water_leak_sensors(self):
|
|
||||||
sensors = []
|
|
||||||
devices = self.get_state()
|
|
||||||
for sensor in devices:
|
|
||||||
if not sensor.startswith(self.args['sensor_prefix']):
|
|
||||||
continue
|
|
||||||
sensors.append(sensor)
|
|
||||||
return sensors
|
|
||||||
|
|
||||||
def water_leak(self, entity, attribute, old, new, kwargs):
|
|
||||||
if new == "on":
|
|
||||||
self.send_alarm(entity)
|
|
||||||
|
|
||||||
def send_alarm(self, entity_id):
|
|
||||||
self.log('water leak alarm triggered!')
|
|
||||||
if 'constraint' in self.args and not self.constrain_input_boolean(self.args['constraint']):
|
|
||||||
return
|
|
||||||
|
|
||||||
#close water valve
|
|
||||||
self.turn_off(self.args['valve'])
|
|
||||||
## to ensure that water is closed. It is important!!!
|
|
||||||
self.call_service("mqtt/publish", topic = 'home/watercontrol/bath/valve/set', payload = 'close')
|
|
||||||
|
|
||||||
message = "ВНИМАНИЕ! В ваше отстутствие сработал датчик протечки воды ({})!!! Выключаю подачу воды.".format(self.friendly_name(entity_id))
|
|
||||||
globals.send_telegram(self, message, target = self.args['notify'])
|
|
||||||
|
|
||||||
message = "Состояние датчиков протечки:\n"
|
|
||||||
for entity_id in self.sensors:
|
|
||||||
message += '{} ({}) = {}\n'.format(self.friendly_name(entity_id), entity_id, self.get_state(entity_id))
|
|
||||||
globals.send_telegram(self, message, target = self.args['notify'])
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
#################################################
|
|
||||||
# WATER #
|
|
||||||
#################################################
|
|
||||||
mqtt:
|
|
||||||
sensor:
|
|
||||||
- unique_id: counter_cold
|
|
||||||
state_topic: 'home/watercontrol/counter2_1'
|
|
||||||
name: 'counter_cold'
|
|
||||||
value_template: '{{ (value | float ) }}'
|
|
||||||
unit_of_measurement: 'm³'
|
|
||||||
device_class: water
|
|
||||||
state_class: total_increasing
|
|
||||||
- unique_id: counter_hot
|
|
||||||
state_topic: 'home/watercontrol/counter2_2'
|
|
||||||
name: 'counter_hot'
|
|
||||||
value_template: '{{ (value | float ) }}'
|
|
||||||
unit_of_measurement: 'm³'
|
|
||||||
device_class: water
|
|
||||||
state_class: total_increasing
|
|
||||||
- unique_id: uptime_watercontrol
|
|
||||||
state_topic: 'home/watercontrol/$uptime'
|
|
||||||
name: 'uptime_watercontrol'
|
|
||||||
value_template: '{{ value | int }}'
|
|
||||||
switch:
|
|
||||||
- unique_id: water_valve
|
|
||||||
name: water_valve
|
|
||||||
state_topic: "home/watercontrol/valves"
|
|
||||||
command_topic: "home/watercontrol/valves/set"
|
|
||||||
payload_on: true
|
|
||||||
payload_off: false
|
|
||||||
optimistic: false
|
|
||||||
retain: false
|
|
||||||
|
|
||||||
template:
|
|
||||||
sensor:
|
|
||||||
- name: counter_hot_outer
|
|
||||||
device_class: water
|
|
||||||
state_class: total_increasing
|
|
||||||
unit_of_measurement: 'm³'
|
|
||||||
state: "{{ states('sensor.counter_hot') | float + 88.97 | round(2) }}"
|
|
||||||
- name: counter_cold_outer
|
|
||||||
device_class: water
|
|
||||||
state_class: total_increasing
|
|
||||||
unit_of_measurement: 'm³'
|
|
||||||
state: "{{ states('sensor.counter_cold') | float + 144.7 | round(2) }}"
|
|
||||||
|
|
||||||
binary_sensor:
|
|
||||||
- name: dryer_running
|
|
||||||
state: "{{ not is_state('sensor.sushilnaia_mash_machine_state', 'stop') }}"
|
|
||||||
- name: washer_running
|
|
||||||
state: "{{ not is_state('sensor.stiralnaia_mashina_machine_state', 'stop') }}"
|
|
||||||
|
|
||||||
homeassistant:
|
|
||||||
customize:
|
|
||||||
switch.water_valve:
|
|
||||||
friendly_name: Подача воды
|
|
||||||
binary_sensor.dryer_running:
|
|
||||||
friendly_name: "Сушильная машина"
|
|
||||||
binary_sensor.washer_running:
|
|
||||||
friendly_name: "Стиральная машина"
|
|
||||||
|
|
||||||
sensor.counter_hot_outer:
|
|
||||||
friendly_name: "Счетчик Внешний Горячая"
|
|
||||||
sensor.counter_cold_outer:
|
|
||||||
friendly_name: "Счетчик Внешний Холодная"
|
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
{
|
{
|
||||||
"nodes": {}
|
"nodes": {},
|
||||||
|
"issues": {
|
||||||
|
"hidden": []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
homeassistant:
|
||||||
|
enabled: true
|
||||||
|
frontend:
|
||||||
|
enabled: true
|
||||||
|
port: 8080
|
||||||
|
mqtt:
|
||||||
|
base_topic: z2m-tcp
|
||||||
|
server: mqtt://192.168.1.6:1883
|
||||||
|
user: hass
|
||||||
|
password: mqttpass
|
||||||
|
serial:
|
||||||
|
adapter: zstack
|
||||||
|
port: tcp://192.168.1.132:6638
|
||||||
|
advanced:
|
||||||
|
log_level: info
|
||||||
|
channel: 11
|
||||||
|
pan_id: 6754
|
||||||
|
last_seen: ISO_8601_local
|
||||||
|
transmit_power: 20
|
||||||
|
log_rotation: true
|
||||||
|
log_directories_to_keep: 50
|
||||||
|
device_options: {}
|
||||||
|
devices:
|
||||||
|
'0x00124b001f315241':
|
||||||
|
friendly_name: light_3
|
||||||
|
'0x00124b001d3a3a12':
|
||||||
|
friendly_name: light_4
|
||||||
|
'0xa4c138b6bab321de':
|
||||||
|
friendly_name: washer
|
||||||
|
'0xa4c138bc0d05b7bf':
|
||||||
|
friendly_name: light_hall_long
|
||||||
|
'0xa4c13809d74d5e1f':
|
||||||
|
friendly_name: dishwasher
|
||||||
|
'0x00124b001f2f63d9':
|
||||||
|
friendly_name: light_2
|
||||||
|
'0x00124b001d644d03':
|
||||||
|
friendly_name: light_1
|
||||||
|
'0x54ef4410000d6708':
|
||||||
|
friendly_name: brightness_balcony
|
||||||
|
'0x54ef4410001ffbb1':
|
||||||
|
friendly_name: shower_light_sensor
|
||||||
|
'0x00158d00012d9ad7':
|
||||||
|
friendly_name: temperature_balcony
|
||||||
|
'0xa4c1388a7a97e475':
|
||||||
|
friendly_name: temperature_outside_bedroom
|
||||||
|
'0x00158d00089761d4':
|
||||||
|
friendly_name: temperature_bedroom
|
||||||
|
'0x54ef4410003138d4':
|
||||||
|
friendly_name: plug_sibur_notebook
|
||||||
|
'0x000b3cfffefadaaa':
|
||||||
|
friendly_name: light_bedroom_2
|
||||||
|
'0x086bd7fffe0042b0':
|
||||||
|
friendly_name: light_bedroom_7
|
||||||
|
'0x00158d00035356c6':
|
||||||
|
friendly_name: button_test
|
||||||
|
'0x00158d000392b34f':
|
||||||
|
friendly_name: door_shower
|
||||||
|
'0x54ef441000673f2a':
|
||||||
|
friendly_name: radiator_kitchen
|
||||||
|
'0x54ef4410006fe989':
|
||||||
|
friendly_name: radiator_bedroom
|
||||||
|
'0x54ef4410006fe51e':
|
||||||
|
friendly_name: radiator_bedroom_balcony
|
||||||
|
'0x00158d000913e2e7':
|
||||||
|
friendly_name: temperature_mainroom
|
||||||
|
'0xa4c1385ae827ec1c':
|
||||||
|
friendly_name: light_bath
|
||||||
|
'0x00158d00053f1e20':
|
||||||
|
friendly_name: curtain_mainroom
|
||||||
|
'0x00158d000398d9e7':
|
||||||
|
friendly_name: motion_bath
|
||||||
|
'0x00158d0003941e67':
|
||||||
|
friendly_name: door_bath
|
||||||
|
'0x00158d00079836a0':
|
||||||
|
friendly_name: curtain_kitchen
|
||||||
|
'0x000d6f0012060764':
|
||||||
|
friendly_name: smoke_detector_kitchen
|
||||||
|
'0x9035eafffe541813':
|
||||||
|
friendly_name: shower_mirror
|
||||||
|
'0x00158d0003969a5d':
|
||||||
|
friendly_name: door_locker_hall_left
|
||||||
|
'0x54ef4410001ff4ec':
|
||||||
|
friendly_name: brightness_hall
|
||||||
|
'0x00158d000398fa90':
|
||||||
|
friendly_name: motion_shower_center
|
||||||
|
'0x00158d00053f38ef':
|
||||||
|
friendly_name: curtain_bedroom
|
||||||
|
'0x00158d0002b0c46a':
|
||||||
|
friendly_name: curtain_bedroom_balcony
|
||||||
|
'0x00158d000398f9a1':
|
||||||
|
friendly_name: motion_hall_1
|
||||||
|
'0x00158d000396c997':
|
||||||
|
friendly_name: door_kitchen
|
||||||
|
'0x00158d000396981f':
|
||||||
|
friendly_name: door_cloakroom
|
||||||
|
'0x00158d0003a10728':
|
||||||
|
friendly_name: door_childroom
|
||||||
|
'0x00158d000396c880':
|
||||||
|
friendly_name: door_bedroom
|
||||||
|
'0x00158d0003f1b8ee':
|
||||||
|
friendly_name: motion_childroom
|
||||||
|
'0x00158d00091c43c7':
|
||||||
|
friendly_name: temperature_bath
|
||||||
|
'0x00158d0008e1b6bc':
|
||||||
|
friendly_name: water_leak_bath_bath
|
||||||
|
'0x00158d00072a2198':
|
||||||
|
friendly_name: water_leak_bath_sink
|
||||||
|
'0x00158d0008e019d0':
|
||||||
|
friendly_name: water_leak_kitchen_fridge
|
||||||
|
'0x9035eafffe891816':
|
||||||
|
friendly_name: light_bath_mirror
|
||||||
|
'0x000d6f00131ca650':
|
||||||
|
friendly_name: smoke_detector_hall
|
||||||
|
'0x54ef4410002c8a61':
|
||||||
|
friendly_name: plug_server
|
||||||
|
'0x00158d0003a3a0f8':
|
||||||
|
friendly_name: button_shower_sink
|
||||||
|
'0x00158d0006ca2293':
|
||||||
|
friendly_name: water_leak_radiator_hub
|
||||||
|
'0xe0798dfffebca4cc':
|
||||||
|
friendly_name: switch_mirror_bath_heater
|
||||||
|
'0x54ef4410001952e3':
|
||||||
|
friendly_name: plug_yolka
|
||||||
|
'0x00158d000398de0b':
|
||||||
|
friendly_name: motion_hall_2
|
||||||
|
'0x00158d00044bac2c':
|
||||||
|
friendly_name: motion_bath_2
|
||||||
|
'0xe0798dfffec0fba2':
|
||||||
|
friendly_name: switch_mirror_shower_heater
|
||||||
|
'0xa4c138e184df9c4b':
|
||||||
|
friendly_name: light_mainroom_mainlamp
|
||||||
|
'0xa4c1382016d75a72':
|
||||||
|
friendly_name: light_mainroom_spotlights
|
||||||
|
'0xa4c13858fba9206a':
|
||||||
|
friendly_name: light_mainroom_pc_spotlight
|
||||||
|
'0xa4c138f30d13e363':
|
||||||
|
friendly_name: light_balcony_spotlight
|
||||||
|
'0xa4c13872c98a123d':
|
||||||
|
friendly_name: light_bedroom_nightlight_left
|
||||||
|
'0xa4c138ec247343ac':
|
||||||
|
friendly_name: light_bedroom_spotlights
|
||||||
|
'0xa4c138ebc36bcef7':
|
||||||
|
friendly_name: light_bedroom_pc_spotlight
|
||||||
|
'0xa4c138665085e34c':
|
||||||
|
friendly_name: light_bedroom_mainlight
|
||||||
|
'0xa4c138c4615096df':
|
||||||
|
friendly_name: light_bedroom_pc_nightlight
|
||||||
|
'0xa4c1381b0d3a90a8':
|
||||||
|
friendly_name: light_bedroom_nightlight_right
|
||||||
|
'0x54ef44100031341f':
|
||||||
|
friendly_name: light_curtain_kitchen
|
||||||
|
'0xa4c138e15f46046e':
|
||||||
|
friendly_name: light_kitchen_table_lamp
|
||||||
|
'0xa4c138881ad5f2a7':
|
||||||
|
friendly_name: light_kitchen_bar_lamp
|
||||||
|
'0xa4c1389d59a26238':
|
||||||
|
friendly_name: light_kitchen_set
|
||||||
|
'0xa4c1385ff0c3da73':
|
||||||
|
friendly_name: light_kitchen_spotlights
|
||||||
|
'0xa4c138227e28f0f5':
|
||||||
|
friendly_name: light_childroom_main
|
||||||
|
'0xa4c138af504f84c7':
|
||||||
|
friendly_name: light_childroom_bed
|
||||||
|
'0xa4c13835b22ecd89':
|
||||||
|
friendly_name: light_childroom_spotlights
|
||||||
|
'0xa4c138a0cfb8e117':
|
||||||
|
friendly_name: ventilation_bath
|
||||||
|
'0xa4c1382b628aacb6':
|
||||||
|
friendly_name: light_bath_shelf
|
||||||
|
'0xa4c1385548a794f5':
|
||||||
|
friendly_name: ventilation_shower
|
||||||
|
'0xa4c138b789359794':
|
||||||
|
friendly_name: light_shower_main_spotlights
|
||||||
|
'0xa4c1385a75956001':
|
||||||
|
friendly_name: light_shower_shower_spotlights
|
||||||
|
'0xa4c138dbf32f44f8':
|
||||||
|
friendly_name: light_shower_shelf_light
|
||||||
|
'0x00158d000398f9f6':
|
||||||
|
friendly_name: motion_shower_shower
|
||||||
|
'0x00158d0003a106ea':
|
||||||
|
friendly_name: door_locker2_hall_right
|
||||||
|
'0x00158d0003536fac':
|
||||||
|
friendly_name: door_locker2_hall_left
|
||||||
|
'0x00158d0003a10585':
|
||||||
|
friendly_name: door_locker3_hall_left
|
||||||
|
'0xa4c138f709069275':
|
||||||
|
friendly_name: curtain_bedroom_bobbinet_balcony
|
||||||
|
'0x00158d00090e5cf7':
|
||||||
|
friendly_name: temperature_childroom
|
||||||
|
'0x00158d000396c7ab':
|
||||||
|
friendly_name: door_locker_hall_right
|
||||||
|
'0x00158d000396c934':
|
||||||
|
friendly_name: door_locker3_hall_right
|
||||||
|
'0x00158d0002113158':
|
||||||
|
friendly_name: water_leak_water_hub
|
||||||
|
'0xa4c1381710c7c92a':
|
||||||
|
friendly_name: curtain_mainroom_bobbinet
|
||||||
|
'0x00158d0003cc41a2':
|
||||||
|
friendly_name: motion_kitchenset_right
|
||||||
|
'0xa4c1381257c87bdc':
|
||||||
|
friendly_name: curtain_kitchen_bobbinet
|
||||||
|
'0x00124b0022145c82':
|
||||||
|
friendly_name: co2_bedroom
|
||||||
|
'0x00158d00039d4fea':
|
||||||
|
friendly_name: door_entrance
|
||||||
|
'0x54ef4410006ff081':
|
||||||
|
friendly_name: radiator_mainroom
|
||||||
|
'0x54ef441000703fc3':
|
||||||
|
friendly_name: radiator_childroom
|
||||||
|
'0x00158d0008e038bc':
|
||||||
|
friendly_name: water_leak_shower_sink
|
||||||
|
'0x00158d00091c7eac':
|
||||||
|
friendly_name: temperature_winecooler
|
||||||
|
'0x00158d0003a5d05d':
|
||||||
|
friendly_name: lock_door
|
||||||
|
homeassistant: {}
|
||||||
|
optimistic: true
|
||||||
|
retain: true
|
||||||
|
'0x00158d0000f6b2fa':
|
||||||
|
friendly_name: motion_kitchenset_left
|
||||||
|
'0x00158d0003d16f77':
|
||||||
|
friendly_name: motion_kitchen_table
|
||||||
|
'0x00158d000396780e':
|
||||||
|
friendly_name: door_bedroom_big_window
|
||||||
|
'0x00158d00039a3055':
|
||||||
|
friendly_name: door_balcony
|
||||||
|
'0x00158d000396c8e0':
|
||||||
|
friendly_name: door_mainroom_slider
|
||||||
|
'0x00158d0001d594fa':
|
||||||
|
friendly_name: water_leak_bath_ceiling
|
||||||
|
'0x00158d00090e6601':
|
||||||
|
friendly_name: temperature_shower
|
||||||
|
'0x00158d0006ec83b3':
|
||||||
|
friendly_name: temperature_kitchen
|
||||||
|
groups: {}
|
||||||
|
availability:
|
||||||
|
enabled: true
|
||||||
|
active:
|
||||||
|
timeout: 1440
|
||||||
|
passive:
|
||||||
|
timeout: 1440
|
||||||
|
version: 4
|
||||||