Привет Habr! Продолжаем серию статей о LVGL в ESPHome. В третьей части статьи речь пойдет о создании своего пользовательского виджета, который может быть подключен к проекту. И не только к данному проекту, а вообще даст небольшое представление как делать виджеты в ESPHome. Итак, Создавать будем виджет умной розетки с индикацией мощности, напряжения и силы тока. Поехали…
Структура виджета
Создадим в нашем проекте в папке widgets папку socket, а в ней файл socket.yaml.
В нашем примере виджет будет состоять из 4 основных блоков:
substitutions: # Замены, попросту говоря, статичные переменные, константы sensor: # Числовые датчики от Home Assistant text_sensor: # Текстовые датчики от Home Assistant lvgl: # Визуальный интерфейс
Substitutions — замены/подстановки/константы
Назначение: Делают виджет переиспользуемым с разными параметрами.
Так как в коде мы много где указываем одни и теже данные, проще использовать одну константу и указывать её далее везде в коде.
Для начала нам нужно название сущности из Home Assistant. В моем случает это switch.rozetka_test_socket, а также 3 сенсора с мощностью, напряжением и силой тока. В моем случае это:
sensor.rozetka_test_power # Мощность sensor.rozetka_test_voltage # Напряжение sensor.rozetka_test_current # Сила тока
Добавляем переменную socket_entity чтобы дальше использовать её вместо switch.rozetka_test_socket и три наших сенсора
substitutions: socket_entity: "switch.rozetka_test_socket" socket_power: "sensor.rozetka_test_power" # Мощность socket_voltage: "sensor.rozetka_test_voltage" # Напряжение socket_current: "sensor.rozetka_test_current" # Сила тока
Также нам потребуются 4 иконки из набора MDI и наш блок substitutions уже будет выглядеть так:
substitutions: socket_entity: "switch.rozetka_test_socket" socket_power: "sensor.rozetka_test_power" # Мощность socket_voltage: "sensor.rozetka_test_voltage" # Напряжение socket_current: "sensor.rozetka_test_current" # Сила тока socket_icon: "\U000F1107" socket_current_icon: "\U000F1480" socket_voltage_icon: "\U000F095B" socket_power_icon: "\U000F0241"
Чтобы иконки отображались их надо добавить в шрифты fonts.yaml
- file: "fonts/materialdesignicons-webfont.ttf" id: mdi_icons_40 size: 40 bpp: 4 glyphs: [ "\U000F1107", # socket "\U000F1480", # current "\U000F095B", # voltage "\U000F0241", # power "\U000F068A", # shield home "\U000F1828", # shield moon "\U000F099D", # shield lock "\U000F06BB", # shield plane "\U000F099E", # shield off "\U000F0498", # shield ] - file: "fonts/materialdesignicons-webfont.ttf" id: mdi_icons_160 size: 160 bpp: 4 glyphs: [ "\U000F1107", # socket ]
Text Sensors — текстовые датчики
Назначение: Получают текстовые данные от Home Assistant.
Для получения информации с текстовых датчиков Home Assistant нам потребуется создать текстовые датчики text_sensor.
Нам нужно получить:
-
состояние объекта
-
название объекта
-
единицы измерения мощности, напряжения и силы тока
Состояние объекта
text_sensor: # Состояние розетки - platform: homeassistant # Указываем платформу Home Assistant id: socket_sensor_state # Придумываем уникальный индификатор для связи в коде entity_id: "${socket_entity}" # Указываем константу нашей сущности из substitutions
Название объекта
# Имя розетки - platform: homeassistant id: socket_sensor_name entity_id: "${socket_entity}" attribute: friendly_name # Указываем атрибут сущности
Единицы измерения мощности, напряжения и силы тока
# Единицы измерения мощности - platform: homeassistant id: socket_sensor_power_uom entity_id: "${socket_power}" attribute: unit_of_measurement # Единицы измерения напряжения - platform: homeassistant id: socket_sensor_voltage_uom entity_id: "${socket_voltage}" attribute: unit_of_measurement # Единицы измерения силы тока - platform: homeassistant id: socket_sensor_current_uom entity_id: "${socket_current}" attribute: unit_of_measurement
Итак, у нас теперь получается вот такая секция text_sensor (но мы к ней ещё вернемся):
text_sensor: # Состояние розетки - platform: homeassistant id: socket_sensor_state entity_id: "${socket_entity}" # Имя розетки - platform: homeassistant id: socket_sensor_name entity_id: "${socket_entity}" attribute: friendly_name # Единицы измерения мощности - platform: homeassistant id: socket_sensor_power_uom entity_id: "${socket_power}" attribute: unit_of_measurement # Единицы измерения напряжения - platform: homeassistant id: socket_sensor_voltage_uom entity_id: "${socket_voltage}" attribute: unit_of_measurement # Единицы измерения силы тока - platform: homeassistant id: socket_sensor_current_uom entity_id: "${socket_current}" attribute: unit_of_measurement
Sensors — числовые датчики
Назначение: Получают числовые данные от Home Assistant.
Для получения информации с числовых датчиков Home Assistant нам потребуется создать числовые датчики sensor
Нам нужно получить значения с датчиков мощности, напряжения и силы тока:
sensor: # Значение мощности - platform: homeassistant id: socket_sensor_power entity_id: "${socket_power}" # Значение напряжения - platform: homeassistant id: socket_sensor_voltage entity_id: "${socket_voltage}" # Значение силы тока - platform: homeassistant id: socket_sensor_current entity_id: "${socket_current}"
К ним мы также позже вернемся чтобы определить действия при получении значений с датчиков.
LVGL интерфейс
Назначение: Создает визуальный интерфейс виджета.
Структура страницы:
Чтобы соответствовать дизайну нашей прошивки, мы создадим страницу в которой будет 7 блоков:
lvgl: pages: - id: socket_page # Уникальный индификатор страницы bg_color: color_slate_blue_gray # Цвет фона widgets: # Список виджетов # Объект с состоянием - obj: id: socket_state x: 20 y: 20 width: 440 height: 60 align: top_left pad_all: 0 bg_color: color_steel_blue bg_opa: 20% border_opa: transp border_width: 0 shadow_opa: transp radius: 10 # Объект с кнопкой включения/выключения розетки - obj: id: socket_icon_bg x: 20 y: 100 width: 210 height: 280 align: top_left pad_all: 0 bg_color: color_steel_blue bg_opa: 20% border_opa: transp border_width: 0 shadow_opa: transp radius: 10 # Объект с идикатором мощности - obj: id: socket_power_bg x: 250 y: 100 width: 210 height: 80 align: top_left pad_all: 0 bg_color: color_steel_blue bg_opa: 20% border_opa: transp border_width: 0 shadow_opa: transp radius: 10 # Объект с идикатором напряжения - obj: id: socket_voltage_bg x: 250 y: 200 width: 210 height: 80 align: top_left pad_all: 0 bg_color: color_steel_blue bg_opa: 20% border_opa: transp border_width: 0 shadow_opa: transp radius: 10 # Объект с идикатором силы тока - obj: id: socket_current_bg x: 250 y: 300 width: 210 height: 80 align: top_left pad_all: 0 bg_color: color_steel_blue bg_opa: 20% border_opa: transp border_width: 0 shadow_opa: transp radius: 10 # Кнопка выхода - obj: id: socket_back_bg x: 20 y: 400 width: 60 height: 60 align: top_left pad_all: 0 bg_color: color_steel_blue bg_opa: 20% border_opa: transp border_width: 0 shadow_opa: transp radius: 10 # Имя - obj: id: socket_name_bg x: 100 y: 400 width: 360 height: 60 align: top_left pad_all: 0 bg_color: color_steel_blue bg_opa: 20% border_opa: transp border_width: 0 shadow_opa: transp radius: 10
Все объекты имеют одинаковую структуру, но разные размеры и координаты, например:
- obj: id: socket_state # Уникальный идентификатор виджета x: 20 # Координата X y: 20 # Координата Y width: 440 # Ширина виджета в пикселях height: 60 # Высота виджета в пикселях align: top_left # Выравнивание (вверху слева) pad_all: 0 # Убираем все отступы bg_color: color_steel_blue # Цвет фона bg_opa: 20% # Прозрачность фона border_opa: transp # Прозрачность обводки (полная) border_width: 0 # Толщина обводки shadow_opa: transp # Прозрачность тени (полная) radius: 10 # Скругляем края
Теперь нам надо наполнить наши блоки контентом
Блок состояния
Добавляем в наш блок текст, который будет отображать состояние розетки (включена или выключена):
# Объект с состоянием - obj: id: socket_state x: 20 y: 20 width: 440 height: 60 align: top_left pad_all: 0 bg_color: color_steel_blue bg_opa: 20% border_opa: transp border_width: 0 shadow_opa: transp radius: 10 widgets: - label: # Виджет текст id: socket_state_label # Уникальный идентификатор align: center # Выравнивание относительно нашего блока, а не страницы text_font: nunito_18 # Шрифт (размер) text_color: color_misty_blue # Цвет шрифта text: " " # Тест (оставляем пустым, передадим через действие)
Теперь возвращаемя к сенсору, который отвечает за состояние. Добавляем ему действие (что надо сделать при получении значения в сенсор):
text_sensor: # Состояние розетки - platform: homeassistant id: socket_sensor_state entity_id: "${socket_entity}" on_value: - lvgl.label.update: id: socket_state_label text: !lambda return x; - if: condition: lambda: 'return x == "on";' then: - lvgl.label.update: id: socket_icon_label text_color: color_yellow else: - lvgl.label.update: id: socket_icon_label text_color: color_misty_blue
Добавляем on_value (при получении значения), указываем сделать два действия:
-
Обновить виджет с id
socket_state_label. Передать ему вtextзначениеx(сырое значение сенсора) вместо пустого, что мы установили -
Обновить виджет с id
socket_icon_label. Передать ему цвет в зависимости от состояния. Иными словами, тут условие, если сенсор состояния получает значениеon, то значок становится желтым цветом, в противном случае цвет будетcolor_misty_blue
Блок-кнопка с индикацией
Добавляем в наш блок текст (значок), c индикацией состояние розетки (включена или выключена):
# Объект с кнопкой включения/выключения розетки - obj: id: socket_icon_bg x: 20 y: 100 width: 210 height: 280 align: top_left pad_all: 0 bg_color: color_steel_blue bg_opa: 20% border_opa: transp border_width: 0 shadow_opa: transp radius: 10 widgets: - label: id: socket_icon_label align: center text_font: mdi_icons_160 text_color: color_misty_blue text: "${socket_icon}"
Делаем из блока кнопку, вызывая службу home assistant switch.toggle
# Объект с кнопкой включения/выключения розетки - obj: id: socket_icon_bg x: 20 y: 100 width: 210 height: 280 align: top_left pad_all: 0 bg_color: color_steel_blue bg_opa: 20% border_opa: transp border_width: 0 shadow_opa: transp radius: 10 widgets: - label: id: socket_icon_label align: center text_font: mdi_icons_160 text_color: color_misty_blue text: "${socket_icon}" on_click: # Действие по клику - homeassistant.action: # Вызываем службу Home Assistant action: switch.toggle # Название службы (переключение выключателя) data: entity_id: "${socket_entity}". # Наша сущность
В предыдущем разделе мы уже добавили действие этому виджету со сменой цвета значка.
Блоки с индикацией мощности, напряжения и силы тока
Добавляем в наш блок мощности 3 текста:
-
значок
-
значение
-
единицы измерения
# Объект с идикатором мощности - obj: id: socket_power_bg x: 250 y: 100 width: 210 height: 80 align: top_left pad_all: 0 bg_color: color_steel_blue bg_opa: 20% border_opa: transp border_width: 0 shadow_opa: transp radius: 10 widgets: - label: id: socket_power_icon_label x: 10 # Делаем небольшой отступ слева align: left_mid text_font: mdi_icons_40 # Иконочный шрифт text_color: color_misty_blue text: "${socket_power_icon}" # Иконка из substitutions - label: id: socket_power_state_label x: 70 # Делаем отступ от значка align: left_mid text_font: nunito_18 text_color: color_misty_blue text: " " # Пустое поле, передадим действием - label: id: socket_power_state_uom_label x: 140 # Делаем отступ от значка align: left_mid text_font: nunito_18 text_color: color_misty_blue text: " " # Пустое поле, передадим действием
Возвращаемся к нашим сенсорам мощности и добавляем им действия:
text_sensor: # Единицы измерения мощности - platform: homeassistant id: socket_sensor_power_uom entity_id: "${socket_power}" attribute: unit_of_measurement on_value: - lvgl.label.update: id: socket_power_state_uom_label text: !lambda return x; sensor: # Значение мощности - platform: homeassistant id: socket_sensor_power entity_id: "${socket_power}" on_value: - lvgl.label.update: id: socket_power_state_label text: !lambda |- if (isnan(x)) return "N/A"; char buf[16]; snprintf(buf, sizeof(buf), "%.1f", x); return buf;
И если с первым сенсором все понятно, то со вторым могут возникнуть вопросы, поясню что здесь происходит:
if (isnan(x)) return "N/A"; char buf[16]; snprintf(buf, sizeof(buf), "%.1f", x); return buf;
-
Проверка на нечисловое значение:
if (isnan(x)) return "N/A";-
isnan(x)— Проверяет, является ли значениеxнечисловым (NaN) -
return "N/A"— Возвращает «N/A» если значение невалидное
-
-
Создание буфера:
char buf[16];-
Создаёт символьный буфер на 16 байт
-
Достаточно для хранения чисел формата
-123456.789
-
-
Форматированный вывод:
snprintf(buf, sizeof(buf), "%.1f", x);Параметр
Описание
bufБуфер для записи результата
sizeof(buf)Максимальный размер данных (16 байт)
"%.1f"Шаблон форматирования (1 знак после точки)
xВходное значение сенсора
-
Возврат результата:
// Возвращает отформатированную строку return buf;
Для разных сенсоров используйте:
// Для мощности и напряжения (1 знак) snprintf(buf, sizeof(buf), "%.1f", x); // Для силы тока (3 знака) snprintf(buf, sizeof(buf), "%.3f", x);
Примеры преобразования
|
Входное значение |
Формат |
Результат |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
— |
|
С напряжением и силой тока все аналогично
Блок с кнопкой возвращения в меню
Добавляем текст с иконкой и действие при нажатии:
# Кнопка выхода - obj: id: socket_back_bg x: 20 y: 400 width: 60 height: 60 align: top_left pad_all: 0 bg_color: color_steel_blue bg_opa: 20% border_opa: transp border_width: 0 shadow_opa: transp radius: 10 widgets: - label: id: socket_back_label align: center text_font: icons_28 text_color: color_misty_blue text: "${exit_icon}" on_press: - lvgl.page.show: devices_page # Показываем страницу Devices вместо текущей - lvgl.widget.show: menu_controls_main # Показываем кнопки меню
Блок с названием сущности
Добавляем текст:
# Имя - obj: id: socket_name_bg x: 100 y: 400 width: 360 height: 60 align: top_left pad_all: 0 bg_color: color_steel_blue bg_opa: 20% border_opa: transp border_width: 0 shadow_opa: transp radius: 10 widgets: - label: id: socket_name_label align: center text_font: nunito_18 text_color: color_misty_blue text: "friendly name"
Возвращаемся к сенсору имени и добавляем действие:
# Имя розетки - platform: homeassistant id: socket_sensor_name entity_id: "${socket_entity}" attribute: friendly_name on_value: - lvgl.label.update: id: socket_name_label text: !lambda return x;
Итоговый код нашего виджета
substitutions: socket_entity: "switch.rozetka_test_socket" socket_power: "sensor.rozetka_test_power" # Мощность socket_voltage: "sensor.rozetka_test_voltage" # Напряжение socket_current: "sensor.rozetka_test_current" # Сила тока socket_icon: "\U000F1107" socket_current_icon: "\U000F1480" socket_voltage_icon: "\U000F095B" socket_power_icon: "\U000F0241" text_sensor: # Состояние розетки - platform: homeassistant id: socket_sensor_state entity_id: "${socket_entity}" on_value: - lvgl.label.update: id: socket_state_label text: !lambda return x; - if: condition: lambda: 'return x == "on";' then: - lvgl.label.update: id: socket_icon_label text_color: color_yellow else: - lvgl.label.update: id: socket_icon_label text_color: color_misty_blue # Имя розетки - platform: homeassistant id: socket_sensor_name entity_id: "${socket_entity}" attribute: friendly_name on_value: - lvgl.label.update: id: socket_name_label text: !lambda return x; # Единицы измерения мощности - platform: homeassistant id: socket_sensor_power_uom entity_id: "${socket_power}" attribute: unit_of_measurement on_value: - lvgl.label.update: id: socket_power_state_uom_label text: !lambda return x; # Единицы измерения напряжения - platform: homeassistant id: socket_sensor_voltage_uom entity_id: "${socket_voltage}" attribute: unit_of_measurement on_value: - lvgl.label.update: id: socket_voltage_state_uom_label text: !lambda return x; # Единицы измерения силы тока - platform: homeassistant id: socket_sensor_current_uom entity_id: "${socket_current}" attribute: unit_of_measurement on_value: - lvgl.label.update: id: socket_current_state_uom_label text: !lambda return x; sensor: # Значение мощности - platform: homeassistant id: socket_sensor_power entity_id: "${socket_power}" on_value: - lvgl.label.update: id: socket_power_state_label text: !lambda |- if (isnan(x)) return "N/A"; char buf[16]; snprintf(buf, sizeof(buf), "%.1f", x); return buf; # Значение напряжения - platform: homeassistant id: socket_sensor_voltage entity_id: "${socket_voltage}" on_value: - lvgl.label.update: id: socket_voltage_state_label text: !lambda |- if (isnan(x)) return "N/A"; char buf[16]; snprintf(buf, sizeof(buf), "%.1f", x); return buf; # Значение силы тока - platform: homeassistant id: socket_sensor_current entity_id: "${socket_current}" on_value: - lvgl.label.update: id: socket_current_state_label text: !lambda |- if (isnan(x)) return "N/A"; char buf[16]; snprintf(buf, sizeof(buf), "%.3f", x); return buf; lvgl: pages: - id: socket_page bg_color: color_slate_blue_gray widgets: # Объект с состоянием - obj: id: socket_state x: 20 y: 20 width: 440 height: 60 align: top_left pad_all: 0 bg_color: color_steel_blue bg_opa: 20% border_opa: transp border_width: 0 shadow_opa: transp radius: 10 widgets: - label: id: socket_state_label align: center text_font: nunito_18 text_color: color_misty_blue text: " " # Объект с кнопкой включения/выключения розетки - obj: id: socket_icon_bg x: 20 y: 100 width: 210 height: 280 align: top_left pad_all: 0 bg_color: color_steel_blue bg_opa: 20% border_opa: transp border_width: 0 shadow_opa: transp radius: 10 widgets: - label: id: socket_icon_label align: center text_font: mdi_icons_160 text_color: color_misty_blue text: "${socket_icon}" on_click: - homeassistant.action: action: switch.toggle data: entity_id: "${socket_entity}" # Объект с идикатором мощности - obj: id: socket_power_bg x: 250 y: 100 width: 210 height: 80 align: top_left pad_all: 0 bg_color: color_steel_blue bg_opa: 20% border_opa: transp border_width: 0 shadow_opa: transp radius: 10 widgets: - label: id: socket_power_icon_label x: 10 align: left_mid text_font: mdi_icons_40 text_color: color_misty_blue text: "${socket_power_icon}" - label: id: socket_power_state_label x: 70 align: left_mid text_font: nunito_18 text_color: color_misty_blue text: " " - label: id: socket_power_state_uom_label x: 140 align: left_mid text_font: nunito_18 text_color: color_misty_blue text: " " # Объект с идикатором напряжения - obj: id: socket_voltage_bg x: 250 y: 200 width: 210 height: 80 align: top_left pad_all: 0 bg_color: color_steel_blue bg_opa: 20% border_opa: transp border_width: 0 shadow_opa: transp radius: 10 widgets: - label: id: socket_voltage_icon_label x: 10 align: left_mid text_font: mdi_icons_40 text_color: color_misty_blue text: "${socket_voltage_icon}" - label: id: socket_voltage_state_label x: 70 align: left_mid text_font: nunito_18 text_color: color_misty_blue text: " " - label: id: socket_voltage_state_uom_label x: 140 align: left_mid text_font: nunito_18 text_color: color_misty_blue text: " " # Объект с идикатором силы тока - obj: id: socket_current_bg x: 250 y: 300 width: 210 height: 80 align: top_left pad_all: 0 bg_color: color_steel_blue bg_opa: 20% border_opa: transp border_width: 0 shadow_opa: transp radius: 10 widgets: - label: id: socket_current_icon_label x: 10 align: left_mid text_font: mdi_icons_40 text_color: color_misty_blue text: "${socket_current_icon}" - label: id: socket_current_state_label x: 70 align: left_mid text_font: nunito_18 text_color: color_misty_blue text: " " - label: id: socket_current_state_uom_label x: 140 align: left_mid text_font: nunito_18 text_color: color_misty_blue text: " " # Кнопка выхода - obj: id: socket_back_bg x: 20 y: 400 width: 60 height: 60 align: top_left pad_all: 0 bg_color: color_steel_blue bg_opa: 20% border_opa: transp border_width: 0 shadow_opa: transp radius: 10 widgets: - label: id: socket_back_label align: center text_font: icons_28 text_color: color_misty_blue text: "${exit_icon}" on_press: - lvgl.page.show: devices_page - lvgl.widget.show: menu_controls_main # Имя - obj: id: socket_name_bg x: 100 y: 400 width: 360 height: 60 align: top_left pad_all: 0 bg_color: color_steel_blue bg_opa: 20% border_opa: transp border_width: 0 shadow_opa: transp radius: 10 widgets: - label: id: socket_name_label align: center text_font: nunito_18 text_color: color_misty_blue text: "friendly name"
Кнопка для отображения виджета
Итак, мы создали виджет, но как же его интегрировать в существующую прошивку?
Для этого нам надо подключить наш виджет в devices.yaml и добавить кнопку перехода.
Подключаем виджет
packages: media_player: !include media_player/media_player.yaml vacuum: !include vacuum/vacuum_widget.yaml shutter: !include shutter/shutter_config.yaml thermostat: !include thermostat/thermostat_widget.yaml air_conditioner: !include air_conditioner/air_conditioner_widget.yaml alarm_panel: !include alarm_panel/alarm_panel.yaml socket: !include socket/socket_widget.yaml
Подключаем кнопку
- obj: y: 260 width: 440 height: 60 pad_all: 0 align: TOP_MID bg_opa: TRANSP shadow_opa: TRANSP border_opa: TRANSP border_width: 0 radius: 10 widgets: - button: id: socket_page_btn x: 35 align: LEFT_MID width: 370 height: 60 radius: 10 bg_color: color_slate_blue_gray shadow_opa: TRANSP widgets: - label: align: CENTER text_color: color_steel_blue text_font: mdi_icons_40 text: "${socket_icon}" on_press: - lvgl.widget.hide: menu_controls_main - lvgl.page.show: id: socket_page animation: OUT_RIGHT time: 300ms
! ВАЖНО Обратите внимание на количество отступов
Заключение
Данный пример демонстрирует лишь малую часть возможностей LVGL в ESPHome и может служить основой для создания более сложных и функциональных пользовательских интерфейсов.
ссылка на оригинал статьи https://habr.com/ru/articles/935528/
Добавить комментарий