4 Commits

3 changed files with 702 additions and 187 deletions
+561 -169
View File
@@ -15,7 +15,7 @@ esphome:
priority: -10 priority: -10
then: then:
- delay: 10s - delay: 10s
- display.page.show: power # temporary for power page dev - display.page.show: environment # temporary for power page dev
- component.update: epaper - component.update: epaper
esp32: esp32:
@@ -78,7 +78,7 @@ binary_sensor:
then: then:
- logger.log: - logger.log:
level: INFO level: INFO
format: 'Button doubleclick, rendering next page!' format: "Button doubleclick, rendering next page!"
- display.page.show_next: epaper - display.page.show_next: epaper
- component.update: epaper - component.update: epaper
- timing: - timing:
@@ -87,7 +87,7 @@ binary_sensor:
then: then:
- logger.log: - logger.log:
level: INFO level: INFO
format: 'Button click, refreshing screen!' format: "Button click, refreshing screen!"
- component.update: epaper - component.update: epaper
# HA pushbutton configuration # HA pushbutton configuration
@@ -99,7 +99,7 @@ button:
on_press: on_press:
- logger.log: - logger.log:
level: INFO level: INFO
format: 'HA Button click, refreshing screen!' format: "HA Button click, refreshing screen!"
- component.update: epaper - component.update: epaper
- platform: template - platform: template
name: Next screen name: Next screen
@@ -108,7 +108,7 @@ button:
on_press: on_press:
- logger.log: - logger.log:
level: INFO level: INFO
format: 'HA Button click, rendering next page!' format: "HA Button click, rendering next page!"
- display.page.show_next: epaper - display.page.show_next: epaper
- component.update: epaper - component.update: epaper
@@ -119,10 +119,6 @@ sensor:
id: wifisignal id: wifisignal
update_interval: 60s update_interval: 60s
- platform: homeassistant
entity_id: sensor.livingroom_wohnzimmer_feinstaub_pm_2_5_m
id: pm25_livingroom
- platform: homeassistant - platform: homeassistant
entity_id: sensor.wohnzimmer_temperatur entity_id: sensor.wohnzimmer_temperatur
id: temp_livingroom id: temp_livingroom
@@ -136,8 +132,8 @@ sensor:
id: co2_livingroom id: co2_livingroom
- platform: homeassistant - platform: homeassistant
entity_id: sensor.alpstuga_air_quality_monitor_pm25_2 entity_id: sensor.livingroom_wohnzimmer_feinstaub_pm_2_5_m
id: pm25_sleepingroom id: pm25_livingroom
- platform: homeassistant - platform: homeassistant
entity_id: sensor.alpstuga_air_quality_monitor_temperatur_2 entity_id: sensor.alpstuga_air_quality_monitor_temperatur_2
@@ -151,6 +147,26 @@ sensor:
entity_id: sensor.alpstuga_air_quality_monitor_kohlendioxid_2 entity_id: sensor.alpstuga_air_quality_monitor_kohlendioxid_2
id: co2_sleepingroom id: co2_sleepingroom
- platform: homeassistant
entity_id: sensor.alpstuga_air_quality_monitor_pm25_2
id: pm25_sleepingroom
- platform: homeassistant
entity_id: sensor.alpstuga_air_quality_monitor_temperatur
id: temp_childrensroom
- platform: homeassistant
entity_id: sensor.alpstuga_air_quality_monitor_luftfeuchtigkeit
id: humid_childrensroom
- platform: homeassistant
entity_id: sensor.alpstuga_air_quality_monitor_kohlendioxid
id: co2_childrensroom
- platform: homeassistant
entity_id: sensor.alpstuga_air_quality_monitor_pm25
id: pm25_childrensroom
- platform: homeassistant - platform: homeassistant
entity_id: sensor.aussentemperatur entity_id: sensor.aussentemperatur
id: temp_outdoor id: temp_outdoor
@@ -186,11 +202,11 @@ sensor:
- platform: homeassistant - platform: homeassistant
entity_id: sensor.schlafzimmer_bett_power entity_id: sensor.schlafzimmer_bett_power
id: bedlight_sleepingroom id: bedlight_sleepingroom
- platform: homeassistant - platform: homeassistant
entity_id: sensor.schlafzimmer_heimkino_power entity_id: sensor.schlafzimmer_heimkino_power
id: media_sleepingroom id: media_sleepingroom
- platform: homeassistant - platform: homeassistant
entity_id: sensor.radioaktive_strahlungsleistung entity_id: sensor.radioaktive_strahlungsleistung
id: radiation id: radiation
@@ -202,7 +218,7 @@ text_sensor:
entity_id: sensor.sun_rising_template entity_id: sensor.sun_rising_template
id: sun_rising id: sun_rising
internal: true internal: true
- platform: homeassistant - platform: homeassistant
name: "Sun Setting ESP" name: "Sun Setting ESP"
#entity_id: sensor.sun_next_setting #entity_id: sensor.sun_next_setting
@@ -245,144 +261,476 @@ time:
- platform: homeassistant - platform: homeassistant
id: homeassistant_time id: homeassistant_time
on_time: on_time:
# Every 10 seconds # Every 10 seconds
- seconds: /10 - seconds: /10
then: then:
- logger.log: - logger.log:
level: INFO level: INFO
format: 'Power button press' format: "Power button press"
- output.turn_on: power_key - output.turn_on: power_key
- delay: 0.5s - delay: 0.5s
- output.turn_off: power_key - output.turn_off: power_key
# Every 10 minutes # Every 10 minutes
- seconds: 0 - seconds: 0
minutes: /10 minutes: /10
then: then:
- logger.log: - logger.log:
level: INFO level: INFO
format: 'Timer based screen refresh' format: "Timer based screen refresh"
- component.update: epaper - component.update: epaper
# Font definitions # Font definitions
font: font:
- file: 'fonts/Roboto-Medium.ttf' - file: "fonts/Roboto-Medium.ttf"
id: roboto_med_30 id: roboto_med_30
size: 30 size: 30
- file: 'fonts/Roboto-Regular.ttf' - file: "fonts/Roboto-Regular.ttf"
id: footer_font id: footer_font
size: 15 size: 15
glyphs: glyphs:
['&', '@', '!', ',', '.', '"', '%', '(', ')', '+', '-', '_', ':', '°', [
'1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', "&",
'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', "@",
'T', 'U', 'V', 'W', 'X', 'Y', 'Z', ' ', 'a', 'b', 'c', 'd', 'e', 'f', "!",
'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', ",",
'u', 'v', 'w', 'x', 'y', 'z', 'Ä', 'ä', 'ö', '/', 'º', 'µ', '³', 'ß', ".",
'Ü', 'ü', 'Ö', '0'] '"',
"%",
"(",
")",
"+",
"-",
"_",
":",
"°",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"A",
"B",
"C",
"D",
"E",
"F",
"G",
"H",
"I",
"J",
"K",
"L",
"M",
"N",
"O",
"P",
"Q",
"R",
"S",
"T",
"U",
"V",
"W",
"X",
"Y",
"Z",
" ",
"a",
"b",
"c",
"d",
"e",
"f",
"g",
"h",
"i",
"j",
"k",
"l",
"m",
"n",
"o",
"p",
"q",
"r",
"s",
"t",
"u",
"v",
"w",
"x",
"y",
"z",
"Ä",
"ä",
"ö",
"/",
"º",
"µ",
"³",
"ß",
"Ü",
"ü",
"Ö",
"0",
]
- file: 'fonts/Roboto-Regular.ttf' - file: "fonts/Roboto-Regular.ttf"
id: sensor_unit id: sensor_unit
size: 20 size: 20
glyphs: glyphs:
['&', '@', '!', ',', '.', '"', '%', '(', ')', '+', '-', '_', ':', '°', [
'1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', "&",
'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', "@",
'T', 'U', 'V', 'W', 'X', 'Y', 'Z', ' ', 'a', 'b', 'c', 'd', 'e', 'f', "!",
'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', ",",
'u', 'v', 'w', 'x', 'y', 'z', 'Ä', 'ä', 'ö', '/', 'º', 'µ', '³', 'ß', ".",
'Ü', 'ü', 'Ö', '0'] '"',
"%",
"(",
")",
"+",
"-",
"_",
":",
"°",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"A",
"B",
"C",
"D",
"E",
"F",
"G",
"H",
"I",
"J",
"K",
"L",
"M",
"N",
"O",
"P",
"Q",
"R",
"S",
"T",
"U",
"V",
"W",
"X",
"Y",
"Z",
" ",
"a",
"b",
"c",
"d",
"e",
"f",
"g",
"h",
"i",
"j",
"k",
"l",
"m",
"n",
"o",
"p",
"q",
"r",
"s",
"t",
"u",
"v",
"w",
"x",
"y",
"z",
"Ä",
"ä",
"ö",
"/",
"º",
"µ",
"³",
"ß",
"Ü",
"ü",
"Ö",
"0",
]
- file: 'fonts/Roboto-Medium.ttf' - file: "fonts/Roboto-Medium.ttf"
id: sub_sensor_font id: sub_sensor_font
size: 30 size: 30
glyphs: glyphs:
['&', '@', '!', ',', '.', '"', '%', '(', ')', '+', '-', '_', ':', '°', [
'1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', "&",
'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', "@",
'T', 'U', 'V', 'W', 'X', 'Y', 'Z', ' ', 'a', 'b', 'c', 'd', 'e', 'f', "!",
'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', ",",
'u', 'v', 'w', 'x', 'y', 'z', 'Ä', 'ä', 'ö', '/', 'º', 'µ', '³', 'ß', ".",
'Ü', 'ü', 'Ö', '0'] '"',
"%",
"(",
")",
"+",
"-",
"_",
":",
"°",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"A",
"B",
"C",
"D",
"E",
"F",
"G",
"H",
"I",
"J",
"K",
"L",
"M",
"N",
"O",
"P",
"Q",
"R",
"S",
"T",
"U",
"V",
"W",
"X",
"Y",
"Z",
" ",
"a",
"b",
"c",
"d",
"e",
"f",
"g",
"h",
"i",
"j",
"k",
"l",
"m",
"n",
"o",
"p",
"q",
"r",
"s",
"t",
"u",
"v",
"w",
"x",
"y",
"z",
"Ä",
"ä",
"ö",
"/",
"º",
"µ",
"³",
"ß",
"Ü",
"ü",
"Ö",
"0",
]
- file: 'fonts/Roboto-Bold.ttf' - file: "fonts/Roboto-Bold.ttf"
id: big_sensor_font id: big_sensor_font
size: 50 size: 50
glyphs: glyphs:
['&', '@', '!', ',', '.', '"', '%', '(', ')', '+', '-', '_', ':', '°', [
'1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', "&",
'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', "@",
'T', 'U', 'V', 'W', 'X', 'Y', 'Z', ' ', 'a', 'b', 'c', 'd', 'e', 'f', "!",
'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', ",",
'u', 'v', 'w', 'x', 'y', 'z', 'Ä', 'ä', 'ö', '/', 'º', 'µ', '³', 'ß', ".",
'Ü', 'ü', 'Ö', '0'] '"',
"%",
"(",
")",
"+",
"-",
"_",
":",
"°",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"A",
"B",
"C",
"D",
"E",
"F",
"G",
"H",
"I",
"J",
"K",
"L",
"M",
"N",
"O",
"P",
"Q",
"R",
"S",
"T",
"U",
"V",
"W",
"X",
"Y",
"Z",
" ",
"a",
"b",
"c",
"d",
"e",
"f",
"g",
"h",
"i",
"j",
"k",
"l",
"m",
"n",
"o",
"p",
"q",
"r",
"s",
"t",
"u",
"v",
"w",
"x",
"y",
"z",
"Ä",
"ä",
"ö",
"/",
"º",
"µ",
"³",
"ß",
"Ü",
"ü",
"Ö",
"0",
]
- file: 'fonts/materialdesignicons-webfont.ttf' - file: "fonts/materialdesignicons-webfont.ttf"
id: mdi_small id: mdi_small
size: 31 size: 31
glyphs: [ glyphs: [
'󰔏', # mdi-thermometer "󰔏", # mdi-thermometer
'󰖎', # mdi-water-percent "󰖎", # mdi-water-percent
'󰮬', # mdi-molecule (pm2.5) "󰮬", # mdi-molecule (pm2.5)
'󰟤', # mdi-co2 "󰟤", # mdi-co2
'󰊚', # mdi-gauge (air pressure) "󰊚", # mdi-gauge (air pressure)
'󰖙', # mdi-weather-sunny "󰖙", # mdi-weather-sunny
'󰖛', # mdi-weather-sunset-down "󰖛", # mdi-weather-sunset-down
'󰖜', # mdi-weather-sunset-up "󰖜", # mdi-weather-sunset-up
'󱐋', # mdi-lightning-bolt "󱐋", # mdi-lightning-bolt
'󰴾', # mdi-transmission-tower "󰴾", # mdi-transmission-tower
'󰚥', # mdi-power-plug "󰚥", # mdi-power-plug
'󱈹', # mdi-desk "󱈹", # mdi-desk
'󰔂', # mdi-television "󰔂", # mdi-television
'󰐼' # mdi-radiation "󰐼", # mdi-radiation
] ]
- file: 'fonts/materialdesignicons-webfont.ttf' - file: "fonts/materialdesignicons-webfont.ttf"
id: mdi_weather id: mdi_weather
size: 80 size: 80
glyphs: [ glyphs: [
'󰽡', # mdi-moon-first-quarter "󰽡", # mdi-moon-first-quarter
'󰽢', # mdi-moon-full "󰽢", # mdi-moon-full
'󰽣', # mdi-moon-last-quarter "󰽣", # mdi-moon-last-quarter
'󰽤', # mdi-moon-new "󰽤", # mdi-moon-new
'󰽥', # mdi-moon-waning-crescent "󰽥", # mdi-moon-waning-crescent
'󰽦', # mdi-moon-waning-gibbous "󰽦", # mdi-moon-waning-gibbous
'󰽧', # mdi-moon-waxing-crescent "󰽧", # mdi-moon-waxing-crescent
'󰽨', # mdi-moon-waxing-gibbous "󰽨", # mdi-moon-waxing-gibbous
'󰖔', # mdi-weather-night "󰖔", # mdi-weather-night
'󰖐', # mdi-weather-cloudy "󰖐", # mdi-weather-cloudy
'󰖑', # mdi-weather-fog "󰖑", # mdi-weather-fog
'󰖒', # mdi-weather-hail "󰖒", # mdi-weather-hail
'󰖓', # mdi-weather-lightning "󰖓", # mdi-weather-lightning
'󰙾', # mdi-weather-lightning-rainy "󰙾", # mdi-weather-lightning-rainy
'󰖕', # mdi-weather-partly-cloudy "󰖕", # mdi-weather-partly-cloudy
'󰖖', # mdi-weather-pouring "󰖖", # mdi-weather-pouring
'󰖗', # mdi-weather-rainy "󰖗", # mdi-weather-rainy
'󰖘', # mdi-weather-snowy "󰖘", # mdi-weather-snowy
'󰙿', # mdi-weather-snowy-rainy "󰙿", # mdi-weather-snowy-rainy
'󰖙', # mdi-weather-sunny "󰖙", # mdi-weather-sunny
'󰖝', # mdi-weather-windy "󰖝", # mdi-weather-windy
'󰖞', # mdi-weather-windy-variant "󰖞", # mdi-weather-windy-variant
'󰳥' # mdi-weather-exceptional "󰳥", # mdi-weather-exceptional
] ]
- file: 'fonts/materialdesignicons-webfont.ttf' - file: "fonts/materialdesignicons-webfont.ttf"
id: mdi_wifi id: mdi_wifi
size: 20 size: 20
glyphs: [ glyphs: [
# WiFi # WiFi
'󰤫', # mdi-wifi-strength-alert-outline "󰤫", # mdi-wifi-strength-alert-outline
'󰤟', # mdi-wifi-strength-1 "󰤟", # mdi-wifi-strength-1
'󰤢', # mdi-wifi-strength-2 "󰤢", # mdi-wifi-strength-2
'󰤥', # mdi-wifi-strength-3 "󰤥", # mdi-wifi-strength-3
'󰤨' # mdi-wifi-strength-4 "󰤨", # mdi-wifi-strength-4
] ]
- file: 'fonts/materialdesignicons-webfont.ttf' - file: "fonts/materialdesignicons-webfont.ttf"
id: mdi_med id: mdi_med
size: 60 size: 60
glyphs: [ glyphs: [
'󰀦' # mdi-alert "󰀦", # mdi-alert
] ]
image: image:
@@ -409,20 +757,29 @@ display:
pages: pages:
- id: environment - id: environment
lambda: |- lambda: |-
/* Outdoor */
float outdoor_temp = id(temp_outdoor).state; float outdoor_temp = id(temp_outdoor).state;
int outdoor_humid = int(id(humid_outdoor).state); int outdoor_humid = int(id(humid_outdoor).state);
float air_pressure = id(airpressure).state; float air_pressure = id(airpressure).state;
float outdoor_radiation = id(radiation).state; float outdoor_radiation = id(radiation).state;
/* Living room */
float livingroom_temp = id(temp_livingroom).state; float livingroom_temp = id(temp_livingroom).state;
int livingroom_humid = int(id(humid_livingroom).state); int livingroom_humid = int(id(humid_livingroom).state);
int livingroom_pm25 = int(id(pm25_livingroom).state);
float livingroom_co2 = id(co2_livingroom).state; float livingroom_co2 = id(co2_livingroom).state;
int livingroom_pm25 = int(id(pm25_livingroom).state);
/* Serverroom */
float serverroom_temp = id(temp_serverroom).state;
/* Sleeping room */
float sleepingroom_temp = id(temp_sleepingroom).state; float sleepingroom_temp = id(temp_sleepingroom).state;
int sleepingroom_humid = int(id(humid_sleepingroom).state); int sleepingroom_humid = int(id(humid_sleepingroom).state);
int sleepingroom_pm25 = int(id(pm25_sleepingroom).state);
float sleepingroom_co2 = id(co2_sleepingroom).state; float sleepingroom_co2 = id(co2_sleepingroom).state;
float serverroom_temp = id(temp_serverroom).state; int sleepingroom_pm25 = int(id(pm25_sleepingroom).state);
/* Children's room */
float childrensroom_temp = id(temp_childrensroom).state;
int childrensroom_humid = int(id(humid_childrensroom).state);
float childrensroom_co2 = id(co2_childrensroom).state;
int childrensroom_pm25 = int(id(pm25_childrensroom).state);
int offsetX = 0; int offsetX = 0;
int offsetY = 0; int offsetY = 0;
@@ -474,90 +831,89 @@ display:
it.print(225, 130, id(sensor_unit), TextAlign::BASELINE_LEFT, "hPa"); it.print(225, 130, id(sensor_unit), TextAlign::BASELINE_LEFT, "hPa");
} }
/* Livingroom */ /* Livingroom */
offsetY = 127; offsetY = 127;
it.print(10, 10 + offsetY, id(sensor_unit), "Wohnzimmer"); it.print(10, 10 + offsetY, id(sensor_unit), "Wohnzimmer");
it.filled_rectangle(136, 21 + offsetY, 247, 3); it.filled_rectangle(136, 21 + offsetY, 169, 3);
it.print(10, 125 + offsetY, id(mdi_small), TextAlign::BASELINE_LEFT, "󰮬"); // PM2.5 it.print(10, 125 + offsetY, id(mdi_small), TextAlign::BASELINE_LEFT, "󰮬"); // PM2.5
it.print(375, 75 + offsetY, id(mdi_small), TextAlign::BASELINE_CENTER, "󰔏"); // thermometer it.print(305, 75 + offsetY, id(mdi_small), TextAlign::BASELINE_CENTER, "󰔏"); // thermometer
it.print(375, 125 + offsetY, id(mdi_small), TextAlign::BASELINE_CENTER, "󰖎"); // water percent it.print(305, 125 + offsetY, id(mdi_small), TextAlign::BASELINE_CENTER, "󰖎"); // water percent
it.print(10, 75 + offsetY, id(mdi_small), TextAlign::BASELINE_LEFT, "󰟤"); // CO2 it.print(10, 75 + offsetY, id(mdi_small), TextAlign::BASELINE_LEFT, "󰟤"); // CO2
if(livingroom_temp > -100 && livingroom_temp < 100) { if(livingroom_temp > -100 && livingroom_temp < 100) {
it.printf(357, 75 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, "%2.1fºC", livingroom_temp); it.printf(287, 75 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, "%2.1fºC", livingroom_temp);
} }
else { else {
it.print(357, 75 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, " - ºC"); it.print(287, 75 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, " - ºC");
} }
if(livingroom_humid >=0 && livingroom_humid <= 100) { if(livingroom_humid >=0 && livingroom_humid <= 100) {
it.printf(357, 125 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, "%3d%%", livingroom_humid); it.printf(287, 125 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, "%3d%%", livingroom_humid);
} }
else { else {
it.print(357, 125 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, " - %"); it.print(287, 125 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, " - %");
} }
if(livingroom_co2 >=380 && livingroom_co2 <= 20000) { if(livingroom_co2 >=380 && livingroom_co2 <= 20000) {
it.printf(170, 75 + offsetY, id(big_sensor_font), TextAlign::BASELINE_RIGHT, "%4.0f", livingroom_co2); it.printf(150, 75 + offsetY, id(big_sensor_font), TextAlign::BASELINE_RIGHT, "%4.0f", livingroom_co2);
it.print(175, 50 + offsetY, id(sensor_unit), TextAlign::BASELINE_LEFT, "ppm"); it.print(155, 50 + offsetY, id(sensor_unit), TextAlign::BASELINE_LEFT, "ppm");
} }
else { else {
it.print(170, 75 + offsetY, id(big_sensor_font), TextAlign::BASELINE_RIGHT, "- "); it.print(150, 75 + offsetY, id(big_sensor_font), TextAlign::BASELINE_RIGHT, "- ");
it.print(175, 50 + offsetY, id(sensor_unit), TextAlign::BASELINE_LEFT, "ppm³"); it.print(155, 50 + offsetY, id(sensor_unit), TextAlign::BASELINE_LEFT, "ppm³");
} }
if(livingroom_pm25 < 255 && livingroom_pm25 >=0) { if(livingroom_pm25 < 255 && livingroom_pm25 >=0) {
it.printf(220, 125 + offsetY, id(big_sensor_font), TextAlign::BASELINE_RIGHT, "%3d", livingroom_pm25); it.printf(150, 125 + offsetY, id(big_sensor_font), TextAlign::BASELINE_RIGHT, "%3d", livingroom_pm25);
it.print(225, 125 + offsetY, id(sensor_unit), TextAlign::BASELINE_LEFT, "µg/m³"); it.print(155, 125 + offsetY, id(sensor_unit), TextAlign::BASELINE_LEFT, "µg/m³");
} }
else { else {
it.print(220, 125 + offsetY, id(big_sensor_font), TextAlign::BASELINE_RIGHT, "- "); it.print(150, 125 + offsetY, id(big_sensor_font), TextAlign::BASELINE_RIGHT, "- ");
it.print(225, 125 + offsetY, id(sensor_unit), TextAlign::BASELINE_LEFT, "µg/m³"); it.print(155, 125 + offsetY, id(sensor_unit), TextAlign::BASELINE_LEFT, "µg/m³");
} }
/* Sleepingroom */ /* Sleepingroom */
offsetY = 247; offsetY = 247;
it.print(10, 10 + offsetY, id(sensor_unit), "Schlafzimmer"); it.print(10, 10 + offsetY, id(sensor_unit), "Schlafzimmer");
it.filled_rectangle(144, 21 + offsetY, 239, 3); it.filled_rectangle(144, 21 + offsetY, 161, 3);
it.print(10, 124 + offsetY, id(mdi_small), TextAlign::BASELINE_LEFT, "󰮬"); // PM2.5 it.print(10, 125 + offsetY, id(mdi_small), TextAlign::BASELINE_LEFT, "󰮬"); // PM2.5
it.print(375, 75 + offsetY, id(mdi_small), TextAlign::BASELINE_CENTER, "󰔏"); // thermometer it.print(305, 75 + offsetY, id(mdi_small), TextAlign::BASELINE_CENTER, "󰔏"); // thermometer
it.print(375, 124 + offsetY, id(mdi_small), TextAlign::BASELINE_CENTER, "󰖎"); // water percent it.print(305, 125 + offsetY, id(mdi_small), TextAlign::BASELINE_CENTER, "󰖎"); // water percent
it.print(10, 75 + offsetY, id(mdi_small), TextAlign::BASELINE_LEFT, "󰟤"); // CO2 it.print(10, 75 + offsetY, id(mdi_small), TextAlign::BASELINE_LEFT, "󰟤"); // CO2
if(sleepingroom_temp > -100 && sleepingroom_temp < 100) { if(sleepingroom_temp > -100 && sleepingroom_temp < 100) {
it.printf(357, 75 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, "%2.1fºC", sleepingroom_temp); it.printf(287, 75 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, "%2.1fºC", sleepingroom_temp);
} }
else { else {
it.print(357, 75 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, " - ºC"); it.print(287, 75 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, " - ºC");
} }
if(sleepingroom_humid >=0 && sleepingroom_humid <= 100) { if(sleepingroom_humid >=0 && sleepingroom_humid <= 100) {
it.printf(357, 125 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, "%3d%%", sleepingroom_humid); it.printf(287, 125 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, "%3d%%", sleepingroom_humid);
} }
else { else {
it.print(357, 125 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, " - %"); it.print(287, 125 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, " - %");
} }
if(sleepingroom_co2 >=380 && sleepingroom_co2 <= 20000) { if(sleepingroom_co2 >=380 && sleepingroom_co2 <= 20000) {
it.printf(170, 75 + offsetY, id(big_sensor_font), TextAlign::BASELINE_RIGHT, "%4.0f", sleepingroom_co2); it.printf(150, 75 + offsetY, id(big_sensor_font), TextAlign::BASELINE_RIGHT, "%4.0f", sleepingroom_co2);
it.print(175, 50 + offsetY, id(sensor_unit), TextAlign::BASELINE_LEFT, "ppm"); it.print(155, 50 + offsetY, id(sensor_unit), TextAlign::BASELINE_LEFT, "ppm");
} }
else { else {
it.print(170, 75 + offsetY, id(big_sensor_font), TextAlign::BASELINE_RIGHT, "- "); it.print(150, 75 + offsetY, id(big_sensor_font), TextAlign::BASELINE_RIGHT, "- ");
it.print(175, 50 + offsetY, id(sensor_unit), TextAlign::BASELINE_LEFT, "ppm³"); it.print(155, 50 + offsetY, id(sensor_unit), TextAlign::BASELINE_LEFT, "ppm³");
} }
if(sleepingroom_pm25 < 255 && sleepingroom_pm25 >=0) { if(sleepingroom_pm25 < 255 && sleepingroom_pm25 >=0) {
it.printf(220, 125 + offsetY, id(big_sensor_font), TextAlign::BASELINE_RIGHT, "%3d", sleepingroom_pm25); it.printf(150, 125 + offsetY, id(big_sensor_font), TextAlign::BASELINE_RIGHT, "%3d", sleepingroom_pm25);
it.print(225, 125 + offsetY, id(sensor_unit), TextAlign::BASELINE_LEFT, "µg/m³"); it.print(155, 125 + offsetY, id(sensor_unit), TextAlign::BASELINE_LEFT, "µg/m³");
} }
else { else {
it.print(220, 125 + offsetY, id(big_sensor_font), TextAlign::BASELINE_RIGHT, "- "); it.print(150, 125 + offsetY, id(big_sensor_font), TextAlign::BASELINE_RIGHT, "- ");
it.print(225, 125 + offsetY, id(sensor_unit), TextAlign::BASELINE_LEFT, "µg/m³"); it.print(155, 125 + offsetY, id(sensor_unit), TextAlign::BASELINE_LEFT, "µg/m³");
} }
/* Weather */ /* Weather */
@@ -568,7 +924,7 @@ display:
/* Current weather */ /* Current weather */
if(id(weather).has_state()) { if(id(weather).has_state()) {
int x = 227 + offsetX; int x = 227 + offsetX;
int y = 116; int y = 96;
if (id(weather).state == "clear-night") { if (id(weather).state == "clear-night") {
// clear night // clear night
it.print(x, y, id(mdi_weather), TextAlign::BASELINE_RIGHT, "󰖔"); it.print(x, y, id(mdi_weather), TextAlign::BASELINE_RIGHT, "󰖔");
@@ -639,7 +995,7 @@ display:
/* Moon phase display */ /* Moon phase display */
if(id(moonphase).has_state()) { if(id(moonphase).has_state()) {
int x = 10 + offsetX; int x = 10 + offsetX;
int y = 116; int y = 96;
if (id(moonphase).state == "new_moon") { if (id(moonphase).state == "new_moon") {
// new moon // new moon
it.print(x, y, id(mdi_weather), TextAlign::BASELINE_LEFT, "󰽤"); it.print(x, y, id(mdi_weather), TextAlign::BASELINE_LEFT, "󰽤");
@@ -679,30 +1035,66 @@ display:
} }
} }
/* Warnings */ it.print(10 + offsetX, 130, id(mdi_small), TextAlign::BASELINE_LEFT, "󰖜"); // sun-up
if(id(szco2).state == "rot" || id(szpm).state == "rot" || id(wzco2).state == "rot" || id(wzpm).state == "rot") { it.printf(50 + offsetX, 130, id(sensor_unit), TextAlign::BASELINE_LEFT, "%s", id(sun_rising).state.c_str());
it.print(10 + offsetX, 200, id(mdi_med), TextAlign::BASELINE_LEFT, "󰀦"); it.print(135 + offsetX, 130, id(mdi_small), TextAlign::BASELINE_LEFT, "󰖛"); // sun-down
it.print(80 + offsetX, 200, id(big_sensor_font), TextAlign::BASELINE_LEFT, "Lüften!"); it.printf(175 + offsetX, 130, id(sensor_unit), TextAlign::BASELINE_LEFT, "%s", id(sun_setting).state.c_str());
/* Children's room */
offsetX = 320;
offsetY = 127;
it.print(10 + offsetX, 10 + offsetY, id(sensor_unit), "Kinderzimmer");
it.filled_rectangle(148 + offsetX, 21 + offsetY, 157, 3);
it.print(10 + offsetX, 125 + offsetY, id(mdi_small), TextAlign::BASELINE_LEFT, "󰮬"); // PM2.5
it.print(305 + offsetX, 75 + offsetY, id(mdi_small), TextAlign::BASELINE_CENTER, "󰔏"); // thermometer
it.print(305 + offsetX, 125 + offsetY, id(mdi_small), TextAlign::BASELINE_CENTER, "󰖎"); // water percent
it.print(10 + offsetX, 75 + offsetY, id(mdi_small), TextAlign::BASELINE_LEFT, "󰟤"); // CO2
if(childrensroom_temp > -100 && childrensroom_temp < 100) {
it.printf(287 + offsetX, 75 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, "%2.1fºC", childrensroom_temp);
}
else {
it.print(287 + offsetX, 75 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, " - ºC");
} }
it.print(10 + offsetX, 250, id(mdi_small), TextAlign::BASELINE_LEFT, "󰖜"); // sun-up if(childrensroom_humid >=0 && childrensroom_humid <= 100) {
it.printf(50 + offsetX, 250, id(sensor_unit), TextAlign::BASELINE_LEFT, "%s", id(sun_rising).state.c_str()); it.printf(287 + offsetX, 125 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, "%3d%%", childrensroom_humid);
it.print(135 + offsetX, 250, id(mdi_small), TextAlign::BASELINE_LEFT, "󰖛"); // sun-down }
it.printf(175 + offsetX, 250, id(sensor_unit), TextAlign::BASELINE_LEFT, "%s", id(sun_setting).state.c_str()); else {
it.print(287 + offsetX, 125 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, " - %");
}
if(childrensroom_co2 >=380 && childrensroom_co2 <= 20000) {
it.printf(150 + offsetX, 75 + offsetY, id(big_sensor_font), TextAlign::BASELINE_RIGHT, "%4.0f", childrensroom_co2);
it.print(155 + offsetX, 50 + offsetY, id(sensor_unit), TextAlign::BASELINE_LEFT, "ppm");
}
else {
it.print(150 + offsetX, 75 + offsetY, id(big_sensor_font), TextAlign::BASELINE_RIGHT, "- ");
it.print(155 + offsetX, 50 + offsetY, id(sensor_unit), TextAlign::BASELINE_LEFT, "ppm³");
}
if(childrensroom_pm25 < 255 && childrensroom_pm25 >=0) {
it.printf(150 + offsetX, 125 + offsetY, id(big_sensor_font), TextAlign::BASELINE_RIGHT, "%3d", childrensroom_pm25);
it.print(155 + offsetX, 125 + offsetY, id(sensor_unit), TextAlign::BASELINE_LEFT, "µg/m³");
}
else {
it.print(150 + offsetX, 125 + offsetY, id(big_sensor_font), TextAlign::BASELINE_RIGHT, "- ");
it.print(155 + offsetX, 125 + offsetY, id(sensor_unit), TextAlign::BASELINE_LEFT, "µg/m³");
}
/* Serverroom */ /* Serverroom */
offsetY = 247; offsetY = 247;
it.print(10 + offsetX, 10 + offsetY, id(sensor_unit), "Serverraum"); it.print(10 + offsetX, 10 + offsetY, id(sensor_unit), "Serverraum");
it.filled_rectangle(125 + offsetX, 21 + offsetY, 110, 3); it.filled_rectangle(125 + offsetX, 21 + offsetY, 180, 3);
it.print(227 + offsetX, 110 + offsetY, id(mdi_small), TextAlign::BASELINE_CENTER, "󰔏"); // thermometer it.print(305 + offsetX, 110 + offsetY, id(mdi_small), TextAlign::BASELINE_CENTER, "󰔏"); // thermometer
if(serverroom_temp > -100 && serverroom_temp < 100) { if(serverroom_temp > -100 && serverroom_temp < 100) {
it.printf(207 + offsetX, 110 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, "ºC"); it.printf(287 + offsetX, 110 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, "ºC");
it.printf(170 + offsetX, 110 + offsetY, id(big_sensor_font), TextAlign::BASELINE_RIGHT, "%2.1f", serverroom_temp); it.printf(250 + offsetX, 110 + offsetY, id(big_sensor_font), TextAlign::BASELINE_RIGHT, "%2.1f", serverroom_temp);
} }
else { else {
it.print(207 + offsetX, 110 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, " - ºC"); it.print(287 + offsetX, 110 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, " - ºC");
} }
/* FOOTER */ /* FOOTER */
@@ -760,7 +1152,7 @@ display:
offsetY = 127; offsetY = 127;
it.print(10, 10 + offsetY, id(sensor_unit), "Wohnzimmer"); it.print(10, 10 + offsetY, id(sensor_unit), "Wohnzimmer");
it.filled_rectangle(136, 21 + offsetY, 247, 3); it.filled_rectangle(136, 21 + offsetY, 247, 3);
it.print(375, 75 + offsetY, id(mdi_small), TextAlign::BASELINE_CENTER, "󰚥"); // power-plug it.print(375, 75 + offsetY, id(mdi_small), TextAlign::BASELINE_CENTER, "󰚥"); // power-plug
it.print(375, 115 + offsetY, id(mdi_small), TextAlign::BASELINE_CENTER, "󰚥"); // power-plug it.print(375, 115 + offsetY, id(mdi_small), TextAlign::BASELINE_CENTER, "󰚥"); // power-plug
@@ -831,7 +1223,7 @@ display:
/* FOOTER */ /* FOOTER */
it.strftime(614, 380, id(footer_font), TextAlign::BASELINE_RIGHT , "Aktualisiert um %d.%m.%Y %H:%M", id(homeassistant_time).now()); it.strftime(614, 380, id(footer_font), TextAlign::BASELINE_RIGHT , "Aktualisiert um %d.%m.%Y %H:%M", id(homeassistant_time).now());
/* WiFi Signal Strenght */ /* WiFi Signal strength */
if(id(wifisignal).has_state()) { if(id(wifisignal).has_state()) {
int x = 630; int x = 630;
int y = 384; int y = 384;
+80 -17
View File
@@ -25,12 +25,16 @@ namespace owon_b35t {
using esphome::Color; using esphome::Color;
using esphome::display::Display; using esphome::display::Display;
// Log tags used by ESPHome for BLE meter parsing and Core2 power-management messages.
static const char *const TAG = "owon_b35t"; static const char *const TAG = "owon_b35t";
static const char *const POWER_TAG = "core2_power"; static const char *const POWER_TAG = "core2_power";
// The M5Stack Core2 routes LCD/backlight power through an AXP192 PMIC on I2C.
static constexpr uint8_t AXP192_ADDR = 0x34; static constexpr uint8_t AXP192_ADDR = 0x34;
static esphome::i2c::I2CDevice axp192; static esphome::i2c::I2CDevice axp192;
static bool axp192_ready = false; static bool axp192_ready = false; // Guard PMIC accesses until the I2C bus/address has been configured.
// Write one byte to an AXP192 register and log failures instead of silently losing power changes.
static bool axp_write(uint8_t reg, uint8_t value) { static bool axp_write(uint8_t reg, uint8_t value) {
if (!axp192_ready) return false; if (!axp192_ready) return false;
bool ok = axp192.write_byte(reg, value); bool ok = axp192.write_byte(reg, value);
@@ -38,6 +42,7 @@ static bool axp_write(uint8_t reg, uint8_t value) {
return ok; return ok;
} }
// Read one byte from an AXP192 register; callers use this before read/modify/write updates.
static bool axp_read(uint8_t reg, uint8_t *value) { static bool axp_read(uint8_t reg, uint8_t *value) {
if (!axp192_ready) return false; if (!axp192_ready) return false;
bool ok = axp192.read_byte(reg, value); bool ok = axp192.read_byte(reg, value);
@@ -45,6 +50,7 @@ static bool axp_read(uint8_t reg, uint8_t *value) {
return ok; return ok;
} }
// Clear and set selected bits in an AXP192 register while preserving all other bits.
static void axp_update(uint8_t reg, uint8_t clear_mask, uint8_t set_mask) { static void axp_update(uint8_t reg, uint8_t clear_mask, uint8_t set_mask) {
uint8_t value = 0; uint8_t value = 0;
if (!axp_read(reg, &value)) return; if (!axp_read(reg, &value)) return;
@@ -52,24 +58,28 @@ static void axp_update(uint8_t reg, uint8_t clear_mask, uint8_t set_mask) {
axp_write(reg, value); axp_write(reg, value);
} }
// Convert a desired DCDC output voltage to the 25 mV-step register encoding used by AXP192.
static uint8_t axp_dc_voltage_data(uint16_t millivolts) { static uint8_t axp_dc_voltage_data(uint16_t millivolts) {
if (millivolts < 700) millivolts = 700; if (millivolts < 700) millivolts = 700;
if (millivolts > 3500) millivolts = 3500; if (millivolts > 3500) millivolts = 3500;
return static_cast<uint8_t>((millivolts - 700) / 25) & 0x7F; return static_cast<uint8_t>((millivolts - 700) / 25) & 0x7F;
} }
// Convert a desired LDO voltage to the 100 mV-step register encoding used by AXP192.
static uint8_t axp_ldo_voltage_data(uint16_t millivolts) { static uint8_t axp_ldo_voltage_data(uint16_t millivolts) {
if (millivolts < 1800) millivolts = 1800; if (millivolts < 1800) millivolts = 1800;
if (millivolts > 3300) millivolts = 3300; if (millivolts > 3300) millivolts = 3300;
return static_cast<uint8_t>((millivolts - 1800) / 100) & 0x0F; return static_cast<uint8_t>((millivolts - 1800) / 100) & 0x0F;
} }
// Set the LCD backlight rail (DCDC3) voltage; brightness is controlled by this rail voltage.
static void core2_axp192_set_lcd_voltage(uint16_t millivolts) { static void core2_axp192_set_lcd_voltage(uint16_t millivolts) {
uint8_t value = 0; uint8_t value = 0;
axp_read(0x27, &value); axp_read(0x27, &value);
axp_write(0x27, (value & 0x80) | axp_dc_voltage_data(millivolts)); // DCDC3, LCD backlight axp_write(0x27, (value & 0x80) | axp_dc_voltage_data(millivolts)); // DCDC3, LCD backlight
} }
// Map a 0.0-1.0 brightness value to the Core2 LCD backlight voltage and enable/disable DCDC3.
static void core2_axp192_set_backlight(float brightness) { static void core2_axp192_set_backlight(float brightness) {
if (brightness <= 0.0f) { if (brightness <= 0.0f) {
axp_update(0x12, 0x02, 0x00); // DCDC3 off axp_update(0x12, 0x02, 0x00); // DCDC3 off
@@ -81,6 +91,7 @@ static void core2_axp192_set_backlight(float brightness) {
axp_update(0x12, 0x00, 0x02); // DCDC3 on axp_update(0x12, 0x00, 0x02); // DCDC3 on
} }
// Initialize the Core2 PMIC rails needed by ESP32, LCD logic, LCD backlight, and the display reset line.
static void core2_axp192_init(esphome::i2c::I2CBus *bus) { static void core2_axp192_init(esphome::i2c::I2CBus *bus) {
axp192.set_i2c_bus(bus); axp192.set_i2c_bus(bus);
axp192.set_i2c_address(AXP192_ADDR); axp192.set_i2c_address(AXP192_ADDR);
@@ -113,6 +124,7 @@ static void core2_axp192_init(esphome::i2c::I2CBus *bus) {
core2_axp192_set_backlight(1.0f); core2_axp192_set_backlight(1.0f);
} }
// 16x16 monochrome status icons drawn manually into the ESPHome display framebuffer.
static const uint8_t ACCU_BMP[32] = { static const uint8_t ACCU_BMP[32] = {
0b00000000, 0b00000000, 0b00000000, 0b00000000,
0b00000000, 0b00000000, 0b00000000, 0b00000000,
@@ -229,6 +241,8 @@ static const uint8_t BUZZ_BMP[32] = {
class Meter { class Meter {
public: public:
// Offsets and bit masks for the 14-byte "classic" OWON frame layout. B35T+ frames are
// normalized into this same buffer so the rest of the code can use one representation.
static constexpr uint8_t REGPLUSMINUS = 0x00; static constexpr uint8_t REGPLUSMINUS = 0x00;
static constexpr uint8_t FLAGPLUS = 0b00101011; static constexpr uint8_t FLAGPLUS = 0b00101011;
static constexpr uint8_t FLAGMINUS = 0b00101101; static constexpr uint8_t FLAGMINUS = 0b00101101;
@@ -268,24 +282,29 @@ class Meter {
static constexpr uint8_t FLAGUNITAMP = 0b01000000; static constexpr uint8_t FLAGUNITAMP = 0b01000000;
static constexpr uint8_t FLAGUNITVOLT = 0b10000000; static constexpr uint8_t FLAGUNITVOLT = 0b10000000;
bool connected{false}; // Connection and decoded meter state consumed by ESPHome sensors and the local display.
bool write_available{false}; bool connected{false}; // BLE link to the OWON meter is currently established.
bool is_plus{false}; bool write_available{false}; // The writable BLE characteristic is ready for button commands.
bool low_battery{false}; bool is_plus{false}; // True when the last notification used the compact B35T+ frame format.
bool overload{false}; bool low_battery{false}; // Battery indicator decoded from the meter status bits.
bool has_reading{false}; bool overload{false}; // True when the meter reports OL instead of numeric digits.
uint8_t selected_button{1}; bool has_reading{false}; // At least one valid BLE notification has been decoded.
uint32_t last_notify_ms{0}; uint8_t selected_button{1}; // UI-selected OWON remote button command (1..6).
uint32_t last_notify_ms{0}; // Timestamp of the last accepted meter notification.
// Decode a BLE notification from either supported OWON protocol variant and refresh cached values.
bool handle_notify(const std::vector<uint8_t> &data) { bool handle_notify(const std::vector<uint8_t> &data) {
if (data.size() > sizeof(this->raw_)) if (data.size() > sizeof(this->raw_))
return false; return false;
// B35T+ sends a compact 6-byte binary frame; convert it into the classic 14-byte layout.
if (data.size() == 6 && data[1] >= 0xF0) { if (data.size() == 6 && data[1] >= 0xF0) {
memset(this->raw_, 0, sizeof(this->raw_)); memset(this->raw_, 0, sizeof(this->raw_));
memcpy(this->raw_, data.data(), data.size()); memcpy(this->raw_, data.data(), data.size());
this->is_plus = true; this->is_plus = true;
this->parse_plus_(); this->parse_plus_();
} else if (data.size() == 14 && data[12] == 0x0D && data[13] == 0x0A) { } else if (data.size() == 14 && data[12] == 0x0D && data[13] == 0x0A) {
// Classic B35T sends an ASCII-like 14-byte frame terminated by CR/LF; it can be used directly.
memset(this->value_, 0, sizeof(this->value_)); memset(this->value_, 0, sizeof(this->value_));
memcpy(this->value_, data.data(), data.size()); memcpy(this->value_, data.data(), data.size());
this->is_plus = false; this->is_plus = false;
@@ -294,26 +313,32 @@ class Meter {
return false; return false;
} }
// Cache derived states so display drawing and Home Assistant sensors do not have to re-parse frames.
this->overload = memcmp(this->value_, OVERLOAD_FRAME, sizeof(OVERLOAD_FRAME)) == 0; this->overload = memcmp(this->value_, OVERLOAD_FRAME, sizeof(OVERLOAD_FRAME)) == 0;
this->display_value = this->calc_display_value_(); this->display_value = this->calc_display_value_(); // Numeric value exactly as shown on the meter.
this->base_value = this->calc_base_value_(); this->base_value = this->calc_base_value_(); // Same reading converted to base SI scale.
this->has_reading = true; this->has_reading = true;
this->last_notify_ms = millis(); this->last_notify_ms = millis();
return true; return true;
} }
// Called by the ESPHome BLE client once the meter connection and write characteristic are usable.
void on_connect() { void on_connect() {
this->connected = true; this->connected = true;
this->write_available = true; this->write_available = true;
} }
// Reset connection flags when BLE disconnects; the last reading is kept for display/sensor continuity.
void on_disconnect() { void on_disconnect() {
this->connected = false; this->connected = false;
this->write_available = false; this->write_available = false;
} }
// Numeric accessors used by ESPHome lambdas when publishing sensor state to Home Assistant.
float value() const { return this->display_value; } float value() const { return this->display_value; }
float value_base() const { return this->base_value; } float value_base() const { return this->base_value; }
// Boolean flag helpers keep the bit-mask details out of UI and sensor publishing code.
bool negative() const { return (this->value_[REGPLUSMINUS] & FLAGMINUS) == FLAGMINUS; } bool negative() const { return (this->value_[REGPLUSMINUS] & FLAGMINUS) == FLAGMINUS; }
bool auto_range() const { return (this->value_[REGMODE] & FLAGMODEAUTO) == FLAGMODEAUTO; } bool auto_range() const { return (this->value_[REGMODE] & FLAGMODEAUTO) == FLAGMODEAUTO; }
bool hold() const { return (this->value_[REGMODE] & FLAGMODEHOLD) == FLAGMODEHOLD; } bool hold() const { return (this->value_[REGMODE] & FLAGMODEHOLD) == FLAGMODEHOLD; }
@@ -325,6 +350,7 @@ class Meter {
bool diode() const { return (this->value_[REGSCALE] & FLAGSCALEDIODE) == FLAGSCALEDIODE; } bool diode() const { return (this->value_[REGSCALE] & FLAGSCALEDIODE) == FLAGSCALEDIODE; }
bool continuity() const { return (this->value_[REGSCALE] & FLAGSCALEBUZZ) == FLAGSCALEBUZZ; } bool continuity() const { return (this->value_[REGSCALE] & FLAGSCALEBUZZ) == FLAGSCALEBUZZ; }
// Translate decoded unit bits into the text suffix shown on the display and exposed via sensors.
const char *unit() const { const char *unit() const {
switch (this->value_[REGUNIT]) { switch (this->value_[REGUNIT]) {
case FLAGUNITFAHR: return "°F"; case FLAGUNITFAHR: return "°F";
@@ -339,6 +365,7 @@ class Meter {
} }
} }
// Translate decoded scale bits into SI prefixes or duty-cycle percent.
const char *scale() const { const char *scale() const {
if ((this->value_[REGSCALE] & FLAGSCALEDUTY) == FLAGSCALEDUTY) return "%"; if ((this->value_[REGSCALE] & FLAGSCALEDUTY) == FLAGSCALEDUTY) return "%";
if ((this->value_[REGSCALE] & FLAGSCALEMEGA) == FLAGSCALEMEGA) return "M"; if ((this->value_[REGSCALE] & FLAGSCALEMEGA) == FLAGSCALEMEGA) return "M";
@@ -348,6 +375,7 @@ class Meter {
return ""; return "";
} }
// Build a compact human-readable list of active meter modes for Home Assistant text sensors.
std::string mode_text() const { std::string mode_text() const {
std::string out; std::string out;
if (this->dc()) out += "DC "; if (this->dc()) out += "DC ";
@@ -363,6 +391,7 @@ class Meter {
return out; return out;
} }
// Format the current reading as a single string, including disconnected/waiting/overload states.
std::string reading_text() const { std::string reading_text() const {
if (!this->connected) return "Disconnected"; if (!this->connected) return "Disconnected";
if (!this->has_reading) return "Waiting for data"; if (!this->has_reading) return "Waiting for data";
@@ -372,6 +401,7 @@ class Meter {
return std::string(buf); return std::string(buf);
} }
// Classify the reading so ESPHome can publish it to the correct measurement-specific sensor.
enum Kind { KIND_OTHER, KIND_VOLTAGE, KIND_CURRENT, KIND_RESISTANCE, KIND_FREQUENCY, KIND_CAPACITANCE, KIND_TEMP_C, KIND_TEMP_F, KIND_DUTY }; enum Kind { KIND_OTHER, KIND_VOLTAGE, KIND_CURRENT, KIND_RESISTANCE, KIND_FREQUENCY, KIND_CAPACITANCE, KIND_TEMP_C, KIND_TEMP_F, KIND_DUTY };
Kind kind() const { Kind kind() const {
if ((this->value_[REGSCALE] & FLAGSCALEDUTY) == FLAGSCALEDUTY) return KIND_DUTY; if ((this->value_[REGSCALE] & FLAGSCALEDUTY) == FLAGSCALEDUTY) return KIND_DUTY;
@@ -387,6 +417,7 @@ class Meter {
} }
} }
// UI label for the currently selected remote-control command sent back to the OWON meter.
const char *selected_button_name() const { const char *selected_button_name() const {
static const char *const names[] = {"SELECT", "RANGE", "HLD/LIG", "REL/BT", "HZ/DUTY", "MAX/MIN"}; static const char *const names[] = {"SELECT", "RANGE", "HLD/LIG", "REL/BT", "HZ/DUTY", "MAX/MIN"};
uint8_t index = this->selected_button; uint8_t index = this->selected_button;
@@ -395,6 +426,7 @@ class Meter {
return names[index - 1]; return names[index - 1];
} }
// Move the selected remote button left/right; bounds match the six commands in selected_button_name().
void previous_button() { void previous_button() {
if (this->selected_button > 1) this->selected_button--; if (this->selected_button > 1) this->selected_button--;
} }
@@ -402,6 +434,7 @@ class Meter {
if (this->selected_button < 6) this->selected_button++; if (this->selected_button < 6) this->selected_button++;
} }
// Draw either the OWON multimeter page or the Atorch DL24 load page on the M5Stack display.
void render(esphome::display::Display &it, esphome::display::BaseFont *font, void render(esphome::display::Display &it, esphome::display::BaseFont *font,
esphome::display::BaseFont *value_font, int display_page = 0, esphome::display::BaseFont *value_font, int display_page = 0,
bool atorch_connected = false, bool atorch_connected = false,
@@ -422,7 +455,9 @@ class Meter {
if (display_page == 0) { if (display_page == 0) {
// --- PAGE 1: OWON Multimeter --- // --- PAGE 1: OWON Multimeter ---
it.fill(bg); it.fill(bg);
bool status_active = this->connected && this->has_reading; bool status_active = this->connected && this->has_reading; // Only highlight decoded flags after data arrives.
// Top status row mirrors the physical meter annunciators: battery, BLE, range, min/max, hold, rel, diode, buzzer.
this->draw_icon_(it, 12, 8, 16, 16, ACCU_BMP, status_active ? (this->low_battery ? red : green) : inactive); this->draw_icon_(it, 12, 8, 16, 16, ACCU_BMP, status_active ? (this->low_battery ? red : green) : inactive);
this->draw_icon_(it, 46, 8, 16, 16, BLE_BMP, this->connected ? blue : inactive); this->draw_icon_(it, 46, 8, 16, 16, BLE_BMP, this->connected ? blue : inactive);
this->label_(it, font, 86, 8, "AUTO", status_active && this->auto_range() ? fg : inactive); this->label_(it, font, 86, 8, "AUTO", status_active && this->auto_range() ? fg : inactive);
@@ -436,6 +471,7 @@ class Meter {
this->label_(it, font, 8, 104, "DC", status_active && this->dc() ? cyan : inactive); this->label_(it, font, 8, 104, "DC", status_active && this->dc() ? cyan : inactive);
this->label_(it, font, 8, 124, "AC", status_active && this->ac() ? magenta : inactive); this->label_(it, font, 8, 124, "AC", status_active && this->ac() ? magenta : inactive);
// Main seven-segment area distinguishes connection, waiting-for-first-frame, overload, and normal readings.
if (!this->connected) { if (!this->connected) {
this->draw_digits_(it, "----", false, inactive); this->draw_digits_(it, "----", false, inactive);
it.print(160, 148, font, inactive, esphome::display::TextAlign::CENTER, "scan/connect"); it.print(160, 148, font, inactive, esphome::display::TextAlign::CENTER, "scan/connect");
@@ -462,6 +498,8 @@ class Meter {
bool bargraph_active = status_active && !this->overload; bool bargraph_active = status_active && !this->overload;
this->draw_bargraph_(it, bargraph_active ? this->digits_from_buffer_() : 0, bargraph_active); this->draw_bargraph_(it, bargraph_active ? this->digits_from_buffer_() : 0, bargraph_active);
// Bottom soft-button row shows the currently selected OWON command and whether BLE writes are available.
it.filled_rectangle(34, 212, 40, 24, this->write_available ? fg : inactive); it.filled_rectangle(34, 212, 40, 24, this->write_available ? fg : inactive);
it.filled_rectangle(108, 212, 100, 24, this->write_available ? fg : inactive); it.filled_rectangle(108, 212, 100, 24, this->write_available ? fg : inactive);
it.filled_rectangle(242, 212, 40, 24, this->write_available ? fg : inactive); it.filled_rectangle(242, 212, 40, 24, this->write_available ? fg : inactive);
@@ -488,6 +526,7 @@ class Meter {
const Color temp_color = atorch_connected ? magenta : dim_accent; const Color temp_color = atorch_connected ? magenta : dim_accent;
const Color header_color = atorch_connected ? cyan : dim_value; const Color header_color = atorch_connected ? cyan : dim_value;
// Prepare formatted metric strings before drawing so missing/NaN Atorch values display consistently.
char voltage_text[24]; char voltage_text[24];
char current_text[24]; char current_text[24];
char power_text[24]; char power_text[24];
@@ -508,6 +547,7 @@ class Meter {
} }
it.print(310, 7, font, inactive, esphome::display::TextAlign::TOP_RIGHT, "DC LOAD"); it.print(310, 7, font, inactive, esphome::display::TextAlign::TOP_RIGHT, "DC LOAD");
// Four primary Atorch measurements are shown as dashboard cards; accumulated counters fit in the footer.
this->draw_metric_card_(it, font, value_font, 10, 42, 145, 76, "VOLTAGE", voltage_text, voltage_color, panel, border, value_color); this->draw_metric_card_(it, font, value_font, 10, 42, 145, 76, "VOLTAGE", voltage_text, voltage_color, panel, border, value_color);
this->draw_metric_card_(it, font, value_font, 165, 42, 145, 76, "CURRENT", current_text, current_color, panel, border, value_color); this->draw_metric_card_(it, font, value_font, 165, 42, 145, 76, "CURRENT", current_text, current_color, panel, border, value_color);
this->draw_metric_card_(it, font, value_font, 10, 128, 145, 76, "POWER", power_text, power_color, panel, border, value_color); this->draw_metric_card_(it, font, value_font, 10, 128, 145, 76, "POWER", power_text, power_color, panel, border, value_color);
@@ -526,12 +566,13 @@ class Meter {
} }
private: private:
uint8_t raw_[14]{}; uint8_t raw_[14]{}; // Last raw BLE payload, padded to the maximum supported OWON frame length.
uint8_t value_[14]{}; uint8_t value_[14]{}; // Normalized classic-frame view used by all accessors and display rendering.
float display_value{NAN}; float display_value{NAN}; // Meter reading with its displayed prefix still applied, e.g. 12.3 kΩ -> 12.3.
float base_value{NAN}; float base_value{NAN}; // Meter reading converted to base units, e.g. 12.3 kΩ -> 12300 Ω.
static constexpr uint8_t OVERLOAD_FRAME[5] = {0x2B, 0x3F, 0x30, 0x3A, 0x3F}; static constexpr uint8_t OVERLOAD_FRAME[5] = {0x2B, 0x3F, 0x30, 0x3A, 0x3F}; // "+?0:?" OL marker.
// Reconstruct the four visible seven-segment digits as an integer for value calculations/bargraph scaling.
uint16_t digits_from_buffer_() const { uint16_t digits_from_buffer_() const {
uint16_t out = 0; uint16_t out = 0;
if (this->value_[REGDIG1] >= '0' && this->value_[REGDIG1] <= '9') out += (this->value_[REGDIG1] - '0') * 1000; if (this->value_[REGDIG1] >= '0' && this->value_[REGDIG1] <= '9') out += (this->value_[REGDIG1] - '0') * 1000;
@@ -541,10 +582,12 @@ class Meter {
return out; return out;
} }
// Convert the normalized frame into the floating-point number shown by the meter display.
float calc_display_value_() const { float calc_display_value_() const {
if (this->overload) return NAN; if (this->overload) return NAN;
if (this->is_plus) { if (this->is_plus) {
// B35T+ encodes decimal placement and sign/magnitude as packed little-endian words.
uint16_t pair1 = static_cast<uint16_t>(this->raw_[0]) | (static_cast<uint16_t>(this->raw_[1]) << 8); uint16_t pair1 = static_cast<uint16_t>(this->raw_[0]) | (static_cast<uint16_t>(this->raw_[1]) << 8);
uint8_t decimal = pair1 & 0x07; uint8_t decimal = pair1 & 0x07;
if (decimal >= 7) return NAN; if (decimal >= 7) return NAN;
@@ -556,6 +599,7 @@ class Meter {
return negative ? -v : v; return negative ? -v : v;
} }
// Classic frames carry decimal placement as one of three point flags in the normalized buffer.
uint8_t decimal = 0; uint8_t decimal = 0;
switch (this->value_[REGPOINT] & 0x07) { switch (this->value_[REGPOINT] & 0x07) {
case 0b001: decimal = 1; break; case 0b001: decimal = 1; break;
@@ -567,6 +611,7 @@ class Meter {
return this->negative() ? -v : v; return this->negative() ? -v : v;
} }
// Apply the decoded SI prefix so Home Assistant receives comparable base-unit sensor values.
float calc_base_value_() const { float calc_base_value_() const {
if (std::isnan(this->display_value)) return NAN; if (std::isnan(this->display_value)) return NAN;
if (this->value_[REGUNIT] == FLAGUNITNF) return this->display_value * 1e-9f; if (this->value_[REGUNIT] == FLAGUNITNF) return this->display_value * 1e-9f;
@@ -577,17 +622,20 @@ class Meter {
return this->display_value; return this->display_value;
} }
// Decode the compact B35T+ payload and synthesize the 14-byte classic representation.
void parse_plus_() { void parse_plus_() {
memset(this->value_, 0, sizeof(this->value_)); memset(this->value_, 0, sizeof(this->value_));
this->value_[5] = 0x20; this->value_[5] = 0x20;
this->value_[12] = 0x0D; this->value_[12] = 0x0D;
this->value_[13] = 0x0A; this->value_[13] = 0x0A;
// First word: measurement function, SI scale, and decimal-point position.
uint16_t pair1 = static_cast<uint16_t>(this->raw_[0]) | (static_cast<uint16_t>(this->raw_[1]) << 8); uint16_t pair1 = static_cast<uint16_t>(this->raw_[0]) | (static_cast<uint16_t>(this->raw_[1]) << 8);
uint8_t function = (pair1 >> 6) & 0x0F; uint8_t function = (pair1 >> 6) & 0x0F;
uint8_t scale = (pair1 >> 3) & 0x07; uint8_t scale = (pair1 >> 3) & 0x07;
uint8_t decimal = pair1 & 0x07; uint8_t decimal = pair1 & 0x07;
// Translate B35T+ decimal count to the classic point-flag convention used by draw_decimal_points_().
switch (decimal) { switch (decimal) {
case 0: this->value_[REGPOINT] = FLAGPOINT0; break; case 0: this->value_[REGPOINT] = FLAGPOINT0; break;
case 1: this->value_[REGPOINT] = FLAGPOINT3; break; case 1: this->value_[REGPOINT] = FLAGPOINT3; break;
@@ -596,6 +644,7 @@ class Meter {
default: break; default: break;
} }
// Translate the function selector into classic unit and AC/DC/mode flags.
switch (function) { switch (function) {
case 0: this->value_[REGUNIT] |= FLAGUNITVOLT; this->value_[REGMODE] |= FLAGMODEDC; break; case 0: this->value_[REGUNIT] |= FLAGUNITVOLT; this->value_[REGMODE] |= FLAGMODEDC; break;
case 1: this->value_[REGUNIT] |= FLAGUNITVOLT; this->value_[REGMODE] |= FLAGMODEAC; break; case 1: this->value_[REGUNIT] |= FLAGUNITVOLT; this->value_[REGMODE] |= FLAGMODEAC; break;
@@ -613,6 +662,7 @@ class Meter {
default: break; default: break;
} }
// Translate the B35T+ scale selector into SI-prefix flags.
switch (scale) { switch (scale) {
case 2: this->value_[REGSCALE] |= FLAGSCALEMICRO; break; case 2: this->value_[REGSCALE] |= FLAGSCALEMICRO; break;
case 3: this->value_[REGSCALE] |= FLAGSCALEMILLI; break; case 3: this->value_[REGSCALE] |= FLAGSCALEMILLI; break;
@@ -621,6 +671,7 @@ class Meter {
default: break; default: break;
} }
// Second word: status flags such as hold, relative, auto-range, battery, and min/max.
uint16_t pair2 = static_cast<uint16_t>(this->raw_[2]) | (static_cast<uint16_t>(this->raw_[3]) << 8); uint16_t pair2 = static_cast<uint16_t>(this->raw_[2]) | (static_cast<uint16_t>(this->raw_[3]) << 8);
if (pair2 & (1 << 0)) this->value_[REGMODE] |= FLAGMODEHOLD; if (pair2 & (1 << 0)) this->value_[REGMODE] |= FLAGMODEHOLD;
if (pair2 & (1 << 1)) this->value_[REGMODE] |= FLAGMODEREL; if (pair2 & (1 << 1)) this->value_[REGMODE] |= FLAGMODEREL;
@@ -629,6 +680,7 @@ class Meter {
if (pair2 & (1 << 4)) this->value_[REGMINMAX] |= FLAGMIN; if (pair2 & (1 << 4)) this->value_[REGMINMAX] |= FLAGMIN;
if (pair2 & (1 << 5)) this->value_[REGMINMAX] |= FLAGMAX; if (pair2 & (1 << 5)) this->value_[REGMINMAX] |= FLAGMAX;
// Third word: signed 4-digit display value, with bit 15 indicating a negative reading.
uint16_t pair3 = static_cast<uint16_t>(this->raw_[4]) | (static_cast<uint16_t>(this->raw_[5]) << 8); uint16_t pair3 = static_cast<uint16_t>(this->raw_[4]) | (static_cast<uint16_t>(this->raw_[5]) << 8);
if (decimal < 7) { if (decimal < 7) {
uint16_t digits = pair3; uint16_t digits = pair3;
@@ -643,15 +695,18 @@ class Meter {
this->value_[REGDIG3] = '0' + ((digits / 10) % 10); this->value_[REGDIG3] = '0' + ((digits / 10) % 10);
this->value_[REGDIG4] = '0' + (digits % 10); this->value_[REGDIG4] = '0' + (digits % 10);
} else { } else {
// Decimal values 7 and above are used by this protocol to indicate overload/OL.
memcpy(this->value_, OVERLOAD_FRAME, sizeof(OVERLOAD_FRAME)); memcpy(this->value_, OVERLOAD_FRAME, sizeof(OVERLOAD_FRAME));
} }
} }
// Return a printable digit for a display register, using a blank for non-digit status bytes.
char digit_char_(uint8_t reg) const { char digit_char_(uint8_t reg) const {
uint8_t c = this->value_[reg]; uint8_t c = this->value_[reg];
return (c >= '0' && c <= '9') ? static_cast<char>(c) : ' '; return (c >= '0' && c <= '9') ? static_cast<char>(c) : ' ';
} }
// Format Atorch metrics, using "--" for missing values while keeping units visible.
void format_metric_(char *buffer, size_t size, float value, const char *unit, uint8_t decimals) const { void format_metric_(char *buffer, size_t size, float value, const char *unit, uint8_t decimals) const {
if (!std::isfinite(value)) { if (!std::isfinite(value)) {
snprintf(buffer, size, "-- %s", unit); snprintf(buffer, size, "-- %s", unit);
@@ -662,6 +717,7 @@ class Meter {
snprintf(buffer, size, format, value, unit); snprintf(buffer, size, format, value, unit);
} }
// Draw one Atorch dashboard tile with a colored accent, label, and centered numeric value.
void draw_metric_card_(Display &it, esphome::display::BaseFont *label_font, esphome::display::BaseFont *value_font, void draw_metric_card_(Display &it, esphome::display::BaseFont *label_font, esphome::display::BaseFont *value_font,
int x, int y, int w, int h, const char *title, const char *value, int x, int y, int w, int h, const char *title, const char *value,
Color accent, Color fill, Color border, Color value_color) { Color accent, Color fill, Color border, Color value_color) {
@@ -678,10 +734,12 @@ class Meter {
it.filled_rectangle(x + 10, y + h - 11, w - 20, 1, accent); it.filled_rectangle(x + 10, y + h - 11, w - 20, 1, accent);
} }
// Small wrapper for consistent top-left aligned labels.
void label_(Display &it, esphome::display::BaseFont *font, int x, int y, const char *text, Color color) { void label_(Display &it, esphome::display::BaseFont *font, int x, int y, const char *text, Color color) {
it.print(x, y, font, color, esphome::display::TextAlign::TOP_LEFT, text); it.print(x, y, font, color, esphome::display::TextAlign::TOP_LEFT, text);
} }
// Draw the large four-digit OWON readout, including a separate minus sign when needed.
void draw_digits_(Display &it, const char *text, bool negative, Color color) { void draw_digits_(Display &it, const char *text, bool negative, Color color) {
if (negative) this->draw_segment_(it, 8, 83, 26, 9, true, color); if (negative) this->draw_segment_(it, 8, 83, 26, 9, true, color);
constexpr int digit_x = 40; constexpr int digit_x = 40;
@@ -694,6 +752,7 @@ class Meter {
} }
} }
// Draw decimal points between the large seven-segment digits according to the normalized frame flags.
void draw_decimal_points_(Display &it, Color color) { void draw_decimal_points_(Display &it, Color color) {
uint8_t p = this->value_[REGPOINT]; uint8_t p = this->value_[REGPOINT];
if ((p & FLAGPOINT1) == FLAGPOINT1) it.filled_rectangle(95, 135, 8, 10, color); if ((p & FLAGPOINT1) == FLAGPOINT1) it.filled_rectangle(95, 135, 8, 10, color);
@@ -701,6 +760,7 @@ class Meter {
if ((p & FLAGPOINT3) == FLAGPOINT3) it.filled_rectangle(223, 135, 8, 10, color); if ((p & FLAGPOINT3) == FLAGPOINT3) it.filled_rectangle(223, 135, 8, 10, color);
} }
// Draw one beveled horizontal or vertical segment used by the custom seven-segment font.
void draw_segment_(Display &it, int x, int y, int w, int h, bool horizontal, Color color) { void draw_segment_(Display &it, int x, int y, int w, int h, bool horizontal, Color color) {
if (horizontal) { if (horizontal) {
int cap = h / 2; int cap = h / 2;
@@ -715,6 +775,7 @@ class Meter {
} }
} }
// Plot a packed 1-bit bitmap icon, where each set bit becomes one colored display pixel.
void draw_icon_(Display &it, int x, int y, int w, int h, const uint8_t *data, Color color) { void draw_icon_(Display &it, int x, int y, int w, int h, const uint8_t *data, Color color) {
for (int row = 0; row < h; row++) { for (int row = 0; row < h; row++) {
for (int col = 0; col < w; col++) { for (int col = 0; col < w; col++) {
@@ -726,6 +787,7 @@ class Meter {
} }
} }
// Map a digit/OL/minus character to seven segment states and draw the active segments.
void draw_seven_segment_(Display &it, int x, int y, int w, int h, char ch, Color color) { void draw_seven_segment_(Display &it, int x, int y, int w, int h, char ch, Color color) {
bool a=false,b=false,c=false,d=false,e=false,f=false,g=false; bool a=false,b=false,c=false,d=false,e=false,f=false,g=false;
switch (ch) { switch (ch) {
@@ -766,6 +828,7 @@ class Meter {
if (g) this->draw_segment_(it, x + t / 2, mid_y, w - t, t, true, color); if (g) this->draw_segment_(it, x + t / 2, mid_y, w - t, t, true, color);
} }
// Draw the OWON-style bottom bargraph by scaling raw 0..6000 display digits to a 240 px ruler.
void draw_bargraph_(Display &it, uint16_t digits, bool active) { void draw_bargraph_(Display &it, uint16_t digits, bool active) {
const Color fg(255, 255, 255); const Color fg(255, 255, 255);
const Color inactive(80, 80, 80); const Color inactive(80, 80, 80);
+61 -1
View File
@@ -29,6 +29,9 @@ esphome:
on_boot: on_boot:
priority: 850 priority: 850
then: then:
# The Core2 LCD/backlight rails are controlled by the AXP192 PMIC. ESPHome's
# stock M5CORE2 display setup does not initialize those rails here, so the
# custom header performs the M5Stack-specific power sequencing once I2C exists.
- lambda: |- - lambda: |-
owon_b35t::core2_axp192_init(id(core2_i2c)); owon_b35t::core2_axp192_init(id(core2_i2c));
project: project:
@@ -77,10 +80,14 @@ wifi:
captive_portal: captive_portal:
globals: globals:
# 0 = OWON meter page, 1 = Atorch DL24 page. This is intentionally volatile so
# every boot starts with the multimeter view until one device connects.
- id: display_page - id: display_page
type: int type: int
initial_value: "0" initial_value: "0"
restore_value: no restore_value: no
# The Atorch external component does not expose a simple connected flag that the
# display lambda can query, so BLE callbacks maintain this state explicitly.
- id: atorch_connected - id: atorch_connected
type: bool type: bool
initial_value: "false" initial_value: "false"
@@ -100,15 +107,19 @@ script:
- delay: 2min - delay: 2min
- if: - if:
condition: condition:
# Only dim when both BLE devices are disconnected; active lab instruments
# keep the display fully lit so readings remain visible at a glance.
lambda: |- lambda: |-
return !owon_meter.connected && !id(atorch_connected); return !owon_meter.connected && !id(atorch_connected);
then: then:
- light.turn_on: - light.turn_on:
id: backlight id: backlight
brightness: 50% brightness: 70%
- delay: 3min - delay: 3min
- if: - if:
condition: condition:
# Re-check before turning the backlight off, because a connection may
# have been established during the preceding delay.
lambda: |- lambda: |-
return !owon_meter.connected && !id(atorch_connected); return !owon_meter.connected && !id(atorch_connected);
then: then:
@@ -117,6 +128,8 @@ script:
interval: interval:
- interval: 10s - interval: 10s
then: then:
# Periodic memory telemetry is useful on the Core2 because BLE, PSRAM, the
# MIPI framebuffer, and external components can fragment different heap pools.
- lambda: |- - lambda: |-
ESP_LOGI("mem", "heap free=%u min_free=%u internal_free=%u internal_largest=%u dma_free=%u dma_largest=%u psram_free=%u psram_largest=%u", ESP_LOGI("mem", "heap free=%u min_free=%u internal_free=%u internal_largest=%u dma_free=%u dma_largest=%u psram_free=%u psram_largest=%u",
static_cast<unsigned>(esp_get_free_heap_size()), static_cast<unsigned>(esp_get_free_heap_size()),
@@ -139,12 +152,16 @@ ble_client:
on_connect: on_connect:
then: then:
- script.execute: wake_backlight - script.execute: wake_backlight
# Notify the custom parser/display object and switch to the meter page when
# the OWON connects, so the on-device screen follows the active instrument.
- lambda: |- - lambda: |-
owon_meter.on_connect(); owon_meter.on_connect();
id(display_page) = 0; id(display_page) = 0;
id(lcd).update(); id(lcd).update();
on_disconnect: on_disconnect:
then: then:
# Keep the UI useful after a disconnect: if the Atorch is still online,
# automatically fall back from the OWON page to the Atorch page.
- lambda: |- - lambda: |-
owon_meter.on_disconnect(); owon_meter.on_disconnect();
if (id(display_page) == 0 && id(atorch_connected)) { if (id(display_page) == 0 && id(atorch_connected)) {
@@ -153,6 +170,8 @@ ble_client:
id(lcd).update(); id(lcd).update();
- if: - if:
condition: condition:
# Start the idle timer only after both BLE clients are gone; otherwise
# the remaining connected instrument still deserves an active display.
lambda: |- lambda: |-
return !owon_meter.connected && !id(atorch_connected); return !owon_meter.connected && !id(atorch_connected);
then: then:
@@ -163,12 +182,16 @@ ble_client:
on_connect: on_connect:
then: then:
- script.execute: wake_backlight - script.execute: wake_backlight
# The Atorch component handles measurements, while this explicit flag/page
# switch keeps the shared display state synchronized with the BLE client.
- lambda: |- - lambda: |-
id(atorch_connected) = true; id(atorch_connected) = true;
id(display_page) = 1; id(display_page) = 1;
id(lcd).update(); id(lcd).update();
on_disconnect: on_disconnect:
then: then:
# If the load disconnects while the meter is still connected, move the
# physical display back to the meter instead of leaving a stale load page.
- lambda: |- - lambda: |-
id(atorch_connected) = false; id(atorch_connected) = false;
if (id(display_page) == 1 && owon_meter.connected) { if (id(display_page) == 1 && owon_meter.connected) {
@@ -177,6 +200,8 @@ ble_client:
id(lcd).update(); id(lcd).update();
- if: - if:
condition: condition:
# Same idle rule as OWON disconnect: only dim/off when neither BLE
# target is connected anymore.
lambda: |- lambda: |-
return !owon_meter.connected && !id(atorch_connected); return !owon_meter.connected && !id(atorch_connected);
then: then:
@@ -203,6 +228,8 @@ output:
type: float type: float
id: lcd_backlight id: lcd_backlight
write_action: write_action:
# Bridge ESPHome's generic monochromatic light level to the Core2-specific
# AXP192 backlight voltage helper implemented in lab-ble-proxy.h.
- lambda: |- - lambda: |-
owon_b35t::core2_axp192_set_backlight(state); owon_b35t::core2_axp192_set_backlight(state);
@@ -217,6 +244,8 @@ font:
- file: "fonts/Roboto-Regular.ttf" - file: "fonts/Roboto-Regular.ttf"
id: meter_font id: meter_font
size: 15 size: 15
# Glyphs are whitelisted to save flash/RAM; include symbols used by units,
# status labels, soft-button names, and Atorch metric text.
glyphs: glyphs:
[ [
" ", " ",
@@ -293,6 +322,7 @@ font:
- file: "fonts/Roboto-Medium.ttf" - file: "fonts/Roboto-Medium.ttf"
id: atorch_value_font id: atorch_value_font
size: 22 size: 22
# Larger Atorch value font only needs numeric characters and measurement units.
glyphs: glyphs:
[ [
" ", " ",
@@ -322,6 +352,8 @@ display:
id: lcd id: lcd
model: M5CORE2 model: M5CORE2
update_interval: 500ms update_interval: 500ms
# Rendering is delegated to the custom C++ helper because ESPHome display YAML
# primitives would be unwieldy for the OWON seven-segment clone and Atorch dashboard.
lambda: |- lambda: |-
owon_meter.render( owon_meter.render(
it, it,
@@ -346,6 +378,8 @@ touchscreen:
- script.execute: wake_backlight - script.execute: wake_backlight
- if: - if:
condition: condition:
# Touching an idle/disconnected unit wakes the display briefly, then
# restarts the idle timer if no BLE target is available.
lambda: |- lambda: |-
return !owon_meter.connected && !id(atorch_connected); return !owon_meter.connected && !id(atorch_connected);
then: then:
@@ -361,6 +395,7 @@ binary_sensor:
y_max: 195 y_max: 195
on_press: on_press:
then: then:
# The large middle touch zone toggles between the two custom render pages.
- lambda: |- - lambda: |-
id(display_page) = 1 - id(display_page); id(display_page) = 1 - id(display_page);
id(lcd).update(); id(lcd).update();
@@ -374,6 +409,7 @@ binary_sensor:
internal: true internal: true
on_press: on_press:
then: then:
# Left soft button changes which OWON remote command the center button will send.
- lambda: |- - lambda: |-
owon_meter.previous_button(); owon_meter.previous_button();
- platform: touchscreen - platform: touchscreen
@@ -396,6 +432,8 @@ binary_sensor:
id: owon_ble_client id: owon_ble_client
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb" service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb" characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb"
# OWON remote-control payload: first byte selects the meter button,
# second byte selects the normal/short-press action variant.
value: !lambda |- value: !lambda |-
std::vector<uint8_t> data = {owon_meter.selected_button, 0x01}; std::vector<uint8_t> data = {owon_meter.selected_button, 0x01};
return data; return data;
@@ -410,6 +448,8 @@ binary_sensor:
id: owon_ble_client id: owon_ble_client
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb" service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb" characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb"
# Long-press encoding is not uniform: SELECT and HZ/DUTY still use
# 0x01, while RANGE/HOLD/REL/MAX-MIN use 0x00 for their alternate action.
value: !lambda |- value: !lambda |-
uint8_t press_type = (owon_meter.selected_button == 1 || owon_meter.selected_button == 5) ? 0x01 : 0x00; uint8_t press_type = (owon_meter.selected_button == 1 || owon_meter.selected_button == 5) ? 0x01 : 0x00;
std::vector<uint8_t> data = {owon_meter.selected_button, press_type}; std::vector<uint8_t> data = {owon_meter.selected_button, press_type};
@@ -424,12 +464,14 @@ binary_sensor:
internal: true internal: true
on_press: on_press:
then: then:
# Right soft button advances to the next OWON command label.
- lambda: |- - lambda: |-
owon_meter.next_button(); owon_meter.next_button();
- platform: template - platform: template
name: "${friendly_name} OWON Connected" name: "${friendly_name} OWON Connected"
icon: "mdi:bluetooth-connect" icon: "mdi:bluetooth-connect"
# Template binary sensors expose state maintained by the custom OWON parser object.
lambda: |- lambda: |-
return owon_meter.connected; return owon_meter.connected;
- platform: template - platform: template
@@ -450,6 +492,8 @@ binary_sensor:
return id(atorch_connected); return id(atorch_connected);
sensor: sensor:
# This hidden characteristic sensor is the bridge from OWON BLE notifications into
# custom C++ parsing. The returned float becomes the source numeric meter reading.
- platform: ble_client - platform: ble_client
type: characteristic type: characteristic
ble_client_id: owon_ble_client ble_client_id: owon_ble_client
@@ -488,6 +532,8 @@ sensor:
name: "${friendly_name} Atorch Dim Backlight" name: "${friendly_name} Atorch Dim Backlight"
id: atorch_dim_backlight id: atorch_dim_backlight
# The DL24 component exposes power, but this config computes Wh locally so the
# accumulated energy can be restored and reset alongside Atorch's own counters.
- platform: integration - platform: integration
name: "${friendly_name} Atorch Energy" name: "${friendly_name} Atorch Energy"
id: atorch_energy_calculated id: atorch_energy_calculated
@@ -506,18 +552,21 @@ text_sensor:
name: "${friendly_name} OWON Reading" name: "${friendly_name} OWON Reading"
update_interval: 1s update_interval: 1s
icon: "mdi:gauge" icon: "mdi:gauge"
# Human-readable mirror of the meter LCD, including waiting/disconnected/OL states.
lambda: |- lambda: |-
return owon_meter.reading_text(); return owon_meter.reading_text();
- platform: template - platform: template
name: "${friendly_name} OWON Unit" name: "${friendly_name} OWON Unit"
update_interval: 1s update_interval: 1s
icon: "mdi:ruler" icon: "mdi:ruler"
# Combines SI prefix and unit into one HA text entity, e.g. mV, kΩ, µA, %.
lambda: |- lambda: |-
return std::string(owon_meter.scale()) + owon_meter.unit(); return std::string(owon_meter.scale()) + owon_meter.unit();
- platform: template - platform: template
name: "${friendly_name} OWON Mode" name: "${friendly_name} OWON Mode"
icon: "mdi:knob" icon: "mdi:knob"
update_interval: 1s update_interval: 1s
# Exposes decoded annunciators such as AC/DC, AUTO, HOLD, REL, MIN/MAX.
lambda: |- lambda: |-
return owon_meter.mode_text(); return owon_meter.mode_text();
@@ -535,6 +584,7 @@ button:
reset_all: reset_all:
name: "${friendly_name} Atorch Reset All" name: "${friendly_name} Atorch Reset All"
on_press: on_press:
# Keep the locally integrated Wh counter in sync with the DL24 reset-all command.
- sensor.integration.reset: atorch_energy_calculated - sensor.integration.reset: atorch_energy_calculated
usb_plus: usb_plus:
name: "${friendly_name} Atorch Plus" name: "${friendly_name} Atorch Plus"
@@ -544,6 +594,8 @@ button:
name: "${friendly_name} Atorch Setup" name: "${friendly_name} Atorch Setup"
enter: enter:
name: "${friendly_name} Atorch Enter" name: "${friendly_name} Atorch Enter"
# Home Assistant button entities for direct OWON remote commands. The payloads
# are the same two-byte command format used by the touchscreen soft buttons.
- platform: template - platform: template
name: "OWON SELECT" name: "OWON SELECT"
id: owon_btn_select id: owon_btn_select
@@ -552,6 +604,7 @@ button:
id: owon_ble_client id: owon_ble_client
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb" service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb" characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb"
# SELECT normal press.
value: !lambda "return std::vector<uint8_t>({1, 0x01});" value: !lambda "return std::vector<uint8_t>({1, 0x01});"
- platform: template - platform: template
@@ -562,6 +615,7 @@ button:
id: owon_ble_client id: owon_ble_client
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb" service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb" characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb"
# RANGE normal press.
value: !lambda "return std::vector<uint8_t>({2, 0x01});" value: !lambda "return std::vector<uint8_t>({2, 0x01});"
- platform: template - platform: template
@@ -572,6 +626,7 @@ button:
id: owon_ble_client id: owon_ble_client
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb" service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb" characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb"
# HOLD is the short/normal action on the shared HLD/LIG physical key.
value: !lambda "return std::vector<uint8_t>({3, 0x01});" value: !lambda "return std::vector<uint8_t>({3, 0x01});"
- platform: template - platform: template
@@ -582,6 +637,7 @@ button:
id: owon_ble_client id: owon_ble_client
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb" service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb" characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb"
# LIGHT is the alternate/long action on the same HLD/LIG key.
value: !lambda "return std::vector<uint8_t>({3, 0x00});" value: !lambda "return std::vector<uint8_t>({3, 0x00});"
- platform: template - platform: template
@@ -592,6 +648,7 @@ button:
id: owon_ble_client id: owon_ble_client
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb" service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb" characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb"
# RELATIVE is the short/normal action on the shared REL/BT key.
value: !lambda "return std::vector<uint8_t>({4, 0x01});" value: !lambda "return std::vector<uint8_t>({4, 0x01});"
- platform: template - platform: template
@@ -602,6 +659,7 @@ button:
id: owon_ble_client id: owon_ble_client
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb" service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb" characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb"
# BT is the alternate/long action on the same REL/BT key.
value: !lambda "return std::vector<uint8_t>({4, 0x00});" value: !lambda "return std::vector<uint8_t>({4, 0x00});"
- platform: template - platform: template
@@ -612,6 +670,7 @@ button:
id: owon_ble_client id: owon_ble_client
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb" service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb" characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb"
# HZ/DUTY command uses the normal action variant.
value: !lambda "return std::vector<uint8_t>({5, 0x01});" value: !lambda "return std::vector<uint8_t>({5, 0x01});"
- platform: template - platform: template
@@ -622,4 +681,5 @@ button:
id: owon_ble_client id: owon_ble_client
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb" service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb" characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb"
# MAX/MIN command uses the normal action variant.
value: !lambda "return std::vector<uint8_t>({6, 0x01});" value: !lambda "return std::vector<uint8_t>({6, 0x01});"