Привет 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 и может служить основой для создания более сложных и функциональных пользовательских интерфейсов.