74 Commits

Author SHA1 Message Date
Commander1024 4e46f60b65 Updated refresh logic to only rerender calendar, when contents changed. 2026-07-13 15:31:39 +02:00
ESPHome Device Builder 9e7684e294 Edit epaperframe.yaml 2026-07-13 14:33:32 +02:00
Commander1024 818b06f3d7 Merge branch 'epaperframe-gpt5.6' 2026-07-13 14:28:28 +02:00
Commander1024 99bb2594e2 Fixed utility meters and unsmoothen internet bandwidth monitors. 2026-07-13 13:08:24 +02:00
Commander1024 9104cd8fdc Reapply "Fix traffic counters?"
This reverts commit 96b88624d9.
2026-07-13 10:30:47 +02:00
Commander1024 96b88624d9 Revert "Fix traffic counters?"
This reverts commit c1ba3012d4.
2026-07-13 08:45:37 +02:00
Commander1024 c1ba3012d4 Fix traffic counters? 2026-07-13 08:36:46 +02:00
Commander1024 e77d9ee8d2 Adopted VyOS SNMP router traffic counter. 2026-07-13 08:23:31 +02:00
Commander1024 bf49633fac Shortened calendar title again... 2026-07-11 23:20:09 +02:00
Commander1024 99579a30f7 Updated HA. 2026-07-11 23:19:48 +02:00
Commander1024 250bfa94bd Add daily e-paper quote rotation 2026-07-11 22:58:23 +02:00
Commander1024 c342efc08f Remove hidden-events footer from epaper frame 2026-07-11 22:47:32 +02:00
Commander1024 13e58355da Finetuning of available space 2026-07-11 22:39:15 +02:00
Commander1024 ee814c4afe Refine calendar event data display, prevent overlapping and display
event's end time.
2026-07-11 22:26:08 +02:00
Commander1024 6149a9e994 Add calendar styling and WiFi indicator 2026-07-11 22:23:45 +02:00
Commander1024 3b7c30634e Add compact calendar feed to e-paper display + HA 2026-07-11 21:41:21 +02:00
Commander1024 38da2cebdd Remove epaper power display components 2026-07-11 21:00:42 +02:00
Commander1024 3da69c19ab Base epaperframe config with Kinderzimmer 2026-07-11 19:14:52 +02:00
Commander1024 35e7f81224 Base epaperframe config with Kinderzimmer 2026-07-11 19:14:03 +02:00
Commander1024 e4a4aacc63 Added missing unique_id 2026-07-10 01:16:51 +02:00
Commander1024 0d12317089 revert 746dd833c8
revert Add unique_ids and calendar text sensors
2026-07-10 00:54:27 +02:00
Commander1024 746dd833c8 Add unique_ids and calendar text sensors 2026-07-10 00:38:16 +02:00
Commander1024 3eb482e40b Added IPv6 support. 2026-07-09 09:03:24 +02:00
Commander1024 e9a6064b4b Updated HA. 2026-07-09 08:59:52 +02:00
Commander1024 38616556cb Updated HA. 2026-07-02 09:12:17 +02:00
Commander1024 bef1cfb1d0 Updated HA. 2026-06-23 09:12:01 +02:00
Commander1024 0a8364d7ed Changed annunciators for climate control. 2026-06-23 09:11:50 +02:00
Commander1024 3eb9eb3a2f Archived deprecated raspiaudio muse luxe config. 2026-06-23 09:11:01 +02:00
Commander1024 7407955465 Removed cached icons from repo. 2026-06-23 09:10:18 +02:00
Commander1024 2ff8b5cafe Merged owon-ai-test into main. 2026-06-01 18:45:27 +02:00
Commander1024 7e08c4a8b9 Updated HA. 2026-06-01 18:42:56 +02:00
Commander1024 855f013095 Freedns test 2026-06-01 18:42:43 +02:00
Commander1024 a68b527c48 Various entity changees 2026-06-01 18:41:43 +02:00
Commander1024 cecec14be1 Added usage info. 2026-05-30 19:12:03 +02:00
Commander1024 4e5df1ad0c Updated file comments 2026-05-30 19:00:18 +02:00
Commander1024 2c16ba1679 Separated long-press buttons into own HA entityies to make them usable 2026-05-30 18:49:16 +02:00
Commander1024 c21f2a5c7b Display aestethics: taller 7-segments, repositioned minus, mode and
icons
2026-05-30 17:49:56 +02:00
Commander1024 2a1902ccf1 Add reset all / reset energy function to also clear the local meter 2026-05-30 17:06:31 +02:00
Commander1024 31848bf26c Add energy monitor using riemann sum algorithm. 2026-05-30 16:49:57 +02:00
Commander1024 d7337fd634 Set (somewhat) appropiate mdi icons for OWON HA sensors. 2026-05-30 08:22:32 +02:00
Commander1024 5b287303f1 Add backlight idle script 2026-05-28 22:58:15 +02:00
Commander1024 816cc35861 Implement dc-load inactivity display 2026-05-28 22:49:32 +02:00
Commander1024 058c186d63 Remove dead values from HA and rendering code. Also increase font size. 2026-05-28 22:24:46 +02:00
Commander1024 22e5f9d767 Implemented rough 1st iteration of dc-load value screen. 2026-05-28 22:09:54 +02:00
Commander1024 f9b32d06a4 Expose buttons Owon multimeter buttons to HA. 2026-05-28 16:04:53 +02:00
Commander1024 3f890ae02f Intregrated Atorch BLE proxy and migrated device name to a generic one. 2026-05-28 14:35:12 +02:00
Commander1024 f0cb74cc27 Deactivate all indicator lights on meter disconnect. 2026-05-28 13:40:46 +02:00
Commander1024 6038075e9e Fixed off-by-one error in rendering of the 7-segment segments. 2026-05-28 13:29:39 +02:00
Commander1024 2dbdfd213c Deactivate bargraph after BLE disconnect 2026-05-28 12:29:52 +02:00
Commander1024 e2eb886eef Renamed .h file to match .yaml filename. 2026-05-26 19:35:45 +02:00
Commander1024 f4f19eb847 Added attribution to original author and AI usage hint. 2026-05-26 19:25:56 +02:00
Commander1024 b3e307dcba RAM optimization, deactivated recording of display values. 2026-05-26 19:19:22 +02:00
Commander1024 1b3b4870e7 Ported the code to M5Stack Core2 (different display) and removed reboot on disconnect 2026-05-26 19:10:30 +02:00
Commander1024 69c2166414 Archived the Core1 Owon multimeter config 2026-05-26 18:00:35 +02:00
Commander1024 2fcf71d8ce Added Bluetooth and battery icons instead of text indicators 2026-05-26 17:36:58 +02:00
Commander1024 676829a988 Remove mode from Reading value - already present in Mode. 2026-05-26 17:21:11 +02:00
Commander1024 08201f1bc1 Uniform separation of 7-segment display font elements. 2026-05-26 17:10:43 +02:00
Commander1024 23cbd520a6 Repositioned DIODE/BUZZ and moved them out of the meter bar. Also slightly reworked 7-segment display font. Not perfect, yet though 2026-05-26 17:03:52 +02:00
Commander1024 6f4e2b9319 Moved AC/DC out of the way of the minus sign, added an h glyph for hFE display. 2026-05-26 16:44:25 +02:00
Commander1024 e5104fc005 Removed 2 unneeded values from HA reporting, fixed mV reporting value 2026-05-26 16:03:25 +02:00
Commander1024 91f3f69bd9 Optimized for mem pressure, inverted colors. 2026-05-26 15:34:03 +02:00
Commander1024 4bd62f539c Initial AI-assisted esphome device code for Owon B35T+ multimeter. 2026-05-26 13:45:06 +02:00
Commander1024 a3bd9ff213 Removed cached png from repo. 2026-05-21 09:35:21 +02:00
Commander1024 24fc4bf9f4 Enabled ipv6 for all esphome devices. 2026-05-21 09:24:48 +02:00
Commander1024 0bfc2eff34 Changed little things... 2026-05-21 09:24:11 +02:00
Commander1024 8f18a043af Updated HA + gitignore 2026-05-21 09:21:52 +02:00
Commander1024 5b0a992686 Changed light automations from time offset to sun elevation. 2026-04-29 07:27:53 +02:00
Commander1024 ef233296e9 Updated HA. 2026-04-29 07:27:22 +02:00
Commander1024 c0af2b734b Added HA Icon cache. 2026-04-23 09:56:53 +02:00
Commander1024 a94386babe Deleted obsolete glances config 2026-04-23 09:56:06 +02:00
Commander1024 f23166b765 Updated HA. 2026-04-23 09:55:38 +02:00
Commander1024 8438785241 Changed dryer zigbee plug. 2026-04-23 09:55:21 +02:00
Commander1024 b0b6093619 Added IKEA Bilresa dual button blueprint 2026-04-23 09:54:49 +02:00
Commander1024 c463a5e28f Added kitchen Music switch, changed 420 players, light offset by sun elevation 2026-04-23 09:52:59 +02:00
30 changed files with 4739 additions and 716 deletions
+1 -1
View File
@@ -1 +1 @@
2026.3.0
2026.7.2
+1 -1
View File
@@ -13,4 +13,4 @@ home-assistant.log*
zigbee.db*
*__pycache__*
.ha_run.lock
.cache
+157 -63
View File
@@ -2,9 +2,11 @@
alias: Licht bei Sonnenuntergang einschalten
description: ''
triggers:
- event: sunset
offset: '-1:00'
trigger: sun
- trigger: numeric_state
entity_id:
- sun.sun
attribute: elevation
below: 5
conditions:
- condition: state
entity_id: person.marcus_scholz
@@ -43,9 +45,11 @@
alias: Licht bei Sonnenaufgang ausschalten
description: ''
triggers:
- event: sunrise
offset: '+1:00'
trigger: sun
- trigger: numeric_state
entity_id:
- sun.sun
attribute: elevation
above: 5
conditions: []
actions:
- target:
@@ -135,27 +139,33 @@
trigger: time
conditions: []
actions:
- action: notify.mobile_app_apollo
data:
message: Lodere es, Lustknabe.
title: 420!
- data:
title: 420!
message: Lodere es, Lustknabe.
action: notify.mobile_app_le2123
- data:
cache: true
media_player_entity_id: media_player.raspiaudio_muse_luxe
message: 4 20 lodere es, Lustknabe
media_player_entity_id: media_player.home_assistant_voice_0a7c6b_media_player
message: 4 20, lodere es, Lustknabe
target:
entity_id: tts.piper
enabled: true
action: tts.speak
- data:
cache: true
media_player_entity_id: media_player.home_assistant_voice_09c0e7_media_player
message: 4 20 lodere es, Lustknabe
message: 4 20, lodere es, Lustknabe
target:
entity_id: tts.piper
enabled: true
action: tts.speak
- data:
cache: true
media_player_entity_id: media_player.m5stack_atom_echo
message: 4 20, lodere es, Lustknabe
target:
entity_id: tts.piper
enabled: true
action: tts.speak
- data:
qos: 0
@@ -169,6 +179,10 @@
topic: awtrix_desk/notify
payload: '{"text": "420, lodere es, Lustknabe!"}'
action: mqtt.publish
- action: notify.mobile_app_apollo
data:
message: Lodere es, Lustknabe.
title: 420!
mode: single
- id: '1623911524804'
alias: TV Anti-Reflexion (undo)
@@ -213,28 +227,28 @@
action: light.turn_on
- data:
cache: true
media_player_entity_id: media_player.home_assistant_voice_0a7c6b
media_player_entity_id: media_player.home_assistant_voice_0a7c6b_media_player
message: Ab ins Bett, Schlafenszeit.
target:
entity_id: tts.piper
action: tts.speak
enabled: true
action: tts.speak
- data:
cache: true
media_player_entity_id: media_player.home_assistant_voice_09c0e7_media_player_2
media_player_entity_id: media_player.home_assistant_voice_09c0e7_media_player
message: Ab ins Bett, Schlafenszeit.
target:
entity_id: tts.piper
action: tts.speak
enabled: true
action: tts.speak
- data:
cache: true
media_player_entity_id: media_player.m5stack_atom_echo
message: Ab ins Bett, Schlafenszeit.
target:
entity_id: tts.piper
action: tts.speak
enabled: true
action: tts.speak
mode: single
- id: '1623954512941'
alias: Licht im Schlafzimmer zum Aufwachen einschalten
@@ -264,8 +278,10 @@
alias: 'Anruf: Beim Klingeln grün blinken'
description: ''
triggers:
- entity_id: sensor.fritz_box_7490_call_monitor_dem_commander1024_seine_cloud
to: ringing
- entity_id:
- sensor.fritz_box_7690_anrufmonitor_dem_commander1024_seine_cloud
to:
- ringing
trigger: state
- entity_id:
- sensor.le2123_phone_state_2
@@ -287,8 +303,8 @@
- 0
target:
entity_id:
- light.awtrix_desk_indicator_1
- light.awtrix_kitchen_indicator_1
- light.awtrix_b8658c_indicator_1
- light.awtrix_528bd4_indicator_1
action: light.turn_on
- data:
scene_id: wled_last_state
@@ -321,16 +337,18 @@
- data: {}
target:
entity_id:
- light.awtrix_desk_indicator_1
- light.awtrix_kitchen_indicator_1
- light.awtrix_b8658c_indicator_1
- light.awtrix_528bd4_indicator_1
action: light.turn_off
mode: single
- id: '1625481640348'
alias: 'Anruf: Beim Telefonieren Musik pausieren'
description: ''
triggers:
- entity_id: sensor.fritz_box_7490_call_monitor_telefonbuch
to: talking
- entity_id:
- sensor.fritz_box_7690_anrufmonitor_dem_commander1024_seine_cloud
to:
- talking
trigger: state
- entity_id:
- sensor.le2123_phone_state_2
@@ -348,9 +366,8 @@
- data:
scene_id: media_last_state
snapshot_entities:
- media_player.wohnzimmer_main
- media_player.wohnzimmer
- media_player.lg_webos_smart_tv
- media_player.ccze
- media_player.spotify_marcus_scholz
- media_player.xboxonex
action: scene.create
@@ -371,7 +388,7 @@
trigger: state
- target:
entity_id:
- scene.media_last_state
- scene.wled_last_state
action: scene.turn_on
data: {}
mode: single
@@ -479,11 +496,10 @@
from: not_home
trigger: state
conditions:
- condition: sun
before: sunrise
before_offset: '+1'
after: sunset
after_offset: '-1'
- condition: numeric_state
entity_id: sun.sun
attribute: elevation
below: '5'
actions:
- data: {}
action: script.moodlight_orange_plasma
@@ -602,12 +618,33 @@
action: notify.mobile_app_le2123
- data:
cache: true
media_player_entity_id: media_player.raspiaudio_muse_luxe
media_player_entity_id: media_player.home_assistant_voice_0a7c6b_media_player
message: '{{message}}
'
target:
entity_id: tts.piper
enabled: true
action: tts.speak
- data:
cache: true
media_player_entity_id: media_player.home_assistant_voice_09c0e7_media_player
message: '{{message}}
'
target:
entity_id: tts.piper
enabled: true
action: tts.speak
- data:
cache: true
media_player_entity_id: media_player.m5stack_atom_echo
message: '{{message}}
'
target:
entity_id: tts.piper
enabled: true
action: tts.speak
variables:
message: 'Die Außentemperatur ist mit {{states(''sensor.aussentemperatur'')}}
@@ -652,12 +689,33 @@
action: notify.mobile_app_le2123
- data:
cache: true
media_player_entity_id: media_player.raspiaudio_muse_luxe
media_player_entity_id: media_player.home_assistant_voice_0a7c6b_media_player
message: '{{message}}
'
target:
entity_id: tts.piper
enabled: true
action: tts.speak
- data:
cache: true
media_player_entity_id: media_player.home_assistant_voice_09c0e7_media_player
message: '{{message}}
'
target:
entity_id: tts.piper
enabled: true
action: tts.speak
- data:
cache: true
media_player_entity_id: media_player.m5stack_atom_echo
message: '{{message}}
'
target:
entity_id: tts.piper
enabled: true
action: tts.speak
mode: single
variables:
@@ -817,9 +875,11 @@
alias: Licht bei Sonnenuntergang einschalten (XMas)
description: ''
triggers:
- event: sunset
offset: '-1:00'
trigger: sun
- trigger: numeric_state
entity_id:
- sun.sun
attribute: elevation
below: 5
conditions:
- condition: state
entity_id: person.marcus_scholz
@@ -846,9 +906,11 @@
alias: Licht bei Sonnenaufgang ausschalten (XMas)
description: ''
triggers:
- event: sunrise
offset: '+1:00'
trigger: sun
- trigger: numeric_state
entity_id:
- sun.sun
attribute: elevation
above: 5
conditions: []
actions:
- target:
@@ -1034,26 +1096,19 @@
domain: switch
action_button_up_double:
- type: turn_on
device_id: 32d15eec63fe68d5770813d47da53657
entity_id: 14edfa97dae4936040b97dc77b171d5f
device_id: 98a9eef603f8c8ae6200c4834a3dbf4f
entity_id: fdcff3852ecc17099c953da17eb4b8a3
domain: light
button_up_double_press: true
button_down_double_press: true
action_button_down_double:
- type: turn_off
device_id: 32d15eec63fe68d5770813d47da53657
entity_id: 14edfa97dae4936040b97dc77b171d5f
domain: light
action_button_up_long:
- type: turn_on
device_id: 98a9eef603f8c8ae6200c4834a3dbf4f
entity_id: fdcff3852ecc17099c953da17eb4b8a3
domain: light
action_button_down_long:
- type: turn_off
device_id: 98a9eef603f8c8ae6200c4834a3dbf4f
entity_id: fdcff3852ecc17099c953da17eb4b8a3
domain: light
action_button_up_long: []
action_button_down_long: []
button_up_long_max_loop_repeats: 4205
- id: '1727953214758'
alias: Waschmaschine läuft
description: Prüfen ob die Waschmaschine läuft, dann den boolschen Schalter "Waschmaschine
@@ -1145,8 +1200,8 @@
aktiv" einschalten
triggers:
- type: power
device_id: 81e357077ac8d1f8f588fbb5324965b5
entity_id: fac93f9504dc8d1267bec7d02849d491
device_id: 25a822b76a0206496bd216235b034fb0
entity_id: 3321a33261062dab534569c3f6f53d69
domain: sensor
trigger: device
above: 150
@@ -1157,8 +1212,8 @@
conditions:
- condition: device
type: is_on
device_id: 81e357077ac8d1f8f588fbb5324965b5
entity_id: 2a2659668772d9345e42667900832f0e
device_id: 25a822b76a0206496bd216235b034fb0
entity_id: 4748aace637a3f1d8b5e896236564024
domain: switch
actions:
- action: input_boolean.turn_on
@@ -1172,11 +1227,11 @@
Uhr.
triggers:
- type: power
device_id: 81e357077ac8d1f8f588fbb5324965b5
entity_id: fac93f9504dc8d1267bec7d02849d491
device_id: 25a822b76a0206496bd216235b034fb0
entity_id: 3321a33261062dab534569c3f6f53d69
domain: sensor
trigger: device
below: 150
below: 250
for:
hours: 0
minutes: 5
@@ -1184,8 +1239,8 @@
conditions:
- condition: device
type: is_on
device_id: 81e357077ac8d1f8f588fbb5324965b5
entity_id: 2a2659668772d9345e42667900832f0e
device_id: 25a822b76a0206496bd216235b034fb0
entity_id: 4748aace637a3f1d8b5e896236564024
domain: switch
- condition: state
entity_id: input_boolean.waschetrockner_aktiv
@@ -1363,3 +1418,42 @@
motion_cutoff: 10
illuminance_low_threshold: 10
battery_low_threshold: 20
- id: '1772920063868'
alias: IKEA BILRESA Dual Button Küche Musik
description: ''
use_blueprint:
path: Skaronator/ikea-bilresa-dual-button.yaml
input:
button1_event:
- event.bilresa_dual_button_taste_1
button2_event:
- event.bilresa_dual_button_taste_2
button1_single:
- type: turn_on
device_id: 6d1be741876624a70ab5b01b54c6fd6f
entity_id: 621e4377a089b7f5d92c7c9f2cc171a1
domain: switch
button2_single:
- type: turn_off
device_id: 6d1be741876624a70ab5b01b54c6fd6f
entity_id: 621e4377a089b7f5d92c7c9f2cc171a1
domain: switch
- id: epaper_daily_demotivational_quote
alias: Epaper tägliches Demotivationszitat
description: Wählt täglich ein festes Zitat aus der lokalen Zitatbibliothek aus.
triggers:
- trigger: homeassistant
event: start
- trigger: time
at: "00:00:00"
conditions: []
actions:
- action: input_select.select_option
target:
entity_id: input_select.epaper_demotivationszitat
data:
option: >-
{% set quotes = state_attr('input_select.epaper_demotivationszitat', 'options') %}
{{ quotes[(now().strftime('%j') | int - 1) % (quotes | count)] }}
mode: single
@@ -0,0 +1,302 @@
mode: restart
blueprint:
name: IKEA BILRESA Dual Button
author: Skaronator
description: "# IKEA BILRESA Dual Button Remote Automation\n\n## Current Version
2.0.1\n\n## Features\n\nSimple automation blueprint for IKEA BILRESA dual-button
remote.\n\n### Supported Actions for Each Button\n\n* Single Press\n* Double Press\n*
Long Press (on press and release)\n* While Holding (with configurable interval)\n\n###
Configuration Options\n\n* Configurable interval for \"while holding\" actions.\n*
Safety limit on maximum iterations for \"while holding\" actions to prevent infinite
loops.\n\n## Changelog\n\n### 2.0.1\n - Removed Matter integration filter for
button event selector. This will allow users to use the Ikea BILRESA remote over
3rd party Matter integration.\n### 2.0.0\n - Added support for selecting multiple
entities for each button. Allowing to have multiple remotes triggering the same
actions.\n - See [these upgrade notes](https://community.home-assistant.io/t/ikea-bilresa-dual-button-remote/965815/28?u=skaronator)
for migrating existing automations using this blueprint.\n### 1.2.2\n - Improved
descriptions and documentation.\n### 1.2.1\n - Improved descriptions for blueprint
inputs.\n### 1.2.0\n - Added max iterations setting for \"while holding\" actions
to prevent infinite loops.\n### 1.1.0\n - Added \"while holding\" actions for
both buttons with configurable interval.\n### 1.0.0\n - Initial release with
support for all button actions.\n\n## Support and Feedback\n\n* GitHub: [Skaronator/home-assistant-blueprints](https://github.com/Skaronator/home-assistant-blueprints)\n*
Blueprint Exchange: [IKEA BILRESA Dual Button](https://community.home-assistant.io/t/ikea-bilresa-dual-button-remote/965815)\n\nThank
you for using this blueprint! Your feedback and contributions are welcome. \U0001F49A\n"
domain: automation
source_url: https://raw.githubusercontent.com/Skaronator/home-assistant-blueprints/refs/heads/main/automation/ikea-bilresa-dual-button.yaml
homeassistant:
min_version: 2025.12.0
input:
setup:
name: ⚙️ Button Setup
description: 'Select the event entities for all 2 buttons on the IKEA BILRESA
dual-button remote.
'
collapsed: true
input:
button1_event:
name: Entity
description: 'Select the event entity for Button 1
'
selector:
entity:
multiple: true
filter:
- domain:
- event
device_class:
- button
reorder: false
button2_event:
name: Entity
description: 'Select the event entity for Button 2
'
selector:
entity:
multiple: true
filter:
- domain:
- event
device_class:
- button
reorder: false
button1:
name: '1️⃣ Button 1 Actions
'
description: 'Define actions when pressing Button 1.
'
collapsed: true
input:
button1_single:
name: Single Press
description: Action for Button 1 single press.
default: []
selector:
action: {}
button1_double:
name: Double Press
description: Action for Button 1 double press.
default: []
selector:
action: {}
button1_long_press:
name: Long Press (on press)
description: Action for Button 1 long press completion.
default: []
selector:
action: {}
button1_long_release:
name: Long Press (on release)
description: Action for Button 1 long press completion.
default: []
selector:
action: {}
button1_holding_interval:
name: Holding Interval (seconds)
description: Interval in seconds for repeating "while holding" actions.
default: 0.2
selector:
number:
min: 0.05
max: 2.0
step: 0.05
mode: slider
button1_holding_max_iterations:
name: Holding Max Iterations
description: Maximum number of iterations for "while holding" actions. This
is a safety limit to prevent infinite loops.
default: 100
selector:
number:
min: 1.0
max: 1000.0
step: 1.0
mode: slider
button1_while_holding:
name: While Holding
description: Action for Button 1 while holding. This action will be repeated
at the defined interval while the button is held down.
default: []
selector:
action: {}
button2:
name: '2️⃣ Button 2 Actions
'
description: 'Define actions when pressing Button 2.
'
collapsed: true
input:
button2_single:
name: Single Press
description: Action for Button 2 single press.
default: []
selector:
action: {}
button2_double:
name: Double Press
description: Action for Button 2 double press.
default: []
selector:
action: {}
button2_long_press:
name: Long Press (on press)
description: Action for Button 2 long press completion.
default: []
selector:
action: {}
button2_long_release:
name: Long Press (on release)
description: Action for Button 2 long press completion.
default: []
selector:
action: {}
button2_holding_interval:
name: Holding Interval (seconds)
description: Interval in seconds for repeating "while holding" actions.
default: 0.2
selector:
number:
min: 0.05
max: 2.0
step: 0.05
mode: slider
button2_holding_max_iterations:
name: Holding Max Iterations
description: Maximum number of iterations for "while holding" actions. This
is a safety limit to prevent infinite loops.
default: 100
selector:
number:
min: 1.0
max: 1000.0
step: 1.0
mode: slider
button2_while_holding:
name: While Holding
description: Action for Button 2 while holding. This action will be repeated
at the defined interval while the button is held down.
default: []
selector:
action: {}
trigger:
- platform: state
id: button1
entity_id: !input button1_event
- platform: state
id: button2
entity_id: !input button2_event
condition:
- condition: template
value_template: '{{ trigger.from_state.state not in [''unknown'', ''unavailable'']
}}
'
- condition: template
value_template: '{{ trigger.to_state.state not in [''unknown'', ''unavailable'']
}}
'
- condition: template
value_template: '{{ trigger.from_state.state != trigger.to_state.state }}
'
action:
- variables:
trigger_id: '{{ trigger.id }}'
press_type: '{{ trigger.to_state.attributes.event_type }}'
button1_max_iterations: !input button1_holding_max_iterations
button2_max_iterations: !input button2_holding_max_iterations
- parallel:
- repeat:
while:
- condition: template
value_template: '{{ press_type == ''long_press'' and trigger_id == ''button1''
and states(trigger.entity_id) == trigger.to_state.state }}
'
- condition: template
value_template: '{{ repeat.index|default(0) <= button1_max_iterations }}
'
sequence:
- sequence: !input button1_while_holding
- delay:
seconds: !input button1_holding_interval
- repeat:
while:
- condition: template
value_template: '{{ press_type == ''long_press'' and trigger_id == ''button2''
and states(trigger.entity_id) == trigger.to_state.state }}
'
- condition: template
value_template: '{{ repeat.index|default(0) <= button2_max_iterations }}
'
sequence:
- sequence: !input button2_while_holding
- delay:
seconds: !input button2_holding_interval
- choose:
- conditions:
- condition: template
value_template: '{{ trigger_id == ''button1'' and press_type == ''multi_press_1''
}}
'
sequence: !input button1_single
- conditions:
- condition: template
value_template: '{{ trigger_id == ''button1'' and press_type == ''multi_press_2''
}}
'
sequence: !input button1_double
- conditions:
- condition: template
value_template: '{{ trigger_id == ''button1'' and press_type == ''long_press''
}}
'
sequence: !input button1_long_press
- conditions:
- condition: template
value_template: '{{ trigger_id == ''button1'' and press_type == ''long_release''
}}
'
sequence: !input button1_long_release
- conditions:
- condition: template
value_template: '{{ trigger_id == ''button2'' and press_type == ''multi_press_1''
}}
'
sequence: !input button2_single
- conditions:
- condition: template
value_template: '{{ trigger_id == ''button2'' and press_type == ''multi_press_2''
}}
'
sequence: !input button2_double
- conditions:
- condition: template
value_template: '{{ trigger_id == ''button2'' and press_type == ''long_press''
}}
'
sequence: !input button2_long_press
- conditions:
- condition: template
value_template: '{{ trigger_id == ''button2'' and press_type == ''long_release''
}}
'
sequence: !input button2_long_release
+11
View File
@@ -35,6 +35,12 @@ utility_meter: !include utility_meters.yaml
mqtt: !include mqtt.yaml
mqtt_statestream: !include mqtt_statestream.yaml
# Calendar data is normalised in a sandboxed script before ESPHome receives it.
python_script:
# Home Assistant-managed e-paper quote library
input_select: !include input_select.yaml
# Template sensors
template: !include template.yaml
@@ -82,3 +88,8 @@ generic_hygrostat:
frontend:
themes: !include_dir_merge_named themes
freedns:
url: !secret freedns6_url
# - name: IPv4
# url: !secret freedns4_url
@@ -38,10 +38,13 @@ ota:
platform: esphome
password: !secret ota
network:
enable_ipv6: true
wifi:
ssid: "Voltage-legacy"
password: !secret voltage_legacy_psk
use_address: atorch-dc-load.home
#use_address: atorch-dc-load.home
power_save_mode: high
fast_connect: on
min_auth_mode: WPA2
@@ -0,0 +1,293 @@
substitutions:
name: "owon-b35t"
friendly_name: "OWON B35T Multimeter"
device_description: "M5Stack Core 1 BLE client for OWON B35T/B35T+ multimeter with local graphical display"
owon_mac_address: !secret owon_b35t_mac_address
esphome:
name: ${name}
friendly_name: ${friendly_name}
comment: ${device_description}
min_version: 2024.6.0
includes:
- owon_b35t-m5stack-core1.h
project:
name: "custom.owon-b35t-m5stack"
version: "1.0"
esp32:
board: m5stack-core-esp32
framework:
type: esp-idf
advanced:
minimum_chip_revision: "3.1"
logger:
level: INFO
api:
encryption:
key: !secret apikey
ota:
platform: esphome
password: !secret ota
wifi:
ssid: "Voltage-legacy"
password: !secret voltage_legacy_psk
#use_address: ${name}.home
power_save_mode: none
fast_connect: on
min_auth_mode: WPA2
ap:
ssid: "OWON B35T Fallback Hotspot"
password: !secret fallback_psk
# Disabled to save RAM on the M5Stack Core 1. Re-enable temporarily if Wi-Fi recovery is needed.
# captive_portal:
interval:
- interval: 10s
then:
- lambda: |-
ESP_LOGI("mem", "heap free=%u min_free=%u internal_free=%u internal_largest=%u dma_free=%u dma_largest=%u",
static_cast<unsigned>(esp_get_free_heap_size()),
static_cast<unsigned>(esp_get_minimum_free_heap_size()),
static_cast<unsigned>(heap_caps_get_free_size(MALLOC_CAP_INTERNAL)),
static_cast<unsigned>(heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL)),
static_cast<unsigned>(heap_caps_get_free_size(MALLOC_CAP_DMA)),
static_cast<unsigned>(heap_caps_get_largest_free_block(MALLOC_CAP_DMA)));
esp32_ble_tracker:
scan_parameters:
active: true
continuous: true
ble_client:
- mac_address: ${owon_mac_address}
id: owon_ble_client
on_connect:
then:
- lambda: |-
owon_meter.on_connect();
on_disconnect:
then:
- lambda: |-
owon_meter.on_disconnect();
- logger.log:
level: WARN
format: "OWON BLE meter disconnected; restarting M5Stack to reclaim heap"
- delay: 1s
- lambda: |-
esp_restart();
spi:
clk_pin: GPIO18
mosi_pin: GPIO23
miso_pin: GPIO19
output:
- platform: ledc
pin: GPIO32
id: lcd_backlight
light:
- platform: monochromatic
output: lcd_backlight
name: "${friendly_name} Backlight"
id: backlight
restore_mode: ALWAYS_ON
font:
- file: "fonts/Roboto-Regular.ttf"
id: meter_font
size: 15
glyphs:
[
" ",
"!",
"%",
"+",
"-",
".",
"/",
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"<",
">",
"A",
"B",
"C",
"D",
"E",
"F",
"G",
"H",
"I",
"L",
"M",
"N",
"O",
"P",
"R",
"S",
"T",
"U",
"V",
"W",
"X",
"Y",
"Z",
"a",
"c",
"d",
"e",
"f",
"g",
"h",
"i",
"k",
"m",
"n",
"o",
"r",
"s",
"t",
"u",
"v",
"w",
"y",
"z",
"°",
"µ",
"Ω",
]
display:
- platform: ili9xxx
id: lcd
model: M5STACK
cs_pin: GPIO14
dc_pin: GPIO27
reset_pin: GPIO33
invert_colors: true
color_palette: 8BIT
rotation: 0
update_interval: 500ms
lambda: |-
owon_meter.render(it, id(meter_font));
binary_sensor:
- platform: gpio
id: button_a
pin:
number: GPIO39
inverted: true
internal: true
on_press:
then:
- lambda: |-
owon_meter.previous_button();
- platform: gpio
id: button_b
pin:
number: GPIO38
inverted: true
internal: true
on_click:
- min_length: 50ms
max_length: 1500ms
then:
- logger.log:
level: INFO
format: "OWON short press: %s"
args: ["owon_meter.selected_button_name()"]
- ble_client.ble_write:
id: owon_ble_client
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb"
value: !lambda |-
std::vector<uint8_t> data = {owon_meter.selected_button, 0x01};
return data;
- min_length: 1500ms
max_length: 5000ms
then:
- logger.log:
level: INFO
format: "OWON long press: %s"
args: ["owon_meter.selected_button_name()"]
- ble_client.ble_write:
id: owon_ble_client
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb"
value: !lambda |-
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};
return data;
- platform: gpio
id: button_c
pin:
number: GPIO37
inverted: true
internal: true
on_press:
then:
- lambda: |-
owon_meter.next_button();
- platform: template
name: "${friendly_name} Connected"
lambda: |-
return owon_meter.connected;
- platform: template
name: "${friendly_name} Overload"
lambda: |-
return owon_meter.overload;
- platform: template
name: "${friendly_name} Low Battery"
lambda: |-
return owon_meter.low_battery;
sensor:
- platform: ble_client
type: characteristic
ble_client_id: owon_ble_client
id: owon_notify_source
internal: true
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
characteristic_uuid: "0000fff4-0000-1000-8000-00805f9b34fb"
notify: true
update_interval: never
lambda: |-
owon_meter.handle_notify(x);
return owon_meter.value();
- platform: wifi_signal
name: "${friendly_name} WiFi Signal"
update_interval: 60s
text_sensor:
- platform: template
name: "${friendly_name} Reading"
update_interval: 2s
lambda: |-
return owon_meter.reading_text();
- platform: template
name: "${friendly_name} Unit"
update_interval: 2s
lambda: |-
return std::string(owon_meter.scale()) + owon_meter.unit();
- platform: template
name: "${friendly_name} Mode"
update_interval: 2s
lambda: |-
return owon_meter.mode_text();
+658
View File
@@ -0,0 +1,658 @@
/*
* ESPHome helper for OWON B35T/B35T+ BLE meter on M5Stack Core 1.
* Parser is based on the standalone Arduino sketch by Reaper7
* (Beerware license, Revision 42) and Dean Cording's owonb35 notes.
*/
#pragma once
#include <cmath>
#include <cstdint>
#include <cstring>
#include <string>
#include <vector>
#include "esp_heap_caps.h"
#include "esp_system.h"
#include "esphome/core/helpers.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/core/log.h"
#include "esphome/components/display/display.h"
namespace owon_b35t {
using esphome::Color;
using esphome::display::Display;
static const char *const TAG = "owon_b35t";
static const char *const POWER_TAG = "core2_power";
static constexpr uint8_t AXP192_ADDR = 0x34;
static esphome::i2c::I2CDevice axp192;
static bool axp192_ready = false;
static bool axp_write(uint8_t reg, uint8_t value) {
if (!axp192_ready) return false;
bool ok = axp192.write_byte(reg, value);
if (!ok) ESP_LOGW(POWER_TAG, "AXP192 write reg 0x%02X failed", reg);
return ok;
}
static bool axp_read(uint8_t reg, uint8_t *value) {
if (!axp192_ready) return false;
bool ok = axp192.read_byte(reg, value);
if (!ok) ESP_LOGW(POWER_TAG, "AXP192 read reg 0x%02X failed", reg);
return ok;
}
static void axp_update(uint8_t reg, uint8_t clear_mask, uint8_t set_mask) {
uint8_t value = 0;
if (!axp_read(reg, &value)) return;
value = (value & ~clear_mask) | set_mask;
axp_write(reg, value);
}
static uint8_t axp_dc_voltage_data(uint16_t millivolts) {
if (millivolts < 700) millivolts = 700;
if (millivolts > 3500) millivolts = 3500;
return static_cast<uint8_t>((millivolts - 700) / 25) & 0x7F;
}
static uint8_t axp_ldo_voltage_data(uint16_t millivolts) {
if (millivolts < 1800) millivolts = 1800;
if (millivolts > 3300) millivolts = 3300;
return static_cast<uint8_t>((millivolts - 1800) / 100) & 0x0F;
}
static void core2_axp192_set_lcd_voltage(uint16_t millivolts) {
uint8_t value = 0;
axp_read(0x27, &value);
axp_write(0x27, (value & 0x80) | axp_dc_voltage_data(millivolts)); // DCDC3, LCD backlight
}
static void core2_axp192_set_backlight(float brightness) {
if (brightness <= 0.0f) {
axp_update(0x12, 0x02, 0x00); // DCDC3 off
return;
}
if (brightness > 1.0f) brightness = 1.0f;
uint16_t millivolts = static_cast<uint16_t>(2400 + brightness * 900);
core2_axp192_set_lcd_voltage(millivolts);
axp_update(0x12, 0x00, 0x02); // DCDC3 on
}
static void core2_axp192_init(esphome::i2c::I2CBus *bus) {
axp192.set_i2c_bus(bus);
axp192.set_i2c_address(AXP192_ADDR);
axp192_ready = true;
ESP_LOGI(POWER_TAG, "Initializing M5Stack Core2 AXP192 LCD power");
axp_update(0x30, 0xF9, 0x02); // Disable VBUS current limit, preserve bit 2.
axp_update(0x92, 0x07, 0x00); // GPIO1 open-drain output.
axp_update(0x93, 0x07, 0x00); // GPIO2 open-drain output.
axp_write(0x35, 0xA2); // RTC battery charging.
uint8_t value = 0;
axp_read(0x26, &value);
axp_write(0x26, (value & 0x80) | axp_dc_voltage_data(3350)); // DCDC1 ESP32 VDD.
core2_axp192_set_lcd_voltage(2800);
uint8_t ldo2 = axp_ldo_voltage_data(3300);
uint8_t ldo3 = axp_ldo_voltage_data(2000);
axp_write(0x28, (ldo2 << 4) | ldo3); // LDO2 LCD logic/SD, LDO3 vibrator.
axp_update(0x12, 0x00, 0x07); // Enable DCDC1, DCDC3, LDO2.
axp_write(0x82, 0xFF); // ADCs on.
axp_update(0x95, 0x8D, 0x84); // GPIO4 setup, as M5Core2 library does.
axp_write(0x36, 0x4C); // Power key timing.
// LCD reset through AXP192 GPIO4.
axp_update(0x96, 0x02, 0x00);
delay(100);
axp_update(0x96, 0x00, 0x02);
delay(100);
core2_axp192_set_backlight(1.0f);
}
static const uint8_t ACCU_BMP[32] = {
0b00000000, 0b00000000,
0b00000000, 0b00000000,
0b00000000, 0b00000000,
0b11111111, 0b11111110,
0b10000000, 0b00000010,
0b10000000, 0b00000011,
0b10000000, 0b00000011,
0b10000000, 0b00000011,
0b10000000, 0b00000011,
0b10000000, 0b00000011,
0b10000000, 0b00000011,
0b10000000, 0b00000010,
0b11111111, 0b11111110,
0b00000000, 0b00000000,
0b00000000, 0b00000000,
0b00000000, 0b00000000,
};
static const uint8_t BLE_BMP[32] = {
0b00000001, 0b10000000,
0b00000001, 0b11000000,
0b00010001, 0b01100000,
0b00011001, 0b00110000,
0b00001101, 0b00011000,
0b00000111, 0b00110000,
0b00000011, 0b01100000,
0b00000001, 0b11000000,
0b00000001, 0b11000000,
0b00000011, 0b01100000,
0b00000111, 0b00110000,
0b00001101, 0b00011000,
0b00011001, 0b00110000,
0b00010001, 0b01100000,
0b00000001, 0b11000000,
0b00000001, 0b10000000,
};
static const uint8_t DIODE_BMP[32] = {
0b00001000, 0b00011000,
0b00001100, 0b00011000,
0b00001110, 0b00011000,
0b00001111, 0b00011000,
0b00001111, 0b10011000,
0b00001111, 0b11011000,
0b00001111, 0b11111000,
0b11111111, 0b11111111,
0b11111111, 0b11111111,
0b00001111, 0b11111000,
0b00001111, 0b11011000,
0b00001111, 0b10011000,
0b00001111, 0b00011000,
0b00001110, 0b00011000,
0b00001100, 0b00011000,
0b00001000, 0b00011000,
};
static const uint8_t BUZZ_BMP[32] = {
0b00000000, 0b11000000,
0b00000001, 0b11000000,
0b00000011, 0b11000001,
0b00000111, 0b11000001,
0b00001111, 0b11000101,
0b11111111, 0b11000101,
0b11111111, 0b11010101,
0b11111111, 0b11010101,
0b11111111, 0b11010101,
0b11111111, 0b11010101,
0b11111111, 0b11000101,
0b00001111, 0b11000101,
0b00000111, 0b11000001,
0b00000011, 0b11000001,
0b00000001, 0b11000000,
0b00000000, 0b11000000,
};
class Meter {
public:
static constexpr uint8_t REGPLUSMINUS = 0x00;
static constexpr uint8_t FLAGPLUS = 0b00101011;
static constexpr uint8_t FLAGMINUS = 0b00101101;
static constexpr uint8_t REGDIG1 = 0x01;
static constexpr uint8_t REGDIG2 = 0x02;
static constexpr uint8_t REGDIG3 = 0x03;
static constexpr uint8_t REGDIG4 = 0x04;
static constexpr uint8_t REGPOINT = 0x06;
static constexpr uint8_t FLAGPOINT0 = 0b00110000;
static constexpr uint8_t FLAGPOINT1 = 0b00110001;
static constexpr uint8_t FLAGPOINT2 = 0b00110010;
static constexpr uint8_t FLAGPOINT3 = 0b00110100;
static constexpr uint8_t REGMODE = 0x07;
static constexpr uint8_t FLAGMODEHOLD = 0b00000010;
static constexpr uint8_t FLAGMODEREL = 0b00000100;
static constexpr uint8_t FLAGMODEAC = 0b00001000;
static constexpr uint8_t FLAGMODEDC = 0b00010000;
static constexpr uint8_t FLAGMODEAUTO = 0b00100000;
static constexpr uint8_t REGMINMAX = 0x08;
static constexpr uint8_t FLAGMIN = 0b00010000;
static constexpr uint8_t FLAGMAX = 0b00100000;
static constexpr uint8_t REGSCALE = 0x09;
static constexpr uint8_t FLAGSCALEDUTY = 0b00000010;
static constexpr uint8_t FLAGSCALEDIODE = 0b00000100;
static constexpr uint8_t FLAGSCALEBUZZ = 0b00001000;
static constexpr uint8_t FLAGSCALEMEGA = 0b00010000;
static constexpr uint8_t FLAGSCALEKILO = 0b00100000;
static constexpr uint8_t FLAGSCALEMILLI = 0b01000000;
static constexpr uint8_t FLAGSCALEMICRO = 0b10000000;
static constexpr uint8_t REGUNIT = 0x0a;
static constexpr uint8_t FLAGUNITFAHR = 0b00000001;
static constexpr uint8_t FLAGUNITGRAD = 0b00000010;
static constexpr uint8_t FLAGUNITNF = 0b00000100;
static constexpr uint8_t FLAGUNITHZ = 0b00001000;
static constexpr uint8_t FLAGUNITHFE = 0b00010000;
static constexpr uint8_t FLAGUNITOHM = 0b00100000;
static constexpr uint8_t FLAGUNITAMP = 0b01000000;
static constexpr uint8_t FLAGUNITVOLT = 0b10000000;
bool connected{false};
bool write_available{false};
bool is_plus{false};
bool low_battery{false};
bool overload{false};
bool has_reading{false};
uint8_t selected_button{1};
uint32_t last_notify_ms{0};
bool handle_notify(const std::vector<uint8_t> &data) {
if (data.size() > sizeof(this->raw_))
return false;
if (data.size() == 6 && data[1] >= 0xF0) {
memset(this->raw_, 0, sizeof(this->raw_));
memcpy(this->raw_, data.data(), data.size());
this->is_plus = true;
this->parse_plus_();
} else if (data.size() == 14 && data[12] == 0x0D && data[13] == 0x0A) {
memset(this->value_, 0, sizeof(this->value_));
memcpy(this->value_, data.data(), data.size());
this->is_plus = false;
} else {
ESP_LOGW(TAG, "Ignoring unexpected OWON frame length=%u", static_cast<unsigned>(data.size()));
return false;
}
this->overload = memcmp(this->value_, OVERLOAD_FRAME, sizeof(OVERLOAD_FRAME)) == 0;
this->display_value = this->calc_display_value_();
this->base_value = this->calc_base_value_();
this->has_reading = true;
this->last_notify_ms = millis();
return true;
}
void on_connect() {
this->connected = true;
this->write_available = true;
}
void on_disconnect() {
this->connected = false;
this->write_available = false;
}
float value() const { return this->display_value; }
float value_base() const { return this->base_value; }
bool negative() const { return (this->value_[REGPLUSMINUS] & FLAGMINUS) == FLAGMINUS; }
bool auto_range() const { return (this->value_[REGMODE] & FLAGMODEAUTO) == FLAGMODEAUTO; }
bool hold() const { return (this->value_[REGMODE] & FLAGMODEHOLD) == FLAGMODEHOLD; }
bool relative() const { return (this->value_[REGMODE] & FLAGMODEREL) == FLAGMODEREL; }
bool ac() const { return (this->value_[REGMODE] & FLAGMODEAC) == FLAGMODEAC; }
bool dc() const { return (this->value_[REGMODE] & FLAGMODEDC) == FLAGMODEDC; }
bool min_mode() const { return (this->value_[REGMINMAX] & FLAGMIN) == FLAGMIN; }
bool max_mode() const { return (this->value_[REGMINMAX] & FLAGMAX) == FLAGMAX; }
bool diode() const { return (this->value_[REGSCALE] & FLAGSCALEDIODE) == FLAGSCALEDIODE; }
bool continuity() const { return (this->value_[REGSCALE] & FLAGSCALEBUZZ) == FLAGSCALEBUZZ; }
const char *unit() const {
switch (this->value_[REGUNIT]) {
case FLAGUNITFAHR: return "°F";
case FLAGUNITGRAD: return "°C";
case FLAGUNITNF: return "nF";
case FLAGUNITHZ: return "Hz";
case FLAGUNITHFE: return "hFE";
case FLAGUNITOHM: return "Ω";
case FLAGUNITAMP: return "A";
case FLAGUNITVOLT: return "V";
default: return "";
}
}
const char *scale() const {
if ((this->value_[REGSCALE] & FLAGSCALEDUTY) == FLAGSCALEDUTY) return "%";
if ((this->value_[REGSCALE] & FLAGSCALEMEGA) == FLAGSCALEMEGA) return "M";
if ((this->value_[REGSCALE] & FLAGSCALEKILO) == FLAGSCALEKILO) return "k";
if ((this->value_[REGSCALE] & FLAGSCALEMILLI) == FLAGSCALEMILLI) return "m";
if ((this->value_[REGSCALE] & FLAGSCALEMICRO) == FLAGSCALEMICRO) return "µ";
return "";
}
std::string mode_text() const {
std::string out;
if (this->dc()) out += "DC ";
if (this->ac()) out += "AC ";
if (this->auto_range()) out += "AUTO ";
if (this->hold()) out += "HOLD ";
if (this->relative()) out += "REL ";
if (this->min_mode()) out += "MIN ";
if (this->max_mode()) out += "MAX ";
if (this->diode()) out += "DIODE ";
if (this->continuity()) out += "CONT ";
if (!out.empty()) out.pop_back();
return out;
}
std::string reading_text() const {
if (!this->connected) return "Disconnected";
if (!this->has_reading) return "Waiting for data";
if (this->overload) return "OL " + std::string(this->scale()) + this->unit();
char buf[48];
snprintf(buf, sizeof(buf), "%s%.4g %s%s", this->negative() ? "-" : "", std::fabs(this->display_value), this->scale(), this->unit());
return std::string(buf);
}
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 {
if ((this->value_[REGSCALE] & FLAGSCALEDUTY) == FLAGSCALEDUTY) return KIND_DUTY;
switch (this->value_[REGUNIT]) {
case FLAGUNITVOLT: return KIND_VOLTAGE;
case FLAGUNITAMP: return KIND_CURRENT;
case FLAGUNITOHM: return KIND_RESISTANCE;
case FLAGUNITHZ: return KIND_FREQUENCY;
case FLAGUNITNF: return KIND_CAPACITANCE;
case FLAGUNITGRAD: return KIND_TEMP_C;
case FLAGUNITFAHR: return KIND_TEMP_F;
default: return KIND_OTHER;
}
}
const char *selected_button_name() const {
static const char *const names[] = {"SELECT", "RANGE", "HLD/LIG", "REL/BT", "HZ/DUTY", "MAX/MIN"};
uint8_t index = this->selected_button;
if (index < 1) index = 1;
if (index > 6) index = 6;
return names[index - 1];
}
void previous_button() {
if (this->selected_button > 1) this->selected_button--;
}
void next_button() {
if (this->selected_button < 6) this->selected_button++;
}
void render(Display &it, esphome::display::BaseFont *font) {
const Color bg(0, 0, 0);
const Color fg(210, 210, 210);
// Chosen to map to a neutral dark gray in the RGB332 8-bit display palette.
const Color inactive(80, 80, 80);
const Color yellow(255, 220, 0);
const Color blue(0, 80, 255);
const Color cyan(0, 255, 255);
const Color magenta(255, 0, 255);
const Color red(255, 0, 0);
const Color green(0, 220, 0);
const Color orange(255, 165, 0);
it.fill(bg);
bool status_active = this->connected && this->has_reading;
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->label_(it, font, 86, 8, "AUTO", status_active && this->auto_range() ? fg : inactive);
this->label_(it, font, 138, 8, "MAX", status_active && this->max_mode() ? red : inactive);
this->label_(it, font, 178, 8, "MIN", status_active && this->min_mode() ? green : inactive);
this->label_(it, font, 218, 8, "HOLD", status_active && this->hold() ? blue : inactive);
this->label_(it, font, 270, 8, "REL", status_active && this->relative() ? Color(128, 128, 0) : inactive);
this->label_(it, font, 8, 66, "DC", status_active && this->dc() ? cyan : inactive);
this->label_(it, font, 8, 102, "AC", status_active && this->ac() ? magenta : inactive);
if (!this->connected) {
this->draw_digits_(it, "----", false, inactive);
it.print(160, 148, font, inactive, esphome::display::TextAlign::CENTER, "scan/connect");
} else if (!this->has_reading) {
this->draw_digits_(it, "8888", false, inactive);
it.print(160, 148, font, inactive, esphome::display::TextAlign::CENTER, "waiting");
} else if (this->overload) {
this->draw_digits_(it, " OL ", false, fg);
} else {
char d[5];
d[0] = this->digit_char_(REGDIG1);
d[1] = this->digit_char_(REGDIG2);
d[2] = this->digit_char_(REGDIG3);
d[3] = this->digit_char_(REGDIG4);
d[4] = 0;
this->draw_digits_(it, d, this->negative(), fg);
this->draw_decimal_points_(it, fg);
}
if (status_active) {
std::string unit_line = std::string(this->scale()) + this->unit();
it.print(270, 140, font, yellow, esphome::display::TextAlign::CENTER, unit_line.c_str());
}
bool bargraph_active = status_active && !this->overload;
this->draw_bargraph_(it, bargraph_active ? this->digits_from_buffer_() : 0, bargraph_active);
this->draw_icon_(it, 300, 148, 16, 16, DIODE_BMP, status_active && this->diode() ? magenta : inactive);
this->draw_icon_(it, 300, 174, 16, 16, BUZZ_BMP, status_active && this->continuity() ? orange : 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(242, 212, 40, 24, this->write_available ? fg : inactive);
it.print(54, 216, font, bg, esphome::display::TextAlign::TOP_CENTER, "<");
it.print(158, 216, font, bg, esphome::display::TextAlign::TOP_CENTER, this->selected_button_name());
it.print(262, 216, font, bg, esphome::display::TextAlign::TOP_CENTER, ">");
}
private:
uint8_t raw_[14]{};
uint8_t value_[14]{};
float display_value{NAN};
float base_value{NAN};
static constexpr uint8_t OVERLOAD_FRAME[5] = {0x2B, 0x3F, 0x30, 0x3A, 0x3F};
uint16_t digits_from_buffer_() const {
uint16_t out = 0;
if (this->value_[REGDIG1] >= '0' && this->value_[REGDIG1] <= '9') out += (this->value_[REGDIG1] - '0') * 1000;
if (this->value_[REGDIG2] >= '0' && this->value_[REGDIG2] <= '9') out += (this->value_[REGDIG2] - '0') * 100;
if (this->value_[REGDIG3] >= '0' && this->value_[REGDIG3] <= '9') out += (this->value_[REGDIG3] - '0') * 10;
if (this->value_[REGDIG4] >= '0' && this->value_[REGDIG4] <= '9') out += (this->value_[REGDIG4] - '0');
return out;
}
float calc_display_value_() const {
if (this->overload) return NAN;
if (this->is_plus) {
uint16_t pair1 = static_cast<uint16_t>(this->raw_[0]) | (static_cast<uint16_t>(this->raw_[1]) << 8);
uint8_t decimal = pair1 & 0x07;
if (decimal >= 7) return NAN;
uint16_t pair3 = static_cast<uint16_t>(this->raw_[4]) | (static_cast<uint16_t>(this->raw_[5]) << 8);
bool negative = pair3 >= 0x7FFF;
uint16_t digits = negative ? (pair3 & 0x7FFF) : pair3;
float v = static_cast<float>(digits) / std::pow(10.0f, decimal);
return negative ? -v : v;
}
uint8_t decimal = 0;
switch (this->value_[REGPOINT] & 0x07) {
case 0b001: decimal = 1; break;
case 0b010: decimal = 2; break;
case 0b100: decimal = 3; break;
default: break;
}
float v = static_cast<float>(this->digits_from_buffer_()) / std::pow(10.0f, decimal);
return this->negative() ? -v : v;
}
float calc_base_value_() const {
if (std::isnan(this->display_value)) return NAN;
if (this->value_[REGUNIT] == FLAGUNITNF) return this->display_value * 1e-9f;
if ((this->value_[REGSCALE] & FLAGSCALEMEGA) == FLAGSCALEMEGA) return this->display_value * 1e6f;
if ((this->value_[REGSCALE] & FLAGSCALEKILO) == FLAGSCALEKILO) return this->display_value * 1e3f;
if ((this->value_[REGSCALE] & FLAGSCALEMILLI) == FLAGSCALEMILLI) return this->display_value * 1e-3f;
if ((this->value_[REGSCALE] & FLAGSCALEMICRO) == FLAGSCALEMICRO) return this->display_value * 1e-6f;
return this->display_value;
}
void parse_plus_() {
memset(this->value_, 0, sizeof(this->value_));
this->value_[5] = 0x20;
this->value_[12] = 0x0D;
this->value_[13] = 0x0A;
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 scale = (pair1 >> 3) & 0x07;
uint8_t decimal = pair1 & 0x07;
switch (decimal) {
case 0: this->value_[REGPOINT] = FLAGPOINT0; break;
case 1: this->value_[REGPOINT] = FLAGPOINT3; break;
case 2: this->value_[REGPOINT] = FLAGPOINT2; break;
case 3: this->value_[REGPOINT] = FLAGPOINT1; break;
default: break;
}
switch (function) {
case 0: this->value_[REGUNIT] |= FLAGUNITVOLT; this->value_[REGMODE] |= FLAGMODEDC; break;
case 1: this->value_[REGUNIT] |= FLAGUNITVOLT; this->value_[REGMODE] |= FLAGMODEAC; break;
case 2: this->value_[REGUNIT] |= FLAGUNITAMP; this->value_[REGMODE] |= FLAGMODEDC; break;
case 3: this->value_[REGUNIT] |= FLAGUNITAMP; this->value_[REGMODE] |= FLAGMODEAC; break;
case 4: this->value_[REGUNIT] |= FLAGUNITOHM; break;
case 5: this->value_[REGUNIT] |= FLAGUNITNF; break;
case 6: this->value_[REGUNIT] |= FLAGUNITHZ; break;
case 7: this->value_[REGSCALE] |= FLAGSCALEDUTY; break;
case 8: this->value_[REGUNIT] |= FLAGUNITGRAD; break;
case 9: this->value_[REGUNIT] |= FLAGUNITFAHR; break;
case 10: this->value_[REGSCALE] |= FLAGSCALEDIODE; break;
case 11: this->value_[REGSCALE] |= FLAGSCALEBUZZ; break;
case 12: this->value_[REGUNIT] |= FLAGUNITHFE; break;
default: break;
}
switch (scale) {
case 2: this->value_[REGSCALE] |= FLAGSCALEMICRO; break;
case 3: this->value_[REGSCALE] |= FLAGSCALEMILLI; break;
case 5: this->value_[REGSCALE] |= FLAGSCALEKILO; break;
case 6: this->value_[REGSCALE] |= FLAGSCALEMEGA; break;
default: break;
}
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 << 1)) this->value_[REGMODE] |= FLAGMODEREL;
if (pair2 & (1 << 2)) this->value_[REGMODE] |= FLAGMODEAUTO;
this->low_battery = (pair2 & (1 << 3)) != 0;
if (pair2 & (1 << 4)) this->value_[REGMINMAX] |= FLAGMIN;
if (pair2 & (1 << 5)) this->value_[REGMINMAX] |= FLAGMAX;
uint16_t pair3 = static_cast<uint16_t>(this->raw_[4]) | (static_cast<uint16_t>(this->raw_[5]) << 8);
if (decimal < 7) {
uint16_t digits = pair3;
if (pair3 < 0x7FFF) {
this->value_[REGPLUSMINUS] = FLAGPLUS;
} else {
this->value_[REGPLUSMINUS] = FLAGMINUS;
digits = pair3 & 0x7FFF;
}
this->value_[REGDIG1] = '0' + ((digits / 1000) % 10);
this->value_[REGDIG2] = '0' + ((digits / 100) % 10);
this->value_[REGDIG3] = '0' + ((digits / 10) % 10);
this->value_[REGDIG4] = '0' + (digits % 10);
} else {
memcpy(this->value_, OVERLOAD_FRAME, sizeof(OVERLOAD_FRAME));
}
}
char digit_char_(uint8_t reg) const {
uint8_t c = this->value_[reg];
return (c >= '0' && c <= '9') ? static_cast<char>(c) : ' ';
}
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);
}
void draw_digits_(Display &it, const char *text, bool negative, Color color) {
if (negative) this->draw_segment_(it, 8, 88, 26, 9, true, color);
constexpr int digit_x = 40;
constexpr int digit_y = 35;
constexpr int digit_w = 50;
constexpr int digit_h = 88;
constexpr int digit_distance = 64;
for (int i = 0; i < 4; i++) {
this->draw_seven_segment_(it, digit_x + i * digit_distance, digit_y, digit_w, digit_h, text[i], color);
}
}
void draw_decimal_points_(Display &it, Color color) {
uint8_t p = this->value_[REGPOINT];
if ((p & FLAGPOINT1) == FLAGPOINT1) it.filled_rectangle(95, 117, 8, 10, color);
if ((p & FLAGPOINT2) == FLAGPOINT2) it.filled_rectangle(159, 117, 8, 10, color);
if ((p & FLAGPOINT3) == FLAGPOINT3) it.filled_rectangle(223, 117, 8, 10, color);
}
void draw_segment_(Display &it, int x, int y, int w, int h, bool horizontal, Color color) {
if (horizontal) {
int cap = h / 2;
it.filled_rectangle(x + cap, y, w - 2 * cap, h +1, color);
it.filled_triangle(x, y + cap, x + cap, y, x + cap, y + h, color);
it.filled_triangle(x + w, y + cap, x + w - cap, y, x + w - cap, y + h, color);
} else {
int cap = w / 2;
it.filled_rectangle(x, y + cap, w +1, h - 2 * cap, color);
it.filled_triangle(x + cap, y, x, y + cap, x + w, y + cap, color);
it.filled_triangle(x + cap, y + h, x, y + h - cap, x + w, y + h - cap, 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 col = 0; col < w; col++) {
uint8_t byte = data[row * ((w + 7) / 8) + (col / 8)];
if ((byte & (0x80 >> (col % 8))) != 0) {
it.draw_pixel_at(x + col, y + row, 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;
switch (ch) {
case '0': a=b=c=d=e=f=true; break;
case '1': b=c=true; break;
case '2': a=b=d=e=g=true; break;
case '3': a=b=c=d=g=true; break;
case '4': b=c=f=g=true; break;
case '5': a=c=d=f=g=true; break;
case '6': a=c=d=e=f=g=true; break;
case '7': a=b=c=true; break;
case '8': a=b=c=d=e=f=g=true; break;
case '9': a=b=c=d=f=g=true; break;
case 'O': a=b=c=d=e=f=true; break;
case 'L': d=e=f=true; break;
case '-': g=true; break;
default: break;
}
int t = 10;
int half = h / 2;
int gap = 1;
int top_y = y;
int mid_y = y + half - t / 2;
int bot_y = y + h - t;
int upper_v_y = top_y + t + gap;
int upper_v_h = mid_y - gap - upper_v_y;
int lower_v_y = mid_y + t + gap;
int lower_v_h = bot_y - gap - lower_v_y;
if (a) this->draw_segment_(it, x + t / 2, top_y, w - t, t, true, color);
if (b && upper_v_h > 0) this->draw_segment_(it, x + w - t, upper_v_y, t, upper_v_h, false, color);
if (c && lower_v_h > 0) this->draw_segment_(it, x + w - t, lower_v_y, t, lower_v_h, false, color);
if (d) this->draw_segment_(it, x + t / 2, bot_y, w - t, t, true, color);
if (e && lower_v_h > 0) this->draw_segment_(it, x, lower_v_y, t, lower_v_h, false, color);
if (f && upper_v_h > 0) this->draw_segment_(it, x, upper_v_y, t, upper_v_h, false, color);
if (g) this->draw_segment_(it, x + t / 2, mid_y, w - t, t, true, color);
}
void draw_bargraph_(Display &it, uint16_t digits, bool active) {
const Color fg(255, 255, 255);
const Color inactive(80, 80, 80);
uint16_t mapped = active ? static_cast<uint16_t>(digits * 240 / 6000) : 0;
if (mapped > 240) mapped = 240;
for (uint16_t i = 0; i <= 240; i += 4) {
Color col = (active && i <= mapped) ? fg : inactive;
int h = (i % 40 == 0) ? 20 : ((i % 20 == 0) ? 15 : 10);
it.vertical_line(40 + i, 185 - h, h, col);
}
it.horizontal_line(35, 185, 250, inactive);
}
};
} // namespace owon_b35t
static owon_b35t::Meter owon_meter;
@@ -0,0 +1,316 @@
# Derived work based on https://github.com/reaper7/M5Stack_BLE_client_Owon_B35T by reaper7.
# AI (ChatGPT) has been used to adopt the Arduino sketch to ESPHome.
# Ported to M5Stack Core2 due to memory constraints.
substitutions:
name: "owon-b35t-m5stack-core2"
friendly_name: "OWON B35T Multimeter Core2"
device_description: "M5Stack Core2 BLE client for OWON B35T/B35T+ multimeter with local graphical display"
owon_mac_address: !secret owon_b35t_mac_address
esphome:
name: ${name}
friendly_name: ${friendly_name}
comment: ${device_description}
min_version: 2024.6.0
includes:
- owon-b35t-m5stack-core2.h
on_boot:
priority: 850
then:
- lambda: |-
owon_b35t::core2_axp192_init(id(core2_i2c));
project:
name: "custom.owon-b35t-m5stack-core2"
version: "1.0"
esp32:
board: m5stack-core2
flash_size: 16MB
framework:
type: esp-idf
advanced:
minimum_chip_revision: "3.1"
sram1_as_iram: true
psram:
mode: quad
speed: 80MHz
logger:
level: INFO
api:
encryption:
key: !secret apikey
ota:
platform: esphome
password: !secret ota
wifi:
ssid: "Voltage-legacy"
password: !secret voltage_legacy_psk
#use_address: ${name}.home
power_save_mode: none
fast_connect: on
min_auth_mode: WPA2
ap:
ssid: "OWON B35T Fallback Hotspot"
password: !secret fallback_psk
captive_portal:
interval:
- interval: 10s
then:
- 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",
static_cast<unsigned>(esp_get_free_heap_size()),
static_cast<unsigned>(esp_get_minimum_free_heap_size()),
static_cast<unsigned>(heap_caps_get_free_size(MALLOC_CAP_INTERNAL)),
static_cast<unsigned>(heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL)),
static_cast<unsigned>(heap_caps_get_free_size(MALLOC_CAP_DMA)),
static_cast<unsigned>(heap_caps_get_largest_free_block(MALLOC_CAP_DMA)),
static_cast<unsigned>(heap_caps_get_free_size(MALLOC_CAP_SPIRAM)),
static_cast<unsigned>(heap_caps_get_largest_free_block(MALLOC_CAP_SPIRAM)));
esp32_ble_tracker:
scan_parameters:
active: true
continuous: true
ble_client:
- mac_address: ${owon_mac_address}
id: owon_ble_client
on_connect:
then:
- lambda: |-
owon_meter.on_connect();
on_disconnect:
then:
- lambda: |-
owon_meter.on_disconnect();
spi:
clk_pin: GPIO18
mosi_pin: GPIO23
i2c:
id: core2_i2c
sda: GPIO21
scl: GPIO22
scan: true
output:
- platform: template
type: float
id: lcd_backlight
write_action:
- lambda: |-
owon_b35t::core2_axp192_set_backlight(state);
light:
- platform: monochromatic
output: lcd_backlight
name: "${friendly_name} Backlight"
id: backlight
restore_mode: ALWAYS_ON
font:
- file: "fonts/Roboto-Regular.ttf"
id: meter_font
size: 15
glyphs:
[
" ",
"!",
"%",
"+",
"-",
".",
"/",
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"<",
">",
"A",
"B",
"C",
"D",
"E",
"F",
"G",
"H",
"I",
"L",
"M",
"N",
"O",
"P",
"R",
"S",
"T",
"U",
"V",
"W",
"X",
"Y",
"Z",
"a",
"c",
"d",
"e",
"f",
"g",
"h",
"i",
"k",
"m",
"n",
"o",
"r",
"s",
"t",
"u",
"v",
"w",
"y",
"z",
"°",
"µ",
"Ω",
]
display:
- platform: mipi_spi
id: lcd
model: M5CORE2
update_interval: 500ms
lambda: |-
owon_meter.render(it, id(meter_font));
touchscreen:
- platform: ft63x6
id: touch
display: lcd
binary_sensor:
- platform: touchscreen
id: button_a
touchscreen_id: touch
x_min: 34
x_max: 74
y_min: 212
y_max: 240
internal: true
on_press:
then:
- lambda: |-
owon_meter.previous_button();
- platform: touchscreen
id: button_b
touchscreen_id: touch
x_min: 108
x_max: 208
y_min: 212
y_max: 240
internal: true
on_click:
- min_length: 50ms
max_length: 1500ms
then:
- logger.log:
level: INFO
format: "OWON short press: %s"
args: ["owon_meter.selected_button_name()"]
- ble_client.ble_write:
id: owon_ble_client
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb"
value: !lambda |-
std::vector<uint8_t> data = {owon_meter.selected_button, 0x01};
return data;
- min_length: 1500ms
max_length: 5000ms
then:
- logger.log:
level: INFO
format: "OWON long press: %s"
args: ["owon_meter.selected_button_name()"]
- ble_client.ble_write:
id: owon_ble_client
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb"
value: !lambda |-
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};
return data;
- platform: touchscreen
id: button_c
touchscreen_id: touch
x_min: 242
x_max: 282
y_min: 212
y_max: 240
internal: true
on_press:
then:
- lambda: |-
owon_meter.next_button();
- platform: template
name: "${friendly_name} Connected"
lambda: |-
return owon_meter.connected;
- platform: template
name: "${friendly_name} Overload"
lambda: |-
return owon_meter.overload;
- platform: template
name: "${friendly_name} Low Battery"
lambda: |-
return owon_meter.low_battery;
sensor:
- platform: ble_client
type: characteristic
ble_client_id: owon_ble_client
id: owon_notify_source
internal: true
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
characteristic_uuid: "0000fff4-0000-1000-8000-00805f9b34fb"
notify: true
update_interval: never
lambda: |-
owon_meter.handle_notify(x);
return owon_meter.value();
- platform: wifi_signal
name: "${friendly_name} WiFi Signal"
update_interval: 60s
text_sensor:
- platform: template
name: "${friendly_name} Reading"
update_interval: 1s
lambda: |-
return owon_meter.reading_text();
- platform: template
name: "${friendly_name} Unit"
update_interval: 1s
lambda: |-
return std::string(owon_meter.scale()) + owon_meter.unit();
- platform: template
name: "${friendly_name} Mode"
update_interval: 1s
lambda: |-
return owon_meter.mode_text();
+567
View File
@@ -0,0 +1,567 @@
/*
* ESPHome helper for OWON B35T/B35T+ BLE meter on M5Stack Core 1.
* Parser is based on the standalone Arduino sketch by Reaper7
* (Beerware license, Revision 42) and Dean Cording's owonb35 notes.
*/
#pragma once
#include <cmath>
#include <cstdint>
#include <cstring>
#include <string>
#include <vector>
#include "esp_heap_caps.h"
#include "esp_system.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/components/display/display.h"
namespace owon_b35t {
using esphome::Color;
using esphome::display::Display;
static const char *const TAG = "owon_b35t";
static const uint8_t ACCU_BMP[32] = {
0b00000000, 0b00000000,
0b00000000, 0b00000000,
0b00000000, 0b00000000,
0b11111111, 0b11111110,
0b10000000, 0b00000010,
0b10000000, 0b00000011,
0b10000000, 0b00000011,
0b10000000, 0b00000011,
0b10000000, 0b00000011,
0b10000000, 0b00000011,
0b10000000, 0b00000011,
0b10000000, 0b00000010,
0b11111111, 0b11111110,
0b00000000, 0b00000000,
0b00000000, 0b00000000,
0b00000000, 0b00000000,
};
static const uint8_t BLE_BMP[32] = {
0b00000001, 0b10000000,
0b00000001, 0b11000000,
0b00010001, 0b01100000,
0b00011001, 0b00110000,
0b00001101, 0b00011000,
0b00000111, 0b00110000,
0b00000011, 0b01100000,
0b00000001, 0b11000000,
0b00000001, 0b11000000,
0b00000011, 0b01100000,
0b00000111, 0b00110000,
0b00001101, 0b00011000,
0b00011001, 0b00110000,
0b00010001, 0b01100000,
0b00000001, 0b11000000,
0b00000001, 0b10000000,
};
static const uint8_t DIODE_BMP[32] = {
0b00001000, 0b00011000,
0b00001100, 0b00011000,
0b00001110, 0b00011000,
0b00001111, 0b00011000,
0b00001111, 0b10011000,
0b00001111, 0b11011000,
0b00001111, 0b11111000,
0b11111111, 0b11111111,
0b11111111, 0b11111111,
0b00001111, 0b11111000,
0b00001111, 0b11011000,
0b00001111, 0b10011000,
0b00001111, 0b00011000,
0b00001110, 0b00011000,
0b00001100, 0b00011000,
0b00001000, 0b00011000,
};
static const uint8_t BUZZ_BMP[32] = {
0b00000000, 0b11000000,
0b00000001, 0b11000000,
0b00000011, 0b11000001,
0b00000111, 0b11000001,
0b00001111, 0b11000101,
0b11111111, 0b11000101,
0b11111111, 0b11010101,
0b11111111, 0b11010101,
0b11111111, 0b11010101,
0b11111111, 0b11010101,
0b11111111, 0b11000101,
0b00001111, 0b11000101,
0b00000111, 0b11000001,
0b00000011, 0b11000001,
0b00000001, 0b11000000,
0b00000000, 0b11000000,
};
class Meter {
public:
static constexpr uint8_t REGPLUSMINUS = 0x00;
static constexpr uint8_t FLAGPLUS = 0b00101011;
static constexpr uint8_t FLAGMINUS = 0b00101101;
static constexpr uint8_t REGDIG1 = 0x01;
static constexpr uint8_t REGDIG2 = 0x02;
static constexpr uint8_t REGDIG3 = 0x03;
static constexpr uint8_t REGDIG4 = 0x04;
static constexpr uint8_t REGPOINT = 0x06;
static constexpr uint8_t FLAGPOINT0 = 0b00110000;
static constexpr uint8_t FLAGPOINT1 = 0b00110001;
static constexpr uint8_t FLAGPOINT2 = 0b00110010;
static constexpr uint8_t FLAGPOINT3 = 0b00110100;
static constexpr uint8_t REGMODE = 0x07;
static constexpr uint8_t FLAGMODEHOLD = 0b00000010;
static constexpr uint8_t FLAGMODEREL = 0b00000100;
static constexpr uint8_t FLAGMODEAC = 0b00001000;
static constexpr uint8_t FLAGMODEDC = 0b00010000;
static constexpr uint8_t FLAGMODEAUTO = 0b00100000;
static constexpr uint8_t REGMINMAX = 0x08;
static constexpr uint8_t FLAGMIN = 0b00010000;
static constexpr uint8_t FLAGMAX = 0b00100000;
static constexpr uint8_t REGSCALE = 0x09;
static constexpr uint8_t FLAGSCALEDUTY = 0b00000010;
static constexpr uint8_t FLAGSCALEDIODE = 0b00000100;
static constexpr uint8_t FLAGSCALEBUZZ = 0b00001000;
static constexpr uint8_t FLAGSCALEMEGA = 0b00010000;
static constexpr uint8_t FLAGSCALEKILO = 0b00100000;
static constexpr uint8_t FLAGSCALEMILLI = 0b01000000;
static constexpr uint8_t FLAGSCALEMICRO = 0b10000000;
static constexpr uint8_t REGUNIT = 0x0a;
static constexpr uint8_t FLAGUNITFAHR = 0b00000001;
static constexpr uint8_t FLAGUNITGRAD = 0b00000010;
static constexpr uint8_t FLAGUNITNF = 0b00000100;
static constexpr uint8_t FLAGUNITHZ = 0b00001000;
static constexpr uint8_t FLAGUNITHFE = 0b00010000;
static constexpr uint8_t FLAGUNITOHM = 0b00100000;
static constexpr uint8_t FLAGUNITAMP = 0b01000000;
static constexpr uint8_t FLAGUNITVOLT = 0b10000000;
bool connected{false};
bool write_available{false};
bool is_plus{false};
bool low_battery{false};
bool overload{false};
bool has_reading{false};
uint8_t selected_button{1};
uint32_t last_notify_ms{0};
bool handle_notify(const std::vector<uint8_t> &data) {
if (data.size() > sizeof(this->raw_))
return false;
if (data.size() == 6 && data[1] >= 0xF0) {
memset(this->raw_, 0, sizeof(this->raw_));
memcpy(this->raw_, data.data(), data.size());
this->is_plus = true;
this->parse_plus_();
} else if (data.size() == 14 && data[12] == 0x0D && data[13] == 0x0A) {
memset(this->value_, 0, sizeof(this->value_));
memcpy(this->value_, data.data(), data.size());
this->is_plus = false;
} else {
ESP_LOGW(TAG, "Ignoring unexpected OWON frame length=%u", static_cast<unsigned>(data.size()));
return false;
}
this->overload = memcmp(this->value_, OVERLOAD_FRAME, sizeof(OVERLOAD_FRAME)) == 0;
this->display_value = this->calc_display_value_();
this->base_value = this->calc_base_value_();
this->has_reading = true;
this->last_notify_ms = millis();
return true;
}
void on_connect() {
this->connected = true;
this->write_available = true;
}
void on_disconnect() {
this->connected = false;
this->write_available = false;
}
float value() const { return this->display_value; }
float value_base() const { return this->base_value; }
bool negative() const { return (this->value_[REGPLUSMINUS] & FLAGMINUS) == FLAGMINUS; }
bool auto_range() const { return (this->value_[REGMODE] & FLAGMODEAUTO) == FLAGMODEAUTO; }
bool hold() const { return (this->value_[REGMODE] & FLAGMODEHOLD) == FLAGMODEHOLD; }
bool relative() const { return (this->value_[REGMODE] & FLAGMODEREL) == FLAGMODEREL; }
bool ac() const { return (this->value_[REGMODE] & FLAGMODEAC) == FLAGMODEAC; }
bool dc() const { return (this->value_[REGMODE] & FLAGMODEDC) == FLAGMODEDC; }
bool min_mode() const { return (this->value_[REGMINMAX] & FLAGMIN) == FLAGMIN; }
bool max_mode() const { return (this->value_[REGMINMAX] & FLAGMAX) == FLAGMAX; }
bool diode() const { return (this->value_[REGSCALE] & FLAGSCALEDIODE) == FLAGSCALEDIODE; }
bool continuity() const { return (this->value_[REGSCALE] & FLAGSCALEBUZZ) == FLAGSCALEBUZZ; }
const char *unit() const {
switch (this->value_[REGUNIT]) {
case FLAGUNITFAHR: return "°F";
case FLAGUNITGRAD: return "°C";
case FLAGUNITNF: return "nF";
case FLAGUNITHZ: return "Hz";
case FLAGUNITHFE: return "hFE";
case FLAGUNITOHM: return "Ω";
case FLAGUNITAMP: return "A";
case FLAGUNITVOLT: return "V";
default: return "";
}
}
const char *scale() const {
if ((this->value_[REGSCALE] & FLAGSCALEDUTY) == FLAGSCALEDUTY) return "%";
if ((this->value_[REGSCALE] & FLAGSCALEMEGA) == FLAGSCALEMEGA) return "M";
if ((this->value_[REGSCALE] & FLAGSCALEKILO) == FLAGSCALEKILO) return "k";
if ((this->value_[REGSCALE] & FLAGSCALEMILLI) == FLAGSCALEMILLI) return "m";
if ((this->value_[REGSCALE] & FLAGSCALEMICRO) == FLAGSCALEMICRO) return "µ";
return "";
}
std::string mode_text() const {
std::string out;
if (this->dc()) out += "DC ";
if (this->ac()) out += "AC ";
if (this->auto_range()) out += "AUTO ";
if (this->hold()) out += "HOLD ";
if (this->relative()) out += "REL ";
if (this->min_mode()) out += "MIN ";
if (this->max_mode()) out += "MAX ";
if (this->diode()) out += "DIODE ";
if (this->continuity()) out += "CONT ";
if (!out.empty()) out.pop_back();
return out;
}
std::string reading_text() const {
if (!this->connected) return "Disconnected";
if (!this->has_reading) return "Waiting for data";
if (this->overload) return "OL " + std::string(this->scale()) + this->unit();
char buf[48];
snprintf(buf, sizeof(buf), "%s%.4g %s%s", this->negative() ? "-" : "", std::fabs(this->display_value), this->scale(), this->unit());
return std::string(buf);
}
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 {
if ((this->value_[REGSCALE] & FLAGSCALEDUTY) == FLAGSCALEDUTY) return KIND_DUTY;
switch (this->value_[REGUNIT]) {
case FLAGUNITVOLT: return KIND_VOLTAGE;
case FLAGUNITAMP: return KIND_CURRENT;
case FLAGUNITOHM: return KIND_RESISTANCE;
case FLAGUNITHZ: return KIND_FREQUENCY;
case FLAGUNITNF: return KIND_CAPACITANCE;
case FLAGUNITGRAD: return KIND_TEMP_C;
case FLAGUNITFAHR: return KIND_TEMP_F;
default: return KIND_OTHER;
}
}
const char *selected_button_name() const {
static const char *const names[] = {"SELECT", "RANGE", "HLD/LIG", "REL/BT", "HZ/DUTY", "MAX/MIN"};
uint8_t index = this->selected_button;
if (index < 1) index = 1;
if (index > 6) index = 6;
return names[index - 1];
}
void previous_button() {
if (this->selected_button > 1) this->selected_button--;
}
void next_button() {
if (this->selected_button < 6) this->selected_button++;
}
void render(Display &it, esphome::display::BaseFont *font) {
const Color bg(0, 0, 0);
const Color fg(210, 210, 210);
// Chosen to map to a neutral dark gray in the RGB332 8-bit display palette.
const Color inactive(80, 80, 80);
const Color yellow(255, 220, 0);
const Color blue(0, 80, 255);
const Color cyan(0, 255, 255);
const Color magenta(255, 0, 255);
const Color red(255, 0, 0);
const Color green(0, 220, 0);
const Color orange(255, 165, 0);
it.fill(bg);
this->draw_icon_(it, 12, 8, 16, 16, ACCU_BMP, this->low_battery ? red : green);
this->draw_icon_(it, 46, 8, 16, 16, BLE_BMP, this->connected ? blue : inactive);
this->label_(it, font, 86, 8, "AUTO", this->auto_range() ? fg : inactive);
this->label_(it, font, 138, 8, "MAX", this->max_mode() ? red : inactive);
this->label_(it, font, 178, 8, "MIN", this->min_mode() ? green : inactive);
this->label_(it, font, 218, 8, "HOLD", this->hold() ? blue : inactive);
this->label_(it, font, 270, 8, "REL", this->relative() ? Color(128, 128, 0) : inactive);
this->label_(it, font, 8, 66, "DC", this->dc() ? cyan : inactive);
this->label_(it, font, 8, 102, "AC", this->ac() ? magenta : inactive);
if (!this->connected) {
this->draw_digits_(it, "----", false, inactive);
it.print(160, 148, font, inactive, esphome::display::TextAlign::CENTER, "scan/connect");
} else if (!this->has_reading) {
this->draw_digits_(it, "8888", false, inactive);
it.print(160, 148, font, inactive, esphome::display::TextAlign::CENTER, "waiting");
} else if (this->overload) {
this->draw_digits_(it, " OL ", false, fg);
} else {
char d[5];
d[0] = this->digit_char_(REGDIG1);
d[1] = this->digit_char_(REGDIG2);
d[2] = this->digit_char_(REGDIG3);
d[3] = this->digit_char_(REGDIG4);
d[4] = 0;
this->draw_digits_(it, d, this->negative(), fg);
this->draw_decimal_points_(it, fg);
}
std::string unit_line = std::string(this->scale()) + this->unit();
it.print(270, 140, font, yellow, esphome::display::TextAlign::CENTER, unit_line.c_str());
this->draw_bargraph_(it, this->has_reading && !this->overload ? this->digits_from_buffer_() : 0, this->has_reading && !this->overload);
this->draw_icon_(it, 300, 148, 16, 16, DIODE_BMP, this->diode() ? magenta : inactive);
this->draw_icon_(it, 300, 174, 16, 16, BUZZ_BMP, this->continuity() ? orange : 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(242, 212, 40, 24, this->write_available ? fg : inactive);
it.print(54, 216, font, bg, esphome::display::TextAlign::TOP_CENTER, "<");
it.print(158, 216, font, bg, esphome::display::TextAlign::TOP_CENTER, this->selected_button_name());
it.print(262, 216, font, bg, esphome::display::TextAlign::TOP_CENTER, ">");
}
private:
uint8_t raw_[14]{};
uint8_t value_[14]{};
float display_value{NAN};
float base_value{NAN};
static constexpr uint8_t OVERLOAD_FRAME[5] = {0x2B, 0x3F, 0x30, 0x3A, 0x3F};
uint16_t digits_from_buffer_() const {
uint16_t out = 0;
if (this->value_[REGDIG1] >= '0' && this->value_[REGDIG1] <= '9') out += (this->value_[REGDIG1] - '0') * 1000;
if (this->value_[REGDIG2] >= '0' && this->value_[REGDIG2] <= '9') out += (this->value_[REGDIG2] - '0') * 100;
if (this->value_[REGDIG3] >= '0' && this->value_[REGDIG3] <= '9') out += (this->value_[REGDIG3] - '0') * 10;
if (this->value_[REGDIG4] >= '0' && this->value_[REGDIG4] <= '9') out += (this->value_[REGDIG4] - '0');
return out;
}
float calc_display_value_() const {
if (this->overload) return NAN;
if (this->is_plus) {
uint16_t pair1 = static_cast<uint16_t>(this->raw_[0]) | (static_cast<uint16_t>(this->raw_[1]) << 8);
uint8_t decimal = pair1 & 0x07;
if (decimal >= 7) return NAN;
uint16_t pair3 = static_cast<uint16_t>(this->raw_[4]) | (static_cast<uint16_t>(this->raw_[5]) << 8);
bool negative = pair3 >= 0x7FFF;
uint16_t digits = negative ? (pair3 & 0x7FFF) : pair3;
float v = static_cast<float>(digits) / std::pow(10.0f, decimal);
return negative ? -v : v;
}
uint8_t decimal = 0;
switch (this->value_[REGPOINT] & 0x07) {
case 0b001: decimal = 1; break;
case 0b010: decimal = 2; break;
case 0b100: decimal = 3; break;
default: break;
}
float v = static_cast<float>(this->digits_from_buffer_()) / std::pow(10.0f, decimal);
return this->negative() ? -v : v;
}
float calc_base_value_() const {
if (std::isnan(this->display_value)) return NAN;
if (this->value_[REGUNIT] == FLAGUNITNF) return this->display_value * 1e-9f;
if ((this->value_[REGSCALE] & FLAGSCALEMEGA) == FLAGSCALEMEGA) return this->display_value * 1e6f;
if ((this->value_[REGSCALE] & FLAGSCALEKILO) == FLAGSCALEKILO) return this->display_value * 1e3f;
if ((this->value_[REGSCALE] & FLAGSCALEMILLI) == FLAGSCALEMILLI) return this->display_value * 1e-3f;
if ((this->value_[REGSCALE] & FLAGSCALEMICRO) == FLAGSCALEMICRO) return this->display_value * 1e-6f;
return this->display_value;
}
void parse_plus_() {
memset(this->value_, 0, sizeof(this->value_));
this->value_[5] = 0x20;
this->value_[12] = 0x0D;
this->value_[13] = 0x0A;
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 scale = (pair1 >> 3) & 0x07;
uint8_t decimal = pair1 & 0x07;
switch (decimal) {
case 0: this->value_[REGPOINT] = FLAGPOINT0; break;
case 1: this->value_[REGPOINT] = FLAGPOINT3; break;
case 2: this->value_[REGPOINT] = FLAGPOINT2; break;
case 3: this->value_[REGPOINT] = FLAGPOINT1; break;
default: break;
}
switch (function) {
case 0: this->value_[REGUNIT] |= FLAGUNITVOLT; this->value_[REGMODE] |= FLAGMODEDC; break;
case 1: this->value_[REGUNIT] |= FLAGUNITVOLT; this->value_[REGMODE] |= FLAGMODEAC; break;
case 2: this->value_[REGUNIT] |= FLAGUNITAMP; this->value_[REGMODE] |= FLAGMODEDC; break;
case 3: this->value_[REGUNIT] |= FLAGUNITAMP; this->value_[REGMODE] |= FLAGMODEAC; break;
case 4: this->value_[REGUNIT] |= FLAGUNITOHM; break;
case 5: this->value_[REGUNIT] |= FLAGUNITNF; break;
case 6: this->value_[REGUNIT] |= FLAGUNITHZ; break;
case 7: this->value_[REGSCALE] |= FLAGSCALEDUTY; break;
case 8: this->value_[REGUNIT] |= FLAGUNITGRAD; break;
case 9: this->value_[REGUNIT] |= FLAGUNITFAHR; break;
case 10: this->value_[REGSCALE] |= FLAGSCALEDIODE; break;
case 11: this->value_[REGSCALE] |= FLAGSCALEBUZZ; break;
case 12: this->value_[REGUNIT] |= FLAGUNITHFE; break;
default: break;
}
switch (scale) {
case 2: this->value_[REGSCALE] |= FLAGSCALEMICRO; break;
case 3: this->value_[REGSCALE] |= FLAGSCALEMILLI; break;
case 5: this->value_[REGSCALE] |= FLAGSCALEKILO; break;
case 6: this->value_[REGSCALE] |= FLAGSCALEMEGA; break;
default: break;
}
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 << 1)) this->value_[REGMODE] |= FLAGMODEREL;
if (pair2 & (1 << 2)) this->value_[REGMODE] |= FLAGMODEAUTO;
this->low_battery = (pair2 & (1 << 3)) != 0;
if (pair2 & (1 << 4)) this->value_[REGMINMAX] |= FLAGMIN;
if (pair2 & (1 << 5)) this->value_[REGMINMAX] |= FLAGMAX;
uint16_t pair3 = static_cast<uint16_t>(this->raw_[4]) | (static_cast<uint16_t>(this->raw_[5]) << 8);
if (decimal < 7) {
uint16_t digits = pair3;
if (pair3 < 0x7FFF) {
this->value_[REGPLUSMINUS] = FLAGPLUS;
} else {
this->value_[REGPLUSMINUS] = FLAGMINUS;
digits = pair3 & 0x7FFF;
}
this->value_[REGDIG1] = '0' + ((digits / 1000) % 10);
this->value_[REGDIG2] = '0' + ((digits / 100) % 10);
this->value_[REGDIG3] = '0' + ((digits / 10) % 10);
this->value_[REGDIG4] = '0' + (digits % 10);
} else {
memcpy(this->value_, OVERLOAD_FRAME, sizeof(OVERLOAD_FRAME));
}
}
char digit_char_(uint8_t reg) const {
uint8_t c = this->value_[reg];
return (c >= '0' && c <= '9') ? static_cast<char>(c) : ' ';
}
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);
}
void draw_digits_(Display &it, const char *text, bool negative, Color color) {
if (negative) this->draw_segment_(it, 8, 88, 26, 9, true, color);
constexpr int digit_x = 40;
constexpr int digit_y = 35;
constexpr int digit_w = 50;
constexpr int digit_h = 88;
constexpr int digit_distance = 64;
for (int i = 0; i < 4; i++) {
this->draw_seven_segment_(it, digit_x + i * digit_distance, digit_y, digit_w, digit_h, text[i], color);
}
}
void draw_decimal_points_(Display &it, Color color) {
uint8_t p = this->value_[REGPOINT];
if ((p & FLAGPOINT1) == FLAGPOINT1) it.filled_rectangle(95, 117, 8, 10, color);
if ((p & FLAGPOINT2) == FLAGPOINT2) it.filled_rectangle(159, 117, 8, 10, color);
if ((p & FLAGPOINT3) == FLAGPOINT3) it.filled_rectangle(223, 117, 8, 10, color);
}
void draw_segment_(Display &it, int x, int y, int w, int h, bool horizontal, Color color) {
if (horizontal) {
int cap = h / 2;
it.filled_rectangle(x + cap, y, w - 2 * cap, h, color);
it.filled_triangle(x, y + cap, x + cap, y, x + cap, y + h, color);
it.filled_triangle(x + w, y + cap, x + w - cap, y, x + w - cap, y + h, color);
} else {
int cap = w / 2;
it.filled_rectangle(x, y + cap, w, h - 2 * cap, color);
it.filled_triangle(x + cap, y, x, y + cap, x + w, y + cap, color);
it.filled_triangle(x + cap, y + h, x, y + h - cap, x + w, y + h - cap, 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 col = 0; col < w; col++) {
uint8_t byte = data[row * ((w + 7) / 8) + (col / 8)];
if ((byte & (0x80 >> (col % 8))) != 0) {
it.draw_pixel_at(x + col, y + row, 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;
switch (ch) {
case '0': a=b=c=d=e=f=true; break;
case '1': b=c=true; break;
case '2': a=b=d=e=g=true; break;
case '3': a=b=c=d=g=true; break;
case '4': b=c=f=g=true; break;
case '5': a=c=d=f=g=true; break;
case '6': a=c=d=e=f=g=true; break;
case '7': a=b=c=true; break;
case '8': a=b=c=d=e=f=g=true; break;
case '9': a=b=c=d=f=g=true; break;
case 'O': a=b=c=d=e=f=true; break;
case 'L': d=e=f=true; break;
case '-': g=true; break;
default: break;
}
int t = 10;
int half = h / 2;
int gap = 1;
int top_y = y;
int mid_y = y + half - t / 2;
int bot_y = y + h - t;
int upper_v_y = top_y + t + gap;
int upper_v_h = mid_y - gap - upper_v_y;
int lower_v_y = mid_y + t + gap;
int lower_v_h = bot_y - gap - lower_v_y;
if (a) this->draw_segment_(it, x + t / 2, top_y, w - t, t, true, color);
if (b && upper_v_h > 0) this->draw_segment_(it, x + w - t, upper_v_y, t, upper_v_h, false, color);
if (c && lower_v_h > 0) this->draw_segment_(it, x + w - t, lower_v_y, t, lower_v_h, false, color);
if (d) this->draw_segment_(it, x + t / 2, bot_y, w - t, t, true, color);
if (e && lower_v_h > 0) this->draw_segment_(it, x, lower_v_y, t, lower_v_h, false, color);
if (f && upper_v_h > 0) this->draw_segment_(it, x, upper_v_y, t, upper_v_h, false, color);
if (g) this->draw_segment_(it, x + t / 2, mid_y, w - t, t, true, color);
}
void draw_bargraph_(Display &it, uint16_t digits, bool active) {
const Color fg(255, 255, 255);
const Color inactive(80, 80, 80);
uint16_t mapped = active ? static_cast<uint16_t>(digits * 240 / 6000) : 0;
if (mapped > 240) mapped = 240;
for (uint16_t i = 0; i <= 240; i += 4) {
Color col = (active && i <= mapped) ? fg : inactive;
int h = (i % 40 == 0) ? 20 : ((i % 20 == 0) ? 15 : 10);
it.vertical_line(40 + i, 185 - h, h, col);
}
it.horizontal_line(35, 185, 250, inactive);
}
};
} // namespace owon_b35t
static owon_b35t::Meter owon_meter;
@@ -25,10 +25,13 @@ external_components:
components: [es8388]
refresh: 0s
network:
enable_ipv6: true
wifi:
ssid: Voltage-legacy
password: !secret voltage_legacy_psk
use_address: ${name}.home
#use_address: ${name}.home
power_save_mode: high
fast_connect: on
min_auth_mode: WPA2
+4 -1
View File
@@ -16,10 +16,13 @@ esp32:
framework:
type: arduino
network:
enable_ipv6: true
wifi:
ssid: "Voltage-legacy"
password: !secret voltage_legacy_psk
use_address: bathroom.home
#use_address: bathroom.home
power_save_mode: high
fast_connect: on
min_auth_mode: WPA2
+762 -279
View File
File diff suppressed because it is too large Load Diff
+4 -1
View File
@@ -16,10 +16,13 @@ esp32:
framework:
type: esp-idf
network:
enable_ipv6: true
wifi:
ssid: "Voltage-legacy"
password: !secret voltage_legacy_psk
use_address: ${name}.home
#use_address: ${name}.home
power_save_mode: high
fast_connect: on
min_auth_mode: WPA2
+3
View File
@@ -16,6 +16,9 @@ esp32:
framework:
type: esp-idf
network:
enable_ipv6: true
wifi:
ssid: "Voltage-legacy"
password: !secret voltage_legacy_psk
+4 -1
View File
@@ -16,10 +16,13 @@ esp32:
framework:
type: esp-idf
network:
enable_ipv6: true
wifi:
ssid: "Voltage-legacy"
password: !secret voltage_legacy_psk
use_address: cam-livingroom.home
#use_address: cam-livingroom.home
power_save_mode: high
fast_connect: on
+785
View File
@@ -0,0 +1,785 @@
/*
* ESPHome helper for OWON B35T/B35T+ BLE meter on M5Stack Core 2.
* Parser is based on the standalone Arduino sketch by Reaper7
* (Beerware license, Revision 42) and Dean Cording's owonb35 notes.
* Rendering code and some functionality modified to suppport the
* Atorch DL24 dc load.
*/
#pragma once
#include <cmath>
#include <cstdint>
#include <cstring>
#include <string>
#include <vector>
#include "esp_heap_caps.h"
#include "esp_system.h"
#include "esphome/core/helpers.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/core/log.h"
#include "esphome/components/display/display.h"
namespace owon_b35t {
using esphome::Color;
using esphome::display::Display;
static const char *const TAG = "owon_b35t";
static const char *const POWER_TAG = "core2_power";
static constexpr uint8_t AXP192_ADDR = 0x34;
static esphome::i2c::I2CDevice axp192;
static bool axp192_ready = false;
static bool axp_write(uint8_t reg, uint8_t value) {
if (!axp192_ready) return false;
bool ok = axp192.write_byte(reg, value);
if (!ok) ESP_LOGW(POWER_TAG, "AXP192 write reg 0x%02X failed", reg);
return ok;
}
static bool axp_read(uint8_t reg, uint8_t *value) {
if (!axp192_ready) return false;
bool ok = axp192.read_byte(reg, value);
if (!ok) ESP_LOGW(POWER_TAG, "AXP192 read reg 0x%02X failed", reg);
return ok;
}
static void axp_update(uint8_t reg, uint8_t clear_mask, uint8_t set_mask) {
uint8_t value = 0;
if (!axp_read(reg, &value)) return;
value = (value & ~clear_mask) | set_mask;
axp_write(reg, value);
}
static uint8_t axp_dc_voltage_data(uint16_t millivolts) {
if (millivolts < 700) millivolts = 700;
if (millivolts > 3500) millivolts = 3500;
return static_cast<uint8_t>((millivolts - 700) / 25) & 0x7F;
}
static uint8_t axp_ldo_voltage_data(uint16_t millivolts) {
if (millivolts < 1800) millivolts = 1800;
if (millivolts > 3300) millivolts = 3300;
return static_cast<uint8_t>((millivolts - 1800) / 100) & 0x0F;
}
static void core2_axp192_set_lcd_voltage(uint16_t millivolts) {
uint8_t value = 0;
axp_read(0x27, &value);
axp_write(0x27, (value & 0x80) | axp_dc_voltage_data(millivolts)); // DCDC3, LCD backlight
}
static void core2_axp192_set_backlight(float brightness) {
if (brightness <= 0.0f) {
axp_update(0x12, 0x02, 0x00); // DCDC3 off
return;
}
if (brightness > 1.0f) brightness = 1.0f;
uint16_t millivolts = static_cast<uint16_t>(2400 + brightness * 900);
core2_axp192_set_lcd_voltage(millivolts);
axp_update(0x12, 0x00, 0x02); // DCDC3 on
}
static void core2_axp192_init(esphome::i2c::I2CBus *bus) {
axp192.set_i2c_bus(bus);
axp192.set_i2c_address(AXP192_ADDR);
axp192_ready = true;
ESP_LOGI(POWER_TAG, "Initializing M5Stack Core2 AXP192 LCD power");
axp_update(0x30, 0xF9, 0x02); // Disable VBUS current limit, preserve bit 2.
axp_update(0x92, 0x07, 0x00); // GPIO1 open-drain output.
axp_update(0x93, 0x07, 0x00); // GPIO2 open-drain output.
axp_write(0x35, 0xA2); // RTC battery charging.
uint8_t value = 0;
axp_read(0x26, &value);
axp_write(0x26, (value & 0x80) | axp_dc_voltage_data(3350)); // DCDC1 ESP32 VDD.
core2_axp192_set_lcd_voltage(2800);
uint8_t ldo2 = axp_ldo_voltage_data(3300);
uint8_t ldo3 = axp_ldo_voltage_data(2000);
axp_write(0x28, (ldo2 << 4) | ldo3); // LDO2 LCD logic/SD, LDO3 vibrator.
axp_update(0x12, 0x00, 0x07); // Enable DCDC1, DCDC3, LDO2.
axp_write(0x82, 0xFF); // ADCs on.
axp_update(0x95, 0x8D, 0x84); // GPIO4 setup, as M5Core2 library does.
axp_write(0x36, 0x4C); // Power key timing.
// LCD reset through AXP192 GPIO4.
axp_update(0x96, 0x02, 0x00);
delay(100);
axp_update(0x96, 0x00, 0x02);
delay(100);
core2_axp192_set_backlight(1.0f);
}
static const uint8_t ACCU_BMP[32] = {
0b00000000, 0b00000000,
0b00000000, 0b00000000,
0b00000000, 0b00000000,
0b11111111, 0b11111110,
0b10000000, 0b00000010,
0b10000000, 0b00000011,
0b10000000, 0b00000011,
0b10000000, 0b00000011,
0b10000000, 0b00000011,
0b10000000, 0b00000011,
0b10000000, 0b00000011,
0b10000000, 0b00000010,
0b11111111, 0b11111110,
0b00000000, 0b00000000,
0b00000000, 0b00000000,
0b00000000, 0b00000000,
};
static const uint8_t BLE_BMP[32] = {
0b00000001, 0b10000000,
0b00000001, 0b11000000,
0b00010001, 0b01100000,
0b00011001, 0b00110000,
0b00001101, 0b00011000,
0b00000111, 0b00110000,
0b00000011, 0b01100000,
0b00000001, 0b11000000,
0b00000001, 0b11000000,
0b00000011, 0b01100000,
0b00000111, 0b00110000,
0b00001101, 0b00011000,
0b00011001, 0b00110000,
0b00010001, 0b01100000,
0b00000001, 0b11000000,
0b00000001, 0b10000000,
};
static const uint8_t HOLD_BMP[32] = {
0b01111111, 0b11111110,
0b11111111, 0b11111111,
0b11100011, 0b11000111,
0b11100011, 0b11000111,
0b11100011, 0b11000111,
0b11100011, 0b11000111,
0b11100011, 0b11000111,
0b11100000, 0b00000111,
0b11100000, 0b00000111,
0b11100011, 0b11000111,
0b11100011, 0b11000111,
0b11100011, 0b11000111,
0b11100011, 0b11000111,
0b11100011, 0b11000111,
0b11111111, 0b11111111,
0b01111111, 0b11111110,
};
static const uint8_t REL_BMP[32] = {
0b00000001, 0b00000000,
0b00000001, 0b00000000,
0b00000011, 0b10000000,
0b00000011, 0b10000000,
0b00000110, 0b11000000,
0b00000110, 0b11000000,
0b00001100, 0b01100000,
0b00001100, 0b01100000,
0b00011000, 0b00110000,
0b00011000, 0b00110000,
0b00110000, 0b00011000,
0b00110000, 0b00011000,
0b01100000, 0b00001100,
0b01100000, 0b00001100,
0b11111111, 0b11111110,
0b11111111, 0b11111110,
};
static const uint8_t DIODE_BMP[32] = {
0b00001000, 0b00011000,
0b00001100, 0b00011000,
0b00001110, 0b00011000,
0b00001111, 0b00011000,
0b00001111, 0b10011000,
0b00001111, 0b11011000,
0b00001111, 0b11111000,
0b11111111, 0b11111111,
0b11111111, 0b11111111,
0b00001111, 0b11111000,
0b00001111, 0b11011000,
0b00001111, 0b10011000,
0b00001111, 0b00011000,
0b00001110, 0b00011000,
0b00001100, 0b00011000,
0b00001000, 0b00011000,
};
static const uint8_t BUZZ_BMP[32] = {
0b00000000, 0b11000000,
0b00000001, 0b11000000,
0b00000011, 0b11000001,
0b00000111, 0b11000001,
0b00001111, 0b11000101,
0b11111111, 0b11000101,
0b11111111, 0b11010101,
0b11111111, 0b11010101,
0b11111111, 0b11010101,
0b11111111, 0b11010101,
0b11111111, 0b11000101,
0b00001111, 0b11000101,
0b00000111, 0b11000001,
0b00000011, 0b11000001,
0b00000001, 0b11000000,
0b00000000, 0b11000000,
};
class Meter {
public:
static constexpr uint8_t REGPLUSMINUS = 0x00;
static constexpr uint8_t FLAGPLUS = 0b00101011;
static constexpr uint8_t FLAGMINUS = 0b00101101;
static constexpr uint8_t REGDIG1 = 0x01;
static constexpr uint8_t REGDIG2 = 0x02;
static constexpr uint8_t REGDIG3 = 0x03;
static constexpr uint8_t REGDIG4 = 0x04;
static constexpr uint8_t REGPOINT = 0x06;
static constexpr uint8_t FLAGPOINT0 = 0b00110000;
static constexpr uint8_t FLAGPOINT1 = 0b00110001;
static constexpr uint8_t FLAGPOINT2 = 0b00110010;
static constexpr uint8_t FLAGPOINT3 = 0b00110100;
static constexpr uint8_t REGMODE = 0x07;
static constexpr uint8_t FLAGMODEHOLD = 0b00000010;
static constexpr uint8_t FLAGMODEREL = 0b00000100;
static constexpr uint8_t FLAGMODEAC = 0b00001000;
static constexpr uint8_t FLAGMODEDC = 0b00010000;
static constexpr uint8_t FLAGMODEAUTO = 0b00100000;
static constexpr uint8_t REGMINMAX = 0x08;
static constexpr uint8_t FLAGMIN = 0b00010000;
static constexpr uint8_t FLAGMAX = 0b00100000;
static constexpr uint8_t REGSCALE = 0x09;
static constexpr uint8_t FLAGSCALEDUTY = 0b00000010;
static constexpr uint8_t FLAGSCALEDIODE = 0b00000100;
static constexpr uint8_t FLAGSCALEBUZZ = 0b00001000;
static constexpr uint8_t FLAGSCALEMEGA = 0b00010000;
static constexpr uint8_t FLAGSCALEKILO = 0b00100000;
static constexpr uint8_t FLAGSCALEMILLI = 0b01000000;
static constexpr uint8_t FLAGSCALEMICRO = 0b10000000;
static constexpr uint8_t REGUNIT = 0x0a;
static constexpr uint8_t FLAGUNITFAHR = 0b00000001;
static constexpr uint8_t FLAGUNITGRAD = 0b00000010;
static constexpr uint8_t FLAGUNITNF = 0b00000100;
static constexpr uint8_t FLAGUNITHZ = 0b00001000;
static constexpr uint8_t FLAGUNITHFE = 0b00010000;
static constexpr uint8_t FLAGUNITOHM = 0b00100000;
static constexpr uint8_t FLAGUNITAMP = 0b01000000;
static constexpr uint8_t FLAGUNITVOLT = 0b10000000;
bool connected{false};
bool write_available{false};
bool is_plus{false};
bool low_battery{false};
bool overload{false};
bool has_reading{false};
uint8_t selected_button{1};
uint32_t last_notify_ms{0};
bool handle_notify(const std::vector<uint8_t> &data) {
if (data.size() > sizeof(this->raw_))
return false;
if (data.size() == 6 && data[1] >= 0xF0) {
memset(this->raw_, 0, sizeof(this->raw_));
memcpy(this->raw_, data.data(), data.size());
this->is_plus = true;
this->parse_plus_();
} else if (data.size() == 14 && data[12] == 0x0D && data[13] == 0x0A) {
memset(this->value_, 0, sizeof(this->value_));
memcpy(this->value_, data.data(), data.size());
this->is_plus = false;
} else {
ESP_LOGW(TAG, "Ignoring unexpected OWON frame length=%u", static_cast<unsigned>(data.size()));
return false;
}
this->overload = memcmp(this->value_, OVERLOAD_FRAME, sizeof(OVERLOAD_FRAME)) == 0;
this->display_value = this->calc_display_value_();
this->base_value = this->calc_base_value_();
this->has_reading = true;
this->last_notify_ms = millis();
return true;
}
void on_connect() {
this->connected = true;
this->write_available = true;
}
void on_disconnect() {
this->connected = false;
this->write_available = false;
}
float value() const { return this->display_value; }
float value_base() const { return this->base_value; }
bool negative() const { return (this->value_[REGPLUSMINUS] & FLAGMINUS) == FLAGMINUS; }
bool auto_range() const { return (this->value_[REGMODE] & FLAGMODEAUTO) == FLAGMODEAUTO; }
bool hold() const { return (this->value_[REGMODE] & FLAGMODEHOLD) == FLAGMODEHOLD; }
bool relative() const { return (this->value_[REGMODE] & FLAGMODEREL) == FLAGMODEREL; }
bool ac() const { return (this->value_[REGMODE] & FLAGMODEAC) == FLAGMODEAC; }
bool dc() const { return (this->value_[REGMODE] & FLAGMODEDC) == FLAGMODEDC; }
bool min_mode() const { return (this->value_[REGMINMAX] & FLAGMIN) == FLAGMIN; }
bool max_mode() const { return (this->value_[REGMINMAX] & FLAGMAX) == FLAGMAX; }
bool diode() const { return (this->value_[REGSCALE] & FLAGSCALEDIODE) == FLAGSCALEDIODE; }
bool continuity() const { return (this->value_[REGSCALE] & FLAGSCALEBUZZ) == FLAGSCALEBUZZ; }
const char *unit() const {
switch (this->value_[REGUNIT]) {
case FLAGUNITFAHR: return "°F";
case FLAGUNITGRAD: return "°C";
case FLAGUNITNF: return "nF";
case FLAGUNITHZ: return "Hz";
case FLAGUNITHFE: return "hFE";
case FLAGUNITOHM: return "Ω";
case FLAGUNITAMP: return "A";
case FLAGUNITVOLT: return "V";
default: return "";
}
}
const char *scale() const {
if ((this->value_[REGSCALE] & FLAGSCALEDUTY) == FLAGSCALEDUTY) return "%";
if ((this->value_[REGSCALE] & FLAGSCALEMEGA) == FLAGSCALEMEGA) return "M";
if ((this->value_[REGSCALE] & FLAGSCALEKILO) == FLAGSCALEKILO) return "k";
if ((this->value_[REGSCALE] & FLAGSCALEMILLI) == FLAGSCALEMILLI) return "m";
if ((this->value_[REGSCALE] & FLAGSCALEMICRO) == FLAGSCALEMICRO) return "µ";
return "";
}
std::string mode_text() const {
std::string out;
if (this->dc()) out += "DC ";
if (this->ac()) out += "AC ";
if (this->auto_range()) out += "AUTO ";
if (this->hold()) out += "HOLD ";
if (this->relative()) out += "REL ";
if (this->min_mode()) out += "MIN ";
if (this->max_mode()) out += "MAX ";
if (this->diode()) out += "DIODE ";
if (this->continuity()) out += "CONT ";
if (!out.empty()) out.pop_back();
return out;
}
std::string reading_text() const {
if (!this->connected) return "Disconnected";
if (!this->has_reading) return "Waiting for data";
if (this->overload) return "OL " + std::string(this->scale()) + this->unit();
char buf[48];
snprintf(buf, sizeof(buf), "%s%.4g %s%s", this->negative() ? "-" : "", std::fabs(this->display_value), this->scale(), this->unit());
return std::string(buf);
}
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 {
if ((this->value_[REGSCALE] & FLAGSCALEDUTY) == FLAGSCALEDUTY) return KIND_DUTY;
switch (this->value_[REGUNIT]) {
case FLAGUNITVOLT: return KIND_VOLTAGE;
case FLAGUNITAMP: return KIND_CURRENT;
case FLAGUNITOHM: return KIND_RESISTANCE;
case FLAGUNITHZ: return KIND_FREQUENCY;
case FLAGUNITNF: return KIND_CAPACITANCE;
case FLAGUNITGRAD: return KIND_TEMP_C;
case FLAGUNITFAHR: return KIND_TEMP_F;
default: return KIND_OTHER;
}
}
const char *selected_button_name() const {
static const char *const names[] = {"SELECT", "RANGE", "HLD/LIG", "REL/BT", "HZ/DUTY", "MAX/MIN"};
uint8_t index = this->selected_button;
if (index < 1) index = 1;
if (index > 6) index = 6;
return names[index - 1];
}
void previous_button() {
if (this->selected_button > 1) this->selected_button--;
}
void next_button() {
if (this->selected_button < 6) this->selected_button++;
}
void render(esphome::display::Display &it, esphome::display::BaseFont *font,
esphome::display::BaseFont *value_font, int display_page = 0,
bool atorch_connected = false,
float atorch_voltage = NAN, float atorch_current = NAN, float atorch_power = NAN,
float atorch_capacity = NAN, float atorch_energy = NAN, float atorch_temperature = NAN) {
const Color bg(0, 0, 0);
const Color fg(210, 210, 210);
// Chosen to map to a neutral dark gray in the RGB332 8-bit display palette.
const Color inactive(80, 80, 80);
const Color yellow(255, 220, 0);
const Color blue(0, 80, 255);
const Color cyan(0, 255, 255);
const Color magenta(255, 0, 255);
const Color red(255, 0, 0);
const Color green(0, 220, 0);
const Color orange(255, 165, 0);
if (display_page == 0) {
// --- PAGE 1: OWON Multimeter ---
it.fill(bg);
bool status_active = this->connected && this->has_reading;
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->label_(it, font, 86, 8, "AUTO", status_active && this->auto_range() ? fg : inactive);
this->label_(it, font, 138, 8, "MAX", status_active && this->max_mode() ? red : inactive);
this->label_(it, font, 178, 8, "MIN", status_active && this->min_mode() ? green : inactive);
this->draw_icon_(it, 218, 8, 16, 16, HOLD_BMP, status_active && this->hold() ? blue : inactive);
this->draw_icon_(it, 244, 8, 16, 16, REL_BMP, status_active && this->relative() ? Color(128, 128, 0) : inactive);
this->draw_icon_(it, 270, 8, 16, 16, DIODE_BMP, status_active && this->diode() ? magenta : inactive);
this->draw_icon_(it, 296, 8, 16, 16, BUZZ_BMP, status_active && this->continuity() ? orange : 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);
if (!this->connected) {
this->draw_digits_(it, "----", false, inactive);
it.print(160, 148, font, inactive, esphome::display::TextAlign::CENTER, "scan/connect");
} else if (!this->has_reading) {
this->draw_digits_(it, "8888", false, inactive);
it.print(160, 148, font, inactive, esphome::display::TextAlign::CENTER, "waiting");
} else if (this->overload) {
this->draw_digits_(it, " OL ", false, fg);
} else {
char d[5];
d[0] = this->digit_char_(REGDIG1);
d[1] = this->digit_char_(REGDIG2);
d[2] = this->digit_char_(REGDIG3);
d[3] = this->digit_char_(REGDIG4);
d[4] = 0;
this->draw_digits_(it, d, this->negative(), fg);
this->draw_decimal_points_(it, fg);
}
if (status_active) {
std::string unit_line = std::string(this->scale()) + this->unit();
it.print(316, 141, font, yellow, esphome::display::TextAlign::BOTTOM_RIGHT, unit_line.c_str());
}
bool bargraph_active = status_active && !this->overload;
this->draw_bargraph_(it, bargraph_active ? this->digits_from_buffer_() : 0, bargraph_active);
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(242, 212, 40, 24, this->write_available ? fg : inactive);
it.print(54, 216, font, bg, esphome::display::TextAlign::TOP_CENTER, "<");
it.print(158, 216, font, bg, esphome::display::TextAlign::TOP_CENTER, this->selected_button_name());
it.print(262, 216, font, bg, esphome::display::TextAlign::TOP_CENTER, ">");
} else {
// --- PAGE 2: Atorch DL24 DC load ---
it.fill(bg);
const Color panel_active(18, 24, 32);
const Color panel_inactive(12, 12, 12);
const Color panel_dim(10, 14, 20);
const Color border_active(55, 70, 86);
const Color border_inactive(42, 42, 42);
const Color white(245, 245, 245);
const Color dim_value(110, 110, 110);
const Color dim_accent(65, 65, 65);
const Color panel = atorch_connected ? panel_active : panel_inactive;
const Color border = atorch_connected ? border_active : border_inactive;
const Color value_color = atorch_connected ? white : dim_value;
const Color voltage_color = atorch_connected ? cyan : dim_accent;
const Color current_color = atorch_connected ? orange : dim_accent;
const Color power_color = atorch_connected ? yellow : dim_accent;
const Color temp_color = atorch_connected ? magenta : dim_accent;
const Color header_color = atorch_connected ? cyan : dim_value;
char voltage_text[24];
char current_text[24];
char power_text[24];
char capacity_text[24];
char energy_text[24];
char temperature_text[24];
this->format_metric_(voltage_text, sizeof(voltage_text), atorch_voltage, "V", 2);
this->format_metric_(current_text, sizeof(current_text), atorch_current, "A", 3);
this->format_metric_(power_text, sizeof(power_text), atorch_power, "W", 2);
this->format_metric_(capacity_text, sizeof(capacity_text), atorch_capacity, "Ah", 3);
this->format_metric_(energy_text, sizeof(energy_text), atorch_energy, "Wh", 3);
this->format_metric_(temperature_text, sizeof(temperature_text), atorch_temperature, "°C", 1);
it.filled_rectangle(0, 0, 320, 30, atorch_connected ? panel_dim : panel_inactive);
it.print(10, 7, font, header_color, esphome::display::TextAlign::TOP_LEFT, "ATORCH DL24");
if (atorch_connected) {
this->draw_icon_(it, 152, 7, 16, 16, BLE_BMP, blue);
}
it.print(310, 7, font, inactive, esphome::display::TextAlign::TOP_RIGHT, "DC LOAD");
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, 10, 128, 145, 76, "POWER", power_text, power_color, panel, border, value_color);
this->draw_metric_card_(it, font, value_font, 165, 128, 145, 76, "TEMP", temperature_text, temp_color, panel, border, value_color);
it.filled_rectangle(10, 212, 300, 22, atorch_connected ? panel_dim : panel_inactive);
it.filled_rectangle(10, 212, 300, 1, border);
it.filled_rectangle(10, 233, 300, 1, border);
it.filled_rectangle(10, 212, 1, 22, border);
it.filled_rectangle(309, 212, 1, 22, border);
it.print(20, 216, font, inactive, esphome::display::TextAlign::TOP_LEFT, "CAP");
it.print(64, 216, font, value_color, esphome::display::TextAlign::TOP_LEFT, capacity_text);
it.print(170, 216, font, inactive, esphome::display::TextAlign::TOP_LEFT, "ENERGY");
it.print(306, 216, font, value_color, esphome::display::TextAlign::TOP_RIGHT, energy_text);
}
}
private:
uint8_t raw_[14]{};
uint8_t value_[14]{};
float display_value{NAN};
float base_value{NAN};
static constexpr uint8_t OVERLOAD_FRAME[5] = {0x2B, 0x3F, 0x30, 0x3A, 0x3F};
uint16_t digits_from_buffer_() const {
uint16_t out = 0;
if (this->value_[REGDIG1] >= '0' && this->value_[REGDIG1] <= '9') out += (this->value_[REGDIG1] - '0') * 1000;
if (this->value_[REGDIG2] >= '0' && this->value_[REGDIG2] <= '9') out += (this->value_[REGDIG2] - '0') * 100;
if (this->value_[REGDIG3] >= '0' && this->value_[REGDIG3] <= '9') out += (this->value_[REGDIG3] - '0') * 10;
if (this->value_[REGDIG4] >= '0' && this->value_[REGDIG4] <= '9') out += (this->value_[REGDIG4] - '0');
return out;
}
float calc_display_value_() const {
if (this->overload) return NAN;
if (this->is_plus) {
uint16_t pair1 = static_cast<uint16_t>(this->raw_[0]) | (static_cast<uint16_t>(this->raw_[1]) << 8);
uint8_t decimal = pair1 & 0x07;
if (decimal >= 7) return NAN;
uint16_t pair3 = static_cast<uint16_t>(this->raw_[4]) | (static_cast<uint16_t>(this->raw_[5]) << 8);
bool negative = pair3 >= 0x7FFF;
uint16_t digits = negative ? (pair3 & 0x7FFF) : pair3;
float v = static_cast<float>(digits) / std::pow(10.0f, decimal);
return negative ? -v : v;
}
uint8_t decimal = 0;
switch (this->value_[REGPOINT] & 0x07) {
case 0b001: decimal = 1; break;
case 0b010: decimal = 2; break;
case 0b100: decimal = 3; break;
default: break;
}
float v = static_cast<float>(this->digits_from_buffer_()) / std::pow(10.0f, decimal);
return this->negative() ? -v : v;
}
float calc_base_value_() const {
if (std::isnan(this->display_value)) return NAN;
if (this->value_[REGUNIT] == FLAGUNITNF) return this->display_value * 1e-9f;
if ((this->value_[REGSCALE] & FLAGSCALEMEGA) == FLAGSCALEMEGA) return this->display_value * 1e6f;
if ((this->value_[REGSCALE] & FLAGSCALEKILO) == FLAGSCALEKILO) return this->display_value * 1e3f;
if ((this->value_[REGSCALE] & FLAGSCALEMILLI) == FLAGSCALEMILLI) return this->display_value * 1e-3f;
if ((this->value_[REGSCALE] & FLAGSCALEMICRO) == FLAGSCALEMICRO) return this->display_value * 1e-6f;
return this->display_value;
}
void parse_plus_() {
memset(this->value_, 0, sizeof(this->value_));
this->value_[5] = 0x20;
this->value_[12] = 0x0D;
this->value_[13] = 0x0A;
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 scale = (pair1 >> 3) & 0x07;
uint8_t decimal = pair1 & 0x07;
switch (decimal) {
case 0: this->value_[REGPOINT] = FLAGPOINT0; break;
case 1: this->value_[REGPOINT] = FLAGPOINT3; break;
case 2: this->value_[REGPOINT] = FLAGPOINT2; break;
case 3: this->value_[REGPOINT] = FLAGPOINT1; break;
default: break;
}
switch (function) {
case 0: this->value_[REGUNIT] |= FLAGUNITVOLT; this->value_[REGMODE] |= FLAGMODEDC; break;
case 1: this->value_[REGUNIT] |= FLAGUNITVOLT; this->value_[REGMODE] |= FLAGMODEAC; break;
case 2: this->value_[REGUNIT] |= FLAGUNITAMP; this->value_[REGMODE] |= FLAGMODEDC; break;
case 3: this->value_[REGUNIT] |= FLAGUNITAMP; this->value_[REGMODE] |= FLAGMODEAC; break;
case 4: this->value_[REGUNIT] |= FLAGUNITOHM; break;
case 5: this->value_[REGUNIT] |= FLAGUNITNF; break;
case 6: this->value_[REGUNIT] |= FLAGUNITHZ; break;
case 7: this->value_[REGSCALE] |= FLAGSCALEDUTY; break;
case 8: this->value_[REGUNIT] |= FLAGUNITGRAD; break;
case 9: this->value_[REGUNIT] |= FLAGUNITFAHR; break;
case 10: this->value_[REGSCALE] |= FLAGSCALEDIODE; break;
case 11: this->value_[REGSCALE] |= FLAGSCALEBUZZ; break;
case 12: this->value_[REGUNIT] |= FLAGUNITHFE; break;
default: break;
}
switch (scale) {
case 2: this->value_[REGSCALE] |= FLAGSCALEMICRO; break;
case 3: this->value_[REGSCALE] |= FLAGSCALEMILLI; break;
case 5: this->value_[REGSCALE] |= FLAGSCALEKILO; break;
case 6: this->value_[REGSCALE] |= FLAGSCALEMEGA; break;
default: break;
}
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 << 1)) this->value_[REGMODE] |= FLAGMODEREL;
if (pair2 & (1 << 2)) this->value_[REGMODE] |= FLAGMODEAUTO;
this->low_battery = (pair2 & (1 << 3)) != 0;
if (pair2 & (1 << 4)) this->value_[REGMINMAX] |= FLAGMIN;
if (pair2 & (1 << 5)) this->value_[REGMINMAX] |= FLAGMAX;
uint16_t pair3 = static_cast<uint16_t>(this->raw_[4]) | (static_cast<uint16_t>(this->raw_[5]) << 8);
if (decimal < 7) {
uint16_t digits = pair3;
if (pair3 < 0x7FFF) {
this->value_[REGPLUSMINUS] = FLAGPLUS;
} else {
this->value_[REGPLUSMINUS] = FLAGMINUS;
digits = pair3 & 0x7FFF;
}
this->value_[REGDIG1] = '0' + ((digits / 1000) % 10);
this->value_[REGDIG2] = '0' + ((digits / 100) % 10);
this->value_[REGDIG3] = '0' + ((digits / 10) % 10);
this->value_[REGDIG4] = '0' + (digits % 10);
} else {
memcpy(this->value_, OVERLOAD_FRAME, sizeof(OVERLOAD_FRAME));
}
}
char digit_char_(uint8_t reg) const {
uint8_t c = this->value_[reg];
return (c >= '0' && c <= '9') ? static_cast<char>(c) : ' ';
}
void format_metric_(char *buffer, size_t size, float value, const char *unit, uint8_t decimals) const {
if (!std::isfinite(value)) {
snprintf(buffer, size, "-- %s", unit);
return;
}
char format[12];
snprintf(format, sizeof(format), "%%.%uf %%s", decimals);
snprintf(buffer, size, format, value, unit);
}
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,
Color accent, Color fill, Color border, Color value_color) {
const Color bg(0, 0, 0);
const Color inactive(90, 100, 110);
it.filled_rectangle(x, y, w, h, fill);
it.filled_rectangle(x, y, w, 2, accent);
it.filled_rectangle(x, y + h - 1, w, 1, border);
it.filled_rectangle(x, y, 1, h, border);
it.filled_rectangle(x + w - 1, y, 1, h, border);
it.print(x + 10, y + 9, label_font, inactive, esphome::display::TextAlign::TOP_LEFT, title);
it.print(x + w / 2, y + 43, value_font, value_color, esphome::display::TextAlign::CENTER, value);
it.filled_rectangle(x + 10, y + h - 11, w - 20, 3, bg);
it.filled_rectangle(x + 10, y + h - 11, w - 20, 1, accent);
}
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);
}
void draw_digits_(Display &it, const char *text, bool negative, Color color) {
if (negative) this->draw_segment_(it, 8, 83, 26, 9, true, color);
constexpr int digit_x = 40;
constexpr int digit_y = 35;
constexpr int digit_w = 50;
constexpr int digit_h = 106;
constexpr int digit_distance = 64;
for (int i = 0; i < 4; i++) {
this->draw_seven_segment_(it, digit_x + i * digit_distance, digit_y, digit_w, digit_h, text[i], color);
}
}
void draw_decimal_points_(Display &it, Color color) {
uint8_t p = this->value_[REGPOINT];
if ((p & FLAGPOINT1) == FLAGPOINT1) it.filled_rectangle(95, 135, 8, 10, color);
if ((p & FLAGPOINT2) == FLAGPOINT2) it.filled_rectangle(159, 135, 8, 10, color);
if ((p & FLAGPOINT3) == FLAGPOINT3) it.filled_rectangle(223, 135, 8, 10, color);
}
void draw_segment_(Display &it, int x, int y, int w, int h, bool horizontal, Color color) {
if (horizontal) {
int cap = h / 2;
it.filled_rectangle(x + cap, y, w - 2 * cap, h +1, color);
it.filled_triangle(x, y + cap, x + cap, y, x + cap, y + h, color);
it.filled_triangle(x + w, y + cap, x + w - cap, y, x + w - cap, y + h, color);
} else {
int cap = w / 2;
it.filled_rectangle(x, y + cap, w +1, h - 2 * cap, color);
it.filled_triangle(x + cap, y, x, y + cap, x + w, y + cap, color);
it.filled_triangle(x + cap, y + h, x, y + h - cap, x + w, y + h - cap, 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 col = 0; col < w; col++) {
uint8_t byte = data[row * ((w + 7) / 8) + (col / 8)];
if ((byte & (0x80 >> (col % 8))) != 0) {
it.draw_pixel_at(x + col, y + row, 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;
switch (ch) {
case '0': a=b=c=d=e=f=true; break;
case '1': b=c=true; break;
case '2': a=b=d=e=g=true; break;
case '3': a=b=c=d=g=true; break;
case '4': b=c=f=g=true; break;
case '5': a=c=d=f=g=true; break;
case '6': a=c=d=e=f=g=true; break;
case '7': a=b=c=true; break;
case '8': a=b=c=d=e=f=g=true; break;
case '9': a=b=c=d=f=g=true; break;
case 'O': a=b=c=d=e=f=true; break;
case 'L': d=e=f=true; break;
case '-': g=true; break;
default: break;
}
int t = 10;
int half = h / 2;
int gap = 1;
int top_y = y;
int mid_y = y + half - t / 2;
int bot_y = y + h - t;
int upper_v_y = top_y + t + gap;
int upper_v_h = mid_y - gap - upper_v_y;
int lower_v_y = mid_y + t + gap;
int lower_v_h = bot_y - gap - lower_v_y;
if (a) this->draw_segment_(it, x + t / 2, top_y, w - t, t, true, color);
if (b && upper_v_h > 0) this->draw_segment_(it, x + w - t, upper_v_y, t, upper_v_h, false, color);
if (c && lower_v_h > 0) this->draw_segment_(it, x + w - t, lower_v_y, t, lower_v_h, false, color);
if (d) this->draw_segment_(it, x + t / 2, bot_y, w - t, t, true, color);
if (e && lower_v_h > 0) this->draw_segment_(it, x, lower_v_y, t, lower_v_h, false, color);
if (f && upper_v_h > 0) this->draw_segment_(it, x, upper_v_y, t, upper_v_h, false, color);
if (g) this->draw_segment_(it, x + t / 2, mid_y, w - t, t, true, color);
}
void draw_bargraph_(Display &it, uint16_t digits, bool active) {
const Color fg(255, 255, 255);
const Color inactive(80, 80, 80);
uint16_t mapped = active ? static_cast<uint16_t>(digits * 240 / 6000) : 0;
if (mapped > 240) mapped = 240;
for (uint16_t i = 0; i <= 240; i += 4) {
Color col = (active && i <= mapped) ? fg : inactive;
int h = (i % 40 == 0) ? 20 : ((i % 20 == 0) ? 15 : 10);
it.vertical_line(40 + i, 185 - h, h, col);
}
it.horizontal_line(35, 185, 250, inactive);
}
};
} // namespace owon_b35t
static owon_b35t::Meter owon_meter;
+628
View File
@@ -0,0 +1,628 @@
# Connects to an OWON BT35T/BT35T+ multimeter and an Atorch DL24 dc-load and mirrors their
# displays. Also sends those values to Home Assistant for logging and dashboard display.
# Also exposes actionable buttons to HA to control the devices.
#
# Derived work based on https://github.com/reaper7/M5Stack_BLE_client_Owon_B35T by reaper7.
# AI (ChatGPT (GPT5.5) has been used to adopt the Arduino sketch to ESPHome
# Ported to M5Stack Core2 due to memory constraints.
# Integrated Atorch BLE proxy functionality from https://github.com/syssi/esphome-atorch-dl24 by syssi.
# Soft buttons work as indicated for OWON meter. Left/right so select action, middle to execute.
# Long-press is supported. Display switches to last connected device, tap center of screen to
# switch. The dc-load has no on-device buttons, but both are available from HA.
substitutions:
name: "lab-ble-proxy"
friendly_name: "Lab BLE Proxy"
device_description: "M5Stack Core2 BLE client for OWON B35T/B35T+ multimeter and Atorch DL24 DC load"
owon_mac_address: !secret owon_b35t_mac_address
dl24_mac_address: !secret dl24_mac_address
external_components_source: github://syssi/esphome-atorch-dl24@main
atorch_project_version: "2.1.0"
esphome:
name: ${name}
friendly_name: ${friendly_name}
comment: ${device_description}
min_version: 2024.6.0
includes:
- lab-ble-proxy.h
on_boot:
priority: 850
then:
- lambda: |-
owon_b35t::core2_axp192_init(id(core2_i2c));
project:
name: "custom.lab-ble-proxy-m5stack-core2"
version: "1.0"
esp32:
board: m5stack-core2
flash_size: 16MB
framework:
type: esp-idf
advanced:
minimum_chip_revision: "3.1"
sram1_as_iram: true
psram:
mode: quad
speed: 80MHz
external_components:
- source: ${external_components_source}
refresh: 0s
logger:
level: INFO
api:
encryption:
key: !secret apikey
ota:
platform: esphome
password: !secret ota
network:
enable_ipv6: true
wifi:
ssid: "Voltage-legacy"
password: !secret voltage_legacy_psk
#use_address: ${name}.home
power_save_mode: none
fast_connect: on
min_auth_mode: WPA2
ap:
ssid: "Lab BLE Proxy Fallback Hotspot"
password: !secret fallback_psk
captive_portal:
globals:
- id: display_page
type: int
initial_value: "0"
restore_value: no
- id: atorch_connected
type: bool
initial_value: "false"
restore_value: no
script:
- id: wake_backlight
mode: restart
then:
- script.stop: backlight_idle
- light.turn_on:
id: backlight
brightness: 100%
- id: backlight_idle
mode: restart
then:
- delay: 2min
- if:
condition:
lambda: |-
return !owon_meter.connected && !id(atorch_connected);
then:
- light.turn_on:
id: backlight
brightness: 50%
- delay: 3min
- if:
condition:
lambda: |-
return !owon_meter.connected && !id(atorch_connected);
then:
- light.turn_off: backlight
interval:
- interval: 10s
then:
- 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",
static_cast<unsigned>(esp_get_free_heap_size()),
static_cast<unsigned>(esp_get_minimum_free_heap_size()),
static_cast<unsigned>(heap_caps_get_free_size(MALLOC_CAP_INTERNAL)),
static_cast<unsigned>(heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL)),
static_cast<unsigned>(heap_caps_get_free_size(MALLOC_CAP_DMA)),
static_cast<unsigned>(heap_caps_get_largest_free_block(MALLOC_CAP_DMA)),
static_cast<unsigned>(heap_caps_get_free_size(MALLOC_CAP_SPIRAM)),
static_cast<unsigned>(heap_caps_get_largest_free_block(MALLOC_CAP_SPIRAM)));
esp32_ble_tracker:
scan_parameters:
active: true
continuous: true
ble_client:
- mac_address: ${owon_mac_address}
id: owon_ble_client
on_connect:
then:
- script.execute: wake_backlight
- lambda: |-
owon_meter.on_connect();
id(display_page) = 0;
id(lcd).update();
on_disconnect:
then:
- lambda: |-
owon_meter.on_disconnect();
if (id(display_page) == 0 && id(atorch_connected)) {
id(display_page) = 1;
}
id(lcd).update();
- if:
condition:
lambda: |-
return !owon_meter.connected && !id(atorch_connected);
then:
- script.execute: backlight_idle
- mac_address: ${dl24_mac_address}
id: atorch_ble_client
on_connect:
then:
- script.execute: wake_backlight
- lambda: |-
id(atorch_connected) = true;
id(display_page) = 1;
id(lcd).update();
on_disconnect:
then:
- lambda: |-
id(atorch_connected) = false;
if (id(display_page) == 1 && owon_meter.connected) {
id(display_page) = 0;
}
id(lcd).update();
- if:
condition:
lambda: |-
return !owon_meter.connected && !id(atorch_connected);
then:
- script.execute: backlight_idle
atorch_dl24:
- id: atorch0
ble_client_id: atorch_ble_client
check_crc: false
throttle: 0s
spi:
clk_pin: GPIO18
mosi_pin: GPIO23
i2c:
id: core2_i2c
sda: GPIO21
scl: GPIO22
scan: true
output:
- platform: template
type: float
id: lcd_backlight
write_action:
- lambda: |-
owon_b35t::core2_axp192_set_backlight(state);
light:
- platform: monochromatic
output: lcd_backlight
name: "${friendly_name} Backlight"
id: backlight
restore_mode: ALWAYS_ON
font:
- file: "fonts/Roboto-Regular.ttf"
id: meter_font
size: 15
glyphs:
[
" ",
"!",
"%",
"+",
"-",
".",
"/",
":",
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"<",
">",
"A",
"B",
"C",
"D",
"E",
"F",
"G",
"H",
"I",
"L",
"M",
"N",
"O",
"P",
"R",
"S",
"T",
"U",
"V",
"W",
"X",
"Y",
"Z",
"a",
"b",
"c",
"d",
"e",
"f",
"g",
"h",
"i",
"k",
"l",
"m",
"n",
"o",
"p",
"r",
"s",
"t",
"u",
"v",
"w",
"y",
"z",
"°",
"µ",
"Ω",
]
- file: "fonts/Roboto-Medium.ttf"
id: atorch_value_font
size: 22
glyphs:
[
" ",
"+",
"-",
".",
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"A",
"C",
"V",
"W",
"h",
"°",
]
display:
- platform: mipi_spi
id: lcd
model: M5CORE2
update_interval: 500ms
lambda: |-
owon_meter.render(
it,
id(meter_font),
id(atorch_value_font),
id(display_page),
id(atorch_connected),
id(atorch_voltage).has_state() ? id(atorch_voltage).state : NAN,
id(atorch_current).has_state() ? id(atorch_current).state : NAN,
id(atorch_power).has_state() ? id(atorch_power).state : NAN,
id(atorch_capacity).has_state() ? id(atorch_capacity).state : NAN,
id(atorch_energy_calculated).has_state() ? id(atorch_energy_calculated).state : NAN,
id(atorch_temperature).has_state() ? id(atorch_temperature).state : NAN
);
touchscreen:
- platform: ft63x6
id: touch
display: lcd
on_touch:
then:
- script.execute: wake_backlight
- if:
condition:
lambda: |-
return !owon_meter.connected && !id(atorch_connected);
then:
- script.execute: backlight_idle
binary_sensor:
- platform: touchscreen
touchscreen_id: touch
id: btn_toggle_page
x_min: 70
x_max: 250
y_min: 45
y_max: 195
on_press:
then:
- lambda: |-
id(display_page) = 1 - id(display_page);
id(lcd).update();
- platform: touchscreen
id: button_a
touchscreen_id: touch
x_min: 34
x_max: 74
y_min: 212
y_max: 240
internal: true
on_press:
then:
- lambda: |-
owon_meter.previous_button();
- platform: touchscreen
id: button_b
touchscreen_id: touch
x_min: 108
x_max: 208
y_min: 212
y_max: 240
internal: true
on_click:
- min_length: 50ms
max_length: 1500ms
then:
- logger.log:
level: INFO
format: "OWON short press: %s"
args: ["owon_meter.selected_button_name()"]
- ble_client.ble_write:
id: owon_ble_client
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb"
value: !lambda |-
std::vector<uint8_t> data = {owon_meter.selected_button, 0x01};
return data;
- min_length: 1500ms
max_length: 5000ms
then:
- logger.log:
level: INFO
format: "OWON long press: %s"
args: ["owon_meter.selected_button_name()"]
- ble_client.ble_write:
id: owon_ble_client
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb"
value: !lambda |-
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};
return data;
- platform: touchscreen
id: button_c
touchscreen_id: touch
x_min: 242
x_max: 282
y_min: 212
y_max: 240
internal: true
on_press:
then:
- lambda: |-
owon_meter.next_button();
- platform: template
name: "${friendly_name} OWON Connected"
icon: "mdi:bluetooth-connect"
lambda: |-
return owon_meter.connected;
- platform: template
name: "${friendly_name} OWON Overload"
icon: "mdi:debug-step-over"
lambda: |-
return owon_meter.overload;
- platform: template
name: "${friendly_name} OWON Low Battery"
icon: "mdi:battery-low"
lambda: |-
return owon_meter.low_battery;
- platform: template
name: "${friendly_name} Atorch Connected"
device_class: connectivity
icon: "mdi:bluetooth-connect"
lambda: |-
return id(atorch_connected);
sensor:
- platform: ble_client
type: characteristic
ble_client_id: owon_ble_client
id: owon_notify_source
internal: true
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
characteristic_uuid: "0000fff4-0000-1000-8000-00805f9b34fb"
notify: true
update_interval: never
lambda: |-
owon_meter.handle_notify(x);
return owon_meter.value();
- platform: wifi_signal
name: "${friendly_name} WiFi Signal"
update_interval: 60s
- platform: atorch_dl24
atorch_dl24_id: atorch0
voltage:
name: "${friendly_name} Atorch Voltage"
id: atorch_voltage
current:
name: "${friendly_name} Atorch Current"
id: atorch_current
power:
name: "${friendly_name} Atorch Power"
id: atorch_power
capacity:
name: "${friendly_name} Atorch Capacity"
id: atorch_capacity
temperature:
name: "${friendly_name} Atorch Temperature"
id: atorch_temperature
dim_backlight:
name: "${friendly_name} Atorch Dim Backlight"
id: atorch_dim_backlight
- platform: integration
name: "${friendly_name} Atorch Energy"
id: atorch_energy_calculated
sensor: atorch_power
time_unit: h
unit_of_measurement: "Wh"
icon: "mdi:lightning-bolt"
device_class: energy
state_class: total_increasing
accuracy_decimals: 3
restore: true
integration_method: trapezoid
text_sensor:
- platform: template
name: "${friendly_name} OWON Reading"
update_interval: 1s
icon: "mdi:gauge"
lambda: |-
return owon_meter.reading_text();
- platform: template
name: "${friendly_name} OWON Unit"
update_interval: 1s
icon: "mdi:ruler"
lambda: |-
return std::string(owon_meter.scale()) + owon_meter.unit();
- platform: template
name: "${friendly_name} OWON Mode"
icon: "mdi:knob"
update_interval: 1s
lambda: |-
return owon_meter.mode_text();
button:
- platform: atorch_dl24
atorch_dl24_id: atorch0
reset_energy:
name: "${friendly_name} Atorch Reset Energy"
on_press:
- sensor.integration.reset: atorch_energy_calculated
reset_capacity:
name: "${friendly_name} Atorch Reset Capacity"
reset_runtime:
name: "${friendly_name} Atorch Reset Runtime"
reset_all:
name: "${friendly_name} Atorch Reset All"
on_press:
- sensor.integration.reset: atorch_energy_calculated
usb_plus:
name: "${friendly_name} Atorch Plus"
usb_minus:
name: "${friendly_name} Atorch Minus"
setup:
name: "${friendly_name} Atorch Setup"
enter:
name: "${friendly_name} Atorch Enter"
- platform: template
name: "OWON SELECT"
id: owon_btn_select
on_press:
- ble_client.ble_write:
id: owon_ble_client
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb"
value: !lambda "return std::vector<uint8_t>({1, 0x01});"
- platform: template
name: "OWON RANGE"
id: owon_btn_range
on_press:
- ble_client.ble_write:
id: owon_ble_client
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb"
value: !lambda "return std::vector<uint8_t>({2, 0x01});"
- platform: template
name: "OWON HOLD "
id: owon_btn_hold
on_press:
- ble_client.ble_write:
id: owon_ble_client
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb"
value: !lambda "return std::vector<uint8_t>({3, 0x01});"
- platform: template
name: "OWON LIGHT"
id: owon_btn_light
on_press:
- ble_client.ble_write:
id: owon_ble_client
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb"
value: !lambda "return std::vector<uint8_t>({3, 0x00});"
- platform: template
name: "OWON RELATIVE"
id: owon_btn_rel
on_press:
- ble_client.ble_write:
id: owon_ble_client
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb"
value: !lambda "return std::vector<uint8_t>({4, 0x01});"
- platform: template
name: "OWON BT"
id: owon_btn_bt
on_press:
- ble_client.ble_write:
id: owon_ble_client
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb"
value: !lambda "return std::vector<uint8_t>({4, 0x00});"
- platform: template
name: "OWON HZ | DUTY"
id: owon_btn_hz
on_press:
- ble_client.ble_write:
id: owon_ble_client
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb"
value: !lambda "return std::vector<uint8_t>({5, 0x01});"
- platform: template
name: "OWON MAX | MIN"
id: owon_btn_maxmin
on_press:
- ble_client.ble_write:
id: owon_ble_client
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb"
value: !lambda "return std::vector<uint8_t>({6, 0x01});"
+6 -1
View File
@@ -53,11 +53,16 @@ esp32:
board: nodemcu-32s
framework:
type: esp-idf
advanced:
minimum_chip_revision: "3.0"
network:
enable_ipv6: true
wifi:
ssid: "Voltage-legacy"
password: !secret voltage_legacy_psk
use_address: ${name}.home
# use_address: ${name}.home
power_save_mode: high
fast_connect: on
min_auth_mode: WPA2
+4 -1
View File
@@ -26,12 +26,15 @@ ota:
id: ota_esphome
password: !secret ota
network:
enable_ipv6: true
wifi:
on_connect:
- delay: 5s # Gives time for improv results to be transmitted
ssid: Voltage-legacy
password: !secret voltage_legacy_psk
use_address: ${name}.home
#use_address: ${name}.home
power_save_mode: high
fast_connect: on
min_auth_mode: WPA2
+4 -1
View File
@@ -50,10 +50,13 @@ esphome:
esp8266:
board: esp12e
network:
enable_ipv6: true
wifi:
ssid: "Voltage-legacy"
password: !secret voltage_legacy_psk
use_address: riden-labornetzteil-18a.home
#use_address: riden-labornetzteil-18a.home
power_save_mode: high
fast_connect: on
min_auth_mode: WPA2
+4 -1
View File
@@ -50,10 +50,13 @@ esphome:
esp8266:
board: esp12e
network:
enable_ipv6: true
wifi:
ssid: "Voltage-legacy"
password: !secret voltage_legacy_psk
use_address: riden-labornetzteil-6a.home
#use_address: riden-labornetzteil-6a.home
power_save_mode: high
fast_connect: on
min_auth_mode: WPA2
+4 -1
View File
@@ -16,10 +16,13 @@ esp32:
framework:
type: esp-idf
network:
enable_ipv6: true
wifi:
ssid: "Voltage-legacy"
password: !secret voltage_legacy_psk
use_address: serverroom.home
#use_address: serverroom.home
power_save_mode: high
fast_connect: on
min_auth_mode: WPA2
-321
View File
@@ -1,321 +0,0 @@
##############################################################################
# Globals Glances parameters
##############################################################################
[global]
# Does Glances should check if a newer version is available on PyPI ?
check_update=false
# History size (maximum number of values)
# Default glances history size to 0
# https://github.com/hassio-addons/addon-glances/issues/61
# https://github.com/nicolargo/glances/issues/1341
history_size=0
##############################################################################
# User interface
##############################################################################
[outputs]
# Theme name for the Curses interface: black or white
curse_theme=black
# Limit the number of processes to display in the WebUI
max_processes_display=30
##############################################################################
# plugins
##############################################################################
[quicklook]
# Define CPU, MEM and SWAP thresholds in %
cpu_careful=50
cpu_warning=70
cpu_critical=90
mem_careful=50
mem_warning=70
mem_critical=90
swap_careful=50
swap_warning=70
swap_critical=90
[cpu]
# Default values if not defined: 50/70/90 (except for iowait)
user_careful=50
user_warning=70
user_critical=90
#user_log=False
#user_critical_action=echo {{user}} {{value}} {{max}} > /tmp/cpu.alert
system_careful=50
system_warning=70
system_critical=90
steal_careful=50
steal_warning=70
steal_critical=90
#steal_log=True
# I/O wait percentage should be lower than 1/# (Logical CPU cores)
# Leave commented to just use the default config (1/#-20% / 1/#-10% / 1/#)
#iowait_careful=30
#iowait_warning=40
#iowait_critical=50
# Context switch limit (core / second)
# Leave commented to just use the default config (critical is 50000*# (Logical CPU cores)
#ctx_switches_careful=10000
#ctx_switches_warning=12000
#ctx_switches_critical=14000
[percpu]
# Define CPU thresholds in %
# Default values if not defined: 50/70/90
user_careful=50
user_warning=70
user_critical=90
iowait_careful=50
iowait_warning=70
iowait_critical=90
system_careful=50
system_warning=70
system_critical=90
[gpu]
# Default processor values if not defined: 50/70/90
proc_careful=50
proc_warning=70
proc_critical=90
# Default memory values if not defined: 50/70/90
mem_careful=50
mem_warning=70
mem_critical=90
[mem]
# Define RAM thresholds in %
# Default values if not defined: 50/70/90
careful=50
#careful_action_repeat=echo {{percent}} >> /tmp/memory.alert
warning=70
critical=90
[memswap]
# Define SWAP thresholds in %
# Default values if not defined: 50/70/90
careful=50
warning=70
critical=90
[load]
# Define LOAD thresholds
# Value * number of cores
# Default values if not defined: 0.7/1.0/5.0 per number of cores
# Source: http://blog.scoutapp.com/articles/2009/07/31/understanding-load-averages
# http://www.linuxjournal.com/article/9001
careful=0.7
warning=1.0
critical=5.0
#log=False
[network]
# Default bitrate thresholds in % of the network interface speed
# Default values if not defined: 70/80/90
rx_careful=70
rx_warning=80
rx_critical=90
tx_careful=70
tx_warning=80
tx_critical=90
# Define the list of hidden network interfaces (comma-separated regexp)
#hide=docker.*,lo
# WLAN 0 alias
#wlan0_alias=Wireless IF
# It is possible to overwrite the bitrate thresholds per interface
# WLAN 0 Default limits (in bits per second aka bps) for interface bitrate
#wlan0_rx_careful=4000000
#wlan0_rx_warning=5000000
#wlan0_rx_critical=6000000
#wlan0_rx_log=True
#wlan0_tx_careful=700000
#wlan0_tx_warning=900000
#wlan0_tx_critical=1000000
#wlan0_tx_log=True
[wifi]
# Define the list of hidden wireless network interfaces (comma-separated regexp)
hide=lo,docker.*
# Define SIGNAL thresholds in db (lower is better...)
# Based on: http://serverfault.com/questions/501025/industry-standard-for-minimum-wifi-signal-strength
careful=-65
warning=-75
critical=-85
[diskio]
# Define the list of hidden disks (comma-separated regexp)
#hide=sda2,sda5,loop.*
hide=loop.*
# Alias for sda1
#sda1_alias=IntDisk
[fs]
# Define the list of hidden file system (comma-separated regexp)
#hide=/boot.*
# Define filesystem space thresholds in %
# Default values if not defined: 50/70/90
# It is also possible to define per mount point value
# Example: /_careful=40
careful=50
warning=70
critical=90
# Allow additional file system types (comma-separated FS type)
#allow=zfs
[folders]
# Define a folder list to monitor
# The list is composed of items (list_#nb <= 10)
# An item is defined by:
# * path: absolute path
# * careful: optional careful threshold (in MB)
# * warning: optional warning threshold (in MB)
# * critical: optional critical threshold (in MB)
folder_1_path=/config
folder_1_careful=4096
folder_1_warning=8196
folder_1_critical=12228
folder_2_path=/share
folder_2_careful=4096
folder_2_warning=8196
folder_2_critical=12228
folder_3_path=/backup
folder_3_careful=4096
folder_3_warning=8196
folder_3_critical=12228
folder_4_path=/media
folder_4_careful=4096
folder_4_warning=8196
folder_4_critical=12228
[sensors]
# Sensors core thresholds (in Celsius...)
# Default values if not defined: 60/70/80
temperature_core_careful=60
temperature_core_warning=70
temperature_core_critical=80
# Temperatures threshold in °C for hddtemp
# Default values if not defined: 45/52/60
temperature_hdd_careful=45
temperature_hdd_warning=52
temperature_hdd_critical=60
# Battery threshold in %
battery_careful=80
battery_warning=90
battery_critical=95
# Sensors alias
#temp1_alias=Motherboard 0
#temp2_alias=Motherboard 1
#core 0_alias=CPU Core 0
#core 1_alias=CPU Core 1
[processlist]
# Define CPU/MEM (per process) thresholds in %
# Default values if not defined: 50/70/90
cpu_careful=50
cpu_warning=70
cpu_critical=90
mem_careful=50
mem_warning=70
mem_critical=90
#
# Nice priorities range from -20 to 19.
# Configure nice levels using a comma separated list.
#
# Nice: Example 1, non-zero is warning (default behavior)
nice_warning=-20,-19,-18,-17,-16,-15,-14,-13,-12,-11,-10,-9,-8,-7,-6,-5,-4,-3,-2,-1,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19
#
# Nice: Example 2, low priority processes escalate from careful to critical
#nice_careful=1,2,3,4,5,6,7,8,9
#nice_warning=10,11,12,13,14
#nice_critical=15,16,17,18,19
[ports]
# Ports scanner plugin configuration
# Interval in second between two scans
refresh=30
# Set the default timeout (in second) for a scan (can be overwritten in the scan list)
timeout=3
# If port_default_gateway is True, add the default gateway on top of the scan list
port_default_gateway=True
#
# Define the scan list (1 < x < 255)
# port_x_host (name or IP) is mandatory
# port_x_port (TCP port number) is optional (if not set, use ICMP)
# port_x_description is optional (if not set, define to host:port)
# port_x_timeout is optional and overwrite the default timeout value
# port_x_rtt_warning is optional and defines the warning threshold in ms
#
#port_1_host=192.168.0.1
#port_1_port=80
#port_1_description=Home Box
#port_1_timeout=1
#port_2_host=www.free.fr
#port_2_description=My ISP
#port_3_host=www.google.com
#port_3_description=Internet ICMP
#port_3_rtt_warning=1000
#port_4_description=Internet Web
#port_4_host=www.google.com
#port_4_port=80
#port_4_rtt_warning=1000
#
# Define Web (URL) monitoring list (1 < x < 255)
# web_x_url is the URL to monitor (example: http://my.site.com/folder)
# web_x_description is optional (if not set, define to URL)
# web_x_timeout is optional and overwrite the default timeout value
# web_x_rtt_warning is optional and defines the warning respond time in ms (approximatively)
#
#web_1_url=https://blog.nicolargo.com
#web_1_description=My Blog
#web_1_rtt_warning=3000
#web_2_url=https://github.com
#web_3_url=http://www.google.fr
#web_3_description=Google Fr
#web_4_url=https://blog.nicolargo.com/nonexist
#web_4_description=Intranet
[docker]
# Thresholds for CPU and MEM (in %)
cpu_careful=50
cpu_warning=70
cpu_critical=90
mem_careful=20
mem_warning=50
mem_critical=70
#
# Per container thresholds
#containername_cpu_careful=10
#containername_cpu_warning=20
#containername_cpu_critical=30
#
# By default, Glances only display running containers
# Set the following key to True to display all containers
all=False
##############################################################################
# Client/server
##############################################################################
[serverlist]
# Define the static servers list
#server_1_name=localhost
#server_1_alias=My local PC
#server_1_port=61209
#server_2_name=localhost
#server_2_port=61235
#server_3_name=192.168.0.17
#server_3_alias=Another PC on my network
#server_3_port=61209
#server_4_name=pasbon
#server_4_port=61237
[passwords]
# Define the passwords list
# Syntax: host=password
# Where: host is the hostname
# password is the clear password
# Additionally (and optionally) a default password could be defined
#localhost=abc
#default=defaultpassword
+40
View File
@@ -0,0 +1,40 @@
epaper_demotivationszitat:
name: Epaper Demotivationszitat
icon: mdi:emoticon-sad-outline
options:
- "Erwarte nichts. Dann kann dich auch wenig enttäuschen."
- "Du musst nicht scheitern. Es reicht, es nie zu versuchen."
- "Der Weg ist das Ziel. Leider führt er im Kreis."
- "Auch ein Rückschritt ist Bewegung. Nur eben rückwärts."
- "Nicht jeder Tag muss gut sein. Dieser hier zum Beispiel nicht."
- "Dein Potenzial ist beeindruckend. Seine Nutzung bleibt optional."
- "Morgen wird alles besser. Wahrscheinlich aber auch nicht."
- "Motivation ist vergänglich. Frust ist zuverlässig."
- "Man kann nicht alles haben. Nicht einmal gute Laune."
- "Wenn Plan A scheitert, war Plan B vermutlich auch keine Hilfe."
- "Es ist nie zu spät, die Erwartungen zu senken."
- "Du bist nicht zu spät. Die anderen sind nur früher gescheitert."
- "Das wird schon. Nur eben nicht gut."
- "Ziele sind wichtig. Besonders wenn man sie verfehlt."
- "Die Konkurrenz schläft nicht. Du darfst trotzdem weitermachen."
- "Du gibst dein Bestes. Das erklärt einiges."
- "Jeder Fehler ist eine Lektion. Du bist bald Experte."
- "Heute ist ein neuer Tag. Die Probleme sind dieselben."
- "Erfolg braucht Geduld. Enttäuschung ist sofort verfügbar."
- "Ein kleiner Schritt für dich. Ein kaum bemerkbarer für alle anderen."
- "Wer früh aufgibt, hat länger frei."
- "Es gibt keine dummen Fragen. Nur ungünstige Zeitpunkte."
- "Deine Komfortzone vermisst dich nicht."
- "Manchmal gewinnt man. Meistens lernt man daraus."
- "Der erste Schritt ist der schwerste. Die anderen sind nur länger."
- "Arbeite hart. Vielleicht merkt es niemand."
- "Nicht aufgeben. Es könnte noch schlimmer werden."
- "Träume groß. Die Realität übernimmt den Rest."
- "Das Glas ist halb voll. Mit Problemen."
- "Jeder Tag bietet Chancen. Manche davon sind Termine."
- "Du kannst alles schaffen. Außer vielleicht heute."
- "Das Leben ist kurz. Besprechungen sind länger."
- "Mut steht am Anfang. Die Rechnung kommt später."
- "Wenn es einfach wäre, würde es jemand anders machen."
- "Deine To-do-Liste glaubt an dich. Sie muss ja nicht mitmachen."
- "Perfektion ist erreichbar. Nur nicht von hier."
+87
View File
@@ -0,0 +1,87 @@
CALENDAR_NAMES = {
"calendar.tkrz_kalender": "Arbeit",
"calendar.privat": "Privat",
}
DAY_NAMES = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"]
MAX_ENTRIES = 8
# Reserve the right side of each row for start and end times.
MAX_TITLE_LENGTH = 22
MAX_META_LENGTH = 35
def shorten(value, maximum):
value = (value or "").strip()
if len(value) <= maximum:
return value
return value[: maximum - 3].rstrip() + "..."
def compact_event(event, calendar_name):
start = event.get("start", "")
all_day = "T" not in start
end = event.get("end", "")
time = "ganztägig" if all_day else start[11:16]
end_time = "" if all_day or "T" not in end else end[11:16]
location = (event.get("location") or "").split("\n")[0].strip()
meta = calendar_name
if location:
meta += " / " + location
return {
"title": shorten(event.get("summary", "Ohne Titel"), MAX_TITLE_LENGTH),
"time": time,
"end": end_time,
"meta": shorten(meta, MAX_META_LENGTH),
"all_day": int(all_day),
}
calendar_data = data.get("calendar", {})
today = str(data.get("now", ""))[:10]
events_by_date = {}
for calendar_id, calendar in calendar_data.items():
calendar_name = CALENDAR_NAMES.get(calendar_id, calendar_id.split(".")[-1].capitalize())
for event in calendar.get("events", []):
start = event.get("start", "")
end = event.get("end", "")
event_date = start[:10]
end_date = end[:10]
# Ignore malformed or already finished entries. All-day event end dates
# are exclusive in Home Assistant; timed events ending today may still be
# active. Multi-day entries remain visible on today's date rail.
all_day = "T" not in start
if not event_date or (all_day and end_date <= today) or (not all_day and end_date < today):
continue
if event_date < today:
event_date = today
events_by_date.setdefault(event_date, []).append(
compact_event(event, calendar_name)
)
entries = []
entry_count = 0
for date in sorted(events_by_date):
if entry_count >= MAX_ENTRIES:
break
events = events_by_date[date]
events.sort(key=lambda item: (item["all_day"] == 0, item["time"]))
remaining = MAX_ENTRIES - entry_count
events = events[:remaining]
entry_count += len(events)
parsed_date = dt_util.parse_datetime(date)
entries.append(
{
"day": parsed_date.day,
"weekday": DAY_NAMES[parsed_date.weekday()],
"today": int(date == today),
"events": events,
}
)
output["entries"] = entries
+30 -28
View File
@@ -1,9 +1,9 @@
db_url: !secret ha_recorder_dburl
# Commit to db only every X seconds
commit_interval: 60
# Delete events and states older than 1 week
auto_purge: true
purge_keep_days: 7
db_url: !secret ha_recorder_dburl
# Commit to db only every X seconds
commit_interval: 60
# Delete events and states older than 1 week
auto_purge: true
purge_keep_days: 7
# include:
# entity_globs:
# - sensor.rd6018_controller_*
@@ -15,27 +15,29 @@
# - sensor
# - switch
# - media_player
exclude:
# domains:
# - automation
# - updater
entity_globs:
- weather.zuhause_*
- sensor.*_power_factor
- sensor.dwd*
- binary_sensor.*firmware_update
- binary_sensor.*_overpowering
- binary_sensor.*_overheating
- sensor.*_wi_fi_signal
- sensor.*_wifi_strenght
- sensor.*_uptime
- sensor.sun*
entities:
- sun.sun # Don't record sun data
- sensor.fritzbox_device_uptime
- sensor.time
- sensor.awtrix_kitchen_current_app
- sensor.awtrix_desk_current_app
exclude:
# domains:
# - automation
# - updater
entity_globs:
- weather.zuhause_*
- sensor.*_power_factor
- sensor.dwd*
- binary_sensor.*firmware_update
- binary_sensor.*_overpowering
- binary_sensor.*_overheating
- sensor.*_wi_fi_signal
- sensor.*_wifi_strenght
- sensor.*_uptime
- sensor.sun*
entities:
- sun.sun # Don't record sun data
- sensor.fritzbox_device_uptime
- sensor.time
- sensor.awtrix_kitchen_current_app
- sensor.awtrix_desk_current_app
- sensor.owon_b35t_multimeter_core2_owon_b35t_multimeter_core2_reading
- sensor.owon_b35t_multimeter_core2_owon_b35t_multimeter_core2_mode
- sensor.owon_b35t_multimeter_core2_owon_b35t_multimeter_core2_unit
# event_types:
# - call_service # Don't record service calls
+6 -4
View File
@@ -10,7 +10,8 @@
unique_id: '3303381540758'
host: !secret router_ip
community: !secret router_community
baseoid: .1.3.6.1.2.1.31.1.1.1.6.511
# baseoid: .1.3.6.1.2.1.31.1.1.1.6.511
baseoid: .1.3.6.1.2.1.2.2.1.10.25
version: 2c
unit_of_measurement: "Octets"
- platform: snmp
@@ -18,7 +19,8 @@
unique_id: '1573258703922'
host: !secret router_ip
community: !secret router_community
baseoid: .1.3.6.1.2.1.31.1.1.1.10.511
# baseoid: .1.3.6.1.2.1.31.1.1.1.10.511
baseoid: .1.3.6.1.2.1.2.2.1.16.25
version: 2c
unit_of_measurement: "Octets"
@@ -38,13 +40,13 @@
unique_id: '9081721471264'
state_characteristic: mean
entity_id: sensor.internet_speed_in
sampling_size: 10
sampling_size: 2
- platform: statistics
name: 'WAN Traffic Out'
unique_id: '8688955223027'
state_characteristic: mean
entity_id: sensor.internet_speed_out
sampling_size: 10
sampling_size: 2
# Sensor for Riemann sum of energy import (W -> Wh)
- platform: integration
+36 -1
View File
@@ -49,6 +49,11 @@
name: Sun Setting Template
state: '{{ as_timestamp(states.sun.sun.attributes.next_setting) | timestamp_custom
(''%H:%M'') }}'
- unique_id: epaper_demotivational_quote
default_entity_id: sensor.epaper_demotivationszitat
name: Epaper Demotivationszitat
state: "{{ states('input_select.epaper_demotivationszitat') }}"
- name: "power_other"
unique_id: '5579422933393'
unit_of_measurement: "W"
@@ -69,7 +74,7 @@
{% set spieleschrank = states('sensor.wohnzimmer_spieleschrank_power') | float %}
{% set kuehlschrank = states('sensor.tz3000_ww6drja5_ts011f_leistung') | float %}
{% set kaffeemaschine = states('sensor.kaffeemaschine_leistung_2') | float %}
{% set waeschetrockner = states('sensor.waschetrockner_leistung') | float %}
{% set waeschetrockner = states('sensor.ikea_of_sweden_inspelning_smart_plug_leistung') | float %}
{% set waschmaschine = states('sensor.waschmaschine_leistung') | float %}
{% set arcade = states('sensor.arcade_automat_leistung') | float %}
{% set spuelmaschine = states('sensor.spulmaschine_leistung') | float %}
@@ -79,6 +84,35 @@
attributes:
last_reset: '1970-01-01T00:00:00+00:00'
# Compact calendar feed for the e-paper display. The actual event data lives in
# the `entries` attribute, which ESPHome reads as a text sensor.
- trigger:
- trigger: homeassistant
event: start
- trigger: time_pattern
minutes: /15
action:
- action: calendar.get_events
target:
entity_id:
- calendar.tkrz_kalender
- calendar.privat
data:
duration:
days: 14
response_variable: calendar_response
- action: python_script.epaper_calendar_data
data:
calendar: "{{ calendar_response }}"
now: "{{ now().date() }}"
response_variable: calendar_converted
sensor:
- name: Epaper Kalenderdaten
unique_id: epaper_calendar_data
state: "{{ calendar_converted.entries | count }}"
attributes:
entries: "{{ calendar_converted.entries }}"
# Shelly 3EM cumulative sensors (incl. PV)
- sensor:
# Template sensor for values of power import (active_power > 0)
@@ -139,6 +173,7 @@
attributes:
last_reset: '1970-01-01T00:00:00+00:00'
- name: internet_speed_out
unique_id: '9519483670667'
state: >
{{ (( states('sensor.wan_out_derivative') | float * 8 / 1000000 ) | round(2)) }}
unit_of_measurement: 'Mbps'
+12 -6
View File
@@ -1,34 +1,40 @@
# Internet traffic
internet_usage_in_monthly:
source: sensor.snmp_wan_in
source: sensor.wan_in_derivative
name: Monthly internet traffic in
unique_id: monthly_internet_traffic_in
cycle: monthly
delta_values: True
internet_usage_out_monthly:
source: sensor.snmp_wan_out
source: sensor.wan_out_derivative
name: Monthly internet traffic out
unique_id: monthly_internet_traffic_out
cycle: monthly
delta_values: True
internet_usage_in_daily:
source: sensor.snmp_wan_in
source: sensor.wan_in_derivative
name: Daily internet traffic in
unique_id: daily_internet_traffic_in
cycle: daily
delta_values: True
internet_usage_out_daily:
source: sensor.snmp_wan_out
source: sensor.wan_out_derivative
name: Daily internet traffic out
unique_id: daily_internet_traffic_out
cycle: daily
delta_values: True
internet_usage_in_hourly:
source: sensor.snmp_wan_in
source: sensor.wan_in_derivative
name: Hourly internet traffic in
unique_id: hourly_internet_traffic_in
cycle: hourly
delta_values: True
internet_usage_out_hourly:
source: sensor.snmp_wan_out
source: sensor.wan_out_derivative
name: Hourly internet traffic out
unique_id: hourly_internet_traffic_out
cycle: hourly
delta_values: True
# Energy
energy_import_daily: