Compare commits
102
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1fb90a6144 | ||
|
|
afaaa9f4eb | ||
|
|
d508c3c6f2 | ||
|
|
bb1f514fee | ||
|
|
08b6b6bc07 | ||
|
|
230126248d | ||
|
|
ce7704c628 | ||
|
|
333484a89a | ||
|
|
ef2047e268 | ||
|
|
dc6cf4d1af | ||
|
|
302bc3a924 | ||
|
|
73354c4447 | ||
|
|
f35f1eee22 | ||
|
|
31644426fc | ||
|
|
059b6c0a4b | ||
|
|
6a3afd71df | ||
|
|
b7b943e4bf | ||
|
|
195c327ed7 | ||
|
|
22034a86e1 | ||
|
|
81665588a8 | ||
|
|
f956e19d3b | ||
|
|
0a94233a36 | ||
|
|
d23c6a23a3 | ||
|
|
166cc3efd5 | ||
|
|
e358c2cffe | ||
|
|
5d4e8ef40c | ||
|
|
8dc781a6b1 | ||
|
|
6c91cc2d59 | ||
|
|
ba2a56a15b | ||
|
|
9e7684e294 | ||
|
|
818b06f3d7 | ||
|
|
99bb2594e2 | ||
|
|
9104cd8fdc | ||
|
|
96b88624d9 | ||
|
|
c1ba3012d4 | ||
|
|
e77d9ee8d2 | ||
|
|
bf49633fac | ||
|
|
99579a30f7 | ||
|
|
250bfa94bd | ||
|
|
c342efc08f | ||
|
|
13e58355da | ||
|
|
ee814c4afe | ||
|
|
6149a9e994 | ||
|
|
3b7c30634e | ||
|
|
38da2cebdd | ||
|
|
3da69c19ab | ||
|
|
35e7f81224 | ||
|
|
e4a4aacc63 | ||
|
|
0d12317089 | ||
|
|
746dd833c8 | ||
|
|
3eb482e40b | ||
|
|
e9a6064b4b | ||
|
|
38616556cb | ||
|
|
bef1cfb1d0 | ||
|
|
0a8364d7ed | ||
|
|
3eb9eb3a2f | ||
|
|
7407955465 | ||
|
|
2ff8b5cafe | ||
|
|
7e08c4a8b9 | ||
|
|
855f013095 | ||
|
|
a68b527c48 | ||
|
|
cecec14be1 | ||
|
|
4e5df1ad0c | ||
|
|
2c16ba1679 | ||
|
|
c21f2a5c7b | ||
|
|
2a1902ccf1 | ||
|
|
31848bf26c | ||
|
|
d7337fd634 | ||
|
|
5b287303f1 | ||
|
|
816cc35861 | ||
|
|
058c186d63 | ||
|
|
22e5f9d767 | ||
|
|
f9b32d06a4 | ||
|
|
3f890ae02f | ||
|
|
f0cb74cc27 | ||
|
|
6038075e9e | ||
|
|
2dbdfd213c | ||
|
|
e2eb886eef | ||
|
|
f4f19eb847 | ||
|
|
b3e307dcba | ||
|
|
1b3b4870e7 | ||
|
|
69c2166414 | ||
|
|
2fcf71d8ce | ||
|
|
676829a988 | ||
|
|
08201f1bc1 | ||
|
|
23cbd520a6 | ||
|
|
6f4e2b9319 | ||
|
|
e5104fc005 | ||
|
|
91f3f69bd9 | ||
|
|
4bd62f539c | ||
|
|
a3bd9ff213 | ||
|
|
24fc4bf9f4 | ||
|
|
0bfc2eff34 | ||
|
|
8f18a043af | ||
|
|
5b0a992686 | ||
|
|
ef233296e9 | ||
|
|
c0af2b734b | ||
|
|
a94386babe | ||
|
|
f23166b765 | ||
|
|
8438785241 | ||
|
|
b0b6093619 | ||
|
|
c463a5e28f |
+1
-1
@@ -1 +1 @@
|
|||||||
2026.3.0
|
2026.7.4
|
||||||
+1
-1
@@ -13,4 +13,4 @@ home-assistant.log*
|
|||||||
zigbee.db*
|
zigbee.db*
|
||||||
*__pycache__*
|
*__pycache__*
|
||||||
.ha_run.lock
|
.ha_run.lock
|
||||||
|
.cache
|
||||||
|
|||||||
+157
-63
@@ -2,9 +2,11 @@
|
|||||||
alias: Licht bei Sonnenuntergang einschalten
|
alias: Licht bei Sonnenuntergang einschalten
|
||||||
description: ''
|
description: ''
|
||||||
triggers:
|
triggers:
|
||||||
- event: sunset
|
- trigger: numeric_state
|
||||||
offset: '-1:00'
|
entity_id:
|
||||||
trigger: sun
|
- sun.sun
|
||||||
|
attribute: elevation
|
||||||
|
below: 5
|
||||||
conditions:
|
conditions:
|
||||||
- condition: state
|
- condition: state
|
||||||
entity_id: person.marcus_scholz
|
entity_id: person.marcus_scholz
|
||||||
@@ -43,9 +45,11 @@
|
|||||||
alias: Licht bei Sonnenaufgang ausschalten
|
alias: Licht bei Sonnenaufgang ausschalten
|
||||||
description: ''
|
description: ''
|
||||||
triggers:
|
triggers:
|
||||||
- event: sunrise
|
- trigger: numeric_state
|
||||||
offset: '+1:00'
|
entity_id:
|
||||||
trigger: sun
|
- sun.sun
|
||||||
|
attribute: elevation
|
||||||
|
above: 5
|
||||||
conditions: []
|
conditions: []
|
||||||
actions:
|
actions:
|
||||||
- target:
|
- target:
|
||||||
@@ -135,27 +139,33 @@
|
|||||||
trigger: time
|
trigger: time
|
||||||
conditions: []
|
conditions: []
|
||||||
actions:
|
actions:
|
||||||
- action: notify.mobile_app_apollo
|
|
||||||
data:
|
|
||||||
message: Lodere es, Lustknabe.
|
|
||||||
title: 420!
|
|
||||||
- data:
|
- data:
|
||||||
title: 420!
|
title: 420!
|
||||||
message: Lodere es, Lustknabe.
|
message: Lodere es, Lustknabe.
|
||||||
action: notify.mobile_app_le2123
|
action: notify.mobile_app_le2123
|
||||||
- data:
|
- data:
|
||||||
cache: true
|
cache: true
|
||||||
media_player_entity_id: media_player.raspiaudio_muse_luxe
|
media_player_entity_id: media_player.home_assistant_voice_0a7c6b_media_player
|
||||||
message: 4 20 lodere es, Lustknabe
|
message: 4 20, lodere es, Lustknabe
|
||||||
target:
|
target:
|
||||||
entity_id: tts.piper
|
entity_id: tts.piper
|
||||||
|
enabled: true
|
||||||
action: tts.speak
|
action: tts.speak
|
||||||
- data:
|
- data:
|
||||||
cache: true
|
cache: true
|
||||||
media_player_entity_id: media_player.home_assistant_voice_09c0e7_media_player
|
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:
|
target:
|
||||||
entity_id: tts.piper
|
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
|
action: tts.speak
|
||||||
- data:
|
- data:
|
||||||
qos: 0
|
qos: 0
|
||||||
@@ -169,6 +179,10 @@
|
|||||||
topic: awtrix_desk/notify
|
topic: awtrix_desk/notify
|
||||||
payload: '{"text": "420, lodere es, Lustknabe!"}'
|
payload: '{"text": "420, lodere es, Lustknabe!"}'
|
||||||
action: mqtt.publish
|
action: mqtt.publish
|
||||||
|
- action: notify.mobile_app_apollo
|
||||||
|
data:
|
||||||
|
message: Lodere es, Lustknabe.
|
||||||
|
title: 420!
|
||||||
mode: single
|
mode: single
|
||||||
- id: '1623911524804'
|
- id: '1623911524804'
|
||||||
alias: TV Anti-Reflexion (undo)
|
alias: TV Anti-Reflexion (undo)
|
||||||
@@ -213,28 +227,28 @@
|
|||||||
action: light.turn_on
|
action: light.turn_on
|
||||||
- data:
|
- data:
|
||||||
cache: true
|
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.
|
message: Ab ins Bett, Schlafenszeit.
|
||||||
target:
|
target:
|
||||||
entity_id: tts.piper
|
entity_id: tts.piper
|
||||||
action: tts.speak
|
|
||||||
enabled: true
|
enabled: true
|
||||||
|
action: tts.speak
|
||||||
- data:
|
- data:
|
||||||
cache: true
|
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.
|
message: Ab ins Bett, Schlafenszeit.
|
||||||
target:
|
target:
|
||||||
entity_id: tts.piper
|
entity_id: tts.piper
|
||||||
action: tts.speak
|
|
||||||
enabled: true
|
enabled: true
|
||||||
|
action: tts.speak
|
||||||
- data:
|
- data:
|
||||||
cache: true
|
cache: true
|
||||||
media_player_entity_id: media_player.m5stack_atom_echo
|
media_player_entity_id: media_player.m5stack_atom_echo
|
||||||
message: Ab ins Bett, Schlafenszeit.
|
message: Ab ins Bett, Schlafenszeit.
|
||||||
target:
|
target:
|
||||||
entity_id: tts.piper
|
entity_id: tts.piper
|
||||||
action: tts.speak
|
|
||||||
enabled: true
|
enabled: true
|
||||||
|
action: tts.speak
|
||||||
mode: single
|
mode: single
|
||||||
- id: '1623954512941'
|
- id: '1623954512941'
|
||||||
alias: Licht im Schlafzimmer zum Aufwachen einschalten
|
alias: Licht im Schlafzimmer zum Aufwachen einschalten
|
||||||
@@ -264,8 +278,10 @@
|
|||||||
alias: 'Anruf: Beim Klingeln grün blinken'
|
alias: 'Anruf: Beim Klingeln grün blinken'
|
||||||
description: ''
|
description: ''
|
||||||
triggers:
|
triggers:
|
||||||
- entity_id: sensor.fritz_box_7490_call_monitor_dem_commander1024_seine_cloud
|
- entity_id:
|
||||||
to: ringing
|
- sensor.fritz_box_7690_anrufmonitor_dem_commander1024_seine_cloud
|
||||||
|
to:
|
||||||
|
- ringing
|
||||||
trigger: state
|
trigger: state
|
||||||
- entity_id:
|
- entity_id:
|
||||||
- sensor.le2123_phone_state_2
|
- sensor.le2123_phone_state_2
|
||||||
@@ -287,8 +303,8 @@
|
|||||||
- 0
|
- 0
|
||||||
target:
|
target:
|
||||||
entity_id:
|
entity_id:
|
||||||
- light.awtrix_desk_indicator_1
|
- light.awtrix_b8658c_indicator_1
|
||||||
- light.awtrix_kitchen_indicator_1
|
- light.awtrix_528bd4_indicator_1
|
||||||
action: light.turn_on
|
action: light.turn_on
|
||||||
- data:
|
- data:
|
||||||
scene_id: wled_last_state
|
scene_id: wled_last_state
|
||||||
@@ -321,16 +337,18 @@
|
|||||||
- data: {}
|
- data: {}
|
||||||
target:
|
target:
|
||||||
entity_id:
|
entity_id:
|
||||||
- light.awtrix_desk_indicator_1
|
- light.awtrix_b8658c_indicator_1
|
||||||
- light.awtrix_kitchen_indicator_1
|
- light.awtrix_528bd4_indicator_1
|
||||||
action: light.turn_off
|
action: light.turn_off
|
||||||
mode: single
|
mode: single
|
||||||
- id: '1625481640348'
|
- id: '1625481640348'
|
||||||
alias: 'Anruf: Beim Telefonieren Musik pausieren'
|
alias: 'Anruf: Beim Telefonieren Musik pausieren'
|
||||||
description: ''
|
description: ''
|
||||||
triggers:
|
triggers:
|
||||||
- entity_id: sensor.fritz_box_7490_call_monitor_telefonbuch
|
- entity_id:
|
||||||
to: talking
|
- sensor.fritz_box_7690_anrufmonitor_dem_commander1024_seine_cloud
|
||||||
|
to:
|
||||||
|
- talking
|
||||||
trigger: state
|
trigger: state
|
||||||
- entity_id:
|
- entity_id:
|
||||||
- sensor.le2123_phone_state_2
|
- sensor.le2123_phone_state_2
|
||||||
@@ -348,9 +366,8 @@
|
|||||||
- data:
|
- data:
|
||||||
scene_id: media_last_state
|
scene_id: media_last_state
|
||||||
snapshot_entities:
|
snapshot_entities:
|
||||||
- media_player.wohnzimmer_main
|
- media_player.wohnzimmer
|
||||||
- media_player.lg_webos_smart_tv
|
- media_player.lg_webos_smart_tv
|
||||||
- media_player.ccze
|
|
||||||
- media_player.spotify_marcus_scholz
|
- media_player.spotify_marcus_scholz
|
||||||
- media_player.xboxonex
|
- media_player.xboxonex
|
||||||
action: scene.create
|
action: scene.create
|
||||||
@@ -371,7 +388,7 @@
|
|||||||
trigger: state
|
trigger: state
|
||||||
- target:
|
- target:
|
||||||
entity_id:
|
entity_id:
|
||||||
- scene.media_last_state
|
- scene.wled_last_state
|
||||||
action: scene.turn_on
|
action: scene.turn_on
|
||||||
data: {}
|
data: {}
|
||||||
mode: single
|
mode: single
|
||||||
@@ -479,11 +496,10 @@
|
|||||||
from: not_home
|
from: not_home
|
||||||
trigger: state
|
trigger: state
|
||||||
conditions:
|
conditions:
|
||||||
- condition: sun
|
- condition: numeric_state
|
||||||
before: sunrise
|
entity_id: sun.sun
|
||||||
before_offset: '+1'
|
attribute: elevation
|
||||||
after: sunset
|
below: '5'
|
||||||
after_offset: '-1'
|
|
||||||
actions:
|
actions:
|
||||||
- data: {}
|
- data: {}
|
||||||
action: script.moodlight_orange_plasma
|
action: script.moodlight_orange_plasma
|
||||||
@@ -602,12 +618,33 @@
|
|||||||
action: notify.mobile_app_le2123
|
action: notify.mobile_app_le2123
|
||||||
- data:
|
- data:
|
||||||
cache: true
|
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}}
|
message: '{{message}}
|
||||||
|
|
||||||
'
|
'
|
||||||
target:
|
target:
|
||||||
entity_id: tts.piper
|
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
|
action: tts.speak
|
||||||
variables:
|
variables:
|
||||||
message: 'Die Außentemperatur ist mit {{states(''sensor.aussentemperatur'')}}
|
message: 'Die Außentemperatur ist mit {{states(''sensor.aussentemperatur'')}}
|
||||||
@@ -652,12 +689,33 @@
|
|||||||
action: notify.mobile_app_le2123
|
action: notify.mobile_app_le2123
|
||||||
- data:
|
- data:
|
||||||
cache: true
|
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}}
|
message: '{{message}}
|
||||||
|
|
||||||
'
|
'
|
||||||
target:
|
target:
|
||||||
entity_id: tts.piper
|
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
|
action: tts.speak
|
||||||
mode: single
|
mode: single
|
||||||
variables:
|
variables:
|
||||||
@@ -817,9 +875,11 @@
|
|||||||
alias: Licht bei Sonnenuntergang einschalten (XMas)
|
alias: Licht bei Sonnenuntergang einschalten (XMas)
|
||||||
description: ''
|
description: ''
|
||||||
triggers:
|
triggers:
|
||||||
- event: sunset
|
- trigger: numeric_state
|
||||||
offset: '-1:00'
|
entity_id:
|
||||||
trigger: sun
|
- sun.sun
|
||||||
|
attribute: elevation
|
||||||
|
below: 5
|
||||||
conditions:
|
conditions:
|
||||||
- condition: state
|
- condition: state
|
||||||
entity_id: person.marcus_scholz
|
entity_id: person.marcus_scholz
|
||||||
@@ -846,9 +906,11 @@
|
|||||||
alias: Licht bei Sonnenaufgang ausschalten (XMas)
|
alias: Licht bei Sonnenaufgang ausschalten (XMas)
|
||||||
description: ''
|
description: ''
|
||||||
triggers:
|
triggers:
|
||||||
- event: sunrise
|
- trigger: numeric_state
|
||||||
offset: '+1:00'
|
entity_id:
|
||||||
trigger: sun
|
- sun.sun
|
||||||
|
attribute: elevation
|
||||||
|
above: 5
|
||||||
conditions: []
|
conditions: []
|
||||||
actions:
|
actions:
|
||||||
- target:
|
- target:
|
||||||
@@ -1034,26 +1096,19 @@
|
|||||||
domain: switch
|
domain: switch
|
||||||
action_button_up_double:
|
action_button_up_double:
|
||||||
- type: turn_on
|
- type: turn_on
|
||||||
device_id: 32d15eec63fe68d5770813d47da53657
|
device_id: 98a9eef603f8c8ae6200c4834a3dbf4f
|
||||||
entity_id: 14edfa97dae4936040b97dc77b171d5f
|
entity_id: fdcff3852ecc17099c953da17eb4b8a3
|
||||||
domain: light
|
domain: light
|
||||||
button_up_double_press: true
|
button_up_double_press: true
|
||||||
button_down_double_press: true
|
button_down_double_press: true
|
||||||
action_button_down_double:
|
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
|
- type: turn_off
|
||||||
device_id: 98a9eef603f8c8ae6200c4834a3dbf4f
|
device_id: 98a9eef603f8c8ae6200c4834a3dbf4f
|
||||||
entity_id: fdcff3852ecc17099c953da17eb4b8a3
|
entity_id: fdcff3852ecc17099c953da17eb4b8a3
|
||||||
domain: light
|
domain: light
|
||||||
|
action_button_up_long: []
|
||||||
|
action_button_down_long: []
|
||||||
|
button_up_long_max_loop_repeats: 4205
|
||||||
- id: '1727953214758'
|
- id: '1727953214758'
|
||||||
alias: Waschmaschine läuft
|
alias: Waschmaschine läuft
|
||||||
description: Prüfen ob die Waschmaschine läuft, dann den boolschen Schalter "Waschmaschine
|
description: Prüfen ob die Waschmaschine läuft, dann den boolschen Schalter "Waschmaschine
|
||||||
@@ -1145,8 +1200,8 @@
|
|||||||
aktiv" einschalten
|
aktiv" einschalten
|
||||||
triggers:
|
triggers:
|
||||||
- type: power
|
- type: power
|
||||||
device_id: 81e357077ac8d1f8f588fbb5324965b5
|
device_id: 25a822b76a0206496bd216235b034fb0
|
||||||
entity_id: fac93f9504dc8d1267bec7d02849d491
|
entity_id: 3321a33261062dab534569c3f6f53d69
|
||||||
domain: sensor
|
domain: sensor
|
||||||
trigger: device
|
trigger: device
|
||||||
above: 150
|
above: 150
|
||||||
@@ -1157,8 +1212,8 @@
|
|||||||
conditions:
|
conditions:
|
||||||
- condition: device
|
- condition: device
|
||||||
type: is_on
|
type: is_on
|
||||||
device_id: 81e357077ac8d1f8f588fbb5324965b5
|
device_id: 25a822b76a0206496bd216235b034fb0
|
||||||
entity_id: 2a2659668772d9345e42667900832f0e
|
entity_id: 4748aace637a3f1d8b5e896236564024
|
||||||
domain: switch
|
domain: switch
|
||||||
actions:
|
actions:
|
||||||
- action: input_boolean.turn_on
|
- action: input_boolean.turn_on
|
||||||
@@ -1172,11 +1227,11 @@
|
|||||||
Uhr.
|
Uhr.
|
||||||
triggers:
|
triggers:
|
||||||
- type: power
|
- type: power
|
||||||
device_id: 81e357077ac8d1f8f588fbb5324965b5
|
device_id: 25a822b76a0206496bd216235b034fb0
|
||||||
entity_id: fac93f9504dc8d1267bec7d02849d491
|
entity_id: 3321a33261062dab534569c3f6f53d69
|
||||||
domain: sensor
|
domain: sensor
|
||||||
trigger: device
|
trigger: device
|
||||||
below: 150
|
below: 250
|
||||||
for:
|
for:
|
||||||
hours: 0
|
hours: 0
|
||||||
minutes: 5
|
minutes: 5
|
||||||
@@ -1184,8 +1239,8 @@
|
|||||||
conditions:
|
conditions:
|
||||||
- condition: device
|
- condition: device
|
||||||
type: is_on
|
type: is_on
|
||||||
device_id: 81e357077ac8d1f8f588fbb5324965b5
|
device_id: 25a822b76a0206496bd216235b034fb0
|
||||||
entity_id: 2a2659668772d9345e42667900832f0e
|
entity_id: 4748aace637a3f1d8b5e896236564024
|
||||||
domain: switch
|
domain: switch
|
||||||
- condition: state
|
- condition: state
|
||||||
entity_id: input_boolean.waschetrockner_aktiv
|
entity_id: input_boolean.waschetrockner_aktiv
|
||||||
@@ -1363,3 +1418,42 @@
|
|||||||
motion_cutoff: 10
|
motion_cutoff: 10
|
||||||
illuminance_low_threshold: 10
|
illuminance_low_threshold: 10
|
||||||
battery_low_threshold: 20
|
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
|
||||||
@@ -35,6 +35,12 @@ utility_meter: !include utility_meters.yaml
|
|||||||
mqtt: !include mqtt.yaml
|
mqtt: !include mqtt.yaml
|
||||||
mqtt_statestream: !include mqtt_statestream.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 sensors
|
||||||
template: !include template.yaml
|
template: !include template.yaml
|
||||||
|
|
||||||
@@ -82,3 +88,8 @@ generic_hygrostat:
|
|||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
themes: !include_dir_merge_named themes
|
themes: !include_dir_merge_named themes
|
||||||
|
|
||||||
|
freedns:
|
||||||
|
url: !secret freedns6_url
|
||||||
|
# - name: IPv4
|
||||||
|
# url: !secret freedns4_url
|
||||||
|
|||||||
@@ -38,10 +38,13 @@ ota:
|
|||||||
platform: esphome
|
platform: esphome
|
||||||
password: !secret ota
|
password: !secret ota
|
||||||
|
|
||||||
|
network:
|
||||||
|
enable_ipv6: true
|
||||||
|
|
||||||
wifi:
|
wifi:
|
||||||
ssid: "Voltage-legacy"
|
ssid: "Voltage-legacy"
|
||||||
password: !secret voltage_legacy_psk
|
password: !secret voltage_legacy_psk
|
||||||
use_address: atorch-dc-load.home
|
#use_address: atorch-dc-load.home
|
||||||
power_save_mode: high
|
power_save_mode: high
|
||||||
fast_connect: on
|
fast_connect: on
|
||||||
min_auth_mode: WPA2
|
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();
|
||||||
@@ -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();
|
||||||
@@ -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]
|
components: [es8388]
|
||||||
refresh: 0s
|
refresh: 0s
|
||||||
|
|
||||||
|
network:
|
||||||
|
enable_ipv6: true
|
||||||
|
|
||||||
wifi:
|
wifi:
|
||||||
ssid: Voltage-legacy
|
ssid: Voltage-legacy
|
||||||
password: !secret voltage_legacy_psk
|
password: !secret voltage_legacy_psk
|
||||||
use_address: ${name}.home
|
#use_address: ${name}.home
|
||||||
power_save_mode: high
|
power_save_mode: high
|
||||||
fast_connect: on
|
fast_connect: on
|
||||||
min_auth_mode: WPA2
|
min_auth_mode: WPA2
|
||||||
@@ -16,10 +16,13 @@ esp32:
|
|||||||
framework:
|
framework:
|
||||||
type: arduino
|
type: arduino
|
||||||
|
|
||||||
|
network:
|
||||||
|
enable_ipv6: true
|
||||||
|
|
||||||
wifi:
|
wifi:
|
||||||
ssid: "Voltage-legacy"
|
ssid: "Voltage-legacy"
|
||||||
password: !secret voltage_legacy_psk
|
password: !secret voltage_legacy_psk
|
||||||
use_address: bathroom.home
|
#use_address: bathroom.home
|
||||||
power_save_mode: high
|
power_save_mode: high
|
||||||
fast_connect: on
|
fast_connect: on
|
||||||
min_auth_mode: WPA2
|
min_auth_mode: WPA2
|
||||||
|
|||||||
@@ -0,0 +1,648 @@
|
|||||||
|
substitutions:
|
||||||
|
name: "energymonitor"
|
||||||
|
friendly_name: "Energy Monitor"
|
||||||
|
grid_power_gauge_max_w: "5000.0"
|
||||||
|
balcony_solar_gauge_max_w: "800.0"
|
||||||
|
|
||||||
|
esphome:
|
||||||
|
name: ${name}
|
||||||
|
friendly_name: ${friendly_name}
|
||||||
|
name_add_mac_suffix: false
|
||||||
|
project:
|
||||||
|
name: monitor.energymonitor
|
||||||
|
version: "1.0"
|
||||||
|
on_boot:
|
||||||
|
then:
|
||||||
|
- light.turn_on: display_backlight
|
||||||
|
|
||||||
|
esp32:
|
||||||
|
board: m5stack-core-esp32
|
||||||
|
framework:
|
||||||
|
type: esp-idf
|
||||||
|
advanced:
|
||||||
|
minimum_chip_revision: "3.1"
|
||||||
|
sram1_as_iram: true
|
||||||
|
|
||||||
|
network:
|
||||||
|
enable_ipv6: true
|
||||||
|
|
||||||
|
wifi:
|
||||||
|
networks:
|
||||||
|
- ssid: "Voltage-legacy"
|
||||||
|
password: !secret voltage_legacy_psk
|
||||||
|
priority: 10
|
||||||
|
- ssid: "Moppelkotze"
|
||||||
|
password: !secret moppelkotze_psk
|
||||||
|
priority: 5
|
||||||
|
- ssid: "TKRZ"
|
||||||
|
password: !secret tkrz_psk
|
||||||
|
priority: 4
|
||||||
|
use_address: 192.168.126.250
|
||||||
|
on_connect:
|
||||||
|
- lambda: id(wifi_connected) = true;
|
||||||
|
on_disconnect:
|
||||||
|
- lambda: id(wifi_connected) = false;
|
||||||
|
power_save_mode: high
|
||||||
|
min_auth_mode: WPA2
|
||||||
|
ap:
|
||||||
|
ssid: "Energymonitor Fallback Hotspot"
|
||||||
|
password: !secret fallback_psk
|
||||||
|
|
||||||
|
captive_portal:
|
||||||
|
|
||||||
|
time:
|
||||||
|
- platform: sntp
|
||||||
|
id: sntp_time
|
||||||
|
timezone: Europe/Berlin
|
||||||
|
servers:
|
||||||
|
- de.pool.ntp.org
|
||||||
|
|
||||||
|
# Values are maintained in secrets.yaml.
|
||||||
|
wireguard:
|
||||||
|
id: energymonitor_wireguard
|
||||||
|
address: !secret energymonitor_wireguard_address
|
||||||
|
netmask: !secret energymonitor_wireguard_netmask
|
||||||
|
private_key: !secret energymonitor_wireguard_private_key
|
||||||
|
peer_endpoint: !secret energymonitor_wireguard_peer_endpoint
|
||||||
|
peer_port: !secret energymonitor_wireguard_peer_port
|
||||||
|
peer_public_key: !secret energymonitor_wireguard_peer_public_key
|
||||||
|
peer_allowed_ips:
|
||||||
|
- !secret energymonitor_wireguard_peer_allowed_ip
|
||||||
|
peer_persistent_keepalive: 25s
|
||||||
|
# Avoid rebooting while the placeholder endpoint is intentionally unreachable.
|
||||||
|
reboot_timeout: 0s
|
||||||
|
|
||||||
|
logger:
|
||||||
|
|
||||||
|
api:
|
||||||
|
id: api_server
|
||||||
|
encryption:
|
||||||
|
key: !secret apikey
|
||||||
|
|
||||||
|
ota:
|
||||||
|
platform: esphome
|
||||||
|
password: !secret ota
|
||||||
|
|
||||||
|
spi:
|
||||||
|
clk_pin: GPIO18
|
||||||
|
mosi_pin: GPIO23
|
||||||
|
|
||||||
|
output:
|
||||||
|
- platform: gpio
|
||||||
|
pin: GPIO32
|
||||||
|
id: display_backlight_output
|
||||||
|
|
||||||
|
light:
|
||||||
|
- platform: binary
|
||||||
|
id: display_backlight
|
||||||
|
name: "Display Backlight"
|
||||||
|
output: display_backlight_output
|
||||||
|
restore_mode: ALWAYS_ON
|
||||||
|
|
||||||
|
font:
|
||||||
|
- file: "fonts/Roboto-Regular.ttf"
|
||||||
|
id: font_small
|
||||||
|
size: 18
|
||||||
|
glyphsets:
|
||||||
|
- GF_Latin_Kernel
|
||||||
|
glyphs: ["äöüÄÖÜß"]
|
||||||
|
- file: "fonts/Roboto-Bold.ttf"
|
||||||
|
id: font_title
|
||||||
|
size: 28
|
||||||
|
glyphsets:
|
||||||
|
- GF_Latin_Kernel
|
||||||
|
glyphs: ["äöüÄÖÜß"]
|
||||||
|
- file: "fonts/Roboto-Bold.ttf"
|
||||||
|
id: font_value
|
||||||
|
size: 52
|
||||||
|
glyphsets:
|
||||||
|
- GF_Latin_Kernel
|
||||||
|
glyphs: ["äöüÄÖÜß"]
|
||||||
|
- file: "fonts/Roboto-Regular.ttf"
|
||||||
|
id: font_device
|
||||||
|
size: 13
|
||||||
|
glyphsets:
|
||||||
|
- GF_Latin_Kernel
|
||||||
|
glyphs: ["äöüÄÖÜß"]
|
||||||
|
- file: "fonts/materialdesignicons-webfont.ttf"
|
||||||
|
id: font_icon
|
||||||
|
size: 24
|
||||||
|
glyphs:
|
||||||
|
- "\U000F0306" # mdi:key
|
||||||
|
- "\U000F07D0" # mdi:home-assistant
|
||||||
|
- "\U000F091F" # mdi:wifi-strength-1
|
||||||
|
- "\U000F0922" # mdi:wifi-strength-2
|
||||||
|
- "\U000F0925" # mdi:wifi-strength-3
|
||||||
|
- "\U000F0928" # mdi:wifi-strength-4
|
||||||
|
- "\U000F092D" # mdi:wifi-strength-off
|
||||||
|
|
||||||
|
globals:
|
||||||
|
- id: page_index
|
||||||
|
type: int
|
||||||
|
restore_value: true
|
||||||
|
initial_value: '0'
|
||||||
|
- id: wifi_connected
|
||||||
|
type: bool
|
||||||
|
restore_value: false
|
||||||
|
initial_value: 'false'
|
||||||
|
- id: page_one_gauge_view
|
||||||
|
type: bool
|
||||||
|
restore_value: true
|
||||||
|
initial_value: 'false'
|
||||||
|
- id: clock_digital_view
|
||||||
|
type: bool
|
||||||
|
restore_value: true
|
||||||
|
initial_value: 'false'
|
||||||
|
- id: device_usage_view
|
||||||
|
type: int
|
||||||
|
restore_value: true
|
||||||
|
initial_value: '0'
|
||||||
|
|
||||||
|
sensor:
|
||||||
|
# WiFi signal strength
|
||||||
|
- platform: wifi_signal
|
||||||
|
name: "WiFi Signalstärke"
|
||||||
|
id: wifisignal
|
||||||
|
update_interval: 10s
|
||||||
|
- platform: homeassistant
|
||||||
|
id: grid_power
|
||||||
|
entity_id: sensor.netzleistung
|
||||||
|
internal: true
|
||||||
|
- platform: homeassistant
|
||||||
|
id: balcony_solar_power
|
||||||
|
entity_id: sensor.balkonkraftwerk_power
|
||||||
|
internal: true
|
||||||
|
- platform: homeassistant
|
||||||
|
id: solar_energy_daily
|
||||||
|
entity_id: sensor.energy_solar_daily
|
||||||
|
internal: true
|
||||||
|
- platform: homeassistant
|
||||||
|
id: grid_import_energy_daily
|
||||||
|
entity_id: sensor.energy_import_daily
|
||||||
|
internal: true
|
||||||
|
- platform: homeassistant
|
||||||
|
id: grid_export_energy_daily
|
||||||
|
entity_id: sensor.energy_export_daily
|
||||||
|
internal: true
|
||||||
|
- platform: homeassistant
|
||||||
|
id: hallway_fragrance_power
|
||||||
|
entity_id: sensor.flur_raumduft_power
|
||||||
|
internal: true
|
||||||
|
- platform: homeassistant
|
||||||
|
id: cellar_power
|
||||||
|
entity_id: sensor.keller_power
|
||||||
|
internal: true
|
||||||
|
- platform: homeassistant
|
||||||
|
id: sideboard_power
|
||||||
|
entity_id: sensor.kuche_anrichte_power
|
||||||
|
internal: true
|
||||||
|
- platform: homeassistant
|
||||||
|
id: kitchen_music_power
|
||||||
|
entity_id: sensor.kuche_musik_power
|
||||||
|
internal: true
|
||||||
|
- platform: homeassistant
|
||||||
|
id: bedroom_bed_power
|
||||||
|
entity_id: sensor.schlafzimmer_bett_power
|
||||||
|
internal: true
|
||||||
|
- platform: homeassistant
|
||||||
|
id: bedroom_cinema_power
|
||||||
|
entity_id: sensor.schlafzimmer_heimkino_power
|
||||||
|
internal: true
|
||||||
|
- platform: homeassistant
|
||||||
|
id: desk_decoration_power
|
||||||
|
entity_id: sensor.schreibtisch_deko_power
|
||||||
|
internal: true
|
||||||
|
- platform: homeassistant
|
||||||
|
id: desk_power
|
||||||
|
entity_id: sensor.schreibtisch_power
|
||||||
|
internal: true
|
||||||
|
- platform: homeassistant
|
||||||
|
id: server_room_power
|
||||||
|
entity_id: sensor.serverraum_power
|
||||||
|
internal: true
|
||||||
|
- platform: homeassistant
|
||||||
|
id: living_room_cinema_power
|
||||||
|
entity_id: sensor.wohnzimmer_heimkino_power
|
||||||
|
internal: true
|
||||||
|
- platform: homeassistant
|
||||||
|
id: games_cabinet_power
|
||||||
|
entity_id: sensor.wohnzimmer_spieleschrank_power
|
||||||
|
internal: true
|
||||||
|
- platform: homeassistant
|
||||||
|
id: refrigerator_power
|
||||||
|
entity_id: sensor.tz3000_ww6drja5_ts011f_leistung
|
||||||
|
internal: true
|
||||||
|
- platform: homeassistant
|
||||||
|
id: coffee_machine_power
|
||||||
|
entity_id: sensor.kaffeemaschine_leistung_2
|
||||||
|
internal: true
|
||||||
|
- platform: homeassistant
|
||||||
|
id: dryer_power
|
||||||
|
entity_id: sensor.ikea_of_sweden_inspelning_smart_plug_leistung
|
||||||
|
internal: true
|
||||||
|
- platform: homeassistant
|
||||||
|
id: washing_machine_power
|
||||||
|
entity_id: sensor.waschmaschine_leistung
|
||||||
|
internal: true
|
||||||
|
- platform: homeassistant
|
||||||
|
id: arcade_power
|
||||||
|
entity_id: sensor.arcade_automat_leistung
|
||||||
|
internal: true
|
||||||
|
- platform: homeassistant
|
||||||
|
id: dishwasher_power
|
||||||
|
entity_id: sensor.spulmaschine_leistung
|
||||||
|
internal: true
|
||||||
|
- platform: homeassistant
|
||||||
|
id: microwave_power
|
||||||
|
entity_id: sensor.waschetrockner_leistung
|
||||||
|
internal: true
|
||||||
|
- platform: wireguard
|
||||||
|
latest_handshake:
|
||||||
|
name: 'WireGuard Latest Handshake'
|
||||||
|
|
||||||
|
script:
|
||||||
|
- id: show_previous_page
|
||||||
|
then:
|
||||||
|
- lambda: id(page_index) = (id(page_index) + 3 - 1) % 3;
|
||||||
|
- component.update: tft
|
||||||
|
- id: toggle_page_mode
|
||||||
|
then:
|
||||||
|
- lambda: |-
|
||||||
|
if (id(page_index) == 0) {
|
||||||
|
id(page_one_gauge_view) = !id(page_one_gauge_view);
|
||||||
|
} else if (id(page_index) == 1) {
|
||||||
|
id(device_usage_view) = (id(device_usage_view) + 1) % 2;
|
||||||
|
} else {
|
||||||
|
id(clock_digital_view) = !id(clock_digital_view);
|
||||||
|
}
|
||||||
|
- component.update: tft
|
||||||
|
- id: show_next_page
|
||||||
|
then:
|
||||||
|
- lambda: id(page_index) = (id(page_index) + 1) % 3;
|
||||||
|
- component.update: tft
|
||||||
|
|
||||||
|
button:
|
||||||
|
- platform: template
|
||||||
|
name: "Zurück"
|
||||||
|
icon: mdi:arrow-left
|
||||||
|
on_press:
|
||||||
|
- script.execute: show_previous_page
|
||||||
|
- platform: template
|
||||||
|
name: "Modus"
|
||||||
|
icon: mdi:view-dashboard-outline
|
||||||
|
on_press:
|
||||||
|
- script.execute: toggle_page_mode
|
||||||
|
- platform: template
|
||||||
|
name: "Weiter"
|
||||||
|
icon: mdi:arrow-right
|
||||||
|
on_press:
|
||||||
|
- script.execute: show_next_page
|
||||||
|
|
||||||
|
binary_sensor:
|
||||||
|
- platform: wireguard
|
||||||
|
status:
|
||||||
|
name: 'WireGuard Status'
|
||||||
|
- platform: wireguard
|
||||||
|
enabled:
|
||||||
|
name: 'WireGuard Enabled'
|
||||||
|
# M5Stack Core button B (middle) switches the current page's view.
|
||||||
|
- platform: gpio
|
||||||
|
pin:
|
||||||
|
number: GPIO38
|
||||||
|
mode: INPUT
|
||||||
|
inverted: true
|
||||||
|
id: button_middle
|
||||||
|
on_press:
|
||||||
|
- script.execute: toggle_page_mode
|
||||||
|
|
||||||
|
# M5Stack Core button A (left). GPIO39 is input-only and has an external pull-up.
|
||||||
|
- platform: gpio
|
||||||
|
pin:
|
||||||
|
number: GPIO39
|
||||||
|
mode: INPUT
|
||||||
|
inverted: true
|
||||||
|
id: button_left
|
||||||
|
on_press:
|
||||||
|
- script.execute: show_previous_page
|
||||||
|
# M5Stack Core button C (right). GPIO37 is input-only and has an external pull-up.
|
||||||
|
- platform: gpio
|
||||||
|
pin:
|
||||||
|
number: GPIO37
|
||||||
|
mode: INPUT
|
||||||
|
inverted: true
|
||||||
|
id: button_right
|
||||||
|
on_press:
|
||||||
|
- script.execute: show_next_page
|
||||||
|
|
||||||
|
text_sensor:
|
||||||
|
- platform: wireguard
|
||||||
|
address:
|
||||||
|
name: 'WireGuard Address'
|
||||||
|
|
||||||
|
# M5CORE is the mipi_spi preset for the M5Stack Core's integrated display.
|
||||||
|
display:
|
||||||
|
- platform: mipi_spi
|
||||||
|
id: tft
|
||||||
|
model: M5CORE
|
||||||
|
cs_pin: GPIO14
|
||||||
|
dc_pin: GPIO27
|
||||||
|
reset_pin: GPIO33
|
||||||
|
invert_colors: true
|
||||||
|
update_interval: 1s
|
||||||
|
lambda: |-
|
||||||
|
it.fill(Color::BLACK);
|
||||||
|
|
||||||
|
if (id(sntp_time).now().is_valid()) {
|
||||||
|
it.strftime(234, 10, id(font_small), Color::WHITE, TextAlign::TOP_RIGHT,
|
||||||
|
"%H:%M", id(sntp_time).now());
|
||||||
|
} else {
|
||||||
|
it.printf(234, 10, id(font_small), Color::WHITE, TextAlign::TOP_RIGHT, "--:--");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Native API connectivity; Home Assistant is normally the connected client.
|
||||||
|
const bool home_assistant_online = id(api_server).is_connected();
|
||||||
|
const Color home_assistant_color = home_assistant_online ? Color(64, 255, 64) : Color(255, 80, 80);
|
||||||
|
it.printf(260, 8, id(font_icon), home_assistant_color, TextAlign::TOP_RIGHT, "\U000F07D0");
|
||||||
|
|
||||||
|
// Green means the WireGuard peer is online; red means it is offline.
|
||||||
|
const bool wireguard_online = id(energymonitor_wireguard).is_peer_up();
|
||||||
|
const Color wireguard_color = wireguard_online ? Color(64, 255, 64) : Color(255, 80, 80);
|
||||||
|
it.printf(286, 8, id(font_icon), wireguard_color, TextAlign::TOP_RIGHT, "\U000F0306");
|
||||||
|
|
||||||
|
// Render the requested MDI Wi-Fi strength glyph in green or red when disconnected.
|
||||||
|
const bool wifi_online = id(wifi_connected);
|
||||||
|
const Color wifi_color = wifi_online ? Color(64, 255, 64) : Color(255, 80, 80);
|
||||||
|
const char *wifi_icon = "\U000F092D"; // mdi:wifi-strength-off
|
||||||
|
if (wifi_online) {
|
||||||
|
if (id(wifisignal).has_state()) {
|
||||||
|
const float rssi = id(wifisignal).state;
|
||||||
|
wifi_icon = rssi >= -55 ? "\U000F0928" : rssi >= -67 ? "\U000F0925"
|
||||||
|
: rssi >= -75 ? "\U000F0922" : "\U000F091F";
|
||||||
|
} else {
|
||||||
|
wifi_icon = "\U000F091F";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
it.printf(312, 8, id(font_icon), wifi_color, TextAlign::TOP_RIGHT, "%s", wifi_icon);
|
||||||
|
|
||||||
|
if (id(page_index) == 0 && !id(page_one_gauge_view)) {
|
||||||
|
it.printf(12, 12, id(font_small), Color::WHITE, "Netzleistung");
|
||||||
|
if (id(grid_power).has_state()) {
|
||||||
|
const Color grid_power_color = id(grid_power).state >= 0
|
||||||
|
? Color(143, 190, 223) // #8FBEDF: positive grid power
|
||||||
|
: Color(150, 110, 217); // #966ED9: negative grid power
|
||||||
|
it.printf(308, 32, id(font_value), grid_power_color, TextAlign::TOP_RIGHT,
|
||||||
|
"%+.0f W", id(grid_power).state);
|
||||||
|
} else {
|
||||||
|
it.printf(308, 42, id(font_value), Color::WHITE, TextAlign::TOP_RIGHT,
|
||||||
|
"-- W");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (id(grid_import_energy_daily).has_state()) {
|
||||||
|
it.printf(12, 96, id(font_small), Color::WHITE, "Bezug: %.2f kWh",
|
||||||
|
id(grid_import_energy_daily).state);
|
||||||
|
} else {
|
||||||
|
it.printf(12, 96, id(font_small), Color::WHITE, "Bezug: -- kWh");
|
||||||
|
}
|
||||||
|
if (id(grid_export_energy_daily).has_state()) {
|
||||||
|
it.printf(12, 116, id(font_small), Color::WHITE, "Einspeisung: %.2f kWh",
|
||||||
|
id(grid_export_energy_daily).state);
|
||||||
|
} else {
|
||||||
|
it.printf(12, 116, id(font_small), Color::WHITE, "Einspeisung: -- kWh");
|
||||||
|
}
|
||||||
|
|
||||||
|
it.printf(12, 138, id(font_small), Color::WHITE, "Balkonkraftwerk");
|
||||||
|
// Daily solar energy is aligned to the left of the current solar-power reading.
|
||||||
|
if (id(solar_energy_daily).has_state()) {
|
||||||
|
it.printf(12, 180, id(font_small), Color(255, 170, 40), "Solar: %.2f kWh",
|
||||||
|
id(solar_energy_daily).state);
|
||||||
|
} else {
|
||||||
|
it.printf(12, 180, id(font_small), Color(255, 170, 40), "Solar: -- kWh");
|
||||||
|
}
|
||||||
|
if (id(balcony_solar_power).has_state()) {
|
||||||
|
it.printf(308, 158, id(font_value), Color(255, 170, 40), TextAlign::TOP_RIGHT,
|
||||||
|
"%.0f W", id(balcony_solar_power).state);
|
||||||
|
} else {
|
||||||
|
it.printf(308, 168, id(font_value), Color(255, 170, 40), TextAlign::TOP_RIGHT,
|
||||||
|
"-- W");
|
||||||
|
}
|
||||||
|
} else if (id(page_index) == 0) {
|
||||||
|
// One shared gauge with two parallel curved bars: grid on the outside, solar inside.
|
||||||
|
const Color gauge_background = Color(42, 42, 42);
|
||||||
|
const int gauge_center_x = 160;
|
||||||
|
const int gauge_center_y = 188;
|
||||||
|
|
||||||
|
// The outer band represents the absolute grid-power value with its signed color.
|
||||||
|
float grid_power_value = id(grid_power).has_state() ? id(grid_power).state : 0.0f;
|
||||||
|
float grid_power_magnitude = grid_power_value < 0.0f ? -grid_power_value : grid_power_value;
|
||||||
|
int grid_gauge_percent = static_cast<int>(grid_power_magnitude * 100.0f / ${grid_power_gauge_max_w});
|
||||||
|
if (grid_gauge_percent > 100) grid_gauge_percent = 100;
|
||||||
|
const Color grid_gauge_color = grid_power_value >= 0.0f
|
||||||
|
? Color(143, 190, 223) // #8FBEDF
|
||||||
|
: Color(150, 110, 217); // #966ED9
|
||||||
|
it.filled_gauge(gauge_center_x, gauge_center_y, 140, 122, 100, gauge_background);
|
||||||
|
it.filled_gauge(gauge_center_x, gauge_center_y, 140, 122, grid_gauge_percent, grid_gauge_color);
|
||||||
|
|
||||||
|
// The inner band is solar power, normalized against the 800 W inverter maximum.
|
||||||
|
float solar_power_value = id(balcony_solar_power).has_state() ? id(balcony_solar_power).state : 0.0f;
|
||||||
|
int solar_gauge_percent = static_cast<int>(solar_power_value * 100.0f / ${balcony_solar_gauge_max_w});
|
||||||
|
if (solar_gauge_percent < 0) solar_gauge_percent = 0;
|
||||||
|
if (solar_gauge_percent > 100) solar_gauge_percent = 100;
|
||||||
|
const Color solar_gauge_color = Color(255, 170, 40); // #FFAA28
|
||||||
|
it.filled_gauge(gauge_center_x, gauge_center_y, 116, 98, 100, gauge_background);
|
||||||
|
it.filled_gauge(gauge_center_x, gauge_center_y, 116, 98, solar_gauge_percent, solar_gauge_color);
|
||||||
|
|
||||||
|
it.printf(gauge_center_x, 102, id(font_small), Color::WHITE, TextAlign::TOP_CENTER,
|
||||||
|
"Netzleistung");
|
||||||
|
if (id(grid_power).has_state()) {
|
||||||
|
it.printf(gauge_center_x, 120, id(font_title), grid_gauge_color, TextAlign::TOP_CENTER,
|
||||||
|
"%+.0f W", grid_power_value);
|
||||||
|
} else {
|
||||||
|
it.printf(gauge_center_x, 120, id(font_title), Color::WHITE, TextAlign::TOP_CENTER, "-- W");
|
||||||
|
}
|
||||||
|
it.printf(gauge_center_x, 151, id(font_small), Color::WHITE, TextAlign::TOP_CENTER,
|
||||||
|
"Balkonkraftwerk");
|
||||||
|
if (id(balcony_solar_power).has_state()) {
|
||||||
|
it.printf(gauge_center_x, 169, id(font_title), solar_gauge_color, TextAlign::TOP_CENTER,
|
||||||
|
"%.0f W", solar_power_value);
|
||||||
|
} else {
|
||||||
|
it.printf(gauge_center_x, 169, id(font_title), solar_gauge_color, TextAlign::TOP_CENTER, "-- W");
|
||||||
|
}
|
||||||
|
} else if (id(page_index) == 1) {
|
||||||
|
// Page 2 lists nine consumers at once in two columns; button B changes group.
|
||||||
|
const int device_count = 18;
|
||||||
|
const int usage_group = id(device_usage_view);
|
||||||
|
const int page_start = usage_group * 9;
|
||||||
|
const char *device_names[device_count] = {
|
||||||
|
"Raumduft", "Keller", "Anrichte", "Musik", "Bett", "HK Schlafzimmer",
|
||||||
|
"Deko", "Schreibtisch", "Serverraum", "HK Wohnzimmer", "Spieleschrank",
|
||||||
|
"Kühlschrank", "Kaffeemaschine", "Wäschetrockner", "Waschmaschine", "Arcade",
|
||||||
|
"Spülmaschine", "Mikrowelle"};
|
||||||
|
float device_values[device_count] = {
|
||||||
|
id(hallway_fragrance_power).state, id(cellar_power).state, id(sideboard_power).state,
|
||||||
|
id(kitchen_music_power).state, id(bedroom_bed_power).state, id(bedroom_cinema_power).state,
|
||||||
|
id(desk_decoration_power).state, id(desk_power).state, id(server_room_power).state,
|
||||||
|
id(living_room_cinema_power).state, id(games_cabinet_power).state, id(refrigerator_power).state,
|
||||||
|
id(coffee_machine_power).state, id(dryer_power).state, id(washing_machine_power).state,
|
||||||
|
id(arcade_power).state, id(dishwasher_power).state, id(microwave_power).state};
|
||||||
|
bool device_has_state[device_count] = {
|
||||||
|
id(hallway_fragrance_power).has_state(), id(cellar_power).has_state(), id(sideboard_power).has_state(),
|
||||||
|
id(kitchen_music_power).has_state(), id(bedroom_bed_power).has_state(), id(bedroom_cinema_power).has_state(),
|
||||||
|
id(desk_decoration_power).has_state(), id(desk_power).has_state(), id(server_room_power).has_state(),
|
||||||
|
id(living_room_cinema_power).has_state(), id(games_cabinet_power).has_state(), id(refrigerator_power).has_state(),
|
||||||
|
id(coffee_machine_power).has_state(), id(dryer_power).has_state(), id(washing_machine_power).has_state(),
|
||||||
|
id(arcade_power).has_state(), id(dishwasher_power).has_state(), id(microwave_power).has_state()};
|
||||||
|
Color device_colors[device_count] = {
|
||||||
|
Color(255, 170, 40), Color(69, 189, 255), Color(170, 116, 217), Color(93, 207, 140),
|
||||||
|
Color(238, 112, 112), Color(107, 139, 255), Color(255, 130, 180), Color(122, 204, 174),
|
||||||
|
Color(240, 195, 90), Color(151, 120, 220), Color(91, 189, 193), Color(212, 132, 82),
|
||||||
|
Color(143, 190, 223), Color(233, 106, 142), Color(132, 205, 92), Color(255, 181, 80),
|
||||||
|
Color(111, 167, 229), Color(190, 137, 226)};
|
||||||
|
|
||||||
|
// The title uses the free area to the left of the status indicators.
|
||||||
|
it.printf(12, 10, id(font_device), Color::WHITE, "Geräteverbrauch");
|
||||||
|
for (int display_index = 0; display_index < 9; display_index++) {
|
||||||
|
const int index = page_start + display_index;
|
||||||
|
const int column = display_index % 2;
|
||||||
|
const int row = display_index / 2;
|
||||||
|
const int x = column == 0 ? 12 : 166;
|
||||||
|
const int y = 43 + row * 25;
|
||||||
|
if (device_has_state[index]) {
|
||||||
|
it.printf(x, y, id(font_device), device_colors[index], "%s: %.0f W",
|
||||||
|
device_names[index], device_values[index]);
|
||||||
|
} else {
|
||||||
|
it.printf(x, y, id(font_device), device_colors[index], "%s: -- W", device_names[index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The stacked bar always represents all 18 consumer devices, not only this view's rows.
|
||||||
|
const int bar_x = 12;
|
||||||
|
const int bar_y = 180;
|
||||||
|
const int bar_width = 296;
|
||||||
|
const int bar_height = 18;
|
||||||
|
float usage_sum = 0.0f;
|
||||||
|
int last_active = -1;
|
||||||
|
for (int index = 0; index < device_count; index++) {
|
||||||
|
if (device_has_state[index] && device_values[index] > 0.0f) {
|
||||||
|
usage_sum += device_values[index];
|
||||||
|
last_active = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
it.filled_rectangle(bar_x, bar_y, bar_width, bar_height, Color(42, 42, 42));
|
||||||
|
if (usage_sum > 0.0f) {
|
||||||
|
int cursor_x = bar_x;
|
||||||
|
for (int index = 0; index < device_count; index++) {
|
||||||
|
if (!device_has_state[index] || device_values[index] <= 0.0f) continue;
|
||||||
|
const int segment_width = index == last_active
|
||||||
|
? bar_x + bar_width - cursor_x
|
||||||
|
: static_cast<int>(device_values[index] * bar_width / usage_sum);
|
||||||
|
if (segment_width > 0) {
|
||||||
|
it.filled_rectangle(cursor_x, bar_y, segment_width, bar_height, device_colors[index]);
|
||||||
|
cursor_x += segment_width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (id(page_index) == 2) {
|
||||||
|
const auto now = id(sntp_time).now();
|
||||||
|
const Color accent(143, 190, 223);
|
||||||
|
const Color gold(255, 170, 40);
|
||||||
|
const Color muted(55, 65, 80);
|
||||||
|
it.print(12, 10, id(font_device), accent, "Uhr · Berlin");
|
||||||
|
if (!now.is_valid()) {
|
||||||
|
it.print(160, 100, id(font_title), accent, TextAlign::TOP_CENTER, "--:--:--");
|
||||||
|
it.print(160, 190, id(font_small), Color::WHITE, TextAlign::TOP_CENTER,
|
||||||
|
"Warte auf Zeitsynchronisation");
|
||||||
|
} else {
|
||||||
|
if (!id(clock_digital_view)) {
|
||||||
|
const int cx = 160, cy = 111, radius = 74;
|
||||||
|
const float tau = 6.28318530718f;
|
||||||
|
it.filled_circle(cx, cy, radius, Color(12, 18, 28));
|
||||||
|
it.circle(cx, cy, radius, accent);
|
||||||
|
it.circle(cx, cy, radius - 3, muted);
|
||||||
|
for (int tick = 0; tick < 60; tick++) {
|
||||||
|
const float angle = tick * tau / 60.0f;
|
||||||
|
const int inner = tick % 5 == 0 ? radius - 13 : radius - 8;
|
||||||
|
it.line(cx + lroundf(sinf(angle) * inner), cy - lroundf(cosf(angle) * inner),
|
||||||
|
cx + lroundf(sinf(angle) * (radius - 5)),
|
||||||
|
cy - lroundf(cosf(angle) * (radius - 5)),
|
||||||
|
tick % 5 == 0 ? accent : muted);
|
||||||
|
}
|
||||||
|
it.print(cx, cy - 49, id(font_device), Color::WHITE, TextAlign::CENTER, "12");
|
||||||
|
it.print(cx + 50, cy, id(font_device), Color::WHITE, TextAlign::CENTER, "3");
|
||||||
|
it.print(cx, cy + 49, id(font_device), Color::WHITE, TextAlign::CENTER, "6");
|
||||||
|
it.print(cx - 50, cy, id(font_device), Color::WHITE, TextAlign::CENTER, "9");
|
||||||
|
// Round-ended hands; minutes and hours advance with the seconds.
|
||||||
|
auto hand = [&](float turns, int length, int thickness, Color color) {
|
||||||
|
const float angle = turns * tau;
|
||||||
|
for (int r = 0; r <= length; r++) {
|
||||||
|
it.filled_circle(cx + lroundf(sinf(angle) * r),
|
||||||
|
cy - lroundf(cosf(angle) * r), thickness, color);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
hand((now.hour % 12 + now.minute / 60.0f + now.second / 3600.0f) / 12.0f,
|
||||||
|
34, 3, Color::WHITE);
|
||||||
|
hand((now.minute + now.second / 60.0f) / 60.0f, 51, 2, accent);
|
||||||
|
const float second_angle = now.second * tau / 60.0f;
|
||||||
|
it.line(cx - lroundf(sinf(second_angle) * 12), cy + lroundf(cosf(second_angle) * 12),
|
||||||
|
cx + lroundf(sinf(second_angle) * 60), cy - lroundf(cosf(second_angle) * 60), gold);
|
||||||
|
it.filled_circle(cx, cy, 4, gold);
|
||||||
|
it.filled_circle(cx, cy, 1, Color::BLACK);
|
||||||
|
} else {
|
||||||
|
// Draw seven-segment digits directly, avoiding an extra font dependency.
|
||||||
|
const uint8_t masks[10] = {0x3F, 0x06, 0x5B, 0x4F, 0x66,
|
||||||
|
0x6D, 0x7D, 0x07, 0x7F, 0x6F};
|
||||||
|
const int digits[6] = {now.hour / 10, now.hour % 10, now.minute / 10,
|
||||||
|
now.minute % 10, now.second / 10, now.second % 10};
|
||||||
|
const int positions[6] = {17, 61, 121, 165, 225, 269};
|
||||||
|
const int y = 77;
|
||||||
|
for (int digit = 0; digit < 6; digit++) {
|
||||||
|
const int x = positions[digit];
|
||||||
|
const Color lit = digit < 4 ? accent : gold;
|
||||||
|
for (int segment = 0; segment < 7; segment++) {
|
||||||
|
const Color color = (masks[digits[digit]] & (1 << segment))
|
||||||
|
? lit : Color(20, 27, 36);
|
||||||
|
const bool horizontal = segment == 0 || segment == 3 || segment == 6;
|
||||||
|
const int sx = horizontal ? x + 6 : x + ((segment == 1 || segment == 2) ? 30 : 0);
|
||||||
|
const int sy = y + (segment == 0 ? 0 : segment == 3 ? 72 : segment == 6 ? 36
|
||||||
|
: (segment == 1 || segment == 5) ? 6 : 42);
|
||||||
|
// Taper each segment's ends for the classic LED clock shape.
|
||||||
|
for (int stripe = 0; stripe < 6; stripe++) {
|
||||||
|
const int inset = stripe < 3 ? 2 - stripe : stripe - 3;
|
||||||
|
if (horizontal)
|
||||||
|
it.line(sx + inset, sy + stripe, sx + 23 - inset, sy + stripe, color);
|
||||||
|
else
|
||||||
|
it.line(sx + stripe, sy + inset, sx + stripe, sy + 29 - inset, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int x : {109, 213}) {
|
||||||
|
it.filled_circle(x, y + 24, 3, accent);
|
||||||
|
it.filled_circle(x, y + 54, 3, accent);
|
||||||
|
}
|
||||||
|
it.line(40, 172, 280, 172, muted);
|
||||||
|
}
|
||||||
|
const char *weekdays[] = {"Sonntag", "Montag", "Dienstag", "Mittwoch",
|
||||||
|
"Donnerstag", "Freitag", "Samstag"};
|
||||||
|
it.printf(160, 190, id(font_small), Color::WHITE, TextAlign::TOP_CENTER,
|
||||||
|
"%s, %02d.%02d.%04d", weekdays[now.day_of_week - 1],
|
||||||
|
now.day_of_month, now.month, now.year);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Labels correspond to the left (A) and right (C) hardware buttons.
|
||||||
|
it.printf(8, 239, id(font_small), Color::WHITE, TextAlign::BOTTOM_LEFT,
|
||||||
|
"< Zurück");
|
||||||
|
it.printf(312, 239, id(font_small), Color::WHITE, TextAlign::BOTTOM_RIGHT,
|
||||||
|
"Weiter >");
|
||||||
|
if (id(page_index) == 0) {
|
||||||
|
it.printf(160, 239, id(font_small), Color::WHITE, TextAlign::BOTTOM_CENTER,
|
||||||
|
"Ansicht · 1 / 3");
|
||||||
|
} else if (id(page_index) == 1) {
|
||||||
|
it.printf(160, 239, id(font_small), Color::WHITE, TextAlign::BOTTOM_CENTER,
|
||||||
|
"Geräte %d / 2", id(device_usage_view) + 1);
|
||||||
|
} else {
|
||||||
|
it.printf(160, 239, id(font_small), Color::WHITE, TextAlign::BOTTOM_CENTER,
|
||||||
|
"%s · 3 / 3", id(clock_digital_view) ? "Analog" : "Digital");
|
||||||
|
}
|
||||||
+808
-285
File diff suppressed because it is too large
Load Diff
@@ -16,10 +16,13 @@ esp32:
|
|||||||
framework:
|
framework:
|
||||||
type: esp-idf
|
type: esp-idf
|
||||||
|
|
||||||
|
network:
|
||||||
|
enable_ipv6: true
|
||||||
|
|
||||||
wifi:
|
wifi:
|
||||||
ssid: "Voltage-legacy"
|
ssid: "Voltage-legacy"
|
||||||
password: !secret voltage_legacy_psk
|
password: !secret voltage_legacy_psk
|
||||||
use_address: ${name}.home
|
#use_address: ${name}.home
|
||||||
power_save_mode: high
|
power_save_mode: high
|
||||||
fast_connect: on
|
fast_connect: on
|
||||||
min_auth_mode: WPA2
|
min_auth_mode: WPA2
|
||||||
|
|||||||
@@ -31,3 +31,5 @@ ota:
|
|||||||
password: !secret ota
|
password: !secret ota
|
||||||
|
|
||||||
http_request:
|
http_request:
|
||||||
|
|
||||||
|
captive_portal:
|
||||||
|
|||||||
@@ -31,3 +31,5 @@ ota:
|
|||||||
password: !secret ota
|
password: !secret ota
|
||||||
|
|
||||||
http_request:
|
http_request:
|
||||||
|
|
||||||
|
captive_portal:
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ esp32:
|
|||||||
framework:
|
framework:
|
||||||
type: esp-idf
|
type: esp-idf
|
||||||
|
|
||||||
|
network:
|
||||||
|
enable_ipv6: true
|
||||||
|
|
||||||
wifi:
|
wifi:
|
||||||
ssid: "Voltage-legacy"
|
ssid: "Voltage-legacy"
|
||||||
password: !secret voltage_legacy_psk
|
password: !secret voltage_legacy_psk
|
||||||
|
|||||||
@@ -16,10 +16,13 @@ esp32:
|
|||||||
framework:
|
framework:
|
||||||
type: esp-idf
|
type: esp-idf
|
||||||
|
|
||||||
|
network:
|
||||||
|
enable_ipv6: true
|
||||||
|
|
||||||
wifi:
|
wifi:
|
||||||
ssid: "Voltage-legacy"
|
ssid: "Voltage-legacy"
|
||||||
password: !secret voltage_legacy_psk
|
password: !secret voltage_legacy_psk
|
||||||
use_address: cam-livingroom.home
|
#use_address: cam-livingroom.home
|
||||||
power_save_mode: high
|
power_save_mode: high
|
||||||
fast_connect: on
|
fast_connect: on
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -0,0 +1,634 @@
|
|||||||
|
# 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));
|
||||||
|
# Start the normal dimming/off cycle after a cold boot as well. The
|
||||||
|
# light component is initialized by this priority, while BLE callbacks
|
||||||
|
# and touch events can still restart the script through wake_backlight.
|
||||||
|
- priority: 600
|
||||||
|
then:
|
||||||
|
- script.execute: backlight_idle
|
||||||
|
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});"
|
||||||
@@ -53,11 +53,16 @@ esp32:
|
|||||||
board: nodemcu-32s
|
board: nodemcu-32s
|
||||||
framework:
|
framework:
|
||||||
type: esp-idf
|
type: esp-idf
|
||||||
|
advanced:
|
||||||
|
minimum_chip_revision: "3.0"
|
||||||
|
|
||||||
|
network:
|
||||||
|
enable_ipv6: true
|
||||||
|
|
||||||
wifi:
|
wifi:
|
||||||
ssid: "Voltage-legacy"
|
ssid: "Voltage-legacy"
|
||||||
password: !secret voltage_legacy_psk
|
password: !secret voltage_legacy_psk
|
||||||
use_address: ${name}.home
|
# use_address: ${name}.home
|
||||||
power_save_mode: high
|
power_save_mode: high
|
||||||
fast_connect: on
|
fast_connect: on
|
||||||
min_auth_mode: WPA2
|
min_auth_mode: WPA2
|
||||||
|
|||||||
@@ -3,18 +3,20 @@
|
|||||||
substitutions:
|
substitutions:
|
||||||
name: m5stack-atom-echo
|
name: m5stack-atom-echo
|
||||||
friendly_name: M5Stack Atom Echo
|
friendly_name: M5Stack Atom Echo
|
||||||
micro_wake_word_model: okay_nabu # alexa, hey_jarvis, hey_mycroft are also supported
|
|
||||||
|
|
||||||
esphome:
|
esphome:
|
||||||
name: ${name}
|
name: ${name}
|
||||||
name_add_mac_suffix: False
|
name_add_mac_suffix: false
|
||||||
friendly_name: ${friendly_name}
|
friendly_name: ${friendly_name}
|
||||||
min_version: 2025.2.0
|
min_version: 2025.5.0
|
||||||
|
|
||||||
esp32:
|
esp32:
|
||||||
board: m5stack-atom
|
board: m5stack-atom
|
||||||
|
cpu_frequency: 240MHz
|
||||||
framework:
|
framework:
|
||||||
type: esp-idf
|
type: esp-idf
|
||||||
|
advanced:
|
||||||
|
sram1_as_iram: true
|
||||||
|
|
||||||
# Enable Home Assistant API
|
# Enable Home Assistant API
|
||||||
api:
|
api:
|
||||||
@@ -26,12 +28,15 @@ ota:
|
|||||||
id: ota_esphome
|
id: ota_esphome
|
||||||
password: !secret ota
|
password: !secret ota
|
||||||
|
|
||||||
|
network:
|
||||||
|
enable_ipv6: true
|
||||||
|
|
||||||
wifi:
|
wifi:
|
||||||
on_connect:
|
on_connect:
|
||||||
- delay: 5s # Gives time for improv results to be transmitted
|
- delay: 5s # Gives time for improv results to be transmitted
|
||||||
ssid: Voltage-legacy
|
ssid: Voltage-legacy
|
||||||
password: !secret voltage_legacy_psk
|
password: !secret voltage_legacy_psk
|
||||||
use_address: ${name}.home
|
#use_address: ${name}.home
|
||||||
power_save_mode: high
|
power_save_mode: high
|
||||||
fast_connect: on
|
fast_connect: on
|
||||||
min_auth_mode: WPA2
|
min_auth_mode: WPA2
|
||||||
@@ -60,14 +65,17 @@ microphone:
|
|||||||
i2s_din_pin: GPIO23
|
i2s_din_pin: GPIO23
|
||||||
adc_type: external
|
adc_type: external
|
||||||
pdm: true
|
pdm: true
|
||||||
|
sample_rate: 16000
|
||||||
|
correct_dc_offset: true
|
||||||
|
|
||||||
speaker:
|
speaker:
|
||||||
- platform: i2s_audio
|
- platform: i2s_audio
|
||||||
id: echo_speaker
|
id: echo_speaker
|
||||||
i2s_dout_pin: GPIO22
|
i2s_dout_pin: GPIO22
|
||||||
dac_type: external
|
dac_type: external
|
||||||
bits_per_sample: 32bit
|
bits_per_sample: 16bit
|
||||||
channel: right
|
sample_rate: 16000
|
||||||
|
channel: stereo # The Echo has poor playback audio quality when using mon audio
|
||||||
buffer_duration: 60ms
|
buffer_duration: 60ms
|
||||||
|
|
||||||
media_player:
|
media_player:
|
||||||
@@ -77,7 +85,6 @@ media_player:
|
|||||||
announcement_pipeline:
|
announcement_pipeline:
|
||||||
speaker: echo_speaker
|
speaker: echo_speaker
|
||||||
format: WAV
|
format: WAV
|
||||||
codec_support_enabled: false
|
|
||||||
buffer_size: 6000
|
buffer_size: 6000
|
||||||
volume_min: 0.4
|
volume_min: 0.4
|
||||||
files:
|
files:
|
||||||
@@ -88,14 +95,7 @@ media_player:
|
|||||||
condition:
|
condition:
|
||||||
- microphone.is_capturing:
|
- microphone.is_capturing:
|
||||||
then:
|
then:
|
||||||
- if:
|
- script.execute: stop_wake_word
|
||||||
condition:
|
|
||||||
lambda: return id(wake_word_engine_location).state == "On device";
|
|
||||||
then:
|
|
||||||
- micro_wake_word.stop:
|
|
||||||
else:
|
|
||||||
- voice_assistant.stop:
|
|
||||||
- script.execute: reset_led
|
|
||||||
- light.turn_on:
|
- light.turn_on:
|
||||||
id: led
|
id: led
|
||||||
blue: 100%
|
blue: 100%
|
||||||
@@ -105,14 +105,18 @@ media_player:
|
|||||||
effect: none
|
effect: none
|
||||||
on_idle:
|
on_idle:
|
||||||
- script.execute: start_wake_word
|
- script.execute: start_wake_word
|
||||||
|
- script.execute: reset_led
|
||||||
|
|
||||||
voice_assistant:
|
voice_assistant:
|
||||||
id: va
|
id: va
|
||||||
microphone: echo_microphone
|
micro_wake_word:
|
||||||
|
microphone:
|
||||||
|
microphone: echo_microphone
|
||||||
|
channels: 0
|
||||||
|
gain_factor: 4
|
||||||
media_player: echo_media_player
|
media_player: echo_media_player
|
||||||
noise_suppression_level: 2
|
noise_suppression_level: 2
|
||||||
auto_gain: 31dBFS
|
auto_gain: 31dBFS
|
||||||
volume_multiplier: 2.0
|
|
||||||
on_listening:
|
on_listening:
|
||||||
- light.turn_on:
|
- light.turn_on:
|
||||||
id: led
|
id: led
|
||||||
@@ -136,8 +140,26 @@ voice_assistant:
|
|||||||
brightness: 100%
|
brightness: 100%
|
||||||
effect: none
|
effect: none
|
||||||
on_end:
|
on_end:
|
||||||
- delay: 100ms
|
# Handle the "nevermind" case where there is no announcement
|
||||||
- script.execute: start_wake_word
|
- wait_until:
|
||||||
|
condition:
|
||||||
|
- media_player.is_announcing:
|
||||||
|
timeout: 0.5s
|
||||||
|
# Restart only mWW if enabled; streaming wake words automatically restart
|
||||||
|
- if:
|
||||||
|
condition:
|
||||||
|
- lambda: |-
|
||||||
|
return id(wake_word_engine_location).current_option() == "On device";
|
||||||
|
then:
|
||||||
|
- wait_until:
|
||||||
|
- and:
|
||||||
|
- not:
|
||||||
|
voice_assistant.is_running:
|
||||||
|
- not:
|
||||||
|
speaker.is_playing:
|
||||||
|
- lambda: id(va).set_use_wake_word(false);
|
||||||
|
- micro_wake_word.start:
|
||||||
|
- script.execute: reset_led
|
||||||
on_error:
|
on_error:
|
||||||
- light.turn_on:
|
- light.turn_on:
|
||||||
id: led
|
id: led
|
||||||
@@ -152,11 +174,9 @@ voice_assistant:
|
|||||||
- delay: 2s # Give the api server time to settle
|
- delay: 2s # Give the api server time to settle
|
||||||
- script.execute: start_wake_word
|
- script.execute: start_wake_word
|
||||||
on_client_disconnected:
|
on_client_disconnected:
|
||||||
- voice_assistant.stop:
|
- script.execute: stop_wake_word
|
||||||
- micro_wake_word.stop:
|
|
||||||
on_timer_finished:
|
on_timer_finished:
|
||||||
- voice_assistant.stop:
|
- script.execute: stop_wake_word
|
||||||
- micro_wake_word.stop:
|
|
||||||
- wait_until:
|
- wait_until:
|
||||||
not:
|
not:
|
||||||
microphone.is_capturing:
|
microphone.is_capturing:
|
||||||
@@ -232,7 +252,8 @@ script:
|
|||||||
then:
|
then:
|
||||||
- if:
|
- if:
|
||||||
condition:
|
condition:
|
||||||
- lambda: return id(wake_word_engine_location).state == "On device";
|
- lambda: |-
|
||||||
|
return id(wake_word_engine_location).current_option() == "On device";
|
||||||
- switch.is_on: use_listen_light
|
- switch.is_on: use_listen_light
|
||||||
then:
|
then:
|
||||||
- light.turn_on:
|
- light.turn_on:
|
||||||
@@ -245,7 +266,8 @@ script:
|
|||||||
else:
|
else:
|
||||||
- if:
|
- if:
|
||||||
condition:
|
condition:
|
||||||
- lambda: return id(wake_word_engine_location).state != "On device";
|
- lambda: |-
|
||||||
|
return id(wake_word_engine_location).current_option() == "On device";
|
||||||
- switch.is_on: use_listen_light
|
- switch.is_on: use_listen_light
|
||||||
then:
|
then:
|
||||||
- light.turn_on:
|
- light.turn_on:
|
||||||
@@ -259,27 +281,41 @@ script:
|
|||||||
- light.turn_off: led
|
- light.turn_off: led
|
||||||
- id: start_wake_word
|
- id: start_wake_word
|
||||||
then:
|
then:
|
||||||
- wait_until:
|
|
||||||
and:
|
|
||||||
- media_player.is_idle:
|
|
||||||
- speaker.is_stopped:
|
|
||||||
- if:
|
- if:
|
||||||
condition:
|
condition:
|
||||||
lambda: return id(wake_word_engine_location).state == "On device";
|
and:
|
||||||
|
- not:
|
||||||
|
- voice_assistant.is_running:
|
||||||
|
- lambda: |-
|
||||||
|
return id(wake_word_engine_location).current_option() == "On device";
|
||||||
then:
|
then:
|
||||||
- voice_assistant.stop
|
- lambda: id(va).set_use_wake_word(false);
|
||||||
- micro_wake_word.stop:
|
|
||||||
- delay: 1s
|
|
||||||
- script.execute: reset_led
|
|
||||||
- script.wait: reset_led
|
|
||||||
- micro_wake_word.start:
|
- micro_wake_word.start:
|
||||||
else:
|
- if:
|
||||||
- if:
|
condition:
|
||||||
condition: voice_assistant.is_running
|
and:
|
||||||
then:
|
- not:
|
||||||
- voice_assistant.stop:
|
- voice_assistant.is_running:
|
||||||
- script.execute: reset_led
|
- lambda: |-
|
||||||
|
return id(wake_word_engine_location).current_option() == "In Home Assistant";
|
||||||
|
then:
|
||||||
|
- lambda: id(va).set_use_wake_word(true);
|
||||||
- voice_assistant.start_continuous:
|
- voice_assistant.start_continuous:
|
||||||
|
- id: stop_wake_word
|
||||||
|
then:
|
||||||
|
- if:
|
||||||
|
condition:
|
||||||
|
lambda: |-
|
||||||
|
return id(wake_word_engine_location).current_option() == "In Home Assistant";
|
||||||
|
then:
|
||||||
|
- lambda: id(va).set_use_wake_word(false);
|
||||||
|
- voice_assistant.stop:
|
||||||
|
- if:
|
||||||
|
condition:
|
||||||
|
lambda: |-
|
||||||
|
return id(wake_word_engine_location).current_option() == "On device";
|
||||||
|
then:
|
||||||
|
- micro_wake_word.stop:
|
||||||
|
|
||||||
switch:
|
switch:
|
||||||
- platform: template
|
- platform: template
|
||||||
@@ -339,7 +375,7 @@ select:
|
|||||||
condition:
|
condition:
|
||||||
lambda: return x == "In Home Assistant";
|
lambda: return x == "In Home Assistant";
|
||||||
then:
|
then:
|
||||||
- micro_wake_word.stop
|
- micro_wake_word.stop:
|
||||||
- delay: 500ms
|
- delay: 500ms
|
||||||
- lambda: id(va).set_use_wake_word(true);
|
- lambda: id(va).set_use_wake_word(true);
|
||||||
- voice_assistant.start_continuous:
|
- voice_assistant.start_continuous:
|
||||||
@@ -348,9 +384,9 @@ select:
|
|||||||
lambda: return x == "On device";
|
lambda: return x == "On device";
|
||||||
then:
|
then:
|
||||||
- lambda: id(va).set_use_wake_word(false);
|
- lambda: id(va).set_use_wake_word(false);
|
||||||
- voice_assistant.stop
|
- voice_assistant.stop:
|
||||||
- delay: 500ms
|
- delay: 500ms
|
||||||
- micro_wake_word.start
|
- micro_wake_word.start:
|
||||||
|
|
||||||
micro_wake_word:
|
micro_wake_word:
|
||||||
on_wake_word_detected:
|
on_wake_word_detected:
|
||||||
@@ -358,4 +394,9 @@ micro_wake_word:
|
|||||||
wake_word: !lambda return wake_word;
|
wake_word: !lambda return wake_word;
|
||||||
vad:
|
vad:
|
||||||
models:
|
models:
|
||||||
- model: ${micro_wake_word_model}
|
- model: okay_nabu
|
||||||
|
id: okay_nabu
|
||||||
|
- model: hey_mycroft
|
||||||
|
id: hey_mycroft
|
||||||
|
- model: hey_jarvis
|
||||||
|
id: hey_jarvis
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# Source: https://github.com/wildekek/rdtech-esphome
|
||||||
|
|
||||||
substitutions:
|
substitutions:
|
||||||
name: "riden-labornetzteil-18a"
|
name: "riden-labornetzteil-18a"
|
||||||
friendly_name: "Riden Labornetzteil 18A"
|
friendly_name: "Riden Labornetzteil 18A"
|
||||||
@@ -8,7 +10,7 @@ substitutions:
|
|||||||
device_friendly_name: "Riden RD6018"
|
device_friendly_name: "Riden RD6018"
|
||||||
device_description: "Monitor and control a RD6018 PSU via WiFi"
|
device_description: "Monitor and control a RD6018 PSU via WiFi"
|
||||||
time_timezone: "Europe/Berlin"
|
time_timezone: "Europe/Berlin"
|
||||||
|
|
||||||
# Model specific settings (Don't change these!)
|
# Model specific settings (Don't change these!)
|
||||||
RD6006_voltage_maximum: "60"
|
RD6006_voltage_maximum: "60"
|
||||||
RD6006_voltage_accuracy: "2"
|
RD6006_voltage_accuracy: "2"
|
||||||
@@ -16,13 +18,17 @@ substitutions:
|
|||||||
RD6006_current_maximum: "6"
|
RD6006_current_maximum: "6"
|
||||||
RD6006_current_accuracy: "3"
|
RD6006_current_accuracy: "3"
|
||||||
RD6006_current_multiplier: "0.001"
|
RD6006_current_multiplier: "0.001"
|
||||||
|
RD6006_power_accuracy: "2"
|
||||||
|
RD6006_power_multiplier: "0.01"
|
||||||
|
|
||||||
RD6006P_voltage_maximum: "60"
|
RD6006P_voltage_maximum: "60"
|
||||||
RD6006P_voltage_accuracy: "3"
|
RD6006P_voltage_accuracy: "3"
|
||||||
RD6006P_voltage_multiplier: "0.001"
|
RD6006P_voltage_multiplier: "0.001"
|
||||||
RD6006P_current_maximum: "6"
|
RD6006P_current_maximum: "6"
|
||||||
RD6006P_current_accuracy: "4"
|
RD6006P_current_accuracy: "4"
|
||||||
RD6006P_current_multiplier: "0.0001"
|
RD6006P_current_multiplier: "0.0001"
|
||||||
|
RD6006P_power_accuracy: "3"
|
||||||
|
RD6006P_power_multiplier: "0.001"
|
||||||
|
|
||||||
RD6012_voltage_maximum: "60"
|
RD6012_voltage_maximum: "60"
|
||||||
RD6012_voltage_accuracy: "2"
|
RD6012_voltage_accuracy: "2"
|
||||||
@@ -30,6 +36,8 @@ substitutions:
|
|||||||
RD6012_current_maximum: "12"
|
RD6012_current_maximum: "12"
|
||||||
RD6012_current_accuracy: "2"
|
RD6012_current_accuracy: "2"
|
||||||
RD6012_current_multiplier: "0.01"
|
RD6012_current_multiplier: "0.01"
|
||||||
|
RD6012_power_accuracy: "2"
|
||||||
|
RD6012_power_multiplier: "0.01"
|
||||||
|
|
||||||
RD6018_voltage_maximum: "60"
|
RD6018_voltage_maximum: "60"
|
||||||
RD6018_voltage_accuracy: "2"
|
RD6018_voltage_accuracy: "2"
|
||||||
@@ -37,6 +45,8 @@ substitutions:
|
|||||||
RD6018_current_maximum: "18"
|
RD6018_current_maximum: "18"
|
||||||
RD6018_current_accuracy: "2"
|
RD6018_current_accuracy: "2"
|
||||||
RD6018_current_multiplier: "0.01"
|
RD6018_current_multiplier: "0.01"
|
||||||
|
RD6018_power_accuracy: "2"
|
||||||
|
RD6018_power_multiplier: "0.01"
|
||||||
|
|
||||||
esphome:
|
esphome:
|
||||||
name: $device_name
|
name: $device_name
|
||||||
@@ -45,15 +55,18 @@ esphome:
|
|||||||
name_add_mac_suffix: false
|
name_add_mac_suffix: false
|
||||||
project:
|
project:
|
||||||
name: "wildekek.rd6006-controller"
|
name: "wildekek.rd6006-controller"
|
||||||
version: "1.4.1"
|
version: "1.5"
|
||||||
|
|
||||||
esp8266:
|
esp8266:
|
||||||
board: esp12e
|
board: esp12e
|
||||||
|
|
||||||
|
network:
|
||||||
|
enable_ipv6: true
|
||||||
|
|
||||||
wifi:
|
wifi:
|
||||||
ssid: "Voltage-legacy"
|
ssid: "Voltage-legacy"
|
||||||
password: !secret voltage_legacy_psk
|
password: !secret voltage_legacy_psk
|
||||||
use_address: riden-labornetzteil-18a.home
|
use_address: riden-labornetzteil-18a.home
|
||||||
power_save_mode: high
|
power_save_mode: high
|
||||||
fast_connect: on
|
fast_connect: on
|
||||||
min_auth_mode: WPA2
|
min_auth_mode: WPA2
|
||||||
@@ -73,7 +86,7 @@ logger:
|
|||||||
|
|
||||||
# Enable Home Assistant API
|
# Enable Home Assistant API
|
||||||
api:
|
api:
|
||||||
encryption:
|
encryption:
|
||||||
key: !secret apikey
|
key: !secret apikey
|
||||||
|
|
||||||
ota:
|
ota:
|
||||||
@@ -149,7 +162,9 @@ sensor:
|
|||||||
on_value:
|
on_value:
|
||||||
then:
|
then:
|
||||||
- lambda: |-
|
- lambda: |-
|
||||||
id(model_name).publish_state(value_accuracy_to_string(x, 0));
|
id(model_name).publish_state(
|
||||||
|
std::to_string(static_cast<uint16_t>(x))
|
||||||
|
);
|
||||||
|
|
||||||
- platform: modbus_controller
|
- platform: modbus_controller
|
||||||
name: "Serial Number"
|
name: "Serial Number"
|
||||||
@@ -161,7 +176,7 @@ sensor:
|
|||||||
register_type: holding
|
register_type: holding
|
||||||
value_type: U_DWORD
|
value_type: U_DWORD
|
||||||
accuracy_decimals: 0
|
accuracy_decimals: 0
|
||||||
|
|
||||||
- platform: modbus_controller
|
- platform: modbus_controller
|
||||||
modbus_controller_id: powersupply
|
modbus_controller_id: powersupply
|
||||||
address: 3
|
address: 3
|
||||||
@@ -210,9 +225,9 @@ sensor:
|
|||||||
unit_of_measurement: "W"
|
unit_of_measurement: "W"
|
||||||
register_type: holding
|
register_type: holding
|
||||||
value_type: U_DWORD
|
value_type: U_DWORD
|
||||||
accuracy_decimals: 2
|
accuracy_decimals: ${${model}_power_accuracy}
|
||||||
filters:
|
filters:
|
||||||
- multiply: 0.01
|
- multiply: ${${model}_power_multiplier}
|
||||||
|
|
||||||
- platform: modbus_controller
|
- platform: modbus_controller
|
||||||
modbus_controller_id: powersupply
|
modbus_controller_id: powersupply
|
||||||
@@ -223,9 +238,9 @@ sensor:
|
|||||||
unit_of_measurement: "V"
|
unit_of_measurement: "V"
|
||||||
register_type: holding
|
register_type: holding
|
||||||
value_type: U_WORD
|
value_type: U_WORD
|
||||||
accuracy_decimals: ${${model}_voltage_accuracy}
|
accuracy_decimals: 2
|
||||||
filters:
|
filters:
|
||||||
- multiply: ${${model}_voltage_multiplier}
|
- multiply: 0.01
|
||||||
|
|
||||||
- platform: modbus_controller
|
- platform: modbus_controller
|
||||||
modbus_controller_id: powersupply
|
modbus_controller_id: powersupply
|
||||||
@@ -263,10 +278,10 @@ sensor:
|
|||||||
unit_of_measurement: "V"
|
unit_of_measurement: "V"
|
||||||
register_type: holding
|
register_type: holding
|
||||||
value_type: U_WORD
|
value_type: U_WORD
|
||||||
accuracy_decimals: ${${model}_voltage_accuracy}
|
accuracy_decimals: 2
|
||||||
filters:
|
filters:
|
||||||
- multiply: ${${model}_voltage_multiplier}
|
- multiply: 0.01
|
||||||
|
|
||||||
- platform: modbus_controller
|
- platform: modbus_controller
|
||||||
name: "Temperature"
|
name: "Temperature"
|
||||||
device_class: temperature
|
device_class: temperature
|
||||||
@@ -276,7 +291,7 @@ sensor:
|
|||||||
address: 4
|
address: 4
|
||||||
value_type: S_DWORD
|
value_type: S_DWORD
|
||||||
unit_of_measurement: "°C"
|
unit_of_measurement: "°C"
|
||||||
|
|
||||||
- platform: modbus_controller
|
- platform: modbus_controller
|
||||||
name: "Temperature external"
|
name: "Temperature external"
|
||||||
state_class: measurement
|
state_class: measurement
|
||||||
@@ -286,7 +301,7 @@ sensor:
|
|||||||
value_type: S_DWORD
|
value_type: S_DWORD
|
||||||
device_class: temperature
|
device_class: temperature
|
||||||
unit_of_measurement: "°C"
|
unit_of_measurement: "°C"
|
||||||
|
|
||||||
- platform: wifi_signal
|
- platform: wifi_signal
|
||||||
name: "Wi-Fi Signal"
|
name: "Wi-Fi Signal"
|
||||||
disabled_by_default: True
|
disabled_by_default: True
|
||||||
@@ -405,7 +420,7 @@ number:
|
|||||||
step: ${${model}_voltage_multiplier}
|
step: ${${model}_voltage_multiplier}
|
||||||
lambda: !lambda return x * ${${model}_voltage_multiplier};
|
lambda: !lambda return x * ${${model}_voltage_multiplier};
|
||||||
write_lambda: !lambda return x * (1/${${model}_voltage_multiplier});
|
write_lambda: !lambda return x * (1/${${model}_voltage_multiplier});
|
||||||
|
|
||||||
- platform: modbus_controller
|
- platform: modbus_controller
|
||||||
modbus_controller_id: powersupply
|
modbus_controller_id: powersupply
|
||||||
name: "Output current"
|
name: "Output current"
|
||||||
@@ -434,7 +449,6 @@ number:
|
|||||||
lambda: !lambda return x * ${${model}_voltage_multiplier};
|
lambda: !lambda return x * ${${model}_voltage_multiplier};
|
||||||
write_lambda: !lambda return x * (1/${${model}_voltage_multiplier});
|
write_lambda: !lambda return x * (1/${${model}_voltage_multiplier});
|
||||||
|
|
||||||
|
|
||||||
- platform: modbus_controller
|
- platform: modbus_controller
|
||||||
modbus_controller_id: powersupply
|
modbus_controller_id: powersupply
|
||||||
name: "Over Current Protection"
|
name: "Over Current Protection"
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# Source: https://github.com/wildekek/rdtech-esphome
|
||||||
|
|
||||||
substitutions:
|
substitutions:
|
||||||
name: "riden-labornetzteil-6a"
|
name: "riden-labornetzteil-6a"
|
||||||
friendly_name: "Riden Labornetzteil 6A"
|
friendly_name: "Riden Labornetzteil 6A"
|
||||||
@@ -8,7 +10,7 @@ substitutions:
|
|||||||
device_friendly_name: "Riden RD6006"
|
device_friendly_name: "Riden RD6006"
|
||||||
device_description: "Monitor and control a RD6006 PSU via WiFi"
|
device_description: "Monitor and control a RD6006 PSU via WiFi"
|
||||||
time_timezone: "Europe/Berlin"
|
time_timezone: "Europe/Berlin"
|
||||||
|
|
||||||
# Model specific settings (Don't change these!)
|
# Model specific settings (Don't change these!)
|
||||||
RD6006_voltage_maximum: "60"
|
RD6006_voltage_maximum: "60"
|
||||||
RD6006_voltage_accuracy: "2"
|
RD6006_voltage_accuracy: "2"
|
||||||
@@ -16,13 +18,17 @@ substitutions:
|
|||||||
RD6006_current_maximum: "6"
|
RD6006_current_maximum: "6"
|
||||||
RD6006_current_accuracy: "3"
|
RD6006_current_accuracy: "3"
|
||||||
RD6006_current_multiplier: "0.001"
|
RD6006_current_multiplier: "0.001"
|
||||||
|
RD6006_power_accuracy: "2"
|
||||||
|
RD6006_power_multiplier: "0.01"
|
||||||
|
|
||||||
RD6006P_voltage_maximum: "60"
|
RD6006P_voltage_maximum: "60"
|
||||||
RD6006P_voltage_accuracy: "3"
|
RD6006P_voltage_accuracy: "3"
|
||||||
RD6006P_voltage_multiplier: "0.001"
|
RD6006P_voltage_multiplier: "0.001"
|
||||||
RD6006P_current_maximum: "6"
|
RD6006P_current_maximum: "6"
|
||||||
RD6006P_current_accuracy: "4"
|
RD6006P_current_accuracy: "4"
|
||||||
RD6006P_current_multiplier: "0.0001"
|
RD6006P_current_multiplier: "0.0001"
|
||||||
|
RD6006P_power_accuracy: "3"
|
||||||
|
RD6006P_power_multiplier: "0.001"
|
||||||
|
|
||||||
RD6012_voltage_maximum: "60"
|
RD6012_voltage_maximum: "60"
|
||||||
RD6012_voltage_accuracy: "2"
|
RD6012_voltage_accuracy: "2"
|
||||||
@@ -30,6 +36,8 @@ substitutions:
|
|||||||
RD6012_current_maximum: "12"
|
RD6012_current_maximum: "12"
|
||||||
RD6012_current_accuracy: "2"
|
RD6012_current_accuracy: "2"
|
||||||
RD6012_current_multiplier: "0.01"
|
RD6012_current_multiplier: "0.01"
|
||||||
|
RD6012_power_accuracy: "2"
|
||||||
|
RD6012_power_multiplier: "0.01"
|
||||||
|
|
||||||
RD6018_voltage_maximum: "60"
|
RD6018_voltage_maximum: "60"
|
||||||
RD6018_voltage_accuracy: "2"
|
RD6018_voltage_accuracy: "2"
|
||||||
@@ -37,6 +45,8 @@ substitutions:
|
|||||||
RD6018_current_maximum: "18"
|
RD6018_current_maximum: "18"
|
||||||
RD6018_current_accuracy: "2"
|
RD6018_current_accuracy: "2"
|
||||||
RD6018_current_multiplier: "0.01"
|
RD6018_current_multiplier: "0.01"
|
||||||
|
RD6018_power_accuracy: "2"
|
||||||
|
RD6018_power_multiplier: "0.01"
|
||||||
|
|
||||||
esphome:
|
esphome:
|
||||||
name: $device_name
|
name: $device_name
|
||||||
@@ -45,15 +55,18 @@ esphome:
|
|||||||
name_add_mac_suffix: false
|
name_add_mac_suffix: false
|
||||||
project:
|
project:
|
||||||
name: "wildekek.rd6006-controller"
|
name: "wildekek.rd6006-controller"
|
||||||
version: "1.4.1"
|
version: "1.5"
|
||||||
|
|
||||||
esp8266:
|
esp8266:
|
||||||
board: esp12e
|
board: esp12e
|
||||||
|
|
||||||
|
network:
|
||||||
|
enable_ipv6: true
|
||||||
|
|
||||||
wifi:
|
wifi:
|
||||||
ssid: "Voltage-legacy"
|
ssid: "Voltage-legacy"
|
||||||
password: !secret voltage_legacy_psk
|
password: !secret voltage_legacy_psk
|
||||||
use_address: riden-labornetzteil-6a.home
|
use_address: riden-labornetzteil-6a.home
|
||||||
power_save_mode: high
|
power_save_mode: high
|
||||||
fast_connect: on
|
fast_connect: on
|
||||||
min_auth_mode: WPA2
|
min_auth_mode: WPA2
|
||||||
@@ -73,7 +86,7 @@ logger:
|
|||||||
|
|
||||||
# Enable Home Assistant API
|
# Enable Home Assistant API
|
||||||
api:
|
api:
|
||||||
encryption:
|
encryption:
|
||||||
key: !secret apikey
|
key: !secret apikey
|
||||||
|
|
||||||
ota:
|
ota:
|
||||||
@@ -149,7 +162,9 @@ sensor:
|
|||||||
on_value:
|
on_value:
|
||||||
then:
|
then:
|
||||||
- lambda: |-
|
- lambda: |-
|
||||||
id(model_name).publish_state(value_accuracy_to_string(x, 0));
|
id(model_name).publish_state(
|
||||||
|
std::to_string(static_cast<uint16_t>(x))
|
||||||
|
);
|
||||||
|
|
||||||
- platform: modbus_controller
|
- platform: modbus_controller
|
||||||
name: "Serial Number"
|
name: "Serial Number"
|
||||||
@@ -161,7 +176,7 @@ sensor:
|
|||||||
register_type: holding
|
register_type: holding
|
||||||
value_type: U_DWORD
|
value_type: U_DWORD
|
||||||
accuracy_decimals: 0
|
accuracy_decimals: 0
|
||||||
|
|
||||||
- platform: modbus_controller
|
- platform: modbus_controller
|
||||||
modbus_controller_id: powersupply
|
modbus_controller_id: powersupply
|
||||||
address: 3
|
address: 3
|
||||||
@@ -210,9 +225,9 @@ sensor:
|
|||||||
unit_of_measurement: "W"
|
unit_of_measurement: "W"
|
||||||
register_type: holding
|
register_type: holding
|
||||||
value_type: U_DWORD
|
value_type: U_DWORD
|
||||||
accuracy_decimals: 2
|
accuracy_decimals: ${${model}_power_accuracy}
|
||||||
filters:
|
filters:
|
||||||
- multiply: 0.01
|
- multiply: ${${model}_power_multiplier}
|
||||||
|
|
||||||
- platform: modbus_controller
|
- platform: modbus_controller
|
||||||
modbus_controller_id: powersupply
|
modbus_controller_id: powersupply
|
||||||
@@ -223,9 +238,9 @@ sensor:
|
|||||||
unit_of_measurement: "V"
|
unit_of_measurement: "V"
|
||||||
register_type: holding
|
register_type: holding
|
||||||
value_type: U_WORD
|
value_type: U_WORD
|
||||||
accuracy_decimals: ${${model}_voltage_accuracy}
|
accuracy_decimals: 2
|
||||||
filters:
|
filters:
|
||||||
- multiply: ${${model}_voltage_multiplier}
|
- multiply: 0.01
|
||||||
|
|
||||||
- platform: modbus_controller
|
- platform: modbus_controller
|
||||||
modbus_controller_id: powersupply
|
modbus_controller_id: powersupply
|
||||||
@@ -263,10 +278,10 @@ sensor:
|
|||||||
unit_of_measurement: "V"
|
unit_of_measurement: "V"
|
||||||
register_type: holding
|
register_type: holding
|
||||||
value_type: U_WORD
|
value_type: U_WORD
|
||||||
accuracy_decimals: ${${model}_voltage_accuracy}
|
accuracy_decimals: 2
|
||||||
filters:
|
filters:
|
||||||
- multiply: ${${model}_voltage_multiplier}
|
- multiply: 0.01
|
||||||
|
|
||||||
- platform: modbus_controller
|
- platform: modbus_controller
|
||||||
name: "Temperature"
|
name: "Temperature"
|
||||||
device_class: temperature
|
device_class: temperature
|
||||||
@@ -276,7 +291,7 @@ sensor:
|
|||||||
address: 4
|
address: 4
|
||||||
value_type: S_DWORD
|
value_type: S_DWORD
|
||||||
unit_of_measurement: "°C"
|
unit_of_measurement: "°C"
|
||||||
|
|
||||||
- platform: modbus_controller
|
- platform: modbus_controller
|
||||||
name: "Temperature external"
|
name: "Temperature external"
|
||||||
state_class: measurement
|
state_class: measurement
|
||||||
@@ -286,7 +301,7 @@ sensor:
|
|||||||
value_type: S_DWORD
|
value_type: S_DWORD
|
||||||
device_class: temperature
|
device_class: temperature
|
||||||
unit_of_measurement: "°C"
|
unit_of_measurement: "°C"
|
||||||
|
|
||||||
- platform: wifi_signal
|
- platform: wifi_signal
|
||||||
name: "Wi-Fi Signal"
|
name: "Wi-Fi Signal"
|
||||||
disabled_by_default: True
|
disabled_by_default: True
|
||||||
@@ -405,7 +420,7 @@ number:
|
|||||||
step: ${${model}_voltage_multiplier}
|
step: ${${model}_voltage_multiplier}
|
||||||
lambda: !lambda return x * ${${model}_voltage_multiplier};
|
lambda: !lambda return x * ${${model}_voltage_multiplier};
|
||||||
write_lambda: !lambda return x * (1/${${model}_voltage_multiplier});
|
write_lambda: !lambda return x * (1/${${model}_voltage_multiplier});
|
||||||
|
|
||||||
- platform: modbus_controller
|
- platform: modbus_controller
|
||||||
modbus_controller_id: powersupply
|
modbus_controller_id: powersupply
|
||||||
name: "Output current"
|
name: "Output current"
|
||||||
|
|||||||
@@ -16,10 +16,13 @@ esp32:
|
|||||||
framework:
|
framework:
|
||||||
type: esp-idf
|
type: esp-idf
|
||||||
|
|
||||||
|
network:
|
||||||
|
enable_ipv6: true
|
||||||
|
|
||||||
wifi:
|
wifi:
|
||||||
ssid: "Voltage-legacy"
|
ssid: "Voltage-legacy"
|
||||||
password: !secret voltage_legacy_psk
|
password: !secret voltage_legacy_psk
|
||||||
use_address: serverroom.home
|
#use_address: serverroom.home
|
||||||
power_save_mode: high
|
power_save_mode: high
|
||||||
fast_connect: on
|
fast_connect: on
|
||||||
min_auth_mode: WPA2
|
min_auth_mode: WPA2
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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."
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
# Display contract shared with the ESPHome calendar page: compact German labels,
|
||||||
|
# a bounded number of events, and strings sized for the e-paper layout.
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
# Keep variable event text within the fixed-width e-paper agenda columns.
|
||||||
|
def shorten(value, maximum):
|
||||||
|
value = (value or "").strip()
|
||||||
|
if len(value) <= maximum:
|
||||||
|
return value
|
||||||
|
return value[: maximum - 3].rstrip() + "..."
|
||||||
|
|
||||||
|
|
||||||
|
# Convert one Home Assistant calendar event into the small JSON shape consumed
|
||||||
|
# by ESPHome, separating all-day, start-time, end-time, and metadata display data.
|
||||||
|
def compact_event(event, calendar_name):
|
||||||
|
start = event.get("start", "")
|
||||||
|
all_day = "T" not in start
|
||||||
|
end = event.get("end", "")
|
||||||
|
time = "ganzt." 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),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Read the calendar.get_events response supplied by the calling template sensor
|
||||||
|
# and collect events under the date on which they should appear in the agenda.
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Sort day groups and events, put all-day entries first, and cap the payload so
|
||||||
|
# deserializing it stays safe on the ESP32 without PSRAM.
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Return only the normalized agenda payload to the Home Assistant template sensor.
|
||||||
|
output["entries"] = entries
|
||||||
+30
-28
@@ -1,9 +1,9 @@
|
|||||||
db_url: !secret ha_recorder_dburl
|
db_url: !secret ha_recorder_dburl
|
||||||
# Commit to db only every X seconds
|
# Commit to db only every X seconds
|
||||||
commit_interval: 60
|
commit_interval: 60
|
||||||
# Delete events and states older than 1 week
|
# Delete events and states older than 1 week
|
||||||
auto_purge: true
|
auto_purge: true
|
||||||
purge_keep_days: 7
|
purge_keep_days: 7
|
||||||
# include:
|
# include:
|
||||||
# entity_globs:
|
# entity_globs:
|
||||||
# - sensor.rd6018_controller_*
|
# - sensor.rd6018_controller_*
|
||||||
@@ -15,27 +15,29 @@
|
|||||||
# - sensor
|
# - sensor
|
||||||
# - switch
|
# - switch
|
||||||
# - media_player
|
# - media_player
|
||||||
exclude:
|
exclude:
|
||||||
# domains:
|
# domains:
|
||||||
# - automation
|
# - automation
|
||||||
# - updater
|
# - updater
|
||||||
entity_globs:
|
entity_globs:
|
||||||
- weather.zuhause_*
|
- weather.zuhause_*
|
||||||
- sensor.*_power_factor
|
- sensor.*_power_factor
|
||||||
- sensor.dwd*
|
- sensor.dwd*
|
||||||
- binary_sensor.*firmware_update
|
- binary_sensor.*firmware_update
|
||||||
- binary_sensor.*_overpowering
|
- binary_sensor.*_overpowering
|
||||||
- binary_sensor.*_overheating
|
- binary_sensor.*_overheating
|
||||||
- sensor.*_wi_fi_signal
|
- sensor.*_wi_fi_signal
|
||||||
- sensor.*_wifi_strenght
|
- sensor.*_wifi_strenght
|
||||||
- sensor.*_uptime
|
- sensor.*_uptime
|
||||||
- sensor.sun*
|
- sensor.sun*
|
||||||
entities:
|
entities:
|
||||||
- sun.sun # Don't record sun data
|
- sun.sun # Don't record sun data
|
||||||
- sensor.fritzbox_device_uptime
|
- sensor.fritzbox_device_uptime
|
||||||
- sensor.time
|
- sensor.time
|
||||||
- sensor.awtrix_kitchen_current_app
|
- sensor.awtrix_kitchen_current_app
|
||||||
- sensor.awtrix_desk_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:
|
# event_types:
|
||||||
# - call_service # Don't record service calls
|
# - call_service # Don't record service calls
|
||||||
|
|
||||||
|
|||||||
+6
-4
@@ -10,7 +10,8 @@
|
|||||||
unique_id: '3303381540758'
|
unique_id: '3303381540758'
|
||||||
host: !secret router_ip
|
host: !secret router_ip
|
||||||
community: !secret router_community
|
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.12
|
||||||
version: 2c
|
version: 2c
|
||||||
unit_of_measurement: "Octets"
|
unit_of_measurement: "Octets"
|
||||||
- platform: snmp
|
- platform: snmp
|
||||||
@@ -18,7 +19,8 @@
|
|||||||
unique_id: '1573258703922'
|
unique_id: '1573258703922'
|
||||||
host: !secret router_ip
|
host: !secret router_ip
|
||||||
community: !secret router_community
|
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.12
|
||||||
version: 2c
|
version: 2c
|
||||||
unit_of_measurement: "Octets"
|
unit_of_measurement: "Octets"
|
||||||
|
|
||||||
@@ -38,13 +40,13 @@
|
|||||||
unique_id: '9081721471264'
|
unique_id: '9081721471264'
|
||||||
state_characteristic: mean
|
state_characteristic: mean
|
||||||
entity_id: sensor.internet_speed_in
|
entity_id: sensor.internet_speed_in
|
||||||
sampling_size: 10
|
sampling_size: 2
|
||||||
- platform: statistics
|
- platform: statistics
|
||||||
name: 'WAN Traffic Out'
|
name: 'WAN Traffic Out'
|
||||||
unique_id: '8688955223027'
|
unique_id: '8688955223027'
|
||||||
state_characteristic: mean
|
state_characteristic: mean
|
||||||
entity_id: sensor.internet_speed_out
|
entity_id: sensor.internet_speed_out
|
||||||
sampling_size: 10
|
sampling_size: 2
|
||||||
|
|
||||||
# Sensor for Riemann sum of energy import (W -> Wh)
|
# Sensor for Riemann sum of energy import (W -> Wh)
|
||||||
- platform: integration
|
- platform: integration
|
||||||
|
|||||||
+36
-1
@@ -49,6 +49,11 @@
|
|||||||
name: Sun Setting Template
|
name: Sun Setting Template
|
||||||
state: '{{ as_timestamp(states.sun.sun.attributes.next_setting) | timestamp_custom
|
state: '{{ as_timestamp(states.sun.sun.attributes.next_setting) | timestamp_custom
|
||||||
(''%H:%M'') }}'
|
(''%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"
|
- name: "power_other"
|
||||||
unique_id: '5579422933393'
|
unique_id: '5579422933393'
|
||||||
unit_of_measurement: "W"
|
unit_of_measurement: "W"
|
||||||
@@ -69,7 +74,7 @@
|
|||||||
{% set spieleschrank = states('sensor.wohnzimmer_spieleschrank_power') | float %}
|
{% set spieleschrank = states('sensor.wohnzimmer_spieleschrank_power') | float %}
|
||||||
{% set kuehlschrank = states('sensor.tz3000_ww6drja5_ts011f_leistung') | float %}
|
{% set kuehlschrank = states('sensor.tz3000_ww6drja5_ts011f_leistung') | float %}
|
||||||
{% set kaffeemaschine = states('sensor.kaffeemaschine_leistung_2') | 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 waschmaschine = states('sensor.waschmaschine_leistung') | float %}
|
||||||
{% set arcade = states('sensor.arcade_automat_leistung') | float %}
|
{% set arcade = states('sensor.arcade_automat_leistung') | float %}
|
||||||
{% set spuelmaschine = states('sensor.spulmaschine_leistung') | float %}
|
{% set spuelmaschine = states('sensor.spulmaschine_leistung') | float %}
|
||||||
@@ -79,6 +84,35 @@
|
|||||||
attributes:
|
attributes:
|
||||||
last_reset: '1970-01-01T00:00:00+00:00'
|
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)
|
# Shelly 3EM cumulative sensors (incl. PV)
|
||||||
- sensor:
|
- sensor:
|
||||||
# Template sensor for values of power import (active_power > 0)
|
# Template sensor for values of power import (active_power > 0)
|
||||||
@@ -139,6 +173,7 @@
|
|||||||
attributes:
|
attributes:
|
||||||
last_reset: '1970-01-01T00:00:00+00:00'
|
last_reset: '1970-01-01T00:00:00+00:00'
|
||||||
- name: internet_speed_out
|
- name: internet_speed_out
|
||||||
|
unique_id: '9519483670667'
|
||||||
state: >
|
state: >
|
||||||
{{ (( states('sensor.wan_out_derivative') | float * 8 / 1000000 ) | round(2)) }}
|
{{ (( states('sensor.wan_out_derivative') | float * 8 / 1000000 ) | round(2)) }}
|
||||||
unit_of_measurement: 'Mbps'
|
unit_of_measurement: 'Mbps'
|
||||||
|
|||||||
+12
-6
@@ -1,34 +1,40 @@
|
|||||||
# Internet traffic
|
# Internet traffic
|
||||||
internet_usage_in_monthly:
|
internet_usage_in_monthly:
|
||||||
source: sensor.snmp_wan_in
|
source: sensor.wan_in_derivative
|
||||||
name: Monthly internet traffic in
|
name: Monthly internet traffic in
|
||||||
unique_id: monthly_internet_traffic_in
|
unique_id: monthly_internet_traffic_in
|
||||||
cycle: monthly
|
cycle: monthly
|
||||||
|
delta_values: True
|
||||||
internet_usage_out_monthly:
|
internet_usage_out_monthly:
|
||||||
source: sensor.snmp_wan_out
|
source: sensor.wan_out_derivative
|
||||||
name: Monthly internet traffic out
|
name: Monthly internet traffic out
|
||||||
unique_id: monthly_internet_traffic_out
|
unique_id: monthly_internet_traffic_out
|
||||||
cycle: monthly
|
cycle: monthly
|
||||||
|
delta_values: True
|
||||||
internet_usage_in_daily:
|
internet_usage_in_daily:
|
||||||
source: sensor.snmp_wan_in
|
source: sensor.wan_in_derivative
|
||||||
name: Daily internet traffic in
|
name: Daily internet traffic in
|
||||||
unique_id: daily_internet_traffic_in
|
unique_id: daily_internet_traffic_in
|
||||||
cycle: daily
|
cycle: daily
|
||||||
|
delta_values: True
|
||||||
internet_usage_out_daily:
|
internet_usage_out_daily:
|
||||||
source: sensor.snmp_wan_out
|
source: sensor.wan_out_derivative
|
||||||
name: Daily internet traffic out
|
name: Daily internet traffic out
|
||||||
unique_id: daily_internet_traffic_out
|
unique_id: daily_internet_traffic_out
|
||||||
cycle: daily
|
cycle: daily
|
||||||
|
delta_values: True
|
||||||
internet_usage_in_hourly:
|
internet_usage_in_hourly:
|
||||||
source: sensor.snmp_wan_in
|
source: sensor.wan_in_derivative
|
||||||
name: Hourly internet traffic in
|
name: Hourly internet traffic in
|
||||||
unique_id: hourly_internet_traffic_in
|
unique_id: hourly_internet_traffic_in
|
||||||
cycle: hourly
|
cycle: hourly
|
||||||
|
delta_values: True
|
||||||
internet_usage_out_hourly:
|
internet_usage_out_hourly:
|
||||||
source: sensor.snmp_wan_out
|
source: sensor.wan_out_derivative
|
||||||
name: Hourly internet traffic out
|
name: Hourly internet traffic out
|
||||||
unique_id: hourly_internet_traffic_out
|
unique_id: hourly_internet_traffic_out
|
||||||
cycle: hourly
|
cycle: hourly
|
||||||
|
delta_values: True
|
||||||
|
|
||||||
# Energy
|
# Energy
|
||||||
energy_import_daily:
|
energy_import_daily:
|
||||||
|
|||||||
Reference in New Issue
Block a user