94 Commits

Author SHA1 Message Date
Commander1024 2cbb15a425 Inline Calendar Event Formatting 2026-07-10 23:25:15 +02:00
Commander1024 6a04638f59 Rename calendar input_text entities 2026-07-10 22:47:18 +02:00
Commander1024 a4a86272a9 Rename calendar helper IDs 2026-07-10 22:39:43 +02:00
Commander1024 4ae08e0400 Fixed stupid AI syntax errors ... 2026-07-10 22:36:38 +02:00
Commander1024 2b93cf16dc Readded FreeDNS config and reset date attribute 2026-07-10 16:27:58 +02:00
Commander1024 2879dbeaa0 Initial draft for calendar on eopaperframe by qwen3.6-30b 2026-07-10 15:16:38 +02:00
Commander1024 e4a4aacc63 Added missing unique_id 2026-07-10 01:16:51 +02:00
Commander1024 0d12317089 revert 746dd833c8
revert Add unique_ids and calendar text sensors
2026-07-10 00:54:27 +02:00
Commander1024 746dd833c8 Add unique_ids and calendar text sensors 2026-07-10 00:38:16 +02:00
Commander1024 3eb482e40b Added IPv6 support. 2026-07-09 09:03:24 +02:00
Commander1024 e9a6064b4b Updated HA. 2026-07-09 08:59:52 +02:00
Commander1024 38616556cb Updated HA. 2026-07-02 09:12:17 +02:00
Commander1024 bef1cfb1d0 Updated HA. 2026-06-23 09:12:01 +02:00
Commander1024 0a8364d7ed Changed annunciators for climate control. 2026-06-23 09:11:50 +02:00
Commander1024 3eb9eb3a2f Archived deprecated raspiaudio muse luxe config. 2026-06-23 09:11:01 +02:00
Commander1024 7407955465 Removed cached icons from repo. 2026-06-23 09:10:18 +02:00
Commander1024 2ff8b5cafe Merged owon-ai-test into main. 2026-06-01 18:45:27 +02:00
Commander1024 7e08c4a8b9 Updated HA. 2026-06-01 18:42:56 +02:00
Commander1024 855f013095 Freedns test 2026-06-01 18:42:43 +02:00
Commander1024 a68b527c48 Various entity changees 2026-06-01 18:41:43 +02:00
Commander1024 cecec14be1 Added usage info. 2026-05-30 19:12:03 +02:00
Commander1024 4e5df1ad0c Updated file comments 2026-05-30 19:00:18 +02:00
Commander1024 2c16ba1679 Separated long-press buttons into own HA entityies to make them usable 2026-05-30 18:49:16 +02:00
Commander1024 c21f2a5c7b Display aestethics: taller 7-segments, repositioned minus, mode and
icons
2026-05-30 17:49:56 +02:00
Commander1024 2a1902ccf1 Add reset all / reset energy function to also clear the local meter 2026-05-30 17:06:31 +02:00
Commander1024 31848bf26c Add energy monitor using riemann sum algorithm. 2026-05-30 16:49:57 +02:00
Commander1024 d7337fd634 Set (somewhat) appropiate mdi icons for OWON HA sensors. 2026-05-30 08:22:32 +02:00
Commander1024 5b287303f1 Add backlight idle script 2026-05-28 22:58:15 +02:00
Commander1024 816cc35861 Implement dc-load inactivity display 2026-05-28 22:49:32 +02:00
Commander1024 058c186d63 Remove dead values from HA and rendering code. Also increase font size. 2026-05-28 22:24:46 +02:00
Commander1024 22e5f9d767 Implemented rough 1st iteration of dc-load value screen. 2026-05-28 22:09:54 +02:00
Commander1024 f9b32d06a4 Expose buttons Owon multimeter buttons to HA. 2026-05-28 16:04:53 +02:00
Commander1024 3f890ae02f Intregrated Atorch BLE proxy and migrated device name to a generic one. 2026-05-28 14:35:12 +02:00
Commander1024 f0cb74cc27 Deactivate all indicator lights on meter disconnect. 2026-05-28 13:40:46 +02:00
Commander1024 6038075e9e Fixed off-by-one error in rendering of the 7-segment segments. 2026-05-28 13:29:39 +02:00
Commander1024 2dbdfd213c Deactivate bargraph after BLE disconnect 2026-05-28 12:29:52 +02:00
Commander1024 e2eb886eef Renamed .h file to match .yaml filename. 2026-05-26 19:35:45 +02:00
Commander1024 f4f19eb847 Added attribution to original author and AI usage hint. 2026-05-26 19:25:56 +02:00
Commander1024 b3e307dcba RAM optimization, deactivated recording of display values. 2026-05-26 19:19:22 +02:00
Commander1024 1b3b4870e7 Ported the code to M5Stack Core2 (different display) and removed reboot on disconnect 2026-05-26 19:10:30 +02:00
Commander1024 69c2166414 Archived the Core1 Owon multimeter config 2026-05-26 18:00:35 +02:00
Commander1024 2fcf71d8ce Added Bluetooth and battery icons instead of text indicators 2026-05-26 17:36:58 +02:00
Commander1024 676829a988 Remove mode from Reading value - already present in Mode. 2026-05-26 17:21:11 +02:00
Commander1024 08201f1bc1 Uniform separation of 7-segment display font elements. 2026-05-26 17:10:43 +02:00
Commander1024 23cbd520a6 Repositioned DIODE/BUZZ and moved them out of the meter bar. Also slightly reworked 7-segment display font. Not perfect, yet though 2026-05-26 17:03:52 +02:00
Commander1024 6f4e2b9319 Moved AC/DC out of the way of the minus sign, added an h glyph for hFE display. 2026-05-26 16:44:25 +02:00
Commander1024 e5104fc005 Removed 2 unneeded values from HA reporting, fixed mV reporting value 2026-05-26 16:03:25 +02:00
Commander1024 91f3f69bd9 Optimized for mem pressure, inverted colors. 2026-05-26 15:34:03 +02:00
Commander1024 4bd62f539c Initial AI-assisted esphome device code for Owon B35T+ multimeter. 2026-05-26 13:45:06 +02:00
Commander1024 a3bd9ff213 Removed cached png from repo. 2026-05-21 09:35:21 +02:00
Commander1024 24fc4bf9f4 Enabled ipv6 for all esphome devices. 2026-05-21 09:24:48 +02:00
Commander1024 0bfc2eff34 Changed little things... 2026-05-21 09:24:11 +02:00
Commander1024 8f18a043af Updated HA + gitignore 2026-05-21 09:21:52 +02:00
Commander1024 5b0a992686 Changed light automations from time offset to sun elevation. 2026-04-29 07:27:53 +02:00
Commander1024 ef233296e9 Updated HA. 2026-04-29 07:27:22 +02:00
Commander1024 c0af2b734b Added HA Icon cache. 2026-04-23 09:56:53 +02:00
Commander1024 a94386babe Deleted obsolete glances config 2026-04-23 09:56:06 +02:00
Commander1024 f23166b765 Updated HA. 2026-04-23 09:55:38 +02:00
Commander1024 8438785241 Changed dryer zigbee plug. 2026-04-23 09:55:21 +02:00
Commander1024 b0b6093619 Added IKEA Bilresa dual button blueprint 2026-04-23 09:54:49 +02:00
Commander1024 c463a5e28f Added kitchen Music switch, changed 420 players, light offset by sun elevation 2026-04-23 09:52:59 +02:00
Commander1024 a3e06a85d6 Changed lighting defaults. 2026-03-06 01:32:30 +01:00
Commander1024 6795c31d43 Updated HA. 2026-03-06 01:31:15 +01:00
Commander1024 e504cdf90c Updated ics_calendar. 2026-03-06 01:30:59 +01:00
Commander1024 c9807a7bf0 Updated epaper dashbord for new air quality sensor entities. 2026-02-14 15:08:14 +01:00
Commander1024 fdbbbb5300 Motion detection in the hallway, new lamp in the living room. 2026-02-14 14:58:06 +01:00
Commander1024 fda6e6abff Updated HA. 2026-02-14 14:56:24 +01:00
Commander1024 ee01b56fb6 Deprecated diy air quality sensor due to dead ESP32 2026-02-14 14:55:43 +01:00
Commander1024 db1edfaf24 Changed speakers for laundry automations. 2026-01-22 13:47:38 +01:00
Commander1024 e07c66563c Adoption of various Thread/Matter devices 2026-01-22 07:30:19 +01:00
Commander1024 2ff40a0da7 Updated HA. 2026-01-22 07:29:49 +01:00
Commander1024 24b4d07228 ESP32 ESPHome migration to esp-idf 2026-01-22 07:29:35 +01:00
Commander1024 8b6f3012f4 Updated HA. 2025-12-17 11:01:05 +01:00
Commander1024 792fc87166 Defined unused dummy TX pin as required by component. 2025-12-17 11:00:43 +01:00
Commander1024 67b2c2edde Updated ics_calendar 2025-12-08 12:06:06 +01:00
Commander1024 db9b8a73d3 Deleted ics_calendar obsolete Backup file. 2025-12-08 11:32:47 +01:00
Commander1024 093be6f2d2 Updated HA. 2025-12-08 11:32:20 +01:00
Commander1024 3d5693ceaa Migrated deprecated template sensors to modern way. 2025-12-04 08:47:45 +01:00
Commander1024 247c96df5d Updated HA. 2025-12-04 08:47:16 +01:00
Commander1024 13cdc7ee67 Updated HA. 2025-11-21 19:15:05 +01:00
Commander1024 24eecc7c97 Deprecation warnings fixed for esphome 2025.11 2025-11-21 19:14:49 +01:00
Commander1024 aea16e59c4 Changed doorbell text. 2025-11-20 15:37:25 +01:00
Commander1024 48bfa10bf7 Update HA. 2025-11-20 15:36:57 +01:00
Commander1024 80560ef9aa Moved DB size sensor due to HA breaking change. 2025-11-20 15:36:41 +01:00
Commander1024 ab46bff258 Moved DB size sensor due to HA breaking change. 2025-11-20 15:36:15 +01:00
Commander1024 fe081bf145 Updated Raspiaudio Muse Luxe to latest microww config. 2025-10-16 12:27:35 +02:00
Commander1024 85abd64ea0 Updated HA. 2025-10-16 12:26:56 +02:00
Commander1024 b7581d3bad GDoor: Fixed longpress on front door. 2025-10-16 12:24:01 +02:00
Commander1024 93b2d4f401 Added zone notification 2025-09-17 14:44:33 +02:00
Commander1024 ca0ef18df3 Added blackened theme for potential use in smartmirror. 2025-09-17 14:43:56 +02:00
Commander1024 cd6625b08e Increased camera resolution 2025-09-17 14:39:21 +02:00
Commander1024 4810652d99 Updated HA. 2025-09-17 14:38:44 +02:00
Commander1024 741b283218 Minor fixes and upgrades. 2025-09-10 17:24:57 +02:00
Commander1024 8800f7b847 Replaced tempsensor with cam with sensor. 2025-09-10 17:24:27 +02:00
60 changed files with 6809 additions and 831 deletions
+1 -1
View File
@@ -1 +1 @@
2025.9.0
2026.7.1
+2
View File
@@ -12,3 +12,5 @@ home-assistant.log*
.storage/
zigbee.db*
*__pycache__*
.ha_run.lock
.cache
+3
View File
@@ -0,0 +1,3 @@
[submodule "themes/blackened"]
path = themes/blackened
url = https://github.com/home-assistant-community-themes/blackened.git
+359 -156
View File
@@ -1,17 +1,28 @@
- id: '1623153060369'
alias: Licht bei Sonnenuntergang einschalten
description: ''
trigger:
- platform: sun
event: sunset
offset: '-1:00'
condition:
triggers:
- trigger: numeric_state
entity_id:
- sun.sun
attribute: elevation
below: 5
conditions:
- condition: state
entity_id: person.marcus_scholz
state: home
action:
- service: script.moodlight_orange_plasma
data: {}
actions:
- data: {}
action: script.moodlight_orange_plasma
- action: light.turn_on
metadata: {}
target:
entity_id:
- light.kuche_anrichte
- light.kajplats_e14_cws_globe_806lm
data:
brightness_pct: 70
color_temp_kelvin: 2000
mode: single
- id: '1623224198352'
alias: TV Anti-Reflexion
@@ -30,26 +41,18 @@
data:
transition: 2
mode: single
- id: '1623350724195'
alias: Zonenbenachrichtigung
description: 'Debug: Präsenzerkennung'
use_blueprint:
path: homeassistant/notify_leaving_zone.yaml
input:
person_entity: person.marcus_scholz
zone_entity: zone.home
notify_device: 773450cae5e93524731940ad081846d9
- id: '1623526683767'
alias: Licht bei Sonnenaufgang ausschalten
description: ''
trigger:
- platform: sun
event: sunrise
offset: '+1:00'
condition: []
action:
- service: light.turn_off
target:
triggers:
- trigger: numeric_state
entity_id:
- sun.sun
attribute: elevation
above: 5
conditions: []
actions:
- target:
area_id:
- schlafzimmer
device_id:
@@ -61,9 +64,10 @@
- 98a9eef603f8c8ae6200c4834a3dbf4f
- 32d15eec63fe68d5770813d47da53657
- 9f42805af5b7e423023595390342b9ac
- 063e41329df7b29bbaa8df2f508ee6cb
entity_id: light.kajplats_e14_cws_globe_806lm
data:
transition: 2
action: light.turn_off
mode: single
- id: '1623673821789'
alias: Gute Nacht!
@@ -82,6 +86,9 @@
- kuche
- schlafzimmer
- kinderzimmer
entity_id:
- light.awtrix_528bd4_matrix
- light.awtrix_b8658c_matrix
data: {}
action: light.turn_off
- type: turn_off
@@ -91,25 +98,6 @@
- data:
message: Gute Nacht!
action: notify.mobile_app_le2123
- data:
media:
media_content_id: media-source://tts/tts.piper?message=Gute+Nacht%2C+schlaf+gut.
media_content_type: provider
metadata:
title: Gute Nacht, schlaf gut.
thumbnail: https://brands.home-assistant.io/_/tts/logo.png
media_class: app
children_media_class:
navigateIds:
- {}
- media_content_type: app
media_content_id: media-source://tts
- media_content_type: provider
media_content_id: media-source://tts/tts.piper?message=Gute+Nacht%2C+schlaf+gut.
action: media_player.play_media
enabled: true
target:
entity_id: media_player.home_assistant_voice_09c0e7_media_player
- data:
media:
media_content_id: media-source://tts/tts.piper?message=Gute+Nacht%2C+schlaf+gut.
@@ -129,25 +117,6 @@
enabled: true
target:
entity_id: media_player.m5stack_atom_echo
- target:
entity_id: media_player.raspiaudio_muse_luxe
data:
media:
media_content_id: media-source://tts/tts.piper?message=Gute+Nacht%2C+schlaf+gut.
media_content_type: provider
metadata:
title: Gute Nacht, schlaf gut.
thumbnail: https://brands.home-assistant.io/_/tts/logo.png
media_class: app
children_media_class:
navigateIds:
- {}
- media_content_type: app
media_content_id: media-source://tts
- media_content_type: provider
media_content_id: media-source://tts/tts.piper?message=Gute+Nacht%2C+schlaf+gut.
action: media_player.play_media
enabled: true
- target:
area_id:
- wohnzimmer
@@ -159,9 +128,8 @@
target:
device_id:
- 1f3c4b5de4aea99bac83688ceb22293b
- 48dafb7f4a8ed6ccbb046758cb660c23
- c4ead7f6227e2ee4c43c4b0df829cd84
enabled: false
enabled: true
mode: single
- id: '1623868115464'
alias: 420!
@@ -181,17 +149,27 @@
action: notify.mobile_app_le2123
- data:
cache: true
media_player_entity_id: media_player.raspiaudio_muse_luxe
message: 4 20 lodere es, Lustknabe
media_player_entity_id: media_player.home_assistant_voice_0a7c6b_media_player
message: 4 20, lodere es, Lustknabe
target:
entity_id: tts.piper
enabled: true
action: tts.speak
- data:
cache: true
media_player_entity_id: media_player.home_assistant_voice_09c0e7_media_player
message: 4 20 lodere es, Lustknabe
message: 4 20, lodere es, Lustknabe
target:
entity_id: tts.piper
enabled: true
action: tts.speak
- data:
cache: true
media_player_entity_id: media_player.m5stack_atom_echo
message: 4 20, lodere es, Lustknabe
target:
entity_id: tts.piper
enabled: true
action: tts.speak
- data:
qos: 0
@@ -236,7 +214,8 @@
entity_id:
- event.sleep_as_android_benutzerbenachrichtigung
attribute: event_type
to: time_to_bed_alarm_alert
to:
- time_to_bed_alarm_alert
conditions:
- condition: state
entity_id: person.marcus_scholz
@@ -248,7 +227,7 @@
action: light.turn_on
- data:
cache: true
media_player_entity_id: media_player.raspiaudio_muse_luxe
media_player_entity_id: media_player.home_assistant_voice_0a7c6b
message: Ab ins Bett, Schlafenszeit.
target:
entity_id: tts.piper
@@ -459,17 +438,17 @@
- id: '1630914505161'
alias: Beim Verlassen der Wohnung alles abschalten
description: ''
trigger:
- platform: zone
entity_id: person.marcus_scholz
triggers:
- entity_id: person.marcus_scholz
zone: zone.home
event: leave
condition:
trigger: zone
conditions:
- condition: state
entity_id: person.marianna_scholz
state: not_home
enabled: false
action:
actions:
- target:
area_id:
- wohnzimmer
@@ -486,6 +465,13 @@
device_id: 5a08ac4c3b3893b540a9934fa92dccfa
entity_id: switch.flur_raumduft
domain: switch
- action: light.turn_off
metadata: {}
data: {}
target:
device_id:
- 231cd7ec37e32690faf126105b03ee02
- a6d24e7d93c114723bf525e9eac1c2df
- type: turn_off
device_id: c4ead7f6227e2ee4c43c4b0df829cd84
entity_id: 7f7284b11f2bf50ae2f0ebeeb35411c0
@@ -501,35 +487,27 @@
- id: '1632477389492'
alias: Licht beim Heimkommen einschalten
description: Nur während Dunkelphase
trigger:
- platform: state
entity_id: person.marcus_scholz
triggers:
- entity_id: person.marcus_scholz
to: home
from: not_home
condition:
- condition: or
conditions:
- condition: sun
before: sunrise
before_offset: '+1'
- condition: sun
after_offset: '-1'
after: sunset
enabled: false
- condition: sun
before: sunrise
before_offset: '+1'
after: sunset
after_offset: '-1'
action:
- service: script.moodlight_orange_plasma
data: {}
- service: light.turn_on
target:
trigger: state
conditions:
- condition: numeric_state
entity_id: sun.sun
attribute: elevation
below: '5'
actions:
- data: {}
action: script.moodlight_orange_plasma
- target:
device_id:
- 32d15eec63fe68d5770813d47da53657
- 53a27edf581fcf40f3bc8b253b1ee369
data: {}
- 5abbb43e5d015ee75c7c900240b4329e
data:
brightness_pct: 70
color_temp_kelvin: 2000
action: light.turn_on
mode: single
- id: '1648485179021'
alias: Schalte den Fernseher mit WoL ein
@@ -561,7 +539,7 @@
actions:
- data:
cache: true
media_player_entity_id: media_player.raspiaudio_muse_luxe
media_player_entity_id: media_player.home_assistant_voice_0a7c6b_media_player
message: '{{message}}
'
@@ -604,18 +582,6 @@
'
mode: single
- id: '1683370485273'
alias: TTS Benachrichtigung an ein Smartphone senden
trigger: []
condition: []
action:
- service: notify.mobile_app_le2123
data:
message: TTS
data:
tts_text: Dies ist ein Beispiel für eine Text-zu-Sprache Benachrichtigung
auf dem Handy.
mode: single
- id: '1683371750405'
alias: Fenster schließen, wenn es warm wird
description: Tagsüber im Sommer, wenn die Außentemperatur sich der Innentemperatur
@@ -788,7 +754,7 @@
= as_timestamp(endOfYear) - as_timestamp(startOfYear) -%} {%- set current
= as_timestamp(now) - as_timestamp(startOfYear) -%} {%- set progress = ( current
/ total * 100 ) | round(0, "floor", 0) -%} {# Output #} { "text": "{{ progress
}} %", "icon": "y2023"}'
}} %", "icon": "year-2025"}'
topic: awtrix_desk/custom/yearprogress
qos: 0
retain: false
@@ -802,7 +768,7 @@
= as_timestamp(endOfYear) - as_timestamp(startOfYear) -%} {%- set current
= as_timestamp(now) - as_timestamp(startOfYear) -%} {%- set progress = ( current
/ total * 100 ) | round(0, "floor", 0) -%} {# Output #} { "text": "{{ progress
}} %", "icon": "y2023"}'
}} %", "icon": "year-2025"}'
topic: awtrix_kitchen/custom/yearprogress
qos: 0
retain: false
@@ -864,9 +830,11 @@
alias: Licht bei Sonnenuntergang einschalten (XMas)
description: ''
triggers:
- event: sunset
offset: '-1:00'
trigger: sun
- trigger: numeric_state
entity_id:
- sun.sun
attribute: elevation
below: 5
conditions:
- condition: state
entity_id: person.marcus_scholz
@@ -874,21 +842,30 @@
actions:
- data: {}
action: script.moodlight_xmas
- target:
device_id:
- 53a27edf581fcf40f3bc8b253b1ee369
- 5abbb43e5d015ee75c7c900240b4329e
data:
brightness_pct: 70
color_temp_kelvin: 2000
action: light.turn_on
- action: switch.turn_on
metadata: {}
data: {}
target:
device_id:
- 1f3c4b5de4aea99bac83688ceb22293b
- 48dafb7f4a8ed6ccbb046758cb660c23
mode: single
- id: '1700483035319'
alias: Licht bei Sonnenaufgang ausschalten (XMas)
description: ''
triggers:
- event: sunrise
offset: '+1:00'
trigger: sun
- trigger: numeric_state
entity_id:
- sun.sun
attribute: elevation
above: 5
conditions: []
actions:
- target:
@@ -903,7 +880,7 @@
- 98a9eef603f8c8ae6200c4834a3dbf4f
- 32d15eec63fe68d5770813d47da53657
- 9f42805af5b7e423023595390342b9ac
- 063e41329df7b29bbaa8df2f508ee6cb
- 5abbb43e5d015ee75c7c900240b4329e
data:
transition: 2
action: light.turn_off
@@ -913,7 +890,6 @@
target:
device_id:
- 1f3c4b5de4aea99bac83688ceb22293b
- 48dafb7f4a8ed6ccbb046758cb660c23
mode: single
- id: '1701774106609'
alias: IKEA STYRBAR Wohnzimmer
@@ -1075,26 +1051,19 @@
domain: switch
action_button_up_double:
- type: turn_on
device_id: 32d15eec63fe68d5770813d47da53657
entity_id: 14edfa97dae4936040b97dc77b171d5f
device_id: 98a9eef603f8c8ae6200c4834a3dbf4f
entity_id: fdcff3852ecc17099c953da17eb4b8a3
domain: light
button_up_double_press: true
button_down_double_press: true
action_button_down_double:
- type: turn_off
device_id: 32d15eec63fe68d5770813d47da53657
entity_id: 14edfa97dae4936040b97dc77b171d5f
domain: light
action_button_up_long:
- type: turn_on
device_id: 98a9eef603f8c8ae6200c4834a3dbf4f
entity_id: fdcff3852ecc17099c953da17eb4b8a3
domain: light
action_button_down_long:
- type: turn_off
device_id: 98a9eef603f8c8ae6200c4834a3dbf4f
entity_id: fdcff3852ecc17099c953da17eb4b8a3
domain: light
action_button_up_long: []
action_button_down_long: []
button_up_long_max_loop_repeats: 4205
- id: '1727953214758'
alias: Waschmaschine läuft
description: Prüfen ob die Waschmaschine läuft, dann den boolschen Schalter "Waschmaschine
@@ -1153,7 +1122,15 @@
action: notify.mobile_app_le2123
- data:
cache: true
media_player_entity_id: media_player.raspiaudio_muse_luxe
media_player_entity_id: media_player.home_assistant_voice_09c0e7_media_player
message: Die Waschmaschine ist fertig! Hol die Wäsche hoch und häng sie auf
oder pack sie in den Trockner.
target:
entity_id: tts.piper
action: tts.speak
- data:
cache: true
media_player_entity_id: media_player.home_assistant_voice_0a7c6b_media_player
message: Die Waschmaschine ist fertig! Hol die Wäsche hoch und häng sie auf
oder pack sie in den Trockner.
target:
@@ -1178,8 +1155,8 @@
aktiv" einschalten
triggers:
- type: power
device_id: 81e357077ac8d1f8f588fbb5324965b5
entity_id: fac93f9504dc8d1267bec7d02849d491
device_id: 25a822b76a0206496bd216235b034fb0
entity_id: 3321a33261062dab534569c3f6f53d69
domain: sensor
trigger: device
above: 150
@@ -1190,8 +1167,8 @@
conditions:
- condition: device
type: is_on
device_id: 81e357077ac8d1f8f588fbb5324965b5
entity_id: 2a2659668772d9345e42667900832f0e
device_id: 25a822b76a0206496bd216235b034fb0
entity_id: 4748aace637a3f1d8b5e896236564024
domain: switch
actions:
- action: input_boolean.turn_on
@@ -1205,11 +1182,11 @@
Uhr.
triggers:
- type: power
device_id: 81e357077ac8d1f8f588fbb5324965b5
entity_id: fac93f9504dc8d1267bec7d02849d491
device_id: 25a822b76a0206496bd216235b034fb0
entity_id: 3321a33261062dab534569c3f6f53d69
domain: sensor
trigger: device
below: 150
below: 250
for:
hours: 0
minutes: 5
@@ -1217,8 +1194,8 @@
conditions:
- condition: device
type: is_on
device_id: 81e357077ac8d1f8f588fbb5324965b5
entity_id: 2a2659668772d9345e42667900832f0e
device_id: 25a822b76a0206496bd216235b034fb0
entity_id: 4748aace637a3f1d8b5e896236564024
domain: switch
- condition: state
entity_id: input_boolean.waschetrockner_aktiv
@@ -1230,7 +1207,14 @@
action: notify.mobile_app_le2123
- data:
cache: true
media_player_entity_id: media_player.raspiaudio_muse_luxe
media_player_entity_id: media_player.home_assistant_voice_09c0e7_media_player
message: Der Wäschetrockner ist fertig! Hol die Wäsche heraus!
target:
entity_id: tts.piper
action: tts.speak
- data:
cache: true
media_player_entity_id: media_player.home_assistant_voice_0a7c6b_media_player
message: Der Wäschetrockner ist fertig! Hol die Wäsche heraus!
target:
entity_id: tts.piper
@@ -1257,14 +1241,16 @@
- choose:
- conditions:
- condition: template
value_template: '{{ trigger.payload_json.action == ''BUTTON_RING'' and trigger.payload_json.parameters
== ''0560'' }}'
value_template: '{{ (trigger.payload_json.action == ''BUTTON_RING'' and trigger.payload_json.parameters
== ''0560'') or ( trigger.payload_json.action == ''BUTTON_RING'' and trigger.payload_json.parameters
== ''05A0'') }}'
sequence:
- data:
data:
push:
interruption-level: time-sensitive
message: Türklingel (außen)
ttl: 0
priority: high
message: Jemand hat an der Haustür geklingelt
title: Türklingel (außen)
action: notify.alle_mobilen_gerate
- conditions:
- condition: template
@@ -1273,8 +1259,225 @@
sequence:
- data:
data:
push:
interruption-level: time-sensitive
message: Türklingel (innen)
ttl: 0
priority: high
message: Jemand hat an der Wohnungstür geklingelt
title: Türklingel (innen)
action: notify.alle_mobilen_gerate
mode: single
- id: '1767373636810'
alias: IKEA Bilresa Scroll Wheel Kinderzimmer
description: ''
use_blueprint:
path: jhol-byte/Ikea_bilresa_scroll_wheel.yaml
input:
remote: 2197b753ff6e9bed3f8fb7f37fc434b3
click_action_ch1:
- type: toggle
device_id: cdc33c408194cf51c106cdee7c6c40b7
entity_id: d4fafbbb3b30a00745ab60222f21383b
domain: light
scroll_wheel_target_ch1:
- light.kajplats_e27_ws_globe_470lm
scroll_wheel_mode_ch1: 'lights: dim'
click_action_ch2:
- action: media_player.media_play_pause
metadata: {}
target:
entity_id: media_player.4k_google_tv_box_2
data: {}
scroll_wheel_target_ch2:
- media_player.4k_google_tv_box_2
scroll_wheel_mode_ch2: 'media: volume control'
click_action_ch3:
- action: light.toggle
metadata: {}
target:
entity_id: light.kinderzimmer_deckenlicht
data: {}
on_hold_action_ch1:
- action: script.light_color_temp_helper
metadata: {}
data:
min_color_temp: 2200
max_color_temp: 5000
color_temp_step: 300
mode: cycle
target_light: light.kajplats_e27_ws_globe_470lm
long_click_action_ch2:
- action: media_player.toggle
metadata: {}
data: {}
long_click_action_ch1:
- action: light.turn_on
metadata: {}
target:
entity_id: light.kajplats_e27_ws_globe_470lm
data: {}
- id: '1768844444161'
alias: 'AVR: Nachtruhe'
description: ''
triggers:
- trigger: time
at: '22:00:00'
conditions: []
actions:
- action: media_player.volume_set
metadata: {}
target:
entity_id: media_player.wohnzimmer_2
data:
volume_level: 0.47
- type: turn_off
device_id: 81d56cc472abaec2a02702def5f9cf66
entity_id: a57a56132635ff48a6d2ae8df322b385
domain: switch
mode: single
- id: '1768845192543'
alias: 'AVR: Extra Bass wieder einschalten'
description: ''
triggers:
- trigger: time
at: 08:00:00
conditions: []
actions:
- type: turn_on
device_id: 81d56cc472abaec2a02702def5f9cf66
entity_id: a57a56132635ff48a6d2ae8df322b385
domain: switch
mode: single
- id: '1771012833905'
alias: Bewegungssteuerung Flurlicht
description: ''
use_blueprint:
path: aledziko/ikea-myggspray-e2494-matter-motion-sensor.yaml
input:
motion_entity: binary_sensor.myggspray_wrlss_mtn_sensor_belegung
no_motion_wait: 0
illuminance_entity: sensor.myggspray_wrlss_mtn_sensor_beleuchtungsstarke
battery_entity: sensor.myggspray_wrlss_mtn_sensor_batterie
motion_on_action:
- action: light.turn_on
metadata: {}
target:
entity_id:
- light.schreibtisch_deko
data: {}
motion_off_action:
- action: light.turn_off
metadata: {}
target:
entity_id:
- light.schreibtisch_deko
data: {}
motion_cutoff: 10
illuminance_low_threshold: 10
battery_low_threshold: 20
- id: '1772920063868'
alias: IKEA BILRESA Dual Button Küche Musik
description: ''
use_blueprint:
path: Skaronator/ikea-bilresa-dual-button.yaml
input:
button1_event:
- event.bilresa_dual_button_taste_1
button2_event:
- event.bilresa_dual_button_taste_2
button1_single:
- type: turn_on
device_id: 6d1be741876624a70ab5b01b54c6fd6f
entity_id: 621e4377a089b7f5d92c7c9f2cc171a1
domain: switch
button2_single:
- type: turn_off
device_id: 6d1be741876624a70ab5b01b54c6fd6f
entity_id: 621e4377a089b7f5d92c7c9f2cc171a1
domain: switch
- id: '1773000000001'
alias: epaperframe - Kalender Events laden
description: Ruft alle Events von calendar.privat und calendar.tkrz_kalender für heute und morgen ab und schreibt sie in input_text-Helfer.
triggers:
# Täglich um 00:01 neu holen
- trigger: time
at: '00:01:00'
id: daily
actions:
# ── Events für heute (00:00 bis 23:59) holen ──
- action: calendar.get_events
target:
entity_id:
- calendar.privat
- calendar.tkrz_kalender
data:
start_date_time: "{{ now().replace(hour=0, minute=0, second=0) }}"
end_date_time: "{{ (now().replace(hour=0, minute=0, second=0) + timedelta(days=1)) }}"
response_variable: today_evts
# ── Events für morgen (00:00 bis 23:59) holen ──
- action: calendar.get_events
target:
entity_id:
- calendar.privat
- calendar.tkrz_kalender
data:
start_date_time: "{{ (now() + timedelta(days=1)).replace(hour=0, minute=0, second=0) }}"
end_date_time: "{{ (now() + timedelta(days=1)).replace(hour=0, minute=0, second=0) + timedelta(days=1) }}"
response_variable: tomorrow_evts
# ── input_text Helfer schreiben ──
- action: input_text.set_value
target:
entity_id: input_text.kalender_heute_privat
data:
value: >-
{%- set evts = today_evts['calendar.privat']['events'] if 'calendar.privat' in today_evts else [] -%}
{%- set items = [] -%}
{%- for e in evts[:5] -%}
{%- set s = (e['summary'] | default('', true) | string)[:40] if e is mapping and 'summary' in e else '-' -%}
{%- set st = e['start'] if e is mapping and 'start' in e else '' -%}
{%- set hhmm = st[11:16] if st|length >= 16 else '??' -%}
{%- set items = items + [hhmm ~ ' - ' ~ s] -%}
{%- endfor -%}
{{ items | join('│') if items else '-' }}
- action: input_text.set_value
target:
entity_id: input_text.kalender_heute_arbeit
data:
value: >-
{%- set evts = today_evts['calendar.tkrz_kalender']['events'] if 'calendar.tkrz_kalender' in today_evts else [] -%}
{%- set items = [] -%}
{%- for e in evts[:5] -%}
{%- set s = (e['summary'] | default('', true) | string)[:40] if e is mapping and 'summary' in e else '-' -%}
{%- set st = e['start'] if e is mapping and 'start' in e else '' -%}
{%- set hhmm = st[11:16] if st|length >= 16 else '??' -%}
{%- set items = items + [hhmm ~ ' - ' ~ s] -%}
{%- endfor -%}
{{ items | join('│') if items else '-' }}
- action: input_text.set_value
target:
entity_id: input_text.kalender_morgen_privat
data:
value: >-
{%- set evts = tomorrow_evts['calendar.privat']['events'] if 'calendar.privat' in tomorrow_evts else [] -%}
{%- set items = [] -%}
{%- for e in evts[:5] -%}
{%- set s = (e['summary'] | default('', true) | string)[:40] if e is mapping and 'summary' in e else '-' -%}
{%- set st = e['start'] if e is mapping and 'start' in e else '' -%}
{%- set hhmm = st[11:16] if st|length >= 16 else '??' -%}
{%- set items = items + [hhmm ~ ' - ' ~ s] -%}
{%- endfor -%}
{{ items | join('│') if items else '-' }}
- action: input_text.set_value
target:
entity_id: input_text.kalender_morgen_arbeit
data:
value: >-
{%- set evts = tomorrow_evts['calendar.tkrz_kalender']['events'] if 'calendar.tkrz_kalender' in tomorrow_evts else [] -%}
{%- set items = [] -%}
{%- for e in evts[:5] -%}
{%- set s = (e['summary'] | default('', true) | string)[:40] if e is mapping and 'summary' in e else '-' -%}
{%- set st = e['start'] if e is mapping and 'start' in e else '' -%}
{%- set hhmm = st[11:16] if st|length >= 16 else '??' -%}
{%- set items = items + [hhmm ~ ' - ' ~ s] -%}
{%- endfor -%}
{{ items | join('│') if items else '-' }}
@@ -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
@@ -0,0 +1,232 @@
blueprint:
name: IKEA MYGGSPRAY E2494 Motion Sensor (Matter) v2.1
description: 'Full-featured automation for the IKEA MYGGSPRAY E2494 Matter motion
sensor. Supports independent actions for motion, illuminance (high/low), and battery
alerts. '
domain: automation
source_url: https://github.com/aledziko/HA-blueprints/blob/main/IKEA/Matter/ikea-myggspray-e2494/ikea-myggspray-e2494-matter-motion-sensor.yaml
author: aledziko
input:
motion_entity:
name: MYGGSPRAY Motion Sensor
description: Select the MYGGSPRAY motion sensor (binary_sensor).
selector:
entity:
filter:
- device_class:
- occupancy
domain:
- binary_sensor
- device_class:
- motion
domain:
- binary_sensor
reorder: false
multiple: false
use_time_window:
name: Enable Active Hours?
description: Restrict automation to a specific time window.
default: false
selector:
boolean: {}
active_start_time:
name: Active Start Time
description: Automation starts working at this time (if enabled).
default: 08:00:00
selector:
time: {}
active_end_time:
name: Active End Time
description: Automation stops working at this time (if enabled).
default: '22:00:00'
selector:
time: {}
no_motion_wait:
name: Wait time (Motion)
description: Time to wait after last motion is detected before running the "off"
action.
default: 120
selector:
number:
min: 0.0
max: 3600.0
unit_of_measurement: seconds
step: 1.0
mode: slider
motion_on_action:
name: 'Action: Motion Detected'
description: Run when motion is detected (and dark enough).
default: []
selector:
action: {}
motion_off_action:
name: 'Action: Motion Stopped'
description: Run after the sensor stops detecting motion and wait time elapsed.
default: []
selector:
action: {}
illuminance_entity:
name: (Optional) Illuminance Sensor
description: Select the light sensor (sensor.illuminance).
default: []
selector:
entity:
filter:
- domain:
- sensor
device_class:
- illuminance
reorder: false
multiple: false
motion_cutoff:
name: (Optional) Motion Light Cutoff
description: Only trigger "Motion Detected" action if LUX is BELOW this value
(0 to disable).
default: 0
selector:
number:
min: 0.0
max: 3000.0
unit_of_measurement: lux
step: 1.0
mode: slider
illuminance_high_threshold:
name: (Optional) Light High Threshold
description: Trigger "High Action" when LUX goes ABOVE this value (e.g. 500
lx).
default: 500
selector:
number:
min: 0.0
max: 3000.0
unit_of_measurement: lux
step: 1.0
mode: slider
high_action:
name: 'Action: Light High'
description: Run when light exceeds threshold (e.g. close curtains).
default: []
selector:
action: {}
illuminance_low_threshold:
name: (Optional) Light Low Threshold
description: Trigger "Low Action" when LUX goes BELOW this value (e.g. 5 lx).
default: 5
selector:
number:
min: 0.0
max: 3000.0
unit_of_measurement: lux
step: 1.0
mode: slider
low_action:
name: 'Action: Light Low'
description: Run when light drops below threshold (e.g. open curtains).
default: []
selector:
action: {}
battery_entity:
name: (Optional) Battery Sensor
description: Select the battery level sensor (sensor.battery).
default: []
selector:
entity:
filter:
- domain:
- sensor
device_class:
- battery
reorder: false
multiple: false
battery_low_threshold:
name: (Optional) Battery Low Level
description: Threshold for battery alert (%).
default: 30
selector:
number:
min: 0.0
max: 100.0
unit_of_measurement: '%'
step: 1.0
mode: slider
battery_low_action:
name: 'Action: Battery Low'
description: Run when battery drops below threshold.
default: []
selector:
action: {}
mode: parallel
max_exceeded: silent
trigger_variables:
motion_entity_input: !input motion_entity
illuminance_entity_input: !input illuminance_entity
motion_cutoff_input: !input motion_cutoff
high_threshold_input: !input illuminance_high_threshold
low_threshold_input: !input illuminance_low_threshold
battery_entity_input: !input battery_entity
battery_threshold_input: !input battery_low_threshold
use_time_window_input: !input use_time_window
triggers:
- trigger: state
entity_id: !input motion_entity
from: 'off'
to: 'on'
id: motion_on
- trigger: state
entity_id: !input motion_entity
from: 'on'
to: 'off'
for:
seconds: !input no_motion_wait
id: motion_off
- trigger: template
value_template: "{{ illuminance_entity_input != none and \n states(illuminance_entity_input)
| is_number and \n states(illuminance_entity_input) | float(0) > high_threshold_input
| float(100000) }}\n"
id: light_high
- trigger: template
value_template: "{{ illuminance_entity_input != none and \n states(illuminance_entity_input)
| is_number and \n states(illuminance_entity_input) | float(100000) < low_threshold_input
| float(-1) }}\n"
id: light_low
- trigger: template
value_template: "{{ battery_entity_input != none and \n states(battery_entity_input)
| is_number and \n states(battery_entity_input) | float(100) < battery_threshold_input
| float(-1) }}\n"
id: battery_low
condition:
- condition: or
conditions:
- condition: template
value_template: '{{ not use_time_window_input }}'
- condition: time
after: !input active_start_time
before: !input active_end_time
actions:
- choose:
- conditions:
- condition: trigger
id: motion_on
sequence:
- delay: 1.5
- condition: template
value_template: "{% set cutoff = motion_cutoff_input | float(0) %} {% if cutoff
== 0 %}\n true\n{% else %}\n {% set lux = states(illuminance_entity_input)
| float(-1) %}\n {{ lux == -1 or lux <= cutoff }}\n{% endif %}\n"
- sequence: !input motion_on_action
- conditions:
- condition: trigger
id: motion_off
sequence: !input motion_off_action
- conditions:
- condition: trigger
id: light_high
sequence: !input high_action
- conditions:
- condition: trigger
id: light_low
sequence: !input low_action
- conditions:
- condition: trigger
id: battery_low
sequence: !input battery_low_action
@@ -0,0 +1,506 @@
blueprint:
name: Ikea_bilresa_scroll_wheel
description: 'Blueprint for Ikea Bilresa (button and scroll wheel for 3 different
channels)
Version: 2025-01-01
'
domain: automation
input:
remote:
name: Remote
description: Ikea Bilresa scroll wheel remote control
selector:
device:
multiple: false
channel1_section:
name: Channel 1
collapsed: true
input:
click_action_ch1:
name: 'Channel 1: <click> action'
default: []
selector:
action: {}
double_click_action_ch1:
name: 'Channel 1: <double-click> action'
default: []
selector:
action: {}
long_click_action_ch1:
name: 'Channel 1: <long-click> action'
description: ''
default: []
selector:
action: {}
on_hold_action_ch1:
name: 'Channel 1: <on-hold> action'
description: repeat action until button is released, see also on_hold_delay
default: []
selector:
action: {}
scroll_wheel_target_ch1:
name: 'Channel 1: Scroll wheel target entity'
description: Light or media player to be controlled by scroll wheel for
channel 1
selector:
entity:
multiple: true
filter:
- domain:
- light
- media_player
reorder: false
default: []
scroll_wheel_mode_ch1:
selector:
select:
options:
- 'lights: dim'
- 'lights: color temp and color hue'
- 'lights: color temp only'
- 'lights: color hue only'
- 'media: volume control'
custom_value: false
sort: false
multiple: false
default: dim_only
name: 'Channel 1: Scroll wheel mode'
description: Select attribute of target entity controlled by scroll wheel
channel2_section:
name: Channel 2
collapsed: true
input:
click_action_ch2:
name: 'Channel 2: <click> action'
default: []
selector:
action: {}
double_click_action_ch2:
name: 'Channel 2: <double-click> action'
default: []
selector:
action: {}
long_click_action_ch2:
name: 'Channel 2: <long-click> action'
description: ''
default: []
selector:
action: {}
on_hold_action_ch2:
name: 'Channel 2: <on-hold> action'
description: repeat action until button is released, see also on_hold_delay
default: []
selector:
action: {}
scroll_wheel_target_ch2:
name: 'Channel 2: Scroll wheel target entity'
description: Light or media player to be controlled by scroll wheel for
channel 2
selector:
entity:
multiple: true
filter:
- domain:
- light
- media_player
reorder: false
default: []
scroll_wheel_mode_ch2:
selector:
select:
options:
- 'lights: dim'
- 'lights: color temp and color hue'
- 'lights: color temp only'
- 'lights: color hue only'
- 'media: volume control'
custom_value: false
sort: false
multiple: false
default: dim_only
name: 'Channel 2: Scroll wheel mode'
description: Select attribute of target entity controlled by scroll wheel
channel3_section:
name: Channel 3
collapsed: true
input:
click_action_ch3:
name: 'Channel 3: <click> action'
default: []
selector:
action: {}
double_click_action_ch3:
name: 'Channel 3: <double-click> action'
default: []
selector:
action: {}
long_click_action_ch3:
name: 'Channel 3: <long-click> action'
description: ''
default: []
selector:
action: {}
on_hold_action_ch3:
name: 'Channel 3: <on-hold> action'
description: repeat action until button is released, see also on_hold_delay
default: []
selector:
action: {}
scroll_wheel_target_ch3:
name: 'Channel 3: Scroll wheel target entity'
description: Light or media player to be controlled by scroll wheel for
channel 3
selector:
entity:
multiple: true
filter:
- domain:
- light
- media_player
reorder: false
default: []
scroll_wheel_mode_ch3:
selector:
select:
options:
- 'lights: dim'
- 'lights: color temp and color hue'
- 'lights: color temp only'
- 'lights: color hue only'
- 'media: volume control'
custom_value: false
sort: false
multiple: false
default: dim_only
name: 'Channel 3: Scroll wheel mode'
description: Select attribute of target entity controlled by scroll wheel
global_section:
name: Global settings
collapsed: true
input:
dim_step_pct:
name: Dimmer step percentage
description: change in brightness in %
default: 5
selector:
number:
mode: box
step: 1.0
dim_min_pct:
name: Minimum brightness
description: minimum brightness in %
default: 5
selector:
number:
mode: box
step: 1.0
min_color_temp:
name: Minimum color temperature
description: minimum color temperature in K (for color_mode = color_temp)
default: 2200
selector:
number:
mode: box
step: 1.0
max_color_temp:
name: Maximum color temperature
description: maximum color temperature in K (for color_mode = color_temp)
default: 4000
selector:
number:
mode: box
step: 1.0
color_temp_step:
name: Color temperature step
description: change in color temperature in K (for color_mode = color_temp)
default: 100
selector:
number:
mode: box
step: 1.0
color_hue_step:
name: Color hue step
description: change in color hue in degree (0..360) (for color_mode = color_hue)
default: 4
selector:
number:
mode: box
step: 1.0
color_saturation:
name: Color saturation
description: color saturation in % (for color_mode = color_hue)
default: 100
selector:
number:
mode: box
step: 1.0
on_hold_delay:
name: On hold delay
description: delay between separate action calls while on hold button
default: 0.1
selector:
number:
mode: box
step: 0.1
volume_step_pct:
name: Media player volume step percentage
description: change in volume in %
default: 2
selector:
number:
mode: box
step: 1.0
volume_max_pct:
name: Media player max volume percentage
description: max volume in %
default: 50
selector:
number:
mode: box
step: 1.0
source_url: https://gist.github.com/jhol-byte/b2731a4d2476f530d76b9ff409f7f3a4
alias: Ikea_bilresa_scroll_wheel
trigger:
- event_type: state_changed
event_data:
entity_id: '{{ var_button_ch1 }}'
trigger: event
id: button_ch1
- event_type: state_changed
event_data:
entity_id: '{{ var_scroll_left_ch1 }}'
trigger: event
id: scroll_left_ch1
- event_type: state_changed
event_data:
entity_id: '{{ var_scroll_right_ch1 }}'
trigger: event
id: scroll_right_ch1
- event_type: state_changed
event_data:
entity_id: '{{ var_button_ch2 }}'
trigger: event
id: button_ch2
- event_type: state_changed
event_data:
entity_id: '{{ var_scroll_left_ch2 }}'
trigger: event
id: scroll_left_ch2
- event_type: state_changed
event_data:
entity_id: '{{ var_scroll_right_ch2 }}'
trigger: event
id: scroll_right_ch2
- event_type: state_changed
event_data:
entity_id: '{{ var_button_ch3 }}'
trigger: event
id: button_ch3
- event_type: state_changed
event_data:
entity_id: '{{ var_scroll_left_ch3 }}'
trigger: event
id: scroll_left_ch3
- event_type: state_changed
event_data:
entity_id: '{{ var_scroll_right_ch3 }}'
trigger: event
id: scroll_right_ch3
condition:
- condition: template
value_template: "{{ trigger.event.data.old_state.state != 'unavailable' \n and
trigger.event.data.new_state.state != 'unavailable'\n}}"
action:
- variables:
channel: "{% if trigger.id in ['button_ch1', 'scroll_left_ch1', 'scroll_right_ch1']
%}\n ch1\n{% elif trigger.id in ['button_ch2', 'scroll_left_ch2', 'scroll_right_ch2']
%}\n ch2\n{% elif trigger.id in ['button_ch3', 'scroll_left_ch3', 'scroll_right_ch3']
%}\n ch3\n{% endif %}"
- choose:
- conditions:
- condition: template
value_template: '{{ trigger.id == (''button_'' ~ channel) and trigger.event.data.new_state.attributes.event_type
== ''multi_press_1'' }}
'
sequence:
- if: '{{ channel == ''ch1''}}'
then: !input click_action_ch1
- if: '{{ channel == ''ch2''}}'
then: !input click_action_ch2
- if: '{{ channel == ''ch3''}}'
then: !input click_action_ch3
- conditions:
- condition: template
value_template: '{{ trigger.id == (''button_'' ~ channel) and trigger.event.data.new_state.attributes.event_type
== ''multi_press_2'' }}
'
sequence:
- if: '{{ channel == ''ch1''}}'
then: !input double_click_action_ch1
- if: '{{ channel == ''ch2''}}'
then: !input double_click_action_ch2
- if: '{{ channel == ''ch3''}}'
then: !input double_click_action_ch3
- conditions:
- condition: template
value_template: '{{ trigger.id == (''button_'' ~ channel) and trigger.event.data.new_state.attributes.event_type
== ''long_press'' }}
'
sequence:
- if: '{{ channel == ''ch1''}}'
then:
sequence:
- sequence: !input long_click_action_ch1
- repeat:
while: '{{ repeat.index < 100 and state_attr(var_button_ch1, ''event_type'')
== ''long_press'' }}'
sequence:
- sequence: !input on_hold_action_ch1
- delay: '{{ var_on_hold_delay }}'
- if: '{{ channel == ''ch2''}}'
then:
sequence:
- sequence: !input long_click_action_ch2
- repeat:
while: '{{ repeat.index < 100 and state_attr(var_button_ch2, ''event_type'')
== ''long_press'' }}'
sequence:
- sequence: !input on_hold_action_ch2
- delay: '{{ var_on_hold_delay }}'
- if: '{{ channel == ''ch3''}}'
then:
sequence:
- sequence: !input long_click_action_ch3
- repeat:
while: '{{ repeat.index < 100 and state_attr(var_button_ch3, ''event_type'')
== ''long_press'' }}'
sequence:
- sequence: !input on_hold_action_ch3
- delay: '{{ var_on_hold_delay }}'
- conditions:
- condition: template
value_template: '{{ trigger.id == (''scroll_left_'' ~ channel) or trigger.id
== (''scroll_right_'' ~ channel) }}
'
sequence:
- repeat:
for_each: '{{ vars[''targets_entities_'' ~ channel] }}'
sequence:
- choose:
- conditions:
- condition: template
value_template: '{{ vars[''scroll_wheel_mode_'' ~ channel] == ''lights:
dim''}}'
sequence:
- if:
- condition: template
value_template: '{{ is_state(repeat.item, ''on'') }}'
then:
- action: light.turn_on
target:
entity_id: '{{ repeat.item }}'
data:
brightness_step_pct: "{{ iif(trigger.id == 'scroll_right_' ~ channel,
trigger.event.data.new_state.attributes.totalNumberOfPressesCounted
* var_dim_step_pct, -1*([trigger.event.data.new_state.attributes.totalNumberOfPressesCounted
* var_dim_step_pct, state_attr(repeat.item, 'brightness')*100/254\n
- var_min_pct]|min)) \n}}"
transition: 0.1
- conditions:
- condition: template
value_template: '{{ vars[''scroll_wheel_mode_'' ~ channel] == ''lights:
color temp only'' or (vars[''scroll_wheel_mode_'' ~ channel] == ''lights:
color temp and color hue'' and state_attr(repeat.item, ''color_temp_kelvin'')
is not none ) }}'
sequence:
- action: script.light_color_temp_helper
data:
mode: '{{ iif(trigger.id == ''scroll_left_'' ~ channel, ''down'',
''up'') }}'
min_color_temp: '{{ var_min_color_temp }}'
max_color_temp: '{{ var_max_color_temp }}'
color_temp_step: '{{ var_color_temp_step * trigger.event.data.new_state.attributes.totalNumberOfPressesCounted
}}'
target_light: '{{ repeat.item }}'
- conditions:
- condition: template
value_template: '{{ vars[''scroll_wheel_mode_'' ~ channel] == ''lights:
color hue only'' or (vars[''scroll_wheel_mode_'' ~ channel] == ''lights:
color temp and color hue'' and state_attr(repeat.item, ''color_temp_kelvin'')
is none ) }}'
sequence:
- action: script.light_color_hs_helper
data:
target_light: '{{ repeat.item }}'
color_hue_step: '{{ var_color_hue_step * trigger.event.data.new_state.attributes.totalNumberOfPressesCounted
}}'
color_saturation: '{{ var_color_saturation }}'
mode: '{{ iif(trigger.id == ''scroll_left_'' ~ channel, ''down'',
''up'') }}'
- conditions:
- condition: template
value_template: '{{ vars[''scroll_wheel_mode_'' ~ channel] == ''media:
volume control'' and is_state(repeat.item, ''playing'')}}'
sequence:
- action: media_player.volume_set
target:
entity_id: '{{ repeat.item }}'
data:
volume_level: "{{ [[state_attr(repeat.item, 'volume_level')*100 +
\n iif(trigger.id == 'scroll_right_' ~ channel, +1, -1)* trigger.event.data.new_state.attributes.totalNumberOfPressesCounted
* var_volume_step_pct, var_volume_max_pct] | min, 0] | max / 100\n}}"
mode: single
max_exceeded: silent
variables:
vars:
targets_entities_ch1: !input scroll_wheel_target_ch1
targets_entities_ch2: !input scroll_wheel_target_ch2
targets_entities_ch3: !input scroll_wheel_target_ch3
scroll_wheel_mode_ch1: !input scroll_wheel_mode_ch1
scroll_wheel_mode_ch2: !input scroll_wheel_mode_ch2
scroll_wheel_mode_ch3: !input scroll_wheel_mode_ch3
var_min_pct: !input dim_min_pct
var_dim_step_pct: !input dim_step_pct
var_color_hue_step: !input color_hue_step
var_color_saturation: !input color_saturation
var_min_color_temp: !input min_color_temp
var_max_color_temp: !input max_color_temp
var_color_temp_step: !input color_temp_step
var_on_hold_delay: !input on_hold_delay
var_volume_step_pct: !input volume_step_pct
var_volume_max_pct: !input volume_max_pct
trigger_variables:
var_remote: !input remote
var_button_ch1: "{% for item in device_entities(var_remote) %} {% set match = item
| regex_findall(find='^event\\..+[a-zA-Z]_3.*$') %} {% if (match | length) > 0
%}\n {{ match[0] }}\n{% endif %} {% endfor %}"
var_scroll_left_ch1: "{% for item in device_entities(var_remote) %} {% set match
= item | regex_findall(find='^event\\..+[a-zA-Z]_2.*$') %} {% if (match | length)
> 0 %}\n {{ match[0] }}\n{% endif %} {% endfor %}"
var_scroll_right_ch1: "{% for item in device_entities(var_remote) %} {% set match
= item | regex_findall(find='^event\\..+[a-zA-Z]_1.*$') %} {% if (match | length)
> 0 %}\n {{ match[0] }}\n{% endif %} {% endfor %}"
var_button_ch2: "{% for item in device_entities(var_remote) %} {% set match = item
| regex_findall(find='^event\\..+[a-zA-Z]_6.*$') %} {% if (match | length) > 0
%}\n {{ match[0] }}\n{% endif %} {% endfor %}"
var_scroll_left_ch2: "{% for item in device_entities(var_remote) %} {% set match
= item | regex_findall(find='^event\\..+[a-zA-Z]_5.*$') %} {% if (match | length)
> 0 %}\n {{ match[0] }}\n{% endif %} {% endfor %}"
var_scroll_right_ch2: "{% for item in device_entities(var_remote) %} {% set match
= item | regex_findall(find='^event\\..+[a-zA-Z]_4.*$') %} {% if (match | length)
> 0 %}\n {{ match[0] }}\n{% endif %} {% endfor %}"
var_button_ch3: "{% for item in device_entities(var_remote) %} {% set match = item
| regex_findall(find='^event\\..+[a-zA-Z]_9.*$') %} {% if (match | length) > 0
%}\n {{ match[0] }}\n{% endif %} {% endfor %}"
var_scroll_left_ch3: "{% for item in device_entities(var_remote) %} {% set match
= item | regex_findall(find='^event\\..+[a-zA-Z]_8.*$') %} {% if (match | length)
> 0 %}\n {{ match[0] }}\n{% endif %} {% endfor %}"
var_scroll_right_ch3: "{% for item in device_entities(var_remote) %} {% set match
= item | regex_findall(find='^event\\..+[a-zA-Z]_7.*$') %} {% if (match | length)
> 0 %}\n {{ match[0] }}\n{% endif %} {% endfor %}"
+19
View File
@@ -38,9 +38,20 @@ mqtt_statestream: !include mqtt_statestream.yaml
# Template sensors
template: !include template.yaml
# Database size sensor
sql:
- name: "Home Assistant Datenbankgröße"
db_url: !secret ha_recorder_dburl
query: 'SELECT table_schema "database", Round(Sum(data_length + index_length) / 1048576, 2) "value" FROM information_schema.tables WHERE table_schema="homeassistant" GROUP BY table_schema;'
column: 'value'
unit_of_measurement: MB
# calendar integration
calendar: !include calendars.yaml
# Helpers for epaperframe calendar display
input_text: !include text_helpers.yaml
# DB-recorder configuration
recorder: !include recorder.yaml
@@ -71,3 +82,11 @@ generic_hygrostat:
away_humidity: 60
away_fixed: true
sensor_stale_duration: 00:15:00
frontend:
themes: !include_dir_merge_named themes
freedns:
url: !secret freedns6_url
# - name: IPv4
# url: !secret freedns4_url
@@ -0,0 +1,35 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Home Assistant Setup**
Please indicate your version of HA and how it is installed.
Version:
Installation Type (put an X between the square brackets for your HA):
[] Home Assistant OS
[] Home Assistant Supervised
[] Home Assistant Container
[] Home Assistant Core
Hardware platform:
[] ARM
[] x86-64
Are you running in a container environment like Docker or Kubernetes?
[] Yes
[] No
If running in a container, how is your image built?
[] Official HA container image
[] Official HA container image with customizations
[] Custom built container image
**Describe the bug**
A clear and concise description of the bug
+14
View File
@@ -0,0 +1,14 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "pip" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"
day: "monday"
reviewers:
- "franc6"
@@ -0,0 +1,10 @@
Fixes #
Description of change:
## Formatting, testing, and code coverage
Please note your pull request won't be accepted if you haven't properly formatted your source code, and ensured the unit tests are appropriate. Please note if you are not running on Windows, you can either run the scripts via a bash installation (like git-bash).
- [] formatstyle.sh reports no errors
- [] All unit tests pass (test.sh)
- [] Code coverage has not decreased (test.sh)
@@ -0,0 +1,21 @@
name: Validate with hassfest and run HACS action
on:
push:
pull_request:
workflow_dispatch:
schedule:
- cron: '0 0 * * *'
jobs:
validate:
runs-on: "ubuntu-latest"
steps:
- name: Checkout
uses: "actions/checkout@v4"
- name: Validate with hassfest
uses: "home-assistant/actions/hassfest@master"
- name: HACS Action
uses: "hacs/action@main"
with:
category: integration
@@ -0,0 +1,46 @@
name: Lint
on:
pull_request:
branches: [releases]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v3
with:
version: "0.4.20"
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version-file: "pyproject.toml"
- name: Install dependencies
run: |
uv sync --prerelease=allow --dev --extra tests
- name: Run isort --check
run: |
uv run --prerelease=allow isort --check custom_components/ics_calendar tests
- name: Run black --check
run: |
uv run --prerelease=allow black --check custom_components/ics_calendar tests
- name: Run flake8
run: |
uv run --prerelease=allow flake8
- name: Run pydocstyle
run: |
uv run --prerelease=allow pydocstyle -v custom_components/ics_calendar tests
- name: Run pylint
run: |
uv run --prerelease=allow pylint custom_components/ics_calendar
@@ -0,0 +1,44 @@
name: Run Tests
on:
push:
branches: [releases]
pull_request:
branches: [releases]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Set timezone
run: |
sudo timedatectl set-timezone America/New_York
timedatectl
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v3
with:
version: "0.4.20"
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version-file: "pyproject.toml"
- name: Install dependencies
run: |
uv sync --prerelease=allow --extra tests
- name: Run pytest
run: |
PYTHONDONTWRITEBYTECODE=1 uv run --prerelease=allow pytest tests/
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}
+1 -1
View File
@@ -203,7 +203,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = full_data
await hass.config_entries.async_forward_entry_setups(entry, ["calendar"])
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
+9 -8
View File
@@ -286,18 +286,15 @@ class ICSCalendarData: # pylint: disable=R0902
minutes=device_data[CONF_DOWNLOAD_INTERVAL]
),
},
)
self._calendar_data.set_headers(
).headers(
device_data[CONF_USERNAME],
device_data[CONF_PASSWORD],
device_data[CONF_USER_AGENT],
device_data[CONF_ACCEPT_HEADER],
)
if device_data.get(CONF_SET_TIMEOUT):
self._calendar_data.set_timeout(
device_data[CONF_CONNECTION_TIMEOUT]
self._calendar_data.timeout(
device_data.get(CONF_CONNECTION_TIMEOUT)
)
async def async_get_events(
@@ -313,7 +310,9 @@ class ICSCalendarData: # pylint: disable=R0902
event_list: list[ParserEvent] = []
if await self._calendar_data.download_calendar():
_LOGGER.debug("%s: Setting calendar content", self.name)
self.parser.set_content(self._calendar_data.get())
await self._hass.async_add_executor_job(
lambda: self.parser.set_content(self._calendar_data.get())
)
try:
event_list = self.parser.get_event_list(
start=start_date,
@@ -345,7 +344,9 @@ class ICSCalendarData: # pylint: disable=R0902
parser_event: ParserEvent | None = None
if await self._calendar_data.download_calendar():
_LOGGER.debug("%s: Setting calendar content", self.name)
self.parser.set_content(self._calendar_data.get())
await self._hass.async_add_executor_job(
lambda: self.parser.set_content(self._calendar_data.get())
)
try:
parser_event: ParserEvent | None = self.parser.get_current_event(
include_all_day=self.include_all_day,
@@ -91,7 +91,7 @@ class CalendarData: # pylint: disable=R0902
"""
return self._calendar_data
def set_headers(
def headers(
self,
user_name: str,
password: str,
@@ -124,14 +124,17 @@ class CalendarData: # pylint: disable=R0902
self._headers.append(("User-agent", user_agent))
if accept_header != "":
self._headers.append(("Accept", accept_header))
return self
def set_timeout(self, connection_timeout: float):
def timeout(self, connection_timeout: float | None):
"""Set the connection timeout.
:param connection_timeout: The timeout value in seconds.
:type connection_timeout: float
"""
self.connection_timeout = connection_timeout
if connection_timeout:
self.connection_timeout = connection_timeout
return self
def _decode_data(self, data):
return data.replace("\0", "")
@@ -202,7 +205,6 @@ class CalendarData: # pylint: disable=R0902
def _get_month_year(self, url: str, month: int, year: int) -> int:
(month, url) = self._get_year_as_months(url, month)
print(f"month: {month}\n")
month_match = re.search("\\{month([-+])([0-9]+)\\}", url)
if month_match:
if month_match.group(1) == "-":
+19 -14
View File
@@ -88,6 +88,10 @@ TIMEOUT_OPTS_SCHEMA = vol.Schema(
{vol.Optional(CONF_CONNECTION_TIMEOUT, default=None): cv.positive_float}
)
FILTER_DOC_URL = (
"https://github.com/franc6/ics_calendar/blob/releases/README.md#filters"
)
def is_array_string(arr_str: str) -> bool:
"""Return true if arr_str starts with [ and ends with ]."""
@@ -231,6 +235,7 @@ class ICSCalendarConfigFlow(ConfigFlow, domain=DOMAIN):
data_schema=CALENDAR_OPTS_SCHEMA,
errors=errors,
last_step=False,
description_placeholders={"filterdoc": FILTER_DOC_URL},
)
async def async_step_connect_opts(
@@ -286,14 +291,14 @@ class ICSCalendarConfigFlow(ConfigFlow, domain=DOMAIN):
errors = {}
if user_input is not None:
if not errors:
self.data.update(user_input)
if user_input.get(CONF_SET_TIMEOUT, False):
return await self.async_step_timeout_opts()
return self.async_create_entry(
title=self.data[CONF_NAME],
data=self.data,
)
# if not errors:
self.data.update(user_input)
if user_input.get(CONF_SET_TIMEOUT, False):
return await self.async_step_timeout_opts()
return self.async_create_entry(
title=self.data[CONF_NAME],
data=self.data,
)
return self.async_show_form(
step_id="adv_connect_opts",
@@ -308,12 +313,12 @@ class ICSCalendarConfigFlow(ConfigFlow, domain=DOMAIN):
errors = {}
if user_input is not None:
if not errors:
self.data.update(user_input)
return self.async_create_entry(
title=self.data[CONF_NAME],
data=self.data,
)
# if not errors:
self.data.update(user_input)
return self.async_create_entry(
title=self.data[CONF_NAME],
data=self.data,
)
return self.async_show_form(
step_id="timeout_opts",
+1 -1
View File
@@ -1,6 +1,6 @@
"""Constants for ics_calendar platform."""
VERSION = "5.1.3"
VERSION = "5.1.7"
DOMAIN = "ics_calendar"
CONF_DEVICE_ID = "device_id"
+8 -2
View File
@@ -1,8 +1,6 @@
"""Provide GetParser class."""
from .icalendarparser import ICalendarParser
from .parsers.parser_ics import ParserICS
from .parsers.parser_rie import ParserRIE
class GetParser: # pylint: disable=R0903
@@ -20,8 +18,16 @@ class GetParser: # pylint: disable=R0903
# if parser_cls is not None:
# return parser_cls(*args)
if parser == "rie":
from .parsers.parser_rie import ( # pylint: disable=C0415
ParserRIE,
)
return ParserRIE(*args)
if parser == "ics":
from .parsers.parser_ics import ( # pylint: disable=C0415
ParserICS,
)
return ParserICS(*args)
return None
+2 -2
View File
@@ -8,6 +8,6 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"issue_tracker": "https://github.com/franc6/ics_calendar/issues",
"requirements": ["icalendar~=6.1","python-dateutil>=2.9.0.post0","pytz>=2024.1","recurring_ical_events~=3.5,>=3.5.2","ics==0.7.2","arrow","httpx_auth>=0.22.0,<=0.23.1"],
"version": "5.1.3"
"requirements": ["icalendar~=6.1","python-dateutil>=2.9.0.post0","pytz>=2024.1","recurring_ical_events~=3.5,>=3.5.2","ics==0.7.2","arrow","httpx_auth>=0.22.0,<=0.23.1","tatsu>=4.2.6,<5.8.0"],
"version": "5.1.7"
}
@@ -76,7 +76,12 @@ class ParserICS(ICalendarParser):
# summary = event.summary
# elif hasattr(event, "name"):
summary = event.name
rrule = None
for extra in event.extra:
if extra.name == "RRULE":
rrule = extra.value
calendar_event: ParserEvent = ParserEvent(
uid=event.uid,
summary=summary,
start=ParserICS.get_date(
event.begin, event.all_day, offset_hours
@@ -86,6 +91,14 @@ class ParserICS(ICalendarParser):
),
location=event.location,
description=event.description,
rrule=rrule,
recurrence_id=(
ParserICS.get_date(
event.begin, event.all_day, offset_hours
)
if rrule
else None
),
)
if self._filter.filter_event(calendar_event):
event_list.append(calendar_event)
@@ -145,7 +158,12 @@ class ParserICS(ICalendarParser):
# summary = temp_event.summary
# elif hasattr(event, "name"):
summary = temp_event.name
rrule = None
for extra in temp_event.extra:
if extra.name == "RRULE":
rrule = extra.value
return ParserEvent(
uid=temp_event.uid,
summary=summary,
start=ParserICS.get_date(
temp_event.begin, temp_event.all_day, offset_hours
@@ -155,6 +173,14 @@ class ParserICS(ICalendarParser):
),
location=temp_event.location,
description=temp_event.description,
rrule=rrule,
recurrence_id=(
ParserICS.get_date(
temp_event.begin, temp_event.all_day, offset_hours
)
if rrule
else None
),
)
@staticmethod
@@ -75,11 +75,16 @@ class ParserRIE(ICalendarParser):
continue
calendar_event: ParserEvent = ParserEvent(
uid=event.get("UID"),
summary=event.get("SUMMARY"),
start=start,
end=end,
location=event.get("LOCATION"),
description=event.get("DESCRIPTION"),
# rrule=event.get("RRULE"),
recurrence_id=ParserRIE.get_date(
event.get("RECURRENCE-ID").dt
),
)
if self._filter.filter_event(calendar_event):
event_list.append(calendar_event)
@@ -141,11 +146,16 @@ class ParserRIE(ICalendarParser):
return None
return ParserEvent(
uid=temp_event.get("UID"),
summary=temp_event.get("SUMMARY"),
start=temp_start,
end=temp_end,
location=temp_event.get("LOCATION"),
description=temp_event.get("DESCRIPTION"),
# rrule=temp_event.get("RRULE"),
recurrence_id=ParserRIE.get_date(
temp_event.get("RECURRENCE-ID").dt
),
)
@staticmethod
+2 -2
View File
@@ -68,8 +68,8 @@
"empty_url": "The url must not be empty.",
"download_interval_too_small": "The download interval must be at least 15.",
"exclude_include_cannot_be_the_same": "The exclude and include strings must not be the same",
"exclude_must_be_array": "The exclude option must be an array of strings or regular expressions. See https://github.com/franc6/ics_calendar/blob/releases/README.md#filters for more information.",
"include_must_be_array": "The include option must be an array of strings or regular expressions. See https://github.com/franc6/ics_calendar/blob/releases/README.md#filters for more information."
"exclude_must_be_array": "The exclude option must be an array of strings or regular expressions. See {filterdoc} for more information.",
"include_must_be_array": "The include option must be an array of strings or regular expressions. See {filterdoc} for more information."
},
"abort": {
}
@@ -67,8 +67,8 @@
"empty_url": "Die URL darf nicht leer sein.",
"download_interval_too_small": "Das Download-Intervall muss mindestens 15 betragen.",
"exclude_include_cannot_be_the_same": "Die Ausschluss- und Einschluss-Strings dürfen nicht identisch sein.",
"exclude_must_be_array": "Die \"auszuschließenden Ereignisse\" müssen ein Array von Zeichenfolgen oder regulären Ausdrücken sein. Weitere Informationen finden Sie unter https://github.com/franc6/ics_calendar/blob/releases/README.md#filters.",
"include_must_be_array": "Die \"einzuschließenden Ereignisse\" müssen ein Array von Zeichenfolgen oder regulären Ausdrücken sein. Weitere Informationen finden Sie unter https://github.com/franc6/ics_calendar/blob/releases/README.md#filters."
"exclude_must_be_array": "Die \"auszuschließenden Ereignisse\" müssen ein Array von Zeichenfolgen oder regulären Ausdrücken sein. Weitere Informationen finden Sie unter {filterdoc}.",
"include_must_be_array": "Die \"einzuschließenden Ereignisse\" müssen ein Array von Zeichenfolgen oder regulären Ausdrücken sein. Weitere Informationen finden Sie unter {filterdoc}."
},
"abort": {
}
@@ -68,8 +68,8 @@
"empty_url": "The url must not be empty.",
"download_interval_too_small": "The download interval must be at least 15.",
"exclude_include_cannot_be_the_same": "The exclude and include strings must not be the same",
"exclude_must_be_array": "The exclude option must be an array of strings or regular expressions. See https://github.com/franc6/ics_calendar/blob/releases/README.md#filters for more information.",
"include_must_be_array": "The include option must be an array of strings or regular expressions. See https://github.com/franc6/ics_calendar/blob/releases/README.md#filters for more information."
"exclude_must_be_array": "The exclude option must be an array of strings or regular expressions. See {filterdoc} for more information.",
"include_must_be_array": "The include option must be an array of strings or regular expressions. See {filterdoc} for more information."
},
"abort": {
}
@@ -68,8 +68,8 @@
"empty_url": "La url no debe estar vacía.",
"download_interval_too_small": "El intervalo de descarga debe ser de al menos 15.",
"exclude_include_cannot_be_the_same": "Las cadenas de exclusión e inclusión no deben ser las mismas",
"exclude_must_be_array": "La opción de exclusión debe ser una matriz de cadenas o expresiones regulares. Consulte https://github.com/franc6/ics_calendar/blob/releases/README.md#filters para obtener más información.",
"include_must_be_array": "La opción de inclusión debe ser un array de cadenas o expresiones regulares. Consulte https://github.com/franc6/ics_calendar/blob/releases/README.md#filters para obtener más información."
"exclude_must_be_array": "La opción de exclusión debe ser una matriz de cadenas o expresiones regulares. Consulte {filterdoc} para obtener más información.",
"include_must_be_array": "La opción de inclusión debe ser un array de cadenas o expresiones regulares. Consulte {filterdoc} para obtener más información."
},
"abort": {
}
@@ -67,8 +67,8 @@
"empty_url": "L'URL du calendrier doit être renseignée.",
"download_interval_too_small": "L'intervalle de téléchargement ne peut pas être inférieur à 15 minutes.",
"exclude_include_cannot_be_the_same": "Les valeurs d'exclusion et d'inclusion ne peuvent pas être identiques.",
"exclude_must_be_array": "The exclude option must be an array of strings or regular expressions. See https://github.com/franc6/ics_calendar/blob/releases/README.md#filters for more information.",
"include_must_be_array": "The include option must be an array of strings or regular expressions. See https://github.com/franc6/ics_calendar/blob/releases/README.md#filters for more information."
"exclude_must_be_array": "L'option d'exclusion doit être un tableau de chaînes ou d'expressions régulières. Voir {filterdoc} pour plus d'informations.",
"include_must_be_array": "L'option d'inclusion doit être un tableau de chaînes ou d'expressions régulières. Voir {filterdoc} pour plus d'informations."
},
"abort": {
}
@@ -68,8 +68,8 @@
"empty_url": "A URL não pode estar vazia.",
"download_interval_too_small": "O intervalo de download deve ser de pelo menos 15.",
"exclude_include_cannot_be_the_same": "As strings de exclusão e inclusão não podem ser as mesmas.",
"exclude_must_be_array": "A opção de exclusão deve ser um array de strings ou expressões regulares. Veja https://github.com/franc6/ics_calendar/blob/releases/README.md#filters para mais informações.",
"include_must_be_array": "A opção de inclusão deve ser um array de strings ou expressões regulares. Veja https://github.com/franc6/ics_calendar/blob/releases/README.md#filters para mais informações."
"exclude_must_be_array": "A opção de exclusão deve ser um array de strings ou expressões regulares. Veja {filterdoc} para mais informações.",
"include_must_be_array": "A opção de inclusão deve ser um array de strings ou expressões regulares. Veja {filterdoc} para mais informações."
},
"abort": {
}
Binary file not shown.
@@ -38,12 +38,16 @@ ota:
platform: esphome
password: !secret ota
network:
enable_ipv6: true
wifi:
ssid: "Voltage-legacy"
password: !secret voltage_legacy_psk
use_address: atorch-dc-load.home
#use_address: atorch-dc-load.home
power_save_mode: high
fast_connect: on
min_auth_mode: WPA2
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
@@ -0,0 +1,293 @@
substitutions:
name: "owon-b35t"
friendly_name: "OWON B35T Multimeter"
device_description: "M5Stack Core 1 BLE client for OWON B35T/B35T+ multimeter with local graphical display"
owon_mac_address: !secret owon_b35t_mac_address
esphome:
name: ${name}
friendly_name: ${friendly_name}
comment: ${device_description}
min_version: 2024.6.0
includes:
- owon_b35t-m5stack-core1.h
project:
name: "custom.owon-b35t-m5stack"
version: "1.0"
esp32:
board: m5stack-core-esp32
framework:
type: esp-idf
advanced:
minimum_chip_revision: "3.1"
logger:
level: INFO
api:
encryption:
key: !secret apikey
ota:
platform: esphome
password: !secret ota
wifi:
ssid: "Voltage-legacy"
password: !secret voltage_legacy_psk
#use_address: ${name}.home
power_save_mode: none
fast_connect: on
min_auth_mode: WPA2
ap:
ssid: "OWON B35T Fallback Hotspot"
password: !secret fallback_psk
# Disabled to save RAM on the M5Stack Core 1. Re-enable temporarily if Wi-Fi recovery is needed.
# captive_portal:
interval:
- interval: 10s
then:
- lambda: |-
ESP_LOGI("mem", "heap free=%u min_free=%u internal_free=%u internal_largest=%u dma_free=%u dma_largest=%u",
static_cast<unsigned>(esp_get_free_heap_size()),
static_cast<unsigned>(esp_get_minimum_free_heap_size()),
static_cast<unsigned>(heap_caps_get_free_size(MALLOC_CAP_INTERNAL)),
static_cast<unsigned>(heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL)),
static_cast<unsigned>(heap_caps_get_free_size(MALLOC_CAP_DMA)),
static_cast<unsigned>(heap_caps_get_largest_free_block(MALLOC_CAP_DMA)));
esp32_ble_tracker:
scan_parameters:
active: true
continuous: true
ble_client:
- mac_address: ${owon_mac_address}
id: owon_ble_client
on_connect:
then:
- lambda: |-
owon_meter.on_connect();
on_disconnect:
then:
- lambda: |-
owon_meter.on_disconnect();
- logger.log:
level: WARN
format: "OWON BLE meter disconnected; restarting M5Stack to reclaim heap"
- delay: 1s
- lambda: |-
esp_restart();
spi:
clk_pin: GPIO18
mosi_pin: GPIO23
miso_pin: GPIO19
output:
- platform: ledc
pin: GPIO32
id: lcd_backlight
light:
- platform: monochromatic
output: lcd_backlight
name: "${friendly_name} Backlight"
id: backlight
restore_mode: ALWAYS_ON
font:
- file: "fonts/Roboto-Regular.ttf"
id: meter_font
size: 15
glyphs:
[
" ",
"!",
"%",
"+",
"-",
".",
"/",
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"<",
">",
"A",
"B",
"C",
"D",
"E",
"F",
"G",
"H",
"I",
"L",
"M",
"N",
"O",
"P",
"R",
"S",
"T",
"U",
"V",
"W",
"X",
"Y",
"Z",
"a",
"c",
"d",
"e",
"f",
"g",
"h",
"i",
"k",
"m",
"n",
"o",
"r",
"s",
"t",
"u",
"v",
"w",
"y",
"z",
"°",
"µ",
"Ω",
]
display:
- platform: ili9xxx
id: lcd
model: M5STACK
cs_pin: GPIO14
dc_pin: GPIO27
reset_pin: GPIO33
invert_colors: true
color_palette: 8BIT
rotation: 0
update_interval: 500ms
lambda: |-
owon_meter.render(it, id(meter_font));
binary_sensor:
- platform: gpio
id: button_a
pin:
number: GPIO39
inverted: true
internal: true
on_press:
then:
- lambda: |-
owon_meter.previous_button();
- platform: gpio
id: button_b
pin:
number: GPIO38
inverted: true
internal: true
on_click:
- min_length: 50ms
max_length: 1500ms
then:
- logger.log:
level: INFO
format: "OWON short press: %s"
args: ["owon_meter.selected_button_name()"]
- ble_client.ble_write:
id: owon_ble_client
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb"
value: !lambda |-
std::vector<uint8_t> data = {owon_meter.selected_button, 0x01};
return data;
- min_length: 1500ms
max_length: 5000ms
then:
- logger.log:
level: INFO
format: "OWON long press: %s"
args: ["owon_meter.selected_button_name()"]
- ble_client.ble_write:
id: owon_ble_client
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb"
value: !lambda |-
uint8_t press_type = (owon_meter.selected_button == 1 || owon_meter.selected_button == 5) ? 0x01 : 0x00;
std::vector<uint8_t> data = {owon_meter.selected_button, press_type};
return data;
- platform: gpio
id: button_c
pin:
number: GPIO37
inverted: true
internal: true
on_press:
then:
- lambda: |-
owon_meter.next_button();
- platform: template
name: "${friendly_name} Connected"
lambda: |-
return owon_meter.connected;
- platform: template
name: "${friendly_name} Overload"
lambda: |-
return owon_meter.overload;
- platform: template
name: "${friendly_name} Low Battery"
lambda: |-
return owon_meter.low_battery;
sensor:
- platform: ble_client
type: characteristic
ble_client_id: owon_ble_client
id: owon_notify_source
internal: true
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
characteristic_uuid: "0000fff4-0000-1000-8000-00805f9b34fb"
notify: true
update_interval: never
lambda: |-
owon_meter.handle_notify(x);
return owon_meter.value();
- platform: wifi_signal
name: "${friendly_name} WiFi Signal"
update_interval: 60s
text_sensor:
- platform: template
name: "${friendly_name} Reading"
update_interval: 2s
lambda: |-
return owon_meter.reading_text();
- platform: template
name: "${friendly_name} Unit"
update_interval: 2s
lambda: |-
return std::string(owon_meter.scale()) + owon_meter.unit();
- platform: template
name: "${friendly_name} Mode"
update_interval: 2s
lambda: |-
return owon_meter.mode_text();
+658
View File
@@ -0,0 +1,658 @@
/*
* ESPHome helper for OWON B35T/B35T+ BLE meter on M5Stack Core 1.
* Parser is based on the standalone Arduino sketch by Reaper7
* (Beerware license, Revision 42) and Dean Cording's owonb35 notes.
*/
#pragma once
#include <cmath>
#include <cstdint>
#include <cstring>
#include <string>
#include <vector>
#include "esp_heap_caps.h"
#include "esp_system.h"
#include "esphome/core/helpers.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/core/log.h"
#include "esphome/components/display/display.h"
namespace owon_b35t {
using esphome::Color;
using esphome::display::Display;
static const char *const TAG = "owon_b35t";
static const char *const POWER_TAG = "core2_power";
static constexpr uint8_t AXP192_ADDR = 0x34;
static esphome::i2c::I2CDevice axp192;
static bool axp192_ready = false;
static bool axp_write(uint8_t reg, uint8_t value) {
if (!axp192_ready) return false;
bool ok = axp192.write_byte(reg, value);
if (!ok) ESP_LOGW(POWER_TAG, "AXP192 write reg 0x%02X failed", reg);
return ok;
}
static bool axp_read(uint8_t reg, uint8_t *value) {
if (!axp192_ready) return false;
bool ok = axp192.read_byte(reg, value);
if (!ok) ESP_LOGW(POWER_TAG, "AXP192 read reg 0x%02X failed", reg);
return ok;
}
static void axp_update(uint8_t reg, uint8_t clear_mask, uint8_t set_mask) {
uint8_t value = 0;
if (!axp_read(reg, &value)) return;
value = (value & ~clear_mask) | set_mask;
axp_write(reg, value);
}
static uint8_t axp_dc_voltage_data(uint16_t millivolts) {
if (millivolts < 700) millivolts = 700;
if (millivolts > 3500) millivolts = 3500;
return static_cast<uint8_t>((millivolts - 700) / 25) & 0x7F;
}
static uint8_t axp_ldo_voltage_data(uint16_t millivolts) {
if (millivolts < 1800) millivolts = 1800;
if (millivolts > 3300) millivolts = 3300;
return static_cast<uint8_t>((millivolts - 1800) / 100) & 0x0F;
}
static void core2_axp192_set_lcd_voltage(uint16_t millivolts) {
uint8_t value = 0;
axp_read(0x27, &value);
axp_write(0x27, (value & 0x80) | axp_dc_voltage_data(millivolts)); // DCDC3, LCD backlight
}
static void core2_axp192_set_backlight(float brightness) {
if (brightness <= 0.0f) {
axp_update(0x12, 0x02, 0x00); // DCDC3 off
return;
}
if (brightness > 1.0f) brightness = 1.0f;
uint16_t millivolts = static_cast<uint16_t>(2400 + brightness * 900);
core2_axp192_set_lcd_voltage(millivolts);
axp_update(0x12, 0x00, 0x02); // DCDC3 on
}
static void core2_axp192_init(esphome::i2c::I2CBus *bus) {
axp192.set_i2c_bus(bus);
axp192.set_i2c_address(AXP192_ADDR);
axp192_ready = true;
ESP_LOGI(POWER_TAG, "Initializing M5Stack Core2 AXP192 LCD power");
axp_update(0x30, 0xF9, 0x02); // Disable VBUS current limit, preserve bit 2.
axp_update(0x92, 0x07, 0x00); // GPIO1 open-drain output.
axp_update(0x93, 0x07, 0x00); // GPIO2 open-drain output.
axp_write(0x35, 0xA2); // RTC battery charging.
uint8_t value = 0;
axp_read(0x26, &value);
axp_write(0x26, (value & 0x80) | axp_dc_voltage_data(3350)); // DCDC1 ESP32 VDD.
core2_axp192_set_lcd_voltage(2800);
uint8_t ldo2 = axp_ldo_voltage_data(3300);
uint8_t ldo3 = axp_ldo_voltage_data(2000);
axp_write(0x28, (ldo2 << 4) | ldo3); // LDO2 LCD logic/SD, LDO3 vibrator.
axp_update(0x12, 0x00, 0x07); // Enable DCDC1, DCDC3, LDO2.
axp_write(0x82, 0xFF); // ADCs on.
axp_update(0x95, 0x8D, 0x84); // GPIO4 setup, as M5Core2 library does.
axp_write(0x36, 0x4C); // Power key timing.
// LCD reset through AXP192 GPIO4.
axp_update(0x96, 0x02, 0x00);
delay(100);
axp_update(0x96, 0x00, 0x02);
delay(100);
core2_axp192_set_backlight(1.0f);
}
static const uint8_t ACCU_BMP[32] = {
0b00000000, 0b00000000,
0b00000000, 0b00000000,
0b00000000, 0b00000000,
0b11111111, 0b11111110,
0b10000000, 0b00000010,
0b10000000, 0b00000011,
0b10000000, 0b00000011,
0b10000000, 0b00000011,
0b10000000, 0b00000011,
0b10000000, 0b00000011,
0b10000000, 0b00000011,
0b10000000, 0b00000010,
0b11111111, 0b11111110,
0b00000000, 0b00000000,
0b00000000, 0b00000000,
0b00000000, 0b00000000,
};
static const uint8_t BLE_BMP[32] = {
0b00000001, 0b10000000,
0b00000001, 0b11000000,
0b00010001, 0b01100000,
0b00011001, 0b00110000,
0b00001101, 0b00011000,
0b00000111, 0b00110000,
0b00000011, 0b01100000,
0b00000001, 0b11000000,
0b00000001, 0b11000000,
0b00000011, 0b01100000,
0b00000111, 0b00110000,
0b00001101, 0b00011000,
0b00011001, 0b00110000,
0b00010001, 0b01100000,
0b00000001, 0b11000000,
0b00000001, 0b10000000,
};
static const uint8_t DIODE_BMP[32] = {
0b00001000, 0b00011000,
0b00001100, 0b00011000,
0b00001110, 0b00011000,
0b00001111, 0b00011000,
0b00001111, 0b10011000,
0b00001111, 0b11011000,
0b00001111, 0b11111000,
0b11111111, 0b11111111,
0b11111111, 0b11111111,
0b00001111, 0b11111000,
0b00001111, 0b11011000,
0b00001111, 0b10011000,
0b00001111, 0b00011000,
0b00001110, 0b00011000,
0b00001100, 0b00011000,
0b00001000, 0b00011000,
};
static const uint8_t BUZZ_BMP[32] = {
0b00000000, 0b11000000,
0b00000001, 0b11000000,
0b00000011, 0b11000001,
0b00000111, 0b11000001,
0b00001111, 0b11000101,
0b11111111, 0b11000101,
0b11111111, 0b11010101,
0b11111111, 0b11010101,
0b11111111, 0b11010101,
0b11111111, 0b11010101,
0b11111111, 0b11000101,
0b00001111, 0b11000101,
0b00000111, 0b11000001,
0b00000011, 0b11000001,
0b00000001, 0b11000000,
0b00000000, 0b11000000,
};
class Meter {
public:
static constexpr uint8_t REGPLUSMINUS = 0x00;
static constexpr uint8_t FLAGPLUS = 0b00101011;
static constexpr uint8_t FLAGMINUS = 0b00101101;
static constexpr uint8_t REGDIG1 = 0x01;
static constexpr uint8_t REGDIG2 = 0x02;
static constexpr uint8_t REGDIG3 = 0x03;
static constexpr uint8_t REGDIG4 = 0x04;
static constexpr uint8_t REGPOINT = 0x06;
static constexpr uint8_t FLAGPOINT0 = 0b00110000;
static constexpr uint8_t FLAGPOINT1 = 0b00110001;
static constexpr uint8_t FLAGPOINT2 = 0b00110010;
static constexpr uint8_t FLAGPOINT3 = 0b00110100;
static constexpr uint8_t REGMODE = 0x07;
static constexpr uint8_t FLAGMODEHOLD = 0b00000010;
static constexpr uint8_t FLAGMODEREL = 0b00000100;
static constexpr uint8_t FLAGMODEAC = 0b00001000;
static constexpr uint8_t FLAGMODEDC = 0b00010000;
static constexpr uint8_t FLAGMODEAUTO = 0b00100000;
static constexpr uint8_t REGMINMAX = 0x08;
static constexpr uint8_t FLAGMIN = 0b00010000;
static constexpr uint8_t FLAGMAX = 0b00100000;
static constexpr uint8_t REGSCALE = 0x09;
static constexpr uint8_t FLAGSCALEDUTY = 0b00000010;
static constexpr uint8_t FLAGSCALEDIODE = 0b00000100;
static constexpr uint8_t FLAGSCALEBUZZ = 0b00001000;
static constexpr uint8_t FLAGSCALEMEGA = 0b00010000;
static constexpr uint8_t FLAGSCALEKILO = 0b00100000;
static constexpr uint8_t FLAGSCALEMILLI = 0b01000000;
static constexpr uint8_t FLAGSCALEMICRO = 0b10000000;
static constexpr uint8_t REGUNIT = 0x0a;
static constexpr uint8_t FLAGUNITFAHR = 0b00000001;
static constexpr uint8_t FLAGUNITGRAD = 0b00000010;
static constexpr uint8_t FLAGUNITNF = 0b00000100;
static constexpr uint8_t FLAGUNITHZ = 0b00001000;
static constexpr uint8_t FLAGUNITHFE = 0b00010000;
static constexpr uint8_t FLAGUNITOHM = 0b00100000;
static constexpr uint8_t FLAGUNITAMP = 0b01000000;
static constexpr uint8_t FLAGUNITVOLT = 0b10000000;
bool connected{false};
bool write_available{false};
bool is_plus{false};
bool low_battery{false};
bool overload{false};
bool has_reading{false};
uint8_t selected_button{1};
uint32_t last_notify_ms{0};
bool handle_notify(const std::vector<uint8_t> &data) {
if (data.size() > sizeof(this->raw_))
return false;
if (data.size() == 6 && data[1] >= 0xF0) {
memset(this->raw_, 0, sizeof(this->raw_));
memcpy(this->raw_, data.data(), data.size());
this->is_plus = true;
this->parse_plus_();
} else if (data.size() == 14 && data[12] == 0x0D && data[13] == 0x0A) {
memset(this->value_, 0, sizeof(this->value_));
memcpy(this->value_, data.data(), data.size());
this->is_plus = false;
} else {
ESP_LOGW(TAG, "Ignoring unexpected OWON frame length=%u", static_cast<unsigned>(data.size()));
return false;
}
this->overload = memcmp(this->value_, OVERLOAD_FRAME, sizeof(OVERLOAD_FRAME)) == 0;
this->display_value = this->calc_display_value_();
this->base_value = this->calc_base_value_();
this->has_reading = true;
this->last_notify_ms = millis();
return true;
}
void on_connect() {
this->connected = true;
this->write_available = true;
}
void on_disconnect() {
this->connected = false;
this->write_available = false;
}
float value() const { return this->display_value; }
float value_base() const { return this->base_value; }
bool negative() const { return (this->value_[REGPLUSMINUS] & FLAGMINUS) == FLAGMINUS; }
bool auto_range() const { return (this->value_[REGMODE] & FLAGMODEAUTO) == FLAGMODEAUTO; }
bool hold() const { return (this->value_[REGMODE] & FLAGMODEHOLD) == FLAGMODEHOLD; }
bool relative() const { return (this->value_[REGMODE] & FLAGMODEREL) == FLAGMODEREL; }
bool ac() const { return (this->value_[REGMODE] & FLAGMODEAC) == FLAGMODEAC; }
bool dc() const { return (this->value_[REGMODE] & FLAGMODEDC) == FLAGMODEDC; }
bool min_mode() const { return (this->value_[REGMINMAX] & FLAGMIN) == FLAGMIN; }
bool max_mode() const { return (this->value_[REGMINMAX] & FLAGMAX) == FLAGMAX; }
bool diode() const { return (this->value_[REGSCALE] & FLAGSCALEDIODE) == FLAGSCALEDIODE; }
bool continuity() const { return (this->value_[REGSCALE] & FLAGSCALEBUZZ) == FLAGSCALEBUZZ; }
const char *unit() const {
switch (this->value_[REGUNIT]) {
case FLAGUNITFAHR: return "°F";
case FLAGUNITGRAD: return "°C";
case FLAGUNITNF: return "nF";
case FLAGUNITHZ: return "Hz";
case FLAGUNITHFE: return "hFE";
case FLAGUNITOHM: return "Ω";
case FLAGUNITAMP: return "A";
case FLAGUNITVOLT: return "V";
default: return "";
}
}
const char *scale() const {
if ((this->value_[REGSCALE] & FLAGSCALEDUTY) == FLAGSCALEDUTY) return "%";
if ((this->value_[REGSCALE] & FLAGSCALEMEGA) == FLAGSCALEMEGA) return "M";
if ((this->value_[REGSCALE] & FLAGSCALEKILO) == FLAGSCALEKILO) return "k";
if ((this->value_[REGSCALE] & FLAGSCALEMILLI) == FLAGSCALEMILLI) return "m";
if ((this->value_[REGSCALE] & FLAGSCALEMICRO) == FLAGSCALEMICRO) return "µ";
return "";
}
std::string mode_text() const {
std::string out;
if (this->dc()) out += "DC ";
if (this->ac()) out += "AC ";
if (this->auto_range()) out += "AUTO ";
if (this->hold()) out += "HOLD ";
if (this->relative()) out += "REL ";
if (this->min_mode()) out += "MIN ";
if (this->max_mode()) out += "MAX ";
if (this->diode()) out += "DIODE ";
if (this->continuity()) out += "CONT ";
if (!out.empty()) out.pop_back();
return out;
}
std::string reading_text() const {
if (!this->connected) return "Disconnected";
if (!this->has_reading) return "Waiting for data";
if (this->overload) return "OL " + std::string(this->scale()) + this->unit();
char buf[48];
snprintf(buf, sizeof(buf), "%s%.4g %s%s", this->negative() ? "-" : "", std::fabs(this->display_value), this->scale(), this->unit());
return std::string(buf);
}
enum Kind { KIND_OTHER, KIND_VOLTAGE, KIND_CURRENT, KIND_RESISTANCE, KIND_FREQUENCY, KIND_CAPACITANCE, KIND_TEMP_C, KIND_TEMP_F, KIND_DUTY };
Kind kind() const {
if ((this->value_[REGSCALE] & FLAGSCALEDUTY) == FLAGSCALEDUTY) return KIND_DUTY;
switch (this->value_[REGUNIT]) {
case FLAGUNITVOLT: return KIND_VOLTAGE;
case FLAGUNITAMP: return KIND_CURRENT;
case FLAGUNITOHM: return KIND_RESISTANCE;
case FLAGUNITHZ: return KIND_FREQUENCY;
case FLAGUNITNF: return KIND_CAPACITANCE;
case FLAGUNITGRAD: return KIND_TEMP_C;
case FLAGUNITFAHR: return KIND_TEMP_F;
default: return KIND_OTHER;
}
}
const char *selected_button_name() const {
static const char *const names[] = {"SELECT", "RANGE", "HLD/LIG", "REL/BT", "HZ/DUTY", "MAX/MIN"};
uint8_t index = this->selected_button;
if (index < 1) index = 1;
if (index > 6) index = 6;
return names[index - 1];
}
void previous_button() {
if (this->selected_button > 1) this->selected_button--;
}
void next_button() {
if (this->selected_button < 6) this->selected_button++;
}
void render(Display &it, esphome::display::BaseFont *font) {
const Color bg(0, 0, 0);
const Color fg(210, 210, 210);
// Chosen to map to a neutral dark gray in the RGB332 8-bit display palette.
const Color inactive(80, 80, 80);
const Color yellow(255, 220, 0);
const Color blue(0, 80, 255);
const Color cyan(0, 255, 255);
const Color magenta(255, 0, 255);
const Color red(255, 0, 0);
const Color green(0, 220, 0);
const Color orange(255, 165, 0);
it.fill(bg);
bool status_active = this->connected && this->has_reading;
this->draw_icon_(it, 12, 8, 16, 16, ACCU_BMP, status_active ? (this->low_battery ? red : green) : inactive);
this->draw_icon_(it, 46, 8, 16, 16, BLE_BMP, this->connected ? blue : inactive);
this->label_(it, font, 86, 8, "AUTO", status_active && this->auto_range() ? fg : inactive);
this->label_(it, font, 138, 8, "MAX", status_active && this->max_mode() ? red : inactive);
this->label_(it, font, 178, 8, "MIN", status_active && this->min_mode() ? green : inactive);
this->label_(it, font, 218, 8, "HOLD", status_active && this->hold() ? blue : inactive);
this->label_(it, font, 270, 8, "REL", status_active && this->relative() ? Color(128, 128, 0) : inactive);
this->label_(it, font, 8, 66, "DC", status_active && this->dc() ? cyan : inactive);
this->label_(it, font, 8, 102, "AC", status_active && this->ac() ? magenta : inactive);
if (!this->connected) {
this->draw_digits_(it, "----", false, inactive);
it.print(160, 148, font, inactive, esphome::display::TextAlign::CENTER, "scan/connect");
} else if (!this->has_reading) {
this->draw_digits_(it, "8888", false, inactive);
it.print(160, 148, font, inactive, esphome::display::TextAlign::CENTER, "waiting");
} else if (this->overload) {
this->draw_digits_(it, " OL ", false, fg);
} else {
char d[5];
d[0] = this->digit_char_(REGDIG1);
d[1] = this->digit_char_(REGDIG2);
d[2] = this->digit_char_(REGDIG3);
d[3] = this->digit_char_(REGDIG4);
d[4] = 0;
this->draw_digits_(it, d, this->negative(), fg);
this->draw_decimal_points_(it, fg);
}
if (status_active) {
std::string unit_line = std::string(this->scale()) + this->unit();
it.print(270, 140, font, yellow, esphome::display::TextAlign::CENTER, unit_line.c_str());
}
bool bargraph_active = status_active && !this->overload;
this->draw_bargraph_(it, bargraph_active ? this->digits_from_buffer_() : 0, bargraph_active);
this->draw_icon_(it, 300, 148, 16, 16, DIODE_BMP, status_active && this->diode() ? magenta : inactive);
this->draw_icon_(it, 300, 174, 16, 16, BUZZ_BMP, status_active && this->continuity() ? orange : inactive);
it.filled_rectangle(34, 212, 40, 24, this->write_available ? fg : inactive);
it.filled_rectangle(108, 212, 100, 24, this->write_available ? fg : inactive);
it.filled_rectangle(242, 212, 40, 24, this->write_available ? fg : inactive);
it.print(54, 216, font, bg, esphome::display::TextAlign::TOP_CENTER, "<");
it.print(158, 216, font, bg, esphome::display::TextAlign::TOP_CENTER, this->selected_button_name());
it.print(262, 216, font, bg, esphome::display::TextAlign::TOP_CENTER, ">");
}
private:
uint8_t raw_[14]{};
uint8_t value_[14]{};
float display_value{NAN};
float base_value{NAN};
static constexpr uint8_t OVERLOAD_FRAME[5] = {0x2B, 0x3F, 0x30, 0x3A, 0x3F};
uint16_t digits_from_buffer_() const {
uint16_t out = 0;
if (this->value_[REGDIG1] >= '0' && this->value_[REGDIG1] <= '9') out += (this->value_[REGDIG1] - '0') * 1000;
if (this->value_[REGDIG2] >= '0' && this->value_[REGDIG2] <= '9') out += (this->value_[REGDIG2] - '0') * 100;
if (this->value_[REGDIG3] >= '0' && this->value_[REGDIG3] <= '9') out += (this->value_[REGDIG3] - '0') * 10;
if (this->value_[REGDIG4] >= '0' && this->value_[REGDIG4] <= '9') out += (this->value_[REGDIG4] - '0');
return out;
}
float calc_display_value_() const {
if (this->overload) return NAN;
if (this->is_plus) {
uint16_t pair1 = static_cast<uint16_t>(this->raw_[0]) | (static_cast<uint16_t>(this->raw_[1]) << 8);
uint8_t decimal = pair1 & 0x07;
if (decimal >= 7) return NAN;
uint16_t pair3 = static_cast<uint16_t>(this->raw_[4]) | (static_cast<uint16_t>(this->raw_[5]) << 8);
bool negative = pair3 >= 0x7FFF;
uint16_t digits = negative ? (pair3 & 0x7FFF) : pair3;
float v = static_cast<float>(digits) / std::pow(10.0f, decimal);
return negative ? -v : v;
}
uint8_t decimal = 0;
switch (this->value_[REGPOINT] & 0x07) {
case 0b001: decimal = 1; break;
case 0b010: decimal = 2; break;
case 0b100: decimal = 3; break;
default: break;
}
float v = static_cast<float>(this->digits_from_buffer_()) / std::pow(10.0f, decimal);
return this->negative() ? -v : v;
}
float calc_base_value_() const {
if (std::isnan(this->display_value)) return NAN;
if (this->value_[REGUNIT] == FLAGUNITNF) return this->display_value * 1e-9f;
if ((this->value_[REGSCALE] & FLAGSCALEMEGA) == FLAGSCALEMEGA) return this->display_value * 1e6f;
if ((this->value_[REGSCALE] & FLAGSCALEKILO) == FLAGSCALEKILO) return this->display_value * 1e3f;
if ((this->value_[REGSCALE] & FLAGSCALEMILLI) == FLAGSCALEMILLI) return this->display_value * 1e-3f;
if ((this->value_[REGSCALE] & FLAGSCALEMICRO) == FLAGSCALEMICRO) return this->display_value * 1e-6f;
return this->display_value;
}
void parse_plus_() {
memset(this->value_, 0, sizeof(this->value_));
this->value_[5] = 0x20;
this->value_[12] = 0x0D;
this->value_[13] = 0x0A;
uint16_t pair1 = static_cast<uint16_t>(this->raw_[0]) | (static_cast<uint16_t>(this->raw_[1]) << 8);
uint8_t function = (pair1 >> 6) & 0x0F;
uint8_t scale = (pair1 >> 3) & 0x07;
uint8_t decimal = pair1 & 0x07;
switch (decimal) {
case 0: this->value_[REGPOINT] = FLAGPOINT0; break;
case 1: this->value_[REGPOINT] = FLAGPOINT3; break;
case 2: this->value_[REGPOINT] = FLAGPOINT2; break;
case 3: this->value_[REGPOINT] = FLAGPOINT1; break;
default: break;
}
switch (function) {
case 0: this->value_[REGUNIT] |= FLAGUNITVOLT; this->value_[REGMODE] |= FLAGMODEDC; break;
case 1: this->value_[REGUNIT] |= FLAGUNITVOLT; this->value_[REGMODE] |= FLAGMODEAC; break;
case 2: this->value_[REGUNIT] |= FLAGUNITAMP; this->value_[REGMODE] |= FLAGMODEDC; break;
case 3: this->value_[REGUNIT] |= FLAGUNITAMP; this->value_[REGMODE] |= FLAGMODEAC; break;
case 4: this->value_[REGUNIT] |= FLAGUNITOHM; break;
case 5: this->value_[REGUNIT] |= FLAGUNITNF; break;
case 6: this->value_[REGUNIT] |= FLAGUNITHZ; break;
case 7: this->value_[REGSCALE] |= FLAGSCALEDUTY; break;
case 8: this->value_[REGUNIT] |= FLAGUNITGRAD; break;
case 9: this->value_[REGUNIT] |= FLAGUNITFAHR; break;
case 10: this->value_[REGSCALE] |= FLAGSCALEDIODE; break;
case 11: this->value_[REGSCALE] |= FLAGSCALEBUZZ; break;
case 12: this->value_[REGUNIT] |= FLAGUNITHFE; break;
default: break;
}
switch (scale) {
case 2: this->value_[REGSCALE] |= FLAGSCALEMICRO; break;
case 3: this->value_[REGSCALE] |= FLAGSCALEMILLI; break;
case 5: this->value_[REGSCALE] |= FLAGSCALEKILO; break;
case 6: this->value_[REGSCALE] |= FLAGSCALEMEGA; break;
default: break;
}
uint16_t pair2 = static_cast<uint16_t>(this->raw_[2]) | (static_cast<uint16_t>(this->raw_[3]) << 8);
if (pair2 & (1 << 0)) this->value_[REGMODE] |= FLAGMODEHOLD;
if (pair2 & (1 << 1)) this->value_[REGMODE] |= FLAGMODEREL;
if (pair2 & (1 << 2)) this->value_[REGMODE] |= FLAGMODEAUTO;
this->low_battery = (pair2 & (1 << 3)) != 0;
if (pair2 & (1 << 4)) this->value_[REGMINMAX] |= FLAGMIN;
if (pair2 & (1 << 5)) this->value_[REGMINMAX] |= FLAGMAX;
uint16_t pair3 = static_cast<uint16_t>(this->raw_[4]) | (static_cast<uint16_t>(this->raw_[5]) << 8);
if (decimal < 7) {
uint16_t digits = pair3;
if (pair3 < 0x7FFF) {
this->value_[REGPLUSMINUS] = FLAGPLUS;
} else {
this->value_[REGPLUSMINUS] = FLAGMINUS;
digits = pair3 & 0x7FFF;
}
this->value_[REGDIG1] = '0' + ((digits / 1000) % 10);
this->value_[REGDIG2] = '0' + ((digits / 100) % 10);
this->value_[REGDIG3] = '0' + ((digits / 10) % 10);
this->value_[REGDIG4] = '0' + (digits % 10);
} else {
memcpy(this->value_, OVERLOAD_FRAME, sizeof(OVERLOAD_FRAME));
}
}
char digit_char_(uint8_t reg) const {
uint8_t c = this->value_[reg];
return (c >= '0' && c <= '9') ? static_cast<char>(c) : ' ';
}
void label_(Display &it, esphome::display::BaseFont *font, int x, int y, const char *text, Color color) {
it.print(x, y, font, color, esphome::display::TextAlign::TOP_LEFT, text);
}
void draw_digits_(Display &it, const char *text, bool negative, Color color) {
if (negative) this->draw_segment_(it, 8, 88, 26, 9, true, color);
constexpr int digit_x = 40;
constexpr int digit_y = 35;
constexpr int digit_w = 50;
constexpr int digit_h = 88;
constexpr int digit_distance = 64;
for (int i = 0; i < 4; i++) {
this->draw_seven_segment_(it, digit_x + i * digit_distance, digit_y, digit_w, digit_h, text[i], color);
}
}
void draw_decimal_points_(Display &it, Color color) {
uint8_t p = this->value_[REGPOINT];
if ((p & FLAGPOINT1) == FLAGPOINT1) it.filled_rectangle(95, 117, 8, 10, color);
if ((p & FLAGPOINT2) == FLAGPOINT2) it.filled_rectangle(159, 117, 8, 10, color);
if ((p & FLAGPOINT3) == FLAGPOINT3) it.filled_rectangle(223, 117, 8, 10, color);
}
void draw_segment_(Display &it, int x, int y, int w, int h, bool horizontal, Color color) {
if (horizontal) {
int cap = h / 2;
it.filled_rectangle(x + cap, y, w - 2 * cap, h +1, color);
it.filled_triangle(x, y + cap, x + cap, y, x + cap, y + h, color);
it.filled_triangle(x + w, y + cap, x + w - cap, y, x + w - cap, y + h, color);
} else {
int cap = w / 2;
it.filled_rectangle(x, y + cap, w +1, h - 2 * cap, color);
it.filled_triangle(x + cap, y, x, y + cap, x + w, y + cap, color);
it.filled_triangle(x + cap, y + h, x, y + h - cap, x + w, y + h - cap, color);
}
}
void draw_icon_(Display &it, int x, int y, int w, int h, const uint8_t *data, Color color) {
for (int row = 0; row < h; row++) {
for (int col = 0; col < w; col++) {
uint8_t byte = data[row * ((w + 7) / 8) + (col / 8)];
if ((byte & (0x80 >> (col % 8))) != 0) {
it.draw_pixel_at(x + col, y + row, color);
}
}
}
}
void draw_seven_segment_(Display &it, int x, int y, int w, int h, char ch, Color color) {
bool a=false,b=false,c=false,d=false,e=false,f=false,g=false;
switch (ch) {
case '0': a=b=c=d=e=f=true; break;
case '1': b=c=true; break;
case '2': a=b=d=e=g=true; break;
case '3': a=b=c=d=g=true; break;
case '4': b=c=f=g=true; break;
case '5': a=c=d=f=g=true; break;
case '6': a=c=d=e=f=g=true; break;
case '7': a=b=c=true; break;
case '8': a=b=c=d=e=f=g=true; break;
case '9': a=b=c=d=f=g=true; break;
case 'O': a=b=c=d=e=f=true; break;
case 'L': d=e=f=true; break;
case '-': g=true; break;
default: break;
}
int t = 10;
int half = h / 2;
int gap = 1;
int top_y = y;
int mid_y = y + half - t / 2;
int bot_y = y + h - t;
int upper_v_y = top_y + t + gap;
int upper_v_h = mid_y - gap - upper_v_y;
int lower_v_y = mid_y + t + gap;
int lower_v_h = bot_y - gap - lower_v_y;
if (a) this->draw_segment_(it, x + t / 2, top_y, w - t, t, true, color);
if (b && upper_v_h > 0) this->draw_segment_(it, x + w - t, upper_v_y, t, upper_v_h, false, color);
if (c && lower_v_h > 0) this->draw_segment_(it, x + w - t, lower_v_y, t, lower_v_h, false, color);
if (d) this->draw_segment_(it, x + t / 2, bot_y, w - t, t, true, color);
if (e && lower_v_h > 0) this->draw_segment_(it, x, lower_v_y, t, lower_v_h, false, color);
if (f && upper_v_h > 0) this->draw_segment_(it, x, upper_v_y, t, upper_v_h, false, color);
if (g) this->draw_segment_(it, x + t / 2, mid_y, w - t, t, true, color);
}
void draw_bargraph_(Display &it, uint16_t digits, bool active) {
const Color fg(255, 255, 255);
const Color inactive(80, 80, 80);
uint16_t mapped = active ? static_cast<uint16_t>(digits * 240 / 6000) : 0;
if (mapped > 240) mapped = 240;
for (uint16_t i = 0; i <= 240; i += 4) {
Color col = (active && i <= mapped) ? fg : inactive;
int h = (i % 40 == 0) ? 20 : ((i % 20 == 0) ? 15 : 10);
it.vertical_line(40 + i, 185 - h, h, col);
}
it.horizontal_line(35, 185, 250, inactive);
}
};
} // namespace owon_b35t
static owon_b35t::Meter owon_meter;
@@ -0,0 +1,316 @@
# Derived work based on https://github.com/reaper7/M5Stack_BLE_client_Owon_B35T by reaper7.
# AI (ChatGPT) has been used to adopt the Arduino sketch to ESPHome.
# Ported to M5Stack Core2 due to memory constraints.
substitutions:
name: "owon-b35t-m5stack-core2"
friendly_name: "OWON B35T Multimeter Core2"
device_description: "M5Stack Core2 BLE client for OWON B35T/B35T+ multimeter with local graphical display"
owon_mac_address: !secret owon_b35t_mac_address
esphome:
name: ${name}
friendly_name: ${friendly_name}
comment: ${device_description}
min_version: 2024.6.0
includes:
- owon-b35t-m5stack-core2.h
on_boot:
priority: 850
then:
- lambda: |-
owon_b35t::core2_axp192_init(id(core2_i2c));
project:
name: "custom.owon-b35t-m5stack-core2"
version: "1.0"
esp32:
board: m5stack-core2
flash_size: 16MB
framework:
type: esp-idf
advanced:
minimum_chip_revision: "3.1"
sram1_as_iram: true
psram:
mode: quad
speed: 80MHz
logger:
level: INFO
api:
encryption:
key: !secret apikey
ota:
platform: esphome
password: !secret ota
wifi:
ssid: "Voltage-legacy"
password: !secret voltage_legacy_psk
#use_address: ${name}.home
power_save_mode: none
fast_connect: on
min_auth_mode: WPA2
ap:
ssid: "OWON B35T Fallback Hotspot"
password: !secret fallback_psk
captive_portal:
interval:
- interval: 10s
then:
- lambda: |-
ESP_LOGI("mem", "heap free=%u min_free=%u internal_free=%u internal_largest=%u dma_free=%u dma_largest=%u psram_free=%u psram_largest=%u",
static_cast<unsigned>(esp_get_free_heap_size()),
static_cast<unsigned>(esp_get_minimum_free_heap_size()),
static_cast<unsigned>(heap_caps_get_free_size(MALLOC_CAP_INTERNAL)),
static_cast<unsigned>(heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL)),
static_cast<unsigned>(heap_caps_get_free_size(MALLOC_CAP_DMA)),
static_cast<unsigned>(heap_caps_get_largest_free_block(MALLOC_CAP_DMA)),
static_cast<unsigned>(heap_caps_get_free_size(MALLOC_CAP_SPIRAM)),
static_cast<unsigned>(heap_caps_get_largest_free_block(MALLOC_CAP_SPIRAM)));
esp32_ble_tracker:
scan_parameters:
active: true
continuous: true
ble_client:
- mac_address: ${owon_mac_address}
id: owon_ble_client
on_connect:
then:
- lambda: |-
owon_meter.on_connect();
on_disconnect:
then:
- lambda: |-
owon_meter.on_disconnect();
spi:
clk_pin: GPIO18
mosi_pin: GPIO23
i2c:
id: core2_i2c
sda: GPIO21
scl: GPIO22
scan: true
output:
- platform: template
type: float
id: lcd_backlight
write_action:
- lambda: |-
owon_b35t::core2_axp192_set_backlight(state);
light:
- platform: monochromatic
output: lcd_backlight
name: "${friendly_name} Backlight"
id: backlight
restore_mode: ALWAYS_ON
font:
- file: "fonts/Roboto-Regular.ttf"
id: meter_font
size: 15
glyphs:
[
" ",
"!",
"%",
"+",
"-",
".",
"/",
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"<",
">",
"A",
"B",
"C",
"D",
"E",
"F",
"G",
"H",
"I",
"L",
"M",
"N",
"O",
"P",
"R",
"S",
"T",
"U",
"V",
"W",
"X",
"Y",
"Z",
"a",
"c",
"d",
"e",
"f",
"g",
"h",
"i",
"k",
"m",
"n",
"o",
"r",
"s",
"t",
"u",
"v",
"w",
"y",
"z",
"°",
"µ",
"Ω",
]
display:
- platform: mipi_spi
id: lcd
model: M5CORE2
update_interval: 500ms
lambda: |-
owon_meter.render(it, id(meter_font));
touchscreen:
- platform: ft63x6
id: touch
display: lcd
binary_sensor:
- platform: touchscreen
id: button_a
touchscreen_id: touch
x_min: 34
x_max: 74
y_min: 212
y_max: 240
internal: true
on_press:
then:
- lambda: |-
owon_meter.previous_button();
- platform: touchscreen
id: button_b
touchscreen_id: touch
x_min: 108
x_max: 208
y_min: 212
y_max: 240
internal: true
on_click:
- min_length: 50ms
max_length: 1500ms
then:
- logger.log:
level: INFO
format: "OWON short press: %s"
args: ["owon_meter.selected_button_name()"]
- ble_client.ble_write:
id: owon_ble_client
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb"
value: !lambda |-
std::vector<uint8_t> data = {owon_meter.selected_button, 0x01};
return data;
- min_length: 1500ms
max_length: 5000ms
then:
- logger.log:
level: INFO
format: "OWON long press: %s"
args: ["owon_meter.selected_button_name()"]
- ble_client.ble_write:
id: owon_ble_client
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb"
value: !lambda |-
uint8_t press_type = (owon_meter.selected_button == 1 || owon_meter.selected_button == 5) ? 0x01 : 0x00;
std::vector<uint8_t> data = {owon_meter.selected_button, press_type};
return data;
- platform: touchscreen
id: button_c
touchscreen_id: touch
x_min: 242
x_max: 282
y_min: 212
y_max: 240
internal: true
on_press:
then:
- lambda: |-
owon_meter.next_button();
- platform: template
name: "${friendly_name} Connected"
lambda: |-
return owon_meter.connected;
- platform: template
name: "${friendly_name} Overload"
lambda: |-
return owon_meter.overload;
- platform: template
name: "${friendly_name} Low Battery"
lambda: |-
return owon_meter.low_battery;
sensor:
- platform: ble_client
type: characteristic
ble_client_id: owon_ble_client
id: owon_notify_source
internal: true
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
characteristic_uuid: "0000fff4-0000-1000-8000-00805f9b34fb"
notify: true
update_interval: never
lambda: |-
owon_meter.handle_notify(x);
return owon_meter.value();
- platform: wifi_signal
name: "${friendly_name} WiFi Signal"
update_interval: 60s
text_sensor:
- platform: template
name: "${friendly_name} Reading"
update_interval: 1s
lambda: |-
return owon_meter.reading_text();
- platform: template
name: "${friendly_name} Unit"
update_interval: 1s
lambda: |-
return std::string(owon_meter.scale()) + owon_meter.unit();
- platform: template
name: "${friendly_name} Mode"
update_interval: 1s
lambda: |-
return owon_meter.mode_text();
+567
View File
@@ -0,0 +1,567 @@
/*
* ESPHome helper for OWON B35T/B35T+ BLE meter on M5Stack Core 1.
* Parser is based on the standalone Arduino sketch by Reaper7
* (Beerware license, Revision 42) and Dean Cording's owonb35 notes.
*/
#pragma once
#include <cmath>
#include <cstdint>
#include <cstring>
#include <string>
#include <vector>
#include "esp_heap_caps.h"
#include "esp_system.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/components/display/display.h"
namespace owon_b35t {
using esphome::Color;
using esphome::display::Display;
static const char *const TAG = "owon_b35t";
static const uint8_t ACCU_BMP[32] = {
0b00000000, 0b00000000,
0b00000000, 0b00000000,
0b00000000, 0b00000000,
0b11111111, 0b11111110,
0b10000000, 0b00000010,
0b10000000, 0b00000011,
0b10000000, 0b00000011,
0b10000000, 0b00000011,
0b10000000, 0b00000011,
0b10000000, 0b00000011,
0b10000000, 0b00000011,
0b10000000, 0b00000010,
0b11111111, 0b11111110,
0b00000000, 0b00000000,
0b00000000, 0b00000000,
0b00000000, 0b00000000,
};
static const uint8_t BLE_BMP[32] = {
0b00000001, 0b10000000,
0b00000001, 0b11000000,
0b00010001, 0b01100000,
0b00011001, 0b00110000,
0b00001101, 0b00011000,
0b00000111, 0b00110000,
0b00000011, 0b01100000,
0b00000001, 0b11000000,
0b00000001, 0b11000000,
0b00000011, 0b01100000,
0b00000111, 0b00110000,
0b00001101, 0b00011000,
0b00011001, 0b00110000,
0b00010001, 0b01100000,
0b00000001, 0b11000000,
0b00000001, 0b10000000,
};
static const uint8_t DIODE_BMP[32] = {
0b00001000, 0b00011000,
0b00001100, 0b00011000,
0b00001110, 0b00011000,
0b00001111, 0b00011000,
0b00001111, 0b10011000,
0b00001111, 0b11011000,
0b00001111, 0b11111000,
0b11111111, 0b11111111,
0b11111111, 0b11111111,
0b00001111, 0b11111000,
0b00001111, 0b11011000,
0b00001111, 0b10011000,
0b00001111, 0b00011000,
0b00001110, 0b00011000,
0b00001100, 0b00011000,
0b00001000, 0b00011000,
};
static const uint8_t BUZZ_BMP[32] = {
0b00000000, 0b11000000,
0b00000001, 0b11000000,
0b00000011, 0b11000001,
0b00000111, 0b11000001,
0b00001111, 0b11000101,
0b11111111, 0b11000101,
0b11111111, 0b11010101,
0b11111111, 0b11010101,
0b11111111, 0b11010101,
0b11111111, 0b11010101,
0b11111111, 0b11000101,
0b00001111, 0b11000101,
0b00000111, 0b11000001,
0b00000011, 0b11000001,
0b00000001, 0b11000000,
0b00000000, 0b11000000,
};
class Meter {
public:
static constexpr uint8_t REGPLUSMINUS = 0x00;
static constexpr uint8_t FLAGPLUS = 0b00101011;
static constexpr uint8_t FLAGMINUS = 0b00101101;
static constexpr uint8_t REGDIG1 = 0x01;
static constexpr uint8_t REGDIG2 = 0x02;
static constexpr uint8_t REGDIG3 = 0x03;
static constexpr uint8_t REGDIG4 = 0x04;
static constexpr uint8_t REGPOINT = 0x06;
static constexpr uint8_t FLAGPOINT0 = 0b00110000;
static constexpr uint8_t FLAGPOINT1 = 0b00110001;
static constexpr uint8_t FLAGPOINT2 = 0b00110010;
static constexpr uint8_t FLAGPOINT3 = 0b00110100;
static constexpr uint8_t REGMODE = 0x07;
static constexpr uint8_t FLAGMODEHOLD = 0b00000010;
static constexpr uint8_t FLAGMODEREL = 0b00000100;
static constexpr uint8_t FLAGMODEAC = 0b00001000;
static constexpr uint8_t FLAGMODEDC = 0b00010000;
static constexpr uint8_t FLAGMODEAUTO = 0b00100000;
static constexpr uint8_t REGMINMAX = 0x08;
static constexpr uint8_t FLAGMIN = 0b00010000;
static constexpr uint8_t FLAGMAX = 0b00100000;
static constexpr uint8_t REGSCALE = 0x09;
static constexpr uint8_t FLAGSCALEDUTY = 0b00000010;
static constexpr uint8_t FLAGSCALEDIODE = 0b00000100;
static constexpr uint8_t FLAGSCALEBUZZ = 0b00001000;
static constexpr uint8_t FLAGSCALEMEGA = 0b00010000;
static constexpr uint8_t FLAGSCALEKILO = 0b00100000;
static constexpr uint8_t FLAGSCALEMILLI = 0b01000000;
static constexpr uint8_t FLAGSCALEMICRO = 0b10000000;
static constexpr uint8_t REGUNIT = 0x0a;
static constexpr uint8_t FLAGUNITFAHR = 0b00000001;
static constexpr uint8_t FLAGUNITGRAD = 0b00000010;
static constexpr uint8_t FLAGUNITNF = 0b00000100;
static constexpr uint8_t FLAGUNITHZ = 0b00001000;
static constexpr uint8_t FLAGUNITHFE = 0b00010000;
static constexpr uint8_t FLAGUNITOHM = 0b00100000;
static constexpr uint8_t FLAGUNITAMP = 0b01000000;
static constexpr uint8_t FLAGUNITVOLT = 0b10000000;
bool connected{false};
bool write_available{false};
bool is_plus{false};
bool low_battery{false};
bool overload{false};
bool has_reading{false};
uint8_t selected_button{1};
uint32_t last_notify_ms{0};
bool handle_notify(const std::vector<uint8_t> &data) {
if (data.size() > sizeof(this->raw_))
return false;
if (data.size() == 6 && data[1] >= 0xF0) {
memset(this->raw_, 0, sizeof(this->raw_));
memcpy(this->raw_, data.data(), data.size());
this->is_plus = true;
this->parse_plus_();
} else if (data.size() == 14 && data[12] == 0x0D && data[13] == 0x0A) {
memset(this->value_, 0, sizeof(this->value_));
memcpy(this->value_, data.data(), data.size());
this->is_plus = false;
} else {
ESP_LOGW(TAG, "Ignoring unexpected OWON frame length=%u", static_cast<unsigned>(data.size()));
return false;
}
this->overload = memcmp(this->value_, OVERLOAD_FRAME, sizeof(OVERLOAD_FRAME)) == 0;
this->display_value = this->calc_display_value_();
this->base_value = this->calc_base_value_();
this->has_reading = true;
this->last_notify_ms = millis();
return true;
}
void on_connect() {
this->connected = true;
this->write_available = true;
}
void on_disconnect() {
this->connected = false;
this->write_available = false;
}
float value() const { return this->display_value; }
float value_base() const { return this->base_value; }
bool negative() const { return (this->value_[REGPLUSMINUS] & FLAGMINUS) == FLAGMINUS; }
bool auto_range() const { return (this->value_[REGMODE] & FLAGMODEAUTO) == FLAGMODEAUTO; }
bool hold() const { return (this->value_[REGMODE] & FLAGMODEHOLD) == FLAGMODEHOLD; }
bool relative() const { return (this->value_[REGMODE] & FLAGMODEREL) == FLAGMODEREL; }
bool ac() const { return (this->value_[REGMODE] & FLAGMODEAC) == FLAGMODEAC; }
bool dc() const { return (this->value_[REGMODE] & FLAGMODEDC) == FLAGMODEDC; }
bool min_mode() const { return (this->value_[REGMINMAX] & FLAGMIN) == FLAGMIN; }
bool max_mode() const { return (this->value_[REGMINMAX] & FLAGMAX) == FLAGMAX; }
bool diode() const { return (this->value_[REGSCALE] & FLAGSCALEDIODE) == FLAGSCALEDIODE; }
bool continuity() const { return (this->value_[REGSCALE] & FLAGSCALEBUZZ) == FLAGSCALEBUZZ; }
const char *unit() const {
switch (this->value_[REGUNIT]) {
case FLAGUNITFAHR: return "°F";
case FLAGUNITGRAD: return "°C";
case FLAGUNITNF: return "nF";
case FLAGUNITHZ: return "Hz";
case FLAGUNITHFE: return "hFE";
case FLAGUNITOHM: return "Ω";
case FLAGUNITAMP: return "A";
case FLAGUNITVOLT: return "V";
default: return "";
}
}
const char *scale() const {
if ((this->value_[REGSCALE] & FLAGSCALEDUTY) == FLAGSCALEDUTY) return "%";
if ((this->value_[REGSCALE] & FLAGSCALEMEGA) == FLAGSCALEMEGA) return "M";
if ((this->value_[REGSCALE] & FLAGSCALEKILO) == FLAGSCALEKILO) return "k";
if ((this->value_[REGSCALE] & FLAGSCALEMILLI) == FLAGSCALEMILLI) return "m";
if ((this->value_[REGSCALE] & FLAGSCALEMICRO) == FLAGSCALEMICRO) return "µ";
return "";
}
std::string mode_text() const {
std::string out;
if (this->dc()) out += "DC ";
if (this->ac()) out += "AC ";
if (this->auto_range()) out += "AUTO ";
if (this->hold()) out += "HOLD ";
if (this->relative()) out += "REL ";
if (this->min_mode()) out += "MIN ";
if (this->max_mode()) out += "MAX ";
if (this->diode()) out += "DIODE ";
if (this->continuity()) out += "CONT ";
if (!out.empty()) out.pop_back();
return out;
}
std::string reading_text() const {
if (!this->connected) return "Disconnected";
if (!this->has_reading) return "Waiting for data";
if (this->overload) return "OL " + std::string(this->scale()) + this->unit();
char buf[48];
snprintf(buf, sizeof(buf), "%s%.4g %s%s", this->negative() ? "-" : "", std::fabs(this->display_value), this->scale(), this->unit());
return std::string(buf);
}
enum Kind { KIND_OTHER, KIND_VOLTAGE, KIND_CURRENT, KIND_RESISTANCE, KIND_FREQUENCY, KIND_CAPACITANCE, KIND_TEMP_C, KIND_TEMP_F, KIND_DUTY };
Kind kind() const {
if ((this->value_[REGSCALE] & FLAGSCALEDUTY) == FLAGSCALEDUTY) return KIND_DUTY;
switch (this->value_[REGUNIT]) {
case FLAGUNITVOLT: return KIND_VOLTAGE;
case FLAGUNITAMP: return KIND_CURRENT;
case FLAGUNITOHM: return KIND_RESISTANCE;
case FLAGUNITHZ: return KIND_FREQUENCY;
case FLAGUNITNF: return KIND_CAPACITANCE;
case FLAGUNITGRAD: return KIND_TEMP_C;
case FLAGUNITFAHR: return KIND_TEMP_F;
default: return KIND_OTHER;
}
}
const char *selected_button_name() const {
static const char *const names[] = {"SELECT", "RANGE", "HLD/LIG", "REL/BT", "HZ/DUTY", "MAX/MIN"};
uint8_t index = this->selected_button;
if (index < 1) index = 1;
if (index > 6) index = 6;
return names[index - 1];
}
void previous_button() {
if (this->selected_button > 1) this->selected_button--;
}
void next_button() {
if (this->selected_button < 6) this->selected_button++;
}
void render(Display &it, esphome::display::BaseFont *font) {
const Color bg(0, 0, 0);
const Color fg(210, 210, 210);
// Chosen to map to a neutral dark gray in the RGB332 8-bit display palette.
const Color inactive(80, 80, 80);
const Color yellow(255, 220, 0);
const Color blue(0, 80, 255);
const Color cyan(0, 255, 255);
const Color magenta(255, 0, 255);
const Color red(255, 0, 0);
const Color green(0, 220, 0);
const Color orange(255, 165, 0);
it.fill(bg);
this->draw_icon_(it, 12, 8, 16, 16, ACCU_BMP, this->low_battery ? red : green);
this->draw_icon_(it, 46, 8, 16, 16, BLE_BMP, this->connected ? blue : inactive);
this->label_(it, font, 86, 8, "AUTO", this->auto_range() ? fg : inactive);
this->label_(it, font, 138, 8, "MAX", this->max_mode() ? red : inactive);
this->label_(it, font, 178, 8, "MIN", this->min_mode() ? green : inactive);
this->label_(it, font, 218, 8, "HOLD", this->hold() ? blue : inactive);
this->label_(it, font, 270, 8, "REL", this->relative() ? Color(128, 128, 0) : inactive);
this->label_(it, font, 8, 66, "DC", this->dc() ? cyan : inactive);
this->label_(it, font, 8, 102, "AC", this->ac() ? magenta : inactive);
if (!this->connected) {
this->draw_digits_(it, "----", false, inactive);
it.print(160, 148, font, inactive, esphome::display::TextAlign::CENTER, "scan/connect");
} else if (!this->has_reading) {
this->draw_digits_(it, "8888", false, inactive);
it.print(160, 148, font, inactive, esphome::display::TextAlign::CENTER, "waiting");
} else if (this->overload) {
this->draw_digits_(it, " OL ", false, fg);
} else {
char d[5];
d[0] = this->digit_char_(REGDIG1);
d[1] = this->digit_char_(REGDIG2);
d[2] = this->digit_char_(REGDIG3);
d[3] = this->digit_char_(REGDIG4);
d[4] = 0;
this->draw_digits_(it, d, this->negative(), fg);
this->draw_decimal_points_(it, fg);
}
std::string unit_line = std::string(this->scale()) + this->unit();
it.print(270, 140, font, yellow, esphome::display::TextAlign::CENTER, unit_line.c_str());
this->draw_bargraph_(it, this->has_reading && !this->overload ? this->digits_from_buffer_() : 0, this->has_reading && !this->overload);
this->draw_icon_(it, 300, 148, 16, 16, DIODE_BMP, this->diode() ? magenta : inactive);
this->draw_icon_(it, 300, 174, 16, 16, BUZZ_BMP, this->continuity() ? orange : inactive);
it.filled_rectangle(34, 212, 40, 24, this->write_available ? fg : inactive);
it.filled_rectangle(108, 212, 100, 24, this->write_available ? fg : inactive);
it.filled_rectangle(242, 212, 40, 24, this->write_available ? fg : inactive);
it.print(54, 216, font, bg, esphome::display::TextAlign::TOP_CENTER, "<");
it.print(158, 216, font, bg, esphome::display::TextAlign::TOP_CENTER, this->selected_button_name());
it.print(262, 216, font, bg, esphome::display::TextAlign::TOP_CENTER, ">");
}
private:
uint8_t raw_[14]{};
uint8_t value_[14]{};
float display_value{NAN};
float base_value{NAN};
static constexpr uint8_t OVERLOAD_FRAME[5] = {0x2B, 0x3F, 0x30, 0x3A, 0x3F};
uint16_t digits_from_buffer_() const {
uint16_t out = 0;
if (this->value_[REGDIG1] >= '0' && this->value_[REGDIG1] <= '9') out += (this->value_[REGDIG1] - '0') * 1000;
if (this->value_[REGDIG2] >= '0' && this->value_[REGDIG2] <= '9') out += (this->value_[REGDIG2] - '0') * 100;
if (this->value_[REGDIG3] >= '0' && this->value_[REGDIG3] <= '9') out += (this->value_[REGDIG3] - '0') * 10;
if (this->value_[REGDIG4] >= '0' && this->value_[REGDIG4] <= '9') out += (this->value_[REGDIG4] - '0');
return out;
}
float calc_display_value_() const {
if (this->overload) return NAN;
if (this->is_plus) {
uint16_t pair1 = static_cast<uint16_t>(this->raw_[0]) | (static_cast<uint16_t>(this->raw_[1]) << 8);
uint8_t decimal = pair1 & 0x07;
if (decimal >= 7) return NAN;
uint16_t pair3 = static_cast<uint16_t>(this->raw_[4]) | (static_cast<uint16_t>(this->raw_[5]) << 8);
bool negative = pair3 >= 0x7FFF;
uint16_t digits = negative ? (pair3 & 0x7FFF) : pair3;
float v = static_cast<float>(digits) / std::pow(10.0f, decimal);
return negative ? -v : v;
}
uint8_t decimal = 0;
switch (this->value_[REGPOINT] & 0x07) {
case 0b001: decimal = 1; break;
case 0b010: decimal = 2; break;
case 0b100: decimal = 3; break;
default: break;
}
float v = static_cast<float>(this->digits_from_buffer_()) / std::pow(10.0f, decimal);
return this->negative() ? -v : v;
}
float calc_base_value_() const {
if (std::isnan(this->display_value)) return NAN;
if (this->value_[REGUNIT] == FLAGUNITNF) return this->display_value * 1e-9f;
if ((this->value_[REGSCALE] & FLAGSCALEMEGA) == FLAGSCALEMEGA) return this->display_value * 1e6f;
if ((this->value_[REGSCALE] & FLAGSCALEKILO) == FLAGSCALEKILO) return this->display_value * 1e3f;
if ((this->value_[REGSCALE] & FLAGSCALEMILLI) == FLAGSCALEMILLI) return this->display_value * 1e-3f;
if ((this->value_[REGSCALE] & FLAGSCALEMICRO) == FLAGSCALEMICRO) return this->display_value * 1e-6f;
return this->display_value;
}
void parse_plus_() {
memset(this->value_, 0, sizeof(this->value_));
this->value_[5] = 0x20;
this->value_[12] = 0x0D;
this->value_[13] = 0x0A;
uint16_t pair1 = static_cast<uint16_t>(this->raw_[0]) | (static_cast<uint16_t>(this->raw_[1]) << 8);
uint8_t function = (pair1 >> 6) & 0x0F;
uint8_t scale = (pair1 >> 3) & 0x07;
uint8_t decimal = pair1 & 0x07;
switch (decimal) {
case 0: this->value_[REGPOINT] = FLAGPOINT0; break;
case 1: this->value_[REGPOINT] = FLAGPOINT3; break;
case 2: this->value_[REGPOINT] = FLAGPOINT2; break;
case 3: this->value_[REGPOINT] = FLAGPOINT1; break;
default: break;
}
switch (function) {
case 0: this->value_[REGUNIT] |= FLAGUNITVOLT; this->value_[REGMODE] |= FLAGMODEDC; break;
case 1: this->value_[REGUNIT] |= FLAGUNITVOLT; this->value_[REGMODE] |= FLAGMODEAC; break;
case 2: this->value_[REGUNIT] |= FLAGUNITAMP; this->value_[REGMODE] |= FLAGMODEDC; break;
case 3: this->value_[REGUNIT] |= FLAGUNITAMP; this->value_[REGMODE] |= FLAGMODEAC; break;
case 4: this->value_[REGUNIT] |= FLAGUNITOHM; break;
case 5: this->value_[REGUNIT] |= FLAGUNITNF; break;
case 6: this->value_[REGUNIT] |= FLAGUNITHZ; break;
case 7: this->value_[REGSCALE] |= FLAGSCALEDUTY; break;
case 8: this->value_[REGUNIT] |= FLAGUNITGRAD; break;
case 9: this->value_[REGUNIT] |= FLAGUNITFAHR; break;
case 10: this->value_[REGSCALE] |= FLAGSCALEDIODE; break;
case 11: this->value_[REGSCALE] |= FLAGSCALEBUZZ; break;
case 12: this->value_[REGUNIT] |= FLAGUNITHFE; break;
default: break;
}
switch (scale) {
case 2: this->value_[REGSCALE] |= FLAGSCALEMICRO; break;
case 3: this->value_[REGSCALE] |= FLAGSCALEMILLI; break;
case 5: this->value_[REGSCALE] |= FLAGSCALEKILO; break;
case 6: this->value_[REGSCALE] |= FLAGSCALEMEGA; break;
default: break;
}
uint16_t pair2 = static_cast<uint16_t>(this->raw_[2]) | (static_cast<uint16_t>(this->raw_[3]) << 8);
if (pair2 & (1 << 0)) this->value_[REGMODE] |= FLAGMODEHOLD;
if (pair2 & (1 << 1)) this->value_[REGMODE] |= FLAGMODEREL;
if (pair2 & (1 << 2)) this->value_[REGMODE] |= FLAGMODEAUTO;
this->low_battery = (pair2 & (1 << 3)) != 0;
if (pair2 & (1 << 4)) this->value_[REGMINMAX] |= FLAGMIN;
if (pair2 & (1 << 5)) this->value_[REGMINMAX] |= FLAGMAX;
uint16_t pair3 = static_cast<uint16_t>(this->raw_[4]) | (static_cast<uint16_t>(this->raw_[5]) << 8);
if (decimal < 7) {
uint16_t digits = pair3;
if (pair3 < 0x7FFF) {
this->value_[REGPLUSMINUS] = FLAGPLUS;
} else {
this->value_[REGPLUSMINUS] = FLAGMINUS;
digits = pair3 & 0x7FFF;
}
this->value_[REGDIG1] = '0' + ((digits / 1000) % 10);
this->value_[REGDIG2] = '0' + ((digits / 100) % 10);
this->value_[REGDIG3] = '0' + ((digits / 10) % 10);
this->value_[REGDIG4] = '0' + (digits % 10);
} else {
memcpy(this->value_, OVERLOAD_FRAME, sizeof(OVERLOAD_FRAME));
}
}
char digit_char_(uint8_t reg) const {
uint8_t c = this->value_[reg];
return (c >= '0' && c <= '9') ? static_cast<char>(c) : ' ';
}
void label_(Display &it, esphome::display::BaseFont *font, int x, int y, const char *text, Color color) {
it.print(x, y, font, color, esphome::display::TextAlign::TOP_LEFT, text);
}
void draw_digits_(Display &it, const char *text, bool negative, Color color) {
if (negative) this->draw_segment_(it, 8, 88, 26, 9, true, color);
constexpr int digit_x = 40;
constexpr int digit_y = 35;
constexpr int digit_w = 50;
constexpr int digit_h = 88;
constexpr int digit_distance = 64;
for (int i = 0; i < 4; i++) {
this->draw_seven_segment_(it, digit_x + i * digit_distance, digit_y, digit_w, digit_h, text[i], color);
}
}
void draw_decimal_points_(Display &it, Color color) {
uint8_t p = this->value_[REGPOINT];
if ((p & FLAGPOINT1) == FLAGPOINT1) it.filled_rectangle(95, 117, 8, 10, color);
if ((p & FLAGPOINT2) == FLAGPOINT2) it.filled_rectangle(159, 117, 8, 10, color);
if ((p & FLAGPOINT3) == FLAGPOINT3) it.filled_rectangle(223, 117, 8, 10, color);
}
void draw_segment_(Display &it, int x, int y, int w, int h, bool horizontal, Color color) {
if (horizontal) {
int cap = h / 2;
it.filled_rectangle(x + cap, y, w - 2 * cap, h, color);
it.filled_triangle(x, y + cap, x + cap, y, x + cap, y + h, color);
it.filled_triangle(x + w, y + cap, x + w - cap, y, x + w - cap, y + h, color);
} else {
int cap = w / 2;
it.filled_rectangle(x, y + cap, w, h - 2 * cap, color);
it.filled_triangle(x + cap, y, x, y + cap, x + w, y + cap, color);
it.filled_triangle(x + cap, y + h, x, y + h - cap, x + w, y + h - cap, color);
}
}
void draw_icon_(Display &it, int x, int y, int w, int h, const uint8_t *data, Color color) {
for (int row = 0; row < h; row++) {
for (int col = 0; col < w; col++) {
uint8_t byte = data[row * ((w + 7) / 8) + (col / 8)];
if ((byte & (0x80 >> (col % 8))) != 0) {
it.draw_pixel_at(x + col, y + row, color);
}
}
}
}
void draw_seven_segment_(Display &it, int x, int y, int w, int h, char ch, Color color) {
bool a=false,b=false,c=false,d=false,e=false,f=false,g=false;
switch (ch) {
case '0': a=b=c=d=e=f=true; break;
case '1': b=c=true; break;
case '2': a=b=d=e=g=true; break;
case '3': a=b=c=d=g=true; break;
case '4': b=c=f=g=true; break;
case '5': a=c=d=f=g=true; break;
case '6': a=c=d=e=f=g=true; break;
case '7': a=b=c=true; break;
case '8': a=b=c=d=e=f=g=true; break;
case '9': a=b=c=d=f=g=true; break;
case 'O': a=b=c=d=e=f=true; break;
case 'L': d=e=f=true; break;
case '-': g=true; break;
default: break;
}
int t = 10;
int half = h / 2;
int gap = 1;
int top_y = y;
int mid_y = y + half - t / 2;
int bot_y = y + h - t;
int upper_v_y = top_y + t + gap;
int upper_v_h = mid_y - gap - upper_v_y;
int lower_v_y = mid_y + t + gap;
int lower_v_h = bot_y - gap - lower_v_y;
if (a) this->draw_segment_(it, x + t / 2, top_y, w - t, t, true, color);
if (b && upper_v_h > 0) this->draw_segment_(it, x + w - t, upper_v_y, t, upper_v_h, false, color);
if (c && lower_v_h > 0) this->draw_segment_(it, x + w - t, lower_v_y, t, lower_v_h, false, color);
if (d) this->draw_segment_(it, x + t / 2, bot_y, w - t, t, true, color);
if (e && lower_v_h > 0) this->draw_segment_(it, x, lower_v_y, t, lower_v_h, false, color);
if (f && upper_v_h > 0) this->draw_segment_(it, x, upper_v_y, t, upper_v_h, false, color);
if (g) this->draw_segment_(it, x + t / 2, mid_y, w - t, t, true, color);
}
void draw_bargraph_(Display &it, uint16_t digits, bool active) {
const Color fg(255, 255, 255);
const Color inactive(80, 80, 80);
uint16_t mapped = active ? static_cast<uint16_t>(digits * 240 / 6000) : 0;
if (mapped > 240) mapped = 240;
for (uint16_t i = 0; i <= 240; i += 4) {
Color col = (active && i <= mapped) ? fg : inactive;
int h = (i % 40 == 0) ? 20 : ((i % 20 == 0) ? 15 : 10);
it.vertical_line(40 + i, 185 - h, h, col);
}
it.horizontal_line(35, 185, 250, inactive);
}
};
} // namespace owon_b35t
static owon_b35t::Meter owon_meter;
@@ -22,18 +22,19 @@ ota:
external_components:
- source: github://RASPIAUDIO/esphomeLuxe@main
# - source:
# type: local
# path: components
components: [es8388]
refresh: 0s
network:
enable_ipv6: true
wifi:
ssid: Voltage-legacy
password: !secret voltage_legacy_psk
use_address: ${name}.home
#use_address: ${name}.home
power_save_mode: high
fast_connect: on
min_auth_mode: WPA2
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "Raspiaudio Fallback Hotspot"
@@ -47,6 +48,9 @@ esphome:
platformio_options:
board_build.flash_mode: dio
board_build.arduino.memory_type: qio_opi
project:
name: raspiaudio.voice-assistant
version: "2025.3.1"
on_boot:
priority: -100.0
then:
@@ -67,6 +71,7 @@ esp32:
micro_wake_word:
id: mww
models:
- model: okay_nabu
- model: https://github.com/kahrendt/microWakeWord/releases/download/okay_nabu_20241226.3/okay_nabu.json
# - model: hey_jarvis
# - model: hey_mycroft
@@ -164,16 +169,20 @@ sensor:
update_interval: 15s
attenuation: auto
filters:
- multiply: 2 # https://forum.raspiaudio.com/t/esp-muse-luxe-bluetooth-speaker/294/12
- multiply: 2
- calibrate_linear:
- 0.0 -> 0.0
# Map measured full charge to real value to compensate divider/ADC bias
- 4.58 -> 4.20
- exponential_moving_average:
alpha: 0.2
send_every: 2
alpha: 0.2
send_every: 2
- delta: 0.002
on_value:
then:
- sensor.template.publish:
id: battery_percent
state: !lambda "return x;"
then:
- sensor.template.publish:
id: battery_percent
state: !lambda "return x ;"
- platform: template
name: Battery
@@ -185,34 +194,47 @@ sensor:
entity_category: diagnostic
update_interval: 15s
filters:
- calibrate_polynomial:
degree: 3
datapoints:
- 4.58 -> 100.0
- 4.5 -> 97.1
- 4.47 -> 94.2
- 4.44 -> 88.4
- 4.42 -> 82.7
- 4.41 -> 76.9
- 4.41 -> 71.1
- 4.37 -> 65.3
- 4.35 -> 59.5
- 4.31 -> 53.8
- 4.28 -> 48.0
- 4.26 -> 42.2
- 4.23 -> 36.4
- 4.21 -> 30.6
- 4.19 -> 24.9
- 4.16 -> 19.1
- 4.1 -> 13.3
- 4.07 -> 10.4
- 4.03 -> 7.5
- 3.97 -> 4.6
- 3.82 -> 1.7
- 3.27 -> 0.0
- lambda: return clamp(x, 0.0f, 100.0f);
- calibrate_polynomial:
degree: 3
datapoints:
- 4.20 -> 100.0
- 4.13 -> 97.1
- 4.10 -> 94.2
- 4.07 -> 88.4
- 4.05 -> 82.7
- 4.04 -> 76.9
- 4.04 -> 71.1
- 4.01 -> 65.3
- 3.99 -> 59.5
- 3.95 -> 53.8
- 3.93 -> 48.0
- 3.91 -> 42.2
- 3.88 -> 36.4
- 3.86 -> 30.6
- 3.84 -> 24.9
- 3.82 -> 19.1
- 3.76 -> 13.3
- 3.73 -> 10.4
- 3.70 -> 7.5
- 3.64 -> 4.6
- 3.50 -> 1.7
- 3.00 -> 0.0
- lambda: return clamp(x, 0.0f, 100.0f);
binary_sensor:
- platform: gpio
pin:
number: GPIO27
mode:
input: true
pullup: true
id: jack_detect
name: Jack Detect
internal: true
on_press:
- script.execute: jack_detect_high
on_release:
- script.execute: jack_detect_low
- platform: gpio
pin:
number: GPIO19
@@ -292,6 +314,7 @@ light:
update_interval: 2s
i2s_audio:
id: i2s0
i2s_lrclk_pin: GPIO25
i2s_bclk_pin: GPIO5
i2s_mclk_pin: GPIO0
@@ -299,6 +322,7 @@ i2s_audio:
microphone:
- platform: i2s_audio
id: luxe_mic
i2s_audio_id: i2s0
sample_rate: 16000
i2s_din_pin: GPIO35
bits_per_sample: 16bit
@@ -312,20 +336,22 @@ speaker:
dac_type: external
sample_rate: 48000
bits_per_sample: 16bit
channel: stereo
num_channels: 2
buffer_duration: 100ms
media_player:
- platform: speaker
name: None
id: luxe_media_player
internal: false
# task_stack_in_psram: true
# volume_min: 0.5
# volume_max: 0.8
announcement_pipeline:
speaker: luxe_speaker
format: FLAC
sample_rate: 48000
num_channels: 2
num_channels: 1
files:
- id: little_sound
file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/timer_finished.flac
@@ -384,6 +410,7 @@ voice_assistant:
on_tts_start:
- logger.log: "answering 4 => phase"
- micro_wake_word.stop:
- lambda: |-
id(phase) = 4;
- script.execute: update_led
@@ -403,8 +430,23 @@ voice_assistant:
#########
# Scripts
script:
- id: jack_detect_high
then:
- logger.log: "Jack detect high -> route to speaker"
- output.turn_off: dac_mute
- lambda: |-
id(my_es8388).write_byte(0x1D, 0x20);
id(my_es8388).write_byte(0x1C, 0x10);
id(my_es8388).write_byte(0x04, 0x30);
- id: jack_detect_low
then:
- logger.log: "Jack detect low -> route to line out"
- output.turn_on: dac_mute
- lambda: |-
id(my_es8388).write_byte(0x1D, 0x00);
id(my_es8388).write_byte(0x1C, 0x00);
id(my_es8388).write_byte(0x04, 0x0C);
- id: update_led
then:
- logger.log: "==>>>update_led"
@@ -429,7 +471,7 @@ script:
- lambda: id(phase) = 1;
- media_player.speaker.play_on_device_media_file:
media_file: little_sound
announcement: true
announcement: false
- script.execute: update_led
- id: waiting
@@ -51,7 +51,7 @@ esphome:
esp32:
board: nodemcu-32s
framework:
type: arduino
type: esp-idf
wifi:
ssid: "Voltage-legacy"
@@ -59,6 +59,7 @@ wifi:
use_address: ${name}.home
power_save_mode: high
fast_connect: on
min_auth_mode: WPA2
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
@@ -113,6 +114,7 @@ light:
# Initialize UART for Vindriktning sensor
uart:
id: uartPM
rx_pin: GPIO26
baud_rate: 9600
@@ -208,9 +210,10 @@ sensor:
ambient_pressure_compensation: 1
automatic_self_calibration: True
address: 0x61
update_interval: 60s
update_interval: 10s
# Vindriktning particulate matter sensor
- platform: pm1006
uart_id: uartPM
pm_2_5:
name: "Feinstaub PM 2.5µm"
on_value_range:
@@ -235,7 +238,7 @@ sensor:
- platform: adc
pin: 32
name: "Spannung Photodiode"
attenuation: 11db
attenuation: 12db
update_interval: 1s
internal: true
on_value_range:
@@ -259,6 +262,7 @@ remote_receiver:
# IR Transmitter
remote_transmitter:
non_blocking: True
pin: GPIO22
# Infrared remotes use a 50% carrier signal
carrier_duty_percent: 50%
+5 -1
View File
@@ -16,12 +16,16 @@ esp32:
framework:
type: arduino
network:
enable_ipv6: true
wifi:
ssid: "Voltage-legacy"
password: !secret voltage_legacy_psk
use_address: bathroom.home
#use_address: bathroom.home
power_save_mode: high
fast_connect: on
min_auth_mode: WPA2
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
+695 -183
View File
File diff suppressed because it is too large Load Diff
+6 -2
View File
@@ -14,14 +14,18 @@ esphome:
esp32:
board: nodemcu-32s
framework:
type: arduino
type: esp-idf
network:
enable_ipv6: true
wifi:
ssid: "Voltage-legacy"
password: !secret voltage_legacy_psk
use_address: ${name}.home
#use_address: ${name}.home
power_save_mode: high
fast_connect: on
min_auth_mode: WPA2
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
+4 -2
View File
@@ -1,6 +1,6 @@
substitutions:
name: home-assistant-voice-09c0e7
friendly_name: Home Assistant Voice 09c0e7
friendly_name: Home Assistant Voice Küche
packages:
Nabu Casa.Home Assistant Voice PE: github://esphome/home-assistant-voice-pe/home-assistant-voice.yaml
esphome:
@@ -14,6 +14,7 @@ wifi:
#use_address: ${name}.home
#power_save_mode: high
#fast_connect: on
min_auth_mode: WPA2
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
@@ -28,4 +29,5 @@ api:
ota:
platform: esphome
password: !secret ota
http_request:
+33
View File
@@ -0,0 +1,33 @@
substitutions:
name: home-assistant-voice-0a7c6b
friendly_name: Home Assistant Voice Wohnzimmer
packages:
Nabu Casa.Home Assistant Voice PE: github://esphome/home-assistant-voice-pe/home-assistant-voice.yaml
esphome:
name: ${name}
name_add_mac_suffix: false
friendly_name: ${friendly_name}
wifi:
ssid: "Voltage-legacy"
password: !secret voltage_legacy_psk
#use_address: ${name}.home
#power_save_mode: high
#fast_connect: on
min_auth_mode: WPA2
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "HA Voice Fallback Hotspot"
password: !secret fallback_psk
# Enable Home Assistant API
api:
encryption:
key: !secret apikey
ota:
platform: esphome
password: !secret ota
http_request:
+15 -6
View File
@@ -14,7 +14,10 @@ esphome:
esp32:
board: esp32cam
framework:
type: arduino
type: esp-idf
network:
enable_ipv6: true
wifi:
ssid: "Voltage-legacy"
@@ -45,15 +48,21 @@ ota:
# Initialize I²C
i2c:
- id: bus_a
sda: GPIO13
scl: GPIO14
sda: GPIO3
#sda: GPIO13
#scl: GPIO14
scl: GPIO1
scan: true
- id: bus_c
sda: GPIO26
scl: GPIO27
psram:
speed: 80MHz
# Camera
esp32_camera:
frame_buffer_location: PSRAM
name: ${friendly_name}
external_clock:
pin: GPIO0
@@ -65,12 +74,12 @@ esp32_camera:
href_pin: GPIO23
pixel_clock_pin: GPIO22
power_down_pin: GPIO32
resolution: SVGA
resolution: XGA
max_framerate: 24 fps
idle_framerate: 0.2 fps
jpeg_quality: 30
jpeg_quality: 15
agc_mode: auto
agc_gain_ceiling: 4x
agc_gain_ceiling: 8x
wb_mode: auto
vertical_flip: true
horizontal_mirror: true
+21 -9
View File
@@ -14,12 +14,15 @@ esphome:
esp32:
board: esp32dev
framework:
type: arduino
type: esp-idf
network:
enable_ipv6: true
wifi:
ssid: "Voltage-legacy"
password: !secret voltage_legacy_psk
use_address: cam-livingroom.home
#use_address: cam-livingroom.home
power_save_mode: high
fast_connect: on
@@ -41,27 +44,36 @@ api:
ota:
platform: esphome
password: !secret ota
# Initialize I²C
i2c:
- id: bus_c
sda: GPIO26
scl: GPIO27
psram:
speed: 80MHz
# Camera
esp32_camera:
frame_buffer_location: PSRAM
name: ${friendly_name}
external_clock:
pin: GPIO0
frequency: 20MHz
i2c_pins:
sda: GPIO26
scl: GPIO27
i2c_id:
bus_c
data_pins: [GPIO5, GPIO18, GPIO19, GPIO21, GPIO36, GPIO39, GPIO34, GPIO35]
vsync_pin: GPIO25
href_pin: GPIO23
pixel_clock_pin: GPIO22
power_down_pin: GPIO32
resolution: SVGA
resolution: XGA
max_framerate: 24 fps
idle_framerate: 0.2 fps
jpeg_quality: 30
jpeg_quality: 15
agc_mode: auto
agc_gain_ceiling: 4x
agc_gain_ceiling: 8x
wb_mode: auto
vertical_flip: true
horizontal_mirror: true
+785
View File
@@ -0,0 +1,785 @@
/*
* ESPHome helper for OWON B35T/B35T+ BLE meter on M5Stack Core 2.
* Parser is based on the standalone Arduino sketch by Reaper7
* (Beerware license, Revision 42) and Dean Cording's owonb35 notes.
* Rendering code and some functionality modified to suppport the
* Atorch DL24 dc load.
*/
#pragma once
#include <cmath>
#include <cstdint>
#include <cstring>
#include <string>
#include <vector>
#include "esp_heap_caps.h"
#include "esp_system.h"
#include "esphome/core/helpers.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/core/log.h"
#include "esphome/components/display/display.h"
namespace owon_b35t {
using esphome::Color;
using esphome::display::Display;
static const char *const TAG = "owon_b35t";
static const char *const POWER_TAG = "core2_power";
static constexpr uint8_t AXP192_ADDR = 0x34;
static esphome::i2c::I2CDevice axp192;
static bool axp192_ready = false;
static bool axp_write(uint8_t reg, uint8_t value) {
if (!axp192_ready) return false;
bool ok = axp192.write_byte(reg, value);
if (!ok) ESP_LOGW(POWER_TAG, "AXP192 write reg 0x%02X failed", reg);
return ok;
}
static bool axp_read(uint8_t reg, uint8_t *value) {
if (!axp192_ready) return false;
bool ok = axp192.read_byte(reg, value);
if (!ok) ESP_LOGW(POWER_TAG, "AXP192 read reg 0x%02X failed", reg);
return ok;
}
static void axp_update(uint8_t reg, uint8_t clear_mask, uint8_t set_mask) {
uint8_t value = 0;
if (!axp_read(reg, &value)) return;
value = (value & ~clear_mask) | set_mask;
axp_write(reg, value);
}
static uint8_t axp_dc_voltage_data(uint16_t millivolts) {
if (millivolts < 700) millivolts = 700;
if (millivolts > 3500) millivolts = 3500;
return static_cast<uint8_t>((millivolts - 700) / 25) & 0x7F;
}
static uint8_t axp_ldo_voltage_data(uint16_t millivolts) {
if (millivolts < 1800) millivolts = 1800;
if (millivolts > 3300) millivolts = 3300;
return static_cast<uint8_t>((millivolts - 1800) / 100) & 0x0F;
}
static void core2_axp192_set_lcd_voltage(uint16_t millivolts) {
uint8_t value = 0;
axp_read(0x27, &value);
axp_write(0x27, (value & 0x80) | axp_dc_voltage_data(millivolts)); // DCDC3, LCD backlight
}
static void core2_axp192_set_backlight(float brightness) {
if (brightness <= 0.0f) {
axp_update(0x12, 0x02, 0x00); // DCDC3 off
return;
}
if (brightness > 1.0f) brightness = 1.0f;
uint16_t millivolts = static_cast<uint16_t>(2400 + brightness * 900);
core2_axp192_set_lcd_voltage(millivolts);
axp_update(0x12, 0x00, 0x02); // DCDC3 on
}
static void core2_axp192_init(esphome::i2c::I2CBus *bus) {
axp192.set_i2c_bus(bus);
axp192.set_i2c_address(AXP192_ADDR);
axp192_ready = true;
ESP_LOGI(POWER_TAG, "Initializing M5Stack Core2 AXP192 LCD power");
axp_update(0x30, 0xF9, 0x02); // Disable VBUS current limit, preserve bit 2.
axp_update(0x92, 0x07, 0x00); // GPIO1 open-drain output.
axp_update(0x93, 0x07, 0x00); // GPIO2 open-drain output.
axp_write(0x35, 0xA2); // RTC battery charging.
uint8_t value = 0;
axp_read(0x26, &value);
axp_write(0x26, (value & 0x80) | axp_dc_voltage_data(3350)); // DCDC1 ESP32 VDD.
core2_axp192_set_lcd_voltage(2800);
uint8_t ldo2 = axp_ldo_voltage_data(3300);
uint8_t ldo3 = axp_ldo_voltage_data(2000);
axp_write(0x28, (ldo2 << 4) | ldo3); // LDO2 LCD logic/SD, LDO3 vibrator.
axp_update(0x12, 0x00, 0x07); // Enable DCDC1, DCDC3, LDO2.
axp_write(0x82, 0xFF); // ADCs on.
axp_update(0x95, 0x8D, 0x84); // GPIO4 setup, as M5Core2 library does.
axp_write(0x36, 0x4C); // Power key timing.
// LCD reset through AXP192 GPIO4.
axp_update(0x96, 0x02, 0x00);
delay(100);
axp_update(0x96, 0x00, 0x02);
delay(100);
core2_axp192_set_backlight(1.0f);
}
static const uint8_t ACCU_BMP[32] = {
0b00000000, 0b00000000,
0b00000000, 0b00000000,
0b00000000, 0b00000000,
0b11111111, 0b11111110,
0b10000000, 0b00000010,
0b10000000, 0b00000011,
0b10000000, 0b00000011,
0b10000000, 0b00000011,
0b10000000, 0b00000011,
0b10000000, 0b00000011,
0b10000000, 0b00000011,
0b10000000, 0b00000010,
0b11111111, 0b11111110,
0b00000000, 0b00000000,
0b00000000, 0b00000000,
0b00000000, 0b00000000,
};
static const uint8_t BLE_BMP[32] = {
0b00000001, 0b10000000,
0b00000001, 0b11000000,
0b00010001, 0b01100000,
0b00011001, 0b00110000,
0b00001101, 0b00011000,
0b00000111, 0b00110000,
0b00000011, 0b01100000,
0b00000001, 0b11000000,
0b00000001, 0b11000000,
0b00000011, 0b01100000,
0b00000111, 0b00110000,
0b00001101, 0b00011000,
0b00011001, 0b00110000,
0b00010001, 0b01100000,
0b00000001, 0b11000000,
0b00000001, 0b10000000,
};
static const uint8_t HOLD_BMP[32] = {
0b01111111, 0b11111110,
0b11111111, 0b11111111,
0b11100011, 0b11000111,
0b11100011, 0b11000111,
0b11100011, 0b11000111,
0b11100011, 0b11000111,
0b11100011, 0b11000111,
0b11100000, 0b00000111,
0b11100000, 0b00000111,
0b11100011, 0b11000111,
0b11100011, 0b11000111,
0b11100011, 0b11000111,
0b11100011, 0b11000111,
0b11100011, 0b11000111,
0b11111111, 0b11111111,
0b01111111, 0b11111110,
};
static const uint8_t REL_BMP[32] = {
0b00000001, 0b00000000,
0b00000001, 0b00000000,
0b00000011, 0b10000000,
0b00000011, 0b10000000,
0b00000110, 0b11000000,
0b00000110, 0b11000000,
0b00001100, 0b01100000,
0b00001100, 0b01100000,
0b00011000, 0b00110000,
0b00011000, 0b00110000,
0b00110000, 0b00011000,
0b00110000, 0b00011000,
0b01100000, 0b00001100,
0b01100000, 0b00001100,
0b11111111, 0b11111110,
0b11111111, 0b11111110,
};
static const uint8_t DIODE_BMP[32] = {
0b00001000, 0b00011000,
0b00001100, 0b00011000,
0b00001110, 0b00011000,
0b00001111, 0b00011000,
0b00001111, 0b10011000,
0b00001111, 0b11011000,
0b00001111, 0b11111000,
0b11111111, 0b11111111,
0b11111111, 0b11111111,
0b00001111, 0b11111000,
0b00001111, 0b11011000,
0b00001111, 0b10011000,
0b00001111, 0b00011000,
0b00001110, 0b00011000,
0b00001100, 0b00011000,
0b00001000, 0b00011000,
};
static const uint8_t BUZZ_BMP[32] = {
0b00000000, 0b11000000,
0b00000001, 0b11000000,
0b00000011, 0b11000001,
0b00000111, 0b11000001,
0b00001111, 0b11000101,
0b11111111, 0b11000101,
0b11111111, 0b11010101,
0b11111111, 0b11010101,
0b11111111, 0b11010101,
0b11111111, 0b11010101,
0b11111111, 0b11000101,
0b00001111, 0b11000101,
0b00000111, 0b11000001,
0b00000011, 0b11000001,
0b00000001, 0b11000000,
0b00000000, 0b11000000,
};
class Meter {
public:
static constexpr uint8_t REGPLUSMINUS = 0x00;
static constexpr uint8_t FLAGPLUS = 0b00101011;
static constexpr uint8_t FLAGMINUS = 0b00101101;
static constexpr uint8_t REGDIG1 = 0x01;
static constexpr uint8_t REGDIG2 = 0x02;
static constexpr uint8_t REGDIG3 = 0x03;
static constexpr uint8_t REGDIG4 = 0x04;
static constexpr uint8_t REGPOINT = 0x06;
static constexpr uint8_t FLAGPOINT0 = 0b00110000;
static constexpr uint8_t FLAGPOINT1 = 0b00110001;
static constexpr uint8_t FLAGPOINT2 = 0b00110010;
static constexpr uint8_t FLAGPOINT3 = 0b00110100;
static constexpr uint8_t REGMODE = 0x07;
static constexpr uint8_t FLAGMODEHOLD = 0b00000010;
static constexpr uint8_t FLAGMODEREL = 0b00000100;
static constexpr uint8_t FLAGMODEAC = 0b00001000;
static constexpr uint8_t FLAGMODEDC = 0b00010000;
static constexpr uint8_t FLAGMODEAUTO = 0b00100000;
static constexpr uint8_t REGMINMAX = 0x08;
static constexpr uint8_t FLAGMIN = 0b00010000;
static constexpr uint8_t FLAGMAX = 0b00100000;
static constexpr uint8_t REGSCALE = 0x09;
static constexpr uint8_t FLAGSCALEDUTY = 0b00000010;
static constexpr uint8_t FLAGSCALEDIODE = 0b00000100;
static constexpr uint8_t FLAGSCALEBUZZ = 0b00001000;
static constexpr uint8_t FLAGSCALEMEGA = 0b00010000;
static constexpr uint8_t FLAGSCALEKILO = 0b00100000;
static constexpr uint8_t FLAGSCALEMILLI = 0b01000000;
static constexpr uint8_t FLAGSCALEMICRO = 0b10000000;
static constexpr uint8_t REGUNIT = 0x0a;
static constexpr uint8_t FLAGUNITFAHR = 0b00000001;
static constexpr uint8_t FLAGUNITGRAD = 0b00000010;
static constexpr uint8_t FLAGUNITNF = 0b00000100;
static constexpr uint8_t FLAGUNITHZ = 0b00001000;
static constexpr uint8_t FLAGUNITHFE = 0b00010000;
static constexpr uint8_t FLAGUNITOHM = 0b00100000;
static constexpr uint8_t FLAGUNITAMP = 0b01000000;
static constexpr uint8_t FLAGUNITVOLT = 0b10000000;
bool connected{false};
bool write_available{false};
bool is_plus{false};
bool low_battery{false};
bool overload{false};
bool has_reading{false};
uint8_t selected_button{1};
uint32_t last_notify_ms{0};
bool handle_notify(const std::vector<uint8_t> &data) {
if (data.size() > sizeof(this->raw_))
return false;
if (data.size() == 6 && data[1] >= 0xF0) {
memset(this->raw_, 0, sizeof(this->raw_));
memcpy(this->raw_, data.data(), data.size());
this->is_plus = true;
this->parse_plus_();
} else if (data.size() == 14 && data[12] == 0x0D && data[13] == 0x0A) {
memset(this->value_, 0, sizeof(this->value_));
memcpy(this->value_, data.data(), data.size());
this->is_plus = false;
} else {
ESP_LOGW(TAG, "Ignoring unexpected OWON frame length=%u", static_cast<unsigned>(data.size()));
return false;
}
this->overload = memcmp(this->value_, OVERLOAD_FRAME, sizeof(OVERLOAD_FRAME)) == 0;
this->display_value = this->calc_display_value_();
this->base_value = this->calc_base_value_();
this->has_reading = true;
this->last_notify_ms = millis();
return true;
}
void on_connect() {
this->connected = true;
this->write_available = true;
}
void on_disconnect() {
this->connected = false;
this->write_available = false;
}
float value() const { return this->display_value; }
float value_base() const { return this->base_value; }
bool negative() const { return (this->value_[REGPLUSMINUS] & FLAGMINUS) == FLAGMINUS; }
bool auto_range() const { return (this->value_[REGMODE] & FLAGMODEAUTO) == FLAGMODEAUTO; }
bool hold() const { return (this->value_[REGMODE] & FLAGMODEHOLD) == FLAGMODEHOLD; }
bool relative() const { return (this->value_[REGMODE] & FLAGMODEREL) == FLAGMODEREL; }
bool ac() const { return (this->value_[REGMODE] & FLAGMODEAC) == FLAGMODEAC; }
bool dc() const { return (this->value_[REGMODE] & FLAGMODEDC) == FLAGMODEDC; }
bool min_mode() const { return (this->value_[REGMINMAX] & FLAGMIN) == FLAGMIN; }
bool max_mode() const { return (this->value_[REGMINMAX] & FLAGMAX) == FLAGMAX; }
bool diode() const { return (this->value_[REGSCALE] & FLAGSCALEDIODE) == FLAGSCALEDIODE; }
bool continuity() const { return (this->value_[REGSCALE] & FLAGSCALEBUZZ) == FLAGSCALEBUZZ; }
const char *unit() const {
switch (this->value_[REGUNIT]) {
case FLAGUNITFAHR: return "°F";
case FLAGUNITGRAD: return "°C";
case FLAGUNITNF: return "nF";
case FLAGUNITHZ: return "Hz";
case FLAGUNITHFE: return "hFE";
case FLAGUNITOHM: return "Ω";
case FLAGUNITAMP: return "A";
case FLAGUNITVOLT: return "V";
default: return "";
}
}
const char *scale() const {
if ((this->value_[REGSCALE] & FLAGSCALEDUTY) == FLAGSCALEDUTY) return "%";
if ((this->value_[REGSCALE] & FLAGSCALEMEGA) == FLAGSCALEMEGA) return "M";
if ((this->value_[REGSCALE] & FLAGSCALEKILO) == FLAGSCALEKILO) return "k";
if ((this->value_[REGSCALE] & FLAGSCALEMILLI) == FLAGSCALEMILLI) return "m";
if ((this->value_[REGSCALE] & FLAGSCALEMICRO) == FLAGSCALEMICRO) return "µ";
return "";
}
std::string mode_text() const {
std::string out;
if (this->dc()) out += "DC ";
if (this->ac()) out += "AC ";
if (this->auto_range()) out += "AUTO ";
if (this->hold()) out += "HOLD ";
if (this->relative()) out += "REL ";
if (this->min_mode()) out += "MIN ";
if (this->max_mode()) out += "MAX ";
if (this->diode()) out += "DIODE ";
if (this->continuity()) out += "CONT ";
if (!out.empty()) out.pop_back();
return out;
}
std::string reading_text() const {
if (!this->connected) return "Disconnected";
if (!this->has_reading) return "Waiting for data";
if (this->overload) return "OL " + std::string(this->scale()) + this->unit();
char buf[48];
snprintf(buf, sizeof(buf), "%s%.4g %s%s", this->negative() ? "-" : "", std::fabs(this->display_value), this->scale(), this->unit());
return std::string(buf);
}
enum Kind { KIND_OTHER, KIND_VOLTAGE, KIND_CURRENT, KIND_RESISTANCE, KIND_FREQUENCY, KIND_CAPACITANCE, KIND_TEMP_C, KIND_TEMP_F, KIND_DUTY };
Kind kind() const {
if ((this->value_[REGSCALE] & FLAGSCALEDUTY) == FLAGSCALEDUTY) return KIND_DUTY;
switch (this->value_[REGUNIT]) {
case FLAGUNITVOLT: return KIND_VOLTAGE;
case FLAGUNITAMP: return KIND_CURRENT;
case FLAGUNITOHM: return KIND_RESISTANCE;
case FLAGUNITHZ: return KIND_FREQUENCY;
case FLAGUNITNF: return KIND_CAPACITANCE;
case FLAGUNITGRAD: return KIND_TEMP_C;
case FLAGUNITFAHR: return KIND_TEMP_F;
default: return KIND_OTHER;
}
}
const char *selected_button_name() const {
static const char *const names[] = {"SELECT", "RANGE", "HLD/LIG", "REL/BT", "HZ/DUTY", "MAX/MIN"};
uint8_t index = this->selected_button;
if (index < 1) index = 1;
if (index > 6) index = 6;
return names[index - 1];
}
void previous_button() {
if (this->selected_button > 1) this->selected_button--;
}
void next_button() {
if (this->selected_button < 6) this->selected_button++;
}
void render(esphome::display::Display &it, esphome::display::BaseFont *font,
esphome::display::BaseFont *value_font, int display_page = 0,
bool atorch_connected = false,
float atorch_voltage = NAN, float atorch_current = NAN, float atorch_power = NAN,
float atorch_capacity = NAN, float atorch_energy = NAN, float atorch_temperature = NAN) {
const Color bg(0, 0, 0);
const Color fg(210, 210, 210);
// Chosen to map to a neutral dark gray in the RGB332 8-bit display palette.
const Color inactive(80, 80, 80);
const Color yellow(255, 220, 0);
const Color blue(0, 80, 255);
const Color cyan(0, 255, 255);
const Color magenta(255, 0, 255);
const Color red(255, 0, 0);
const Color green(0, 220, 0);
const Color orange(255, 165, 0);
if (display_page == 0) {
// --- PAGE 1: OWON Multimeter ---
it.fill(bg);
bool status_active = this->connected && this->has_reading;
this->draw_icon_(it, 12, 8, 16, 16, ACCU_BMP, status_active ? (this->low_battery ? red : green) : inactive);
this->draw_icon_(it, 46, 8, 16, 16, BLE_BMP, this->connected ? blue : inactive);
this->label_(it, font, 86, 8, "AUTO", status_active && this->auto_range() ? fg : inactive);
this->label_(it, font, 138, 8, "MAX", status_active && this->max_mode() ? red : inactive);
this->label_(it, font, 178, 8, "MIN", status_active && this->min_mode() ? green : inactive);
this->draw_icon_(it, 218, 8, 16, 16, HOLD_BMP, status_active && this->hold() ? blue : inactive);
this->draw_icon_(it, 244, 8, 16, 16, REL_BMP, status_active && this->relative() ? Color(128, 128, 0) : inactive);
this->draw_icon_(it, 270, 8, 16, 16, DIODE_BMP, status_active && this->diode() ? magenta : inactive);
this->draw_icon_(it, 296, 8, 16, 16, BUZZ_BMP, status_active && this->continuity() ? orange : inactive);
this->label_(it, font, 8, 104, "DC", status_active && this->dc() ? cyan : inactive);
this->label_(it, font, 8, 124, "AC", status_active && this->ac() ? magenta : inactive);
if (!this->connected) {
this->draw_digits_(it, "----", false, inactive);
it.print(160, 148, font, inactive, esphome::display::TextAlign::CENTER, "scan/connect");
} else if (!this->has_reading) {
this->draw_digits_(it, "8888", false, inactive);
it.print(160, 148, font, inactive, esphome::display::TextAlign::CENTER, "waiting");
} else if (this->overload) {
this->draw_digits_(it, " OL ", false, fg);
} else {
char d[5];
d[0] = this->digit_char_(REGDIG1);
d[1] = this->digit_char_(REGDIG2);
d[2] = this->digit_char_(REGDIG3);
d[3] = this->digit_char_(REGDIG4);
d[4] = 0;
this->draw_digits_(it, d, this->negative(), fg);
this->draw_decimal_points_(it, fg);
}
if (status_active) {
std::string unit_line = std::string(this->scale()) + this->unit();
it.print(316, 141, font, yellow, esphome::display::TextAlign::BOTTOM_RIGHT, unit_line.c_str());
}
bool bargraph_active = status_active && !this->overload;
this->draw_bargraph_(it, bargraph_active ? this->digits_from_buffer_() : 0, bargraph_active);
it.filled_rectangle(34, 212, 40, 24, this->write_available ? fg : inactive);
it.filled_rectangle(108, 212, 100, 24, this->write_available ? fg : inactive);
it.filled_rectangle(242, 212, 40, 24, this->write_available ? fg : inactive);
it.print(54, 216, font, bg, esphome::display::TextAlign::TOP_CENTER, "<");
it.print(158, 216, font, bg, esphome::display::TextAlign::TOP_CENTER, this->selected_button_name());
it.print(262, 216, font, bg, esphome::display::TextAlign::TOP_CENTER, ">");
} else {
// --- PAGE 2: Atorch DL24 DC load ---
it.fill(bg);
const Color panel_active(18, 24, 32);
const Color panel_inactive(12, 12, 12);
const Color panel_dim(10, 14, 20);
const Color border_active(55, 70, 86);
const Color border_inactive(42, 42, 42);
const Color white(245, 245, 245);
const Color dim_value(110, 110, 110);
const Color dim_accent(65, 65, 65);
const Color panel = atorch_connected ? panel_active : panel_inactive;
const Color border = atorch_connected ? border_active : border_inactive;
const Color value_color = atorch_connected ? white : dim_value;
const Color voltage_color = atorch_connected ? cyan : dim_accent;
const Color current_color = atorch_connected ? orange : dim_accent;
const Color power_color = atorch_connected ? yellow : dim_accent;
const Color temp_color = atorch_connected ? magenta : dim_accent;
const Color header_color = atorch_connected ? cyan : dim_value;
char voltage_text[24];
char current_text[24];
char power_text[24];
char capacity_text[24];
char energy_text[24];
char temperature_text[24];
this->format_metric_(voltage_text, sizeof(voltage_text), atorch_voltage, "V", 2);
this->format_metric_(current_text, sizeof(current_text), atorch_current, "A", 3);
this->format_metric_(power_text, sizeof(power_text), atorch_power, "W", 2);
this->format_metric_(capacity_text, sizeof(capacity_text), atorch_capacity, "Ah", 3);
this->format_metric_(energy_text, sizeof(energy_text), atorch_energy, "Wh", 3);
this->format_metric_(temperature_text, sizeof(temperature_text), atorch_temperature, "°C", 1);
it.filled_rectangle(0, 0, 320, 30, atorch_connected ? panel_dim : panel_inactive);
it.print(10, 7, font, header_color, esphome::display::TextAlign::TOP_LEFT, "ATORCH DL24");
if (atorch_connected) {
this->draw_icon_(it, 152, 7, 16, 16, BLE_BMP, blue);
}
it.print(310, 7, font, inactive, esphome::display::TextAlign::TOP_RIGHT, "DC LOAD");
this->draw_metric_card_(it, font, value_font, 10, 42, 145, 76, "VOLTAGE", voltage_text, voltage_color, panel, border, value_color);
this->draw_metric_card_(it, font, value_font, 165, 42, 145, 76, "CURRENT", current_text, current_color, panel, border, value_color);
this->draw_metric_card_(it, font, value_font, 10, 128, 145, 76, "POWER", power_text, power_color, panel, border, value_color);
this->draw_metric_card_(it, font, value_font, 165, 128, 145, 76, "TEMP", temperature_text, temp_color, panel, border, value_color);
it.filled_rectangle(10, 212, 300, 22, atorch_connected ? panel_dim : panel_inactive);
it.filled_rectangle(10, 212, 300, 1, border);
it.filled_rectangle(10, 233, 300, 1, border);
it.filled_rectangle(10, 212, 1, 22, border);
it.filled_rectangle(309, 212, 1, 22, border);
it.print(20, 216, font, inactive, esphome::display::TextAlign::TOP_LEFT, "CAP");
it.print(64, 216, font, value_color, esphome::display::TextAlign::TOP_LEFT, capacity_text);
it.print(170, 216, font, inactive, esphome::display::TextAlign::TOP_LEFT, "ENERGY");
it.print(306, 216, font, value_color, esphome::display::TextAlign::TOP_RIGHT, energy_text);
}
}
private:
uint8_t raw_[14]{};
uint8_t value_[14]{};
float display_value{NAN};
float base_value{NAN};
static constexpr uint8_t OVERLOAD_FRAME[5] = {0x2B, 0x3F, 0x30, 0x3A, 0x3F};
uint16_t digits_from_buffer_() const {
uint16_t out = 0;
if (this->value_[REGDIG1] >= '0' && this->value_[REGDIG1] <= '9') out += (this->value_[REGDIG1] - '0') * 1000;
if (this->value_[REGDIG2] >= '0' && this->value_[REGDIG2] <= '9') out += (this->value_[REGDIG2] - '0') * 100;
if (this->value_[REGDIG3] >= '0' && this->value_[REGDIG3] <= '9') out += (this->value_[REGDIG3] - '0') * 10;
if (this->value_[REGDIG4] >= '0' && this->value_[REGDIG4] <= '9') out += (this->value_[REGDIG4] - '0');
return out;
}
float calc_display_value_() const {
if (this->overload) return NAN;
if (this->is_plus) {
uint16_t pair1 = static_cast<uint16_t>(this->raw_[0]) | (static_cast<uint16_t>(this->raw_[1]) << 8);
uint8_t decimal = pair1 & 0x07;
if (decimal >= 7) return NAN;
uint16_t pair3 = static_cast<uint16_t>(this->raw_[4]) | (static_cast<uint16_t>(this->raw_[5]) << 8);
bool negative = pair3 >= 0x7FFF;
uint16_t digits = negative ? (pair3 & 0x7FFF) : pair3;
float v = static_cast<float>(digits) / std::pow(10.0f, decimal);
return negative ? -v : v;
}
uint8_t decimal = 0;
switch (this->value_[REGPOINT] & 0x07) {
case 0b001: decimal = 1; break;
case 0b010: decimal = 2; break;
case 0b100: decimal = 3; break;
default: break;
}
float v = static_cast<float>(this->digits_from_buffer_()) / std::pow(10.0f, decimal);
return this->negative() ? -v : v;
}
float calc_base_value_() const {
if (std::isnan(this->display_value)) return NAN;
if (this->value_[REGUNIT] == FLAGUNITNF) return this->display_value * 1e-9f;
if ((this->value_[REGSCALE] & FLAGSCALEMEGA) == FLAGSCALEMEGA) return this->display_value * 1e6f;
if ((this->value_[REGSCALE] & FLAGSCALEKILO) == FLAGSCALEKILO) return this->display_value * 1e3f;
if ((this->value_[REGSCALE] & FLAGSCALEMILLI) == FLAGSCALEMILLI) return this->display_value * 1e-3f;
if ((this->value_[REGSCALE] & FLAGSCALEMICRO) == FLAGSCALEMICRO) return this->display_value * 1e-6f;
return this->display_value;
}
void parse_plus_() {
memset(this->value_, 0, sizeof(this->value_));
this->value_[5] = 0x20;
this->value_[12] = 0x0D;
this->value_[13] = 0x0A;
uint16_t pair1 = static_cast<uint16_t>(this->raw_[0]) | (static_cast<uint16_t>(this->raw_[1]) << 8);
uint8_t function = (pair1 >> 6) & 0x0F;
uint8_t scale = (pair1 >> 3) & 0x07;
uint8_t decimal = pair1 & 0x07;
switch (decimal) {
case 0: this->value_[REGPOINT] = FLAGPOINT0; break;
case 1: this->value_[REGPOINT] = FLAGPOINT3; break;
case 2: this->value_[REGPOINT] = FLAGPOINT2; break;
case 3: this->value_[REGPOINT] = FLAGPOINT1; break;
default: break;
}
switch (function) {
case 0: this->value_[REGUNIT] |= FLAGUNITVOLT; this->value_[REGMODE] |= FLAGMODEDC; break;
case 1: this->value_[REGUNIT] |= FLAGUNITVOLT; this->value_[REGMODE] |= FLAGMODEAC; break;
case 2: this->value_[REGUNIT] |= FLAGUNITAMP; this->value_[REGMODE] |= FLAGMODEDC; break;
case 3: this->value_[REGUNIT] |= FLAGUNITAMP; this->value_[REGMODE] |= FLAGMODEAC; break;
case 4: this->value_[REGUNIT] |= FLAGUNITOHM; break;
case 5: this->value_[REGUNIT] |= FLAGUNITNF; break;
case 6: this->value_[REGUNIT] |= FLAGUNITHZ; break;
case 7: this->value_[REGSCALE] |= FLAGSCALEDUTY; break;
case 8: this->value_[REGUNIT] |= FLAGUNITGRAD; break;
case 9: this->value_[REGUNIT] |= FLAGUNITFAHR; break;
case 10: this->value_[REGSCALE] |= FLAGSCALEDIODE; break;
case 11: this->value_[REGSCALE] |= FLAGSCALEBUZZ; break;
case 12: this->value_[REGUNIT] |= FLAGUNITHFE; break;
default: break;
}
switch (scale) {
case 2: this->value_[REGSCALE] |= FLAGSCALEMICRO; break;
case 3: this->value_[REGSCALE] |= FLAGSCALEMILLI; break;
case 5: this->value_[REGSCALE] |= FLAGSCALEKILO; break;
case 6: this->value_[REGSCALE] |= FLAGSCALEMEGA; break;
default: break;
}
uint16_t pair2 = static_cast<uint16_t>(this->raw_[2]) | (static_cast<uint16_t>(this->raw_[3]) << 8);
if (pair2 & (1 << 0)) this->value_[REGMODE] |= FLAGMODEHOLD;
if (pair2 & (1 << 1)) this->value_[REGMODE] |= FLAGMODEREL;
if (pair2 & (1 << 2)) this->value_[REGMODE] |= FLAGMODEAUTO;
this->low_battery = (pair2 & (1 << 3)) != 0;
if (pair2 & (1 << 4)) this->value_[REGMINMAX] |= FLAGMIN;
if (pair2 & (1 << 5)) this->value_[REGMINMAX] |= FLAGMAX;
uint16_t pair3 = static_cast<uint16_t>(this->raw_[4]) | (static_cast<uint16_t>(this->raw_[5]) << 8);
if (decimal < 7) {
uint16_t digits = pair3;
if (pair3 < 0x7FFF) {
this->value_[REGPLUSMINUS] = FLAGPLUS;
} else {
this->value_[REGPLUSMINUS] = FLAGMINUS;
digits = pair3 & 0x7FFF;
}
this->value_[REGDIG1] = '0' + ((digits / 1000) % 10);
this->value_[REGDIG2] = '0' + ((digits / 100) % 10);
this->value_[REGDIG3] = '0' + ((digits / 10) % 10);
this->value_[REGDIG4] = '0' + (digits % 10);
} else {
memcpy(this->value_, OVERLOAD_FRAME, sizeof(OVERLOAD_FRAME));
}
}
char digit_char_(uint8_t reg) const {
uint8_t c = this->value_[reg];
return (c >= '0' && c <= '9') ? static_cast<char>(c) : ' ';
}
void format_metric_(char *buffer, size_t size, float value, const char *unit, uint8_t decimals) const {
if (!std::isfinite(value)) {
snprintf(buffer, size, "-- %s", unit);
return;
}
char format[12];
snprintf(format, sizeof(format), "%%.%uf %%s", decimals);
snprintf(buffer, size, format, value, unit);
}
void draw_metric_card_(Display &it, esphome::display::BaseFont *label_font, esphome::display::BaseFont *value_font,
int x, int y, int w, int h, const char *title, const char *value,
Color accent, Color fill, Color border, Color value_color) {
const Color bg(0, 0, 0);
const Color inactive(90, 100, 110);
it.filled_rectangle(x, y, w, h, fill);
it.filled_rectangle(x, y, w, 2, accent);
it.filled_rectangle(x, y + h - 1, w, 1, border);
it.filled_rectangle(x, y, 1, h, border);
it.filled_rectangle(x + w - 1, y, 1, h, border);
it.print(x + 10, y + 9, label_font, inactive, esphome::display::TextAlign::TOP_LEFT, title);
it.print(x + w / 2, y + 43, value_font, value_color, esphome::display::TextAlign::CENTER, value);
it.filled_rectangle(x + 10, y + h - 11, w - 20, 3, bg);
it.filled_rectangle(x + 10, y + h - 11, w - 20, 1, accent);
}
void label_(Display &it, esphome::display::BaseFont *font, int x, int y, const char *text, Color color) {
it.print(x, y, font, color, esphome::display::TextAlign::TOP_LEFT, text);
}
void draw_digits_(Display &it, const char *text, bool negative, Color color) {
if (negative) this->draw_segment_(it, 8, 83, 26, 9, true, color);
constexpr int digit_x = 40;
constexpr int digit_y = 35;
constexpr int digit_w = 50;
constexpr int digit_h = 106;
constexpr int digit_distance = 64;
for (int i = 0; i < 4; i++) {
this->draw_seven_segment_(it, digit_x + i * digit_distance, digit_y, digit_w, digit_h, text[i], color);
}
}
void draw_decimal_points_(Display &it, Color color) {
uint8_t p = this->value_[REGPOINT];
if ((p & FLAGPOINT1) == FLAGPOINT1) it.filled_rectangle(95, 135, 8, 10, color);
if ((p & FLAGPOINT2) == FLAGPOINT2) it.filled_rectangle(159, 135, 8, 10, color);
if ((p & FLAGPOINT3) == FLAGPOINT3) it.filled_rectangle(223, 135, 8, 10, color);
}
void draw_segment_(Display &it, int x, int y, int w, int h, bool horizontal, Color color) {
if (horizontal) {
int cap = h / 2;
it.filled_rectangle(x + cap, y, w - 2 * cap, h +1, color);
it.filled_triangle(x, y + cap, x + cap, y, x + cap, y + h, color);
it.filled_triangle(x + w, y + cap, x + w - cap, y, x + w - cap, y + h, color);
} else {
int cap = w / 2;
it.filled_rectangle(x, y + cap, w +1, h - 2 * cap, color);
it.filled_triangle(x + cap, y, x, y + cap, x + w, y + cap, color);
it.filled_triangle(x + cap, y + h, x, y + h - cap, x + w, y + h - cap, color);
}
}
void draw_icon_(Display &it, int x, int y, int w, int h, const uint8_t *data, Color color) {
for (int row = 0; row < h; row++) {
for (int col = 0; col < w; col++) {
uint8_t byte = data[row * ((w + 7) / 8) + (col / 8)];
if ((byte & (0x80 >> (col % 8))) != 0) {
it.draw_pixel_at(x + col, y + row, color);
}
}
}
}
void draw_seven_segment_(Display &it, int x, int y, int w, int h, char ch, Color color) {
bool a=false,b=false,c=false,d=false,e=false,f=false,g=false;
switch (ch) {
case '0': a=b=c=d=e=f=true; break;
case '1': b=c=true; break;
case '2': a=b=d=e=g=true; break;
case '3': a=b=c=d=g=true; break;
case '4': b=c=f=g=true; break;
case '5': a=c=d=f=g=true; break;
case '6': a=c=d=e=f=g=true; break;
case '7': a=b=c=true; break;
case '8': a=b=c=d=e=f=g=true; break;
case '9': a=b=c=d=f=g=true; break;
case 'O': a=b=c=d=e=f=true; break;
case 'L': d=e=f=true; break;
case '-': g=true; break;
default: break;
}
int t = 10;
int half = h / 2;
int gap = 1;
int top_y = y;
int mid_y = y + half - t / 2;
int bot_y = y + h - t;
int upper_v_y = top_y + t + gap;
int upper_v_h = mid_y - gap - upper_v_y;
int lower_v_y = mid_y + t + gap;
int lower_v_h = bot_y - gap - lower_v_y;
if (a) this->draw_segment_(it, x + t / 2, top_y, w - t, t, true, color);
if (b && upper_v_h > 0) this->draw_segment_(it, x + w - t, upper_v_y, t, upper_v_h, false, color);
if (c && lower_v_h > 0) this->draw_segment_(it, x + w - t, lower_v_y, t, lower_v_h, false, color);
if (d) this->draw_segment_(it, x + t / 2, bot_y, w - t, t, true, color);
if (e && lower_v_h > 0) this->draw_segment_(it, x, lower_v_y, t, lower_v_h, false, color);
if (f && upper_v_h > 0) this->draw_segment_(it, x, upper_v_y, t, upper_v_h, false, color);
if (g) this->draw_segment_(it, x + t / 2, mid_y, w - t, t, true, color);
}
void draw_bargraph_(Display &it, uint16_t digits, bool active) {
const Color fg(255, 255, 255);
const Color inactive(80, 80, 80);
uint16_t mapped = active ? static_cast<uint16_t>(digits * 240 / 6000) : 0;
if (mapped > 240) mapped = 240;
for (uint16_t i = 0; i <= 240; i += 4) {
Color col = (active && i <= mapped) ? fg : inactive;
int h = (i % 40 == 0) ? 20 : ((i % 20 == 0) ? 15 : 10);
it.vertical_line(40 + i, 185 - h, h, col);
}
it.horizontal_line(35, 185, 250, inactive);
}
};
} // namespace owon_b35t
static owon_b35t::Meter owon_meter;
+628
View File
@@ -0,0 +1,628 @@
# Connects to an OWON BT35T/BT35T+ multimeter and an Atorch DL24 dc-load and mirrors their
# displays. Also sends those values to Home Assistant for logging and dashboard display.
# Also exposes actionable buttons to HA to control the devices.
#
# Derived work based on https://github.com/reaper7/M5Stack_BLE_client_Owon_B35T by reaper7.
# AI (ChatGPT (GPT5.5) has been used to adopt the Arduino sketch to ESPHome
# Ported to M5Stack Core2 due to memory constraints.
# Integrated Atorch BLE proxy functionality from https://github.com/syssi/esphome-atorch-dl24 by syssi.
# Soft buttons work as indicated for OWON meter. Left/right so select action, middle to execute.
# Long-press is supported. Display switches to last connected device, tap center of screen to
# switch. The dc-load has no on-device buttons, but both are available from HA.
substitutions:
name: "lab-ble-proxy"
friendly_name: "Lab BLE Proxy"
device_description: "M5Stack Core2 BLE client for OWON B35T/B35T+ multimeter and Atorch DL24 DC load"
owon_mac_address: !secret owon_b35t_mac_address
dl24_mac_address: !secret dl24_mac_address
external_components_source: github://syssi/esphome-atorch-dl24@main
atorch_project_version: "2.1.0"
esphome:
name: ${name}
friendly_name: ${friendly_name}
comment: ${device_description}
min_version: 2024.6.0
includes:
- lab-ble-proxy.h
on_boot:
priority: 850
then:
- lambda: |-
owon_b35t::core2_axp192_init(id(core2_i2c));
project:
name: "custom.lab-ble-proxy-m5stack-core2"
version: "1.0"
esp32:
board: m5stack-core2
flash_size: 16MB
framework:
type: esp-idf
advanced:
minimum_chip_revision: "3.1"
sram1_as_iram: true
psram:
mode: quad
speed: 80MHz
external_components:
- source: ${external_components_source}
refresh: 0s
logger:
level: INFO
api:
encryption:
key: !secret apikey
ota:
platform: esphome
password: !secret ota
network:
enable_ipv6: true
wifi:
ssid: "Voltage-legacy"
password: !secret voltage_legacy_psk
#use_address: ${name}.home
power_save_mode: none
fast_connect: on
min_auth_mode: WPA2
ap:
ssid: "Lab BLE Proxy Fallback Hotspot"
password: !secret fallback_psk
captive_portal:
globals:
- id: display_page
type: int
initial_value: "0"
restore_value: no
- id: atorch_connected
type: bool
initial_value: "false"
restore_value: no
script:
- id: wake_backlight
mode: restart
then:
- script.stop: backlight_idle
- light.turn_on:
id: backlight
brightness: 100%
- id: backlight_idle
mode: restart
then:
- delay: 2min
- if:
condition:
lambda: |-
return !owon_meter.connected && !id(atorch_connected);
then:
- light.turn_on:
id: backlight
brightness: 50%
- delay: 3min
- if:
condition:
lambda: |-
return !owon_meter.connected && !id(atorch_connected);
then:
- light.turn_off: backlight
interval:
- interval: 10s
then:
- lambda: |-
ESP_LOGI("mem", "heap free=%u min_free=%u internal_free=%u internal_largest=%u dma_free=%u dma_largest=%u psram_free=%u psram_largest=%u",
static_cast<unsigned>(esp_get_free_heap_size()),
static_cast<unsigned>(esp_get_minimum_free_heap_size()),
static_cast<unsigned>(heap_caps_get_free_size(MALLOC_CAP_INTERNAL)),
static_cast<unsigned>(heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL)),
static_cast<unsigned>(heap_caps_get_free_size(MALLOC_CAP_DMA)),
static_cast<unsigned>(heap_caps_get_largest_free_block(MALLOC_CAP_DMA)),
static_cast<unsigned>(heap_caps_get_free_size(MALLOC_CAP_SPIRAM)),
static_cast<unsigned>(heap_caps_get_largest_free_block(MALLOC_CAP_SPIRAM)));
esp32_ble_tracker:
scan_parameters:
active: true
continuous: true
ble_client:
- mac_address: ${owon_mac_address}
id: owon_ble_client
on_connect:
then:
- script.execute: wake_backlight
- lambda: |-
owon_meter.on_connect();
id(display_page) = 0;
id(lcd).update();
on_disconnect:
then:
- lambda: |-
owon_meter.on_disconnect();
if (id(display_page) == 0 && id(atorch_connected)) {
id(display_page) = 1;
}
id(lcd).update();
- if:
condition:
lambda: |-
return !owon_meter.connected && !id(atorch_connected);
then:
- script.execute: backlight_idle
- mac_address: ${dl24_mac_address}
id: atorch_ble_client
on_connect:
then:
- script.execute: wake_backlight
- lambda: |-
id(atorch_connected) = true;
id(display_page) = 1;
id(lcd).update();
on_disconnect:
then:
- lambda: |-
id(atorch_connected) = false;
if (id(display_page) == 1 && owon_meter.connected) {
id(display_page) = 0;
}
id(lcd).update();
- if:
condition:
lambda: |-
return !owon_meter.connected && !id(atorch_connected);
then:
- script.execute: backlight_idle
atorch_dl24:
- id: atorch0
ble_client_id: atorch_ble_client
check_crc: false
throttle: 0s
spi:
clk_pin: GPIO18
mosi_pin: GPIO23
i2c:
id: core2_i2c
sda: GPIO21
scl: GPIO22
scan: true
output:
- platform: template
type: float
id: lcd_backlight
write_action:
- lambda: |-
owon_b35t::core2_axp192_set_backlight(state);
light:
- platform: monochromatic
output: lcd_backlight
name: "${friendly_name} Backlight"
id: backlight
restore_mode: ALWAYS_ON
font:
- file: "fonts/Roboto-Regular.ttf"
id: meter_font
size: 15
glyphs:
[
" ",
"!",
"%",
"+",
"-",
".",
"/",
":",
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"<",
">",
"A",
"B",
"C",
"D",
"E",
"F",
"G",
"H",
"I",
"L",
"M",
"N",
"O",
"P",
"R",
"S",
"T",
"U",
"V",
"W",
"X",
"Y",
"Z",
"a",
"b",
"c",
"d",
"e",
"f",
"g",
"h",
"i",
"k",
"l",
"m",
"n",
"o",
"p",
"r",
"s",
"t",
"u",
"v",
"w",
"y",
"z",
"°",
"µ",
"Ω",
]
- file: "fonts/Roboto-Medium.ttf"
id: atorch_value_font
size: 22
glyphs:
[
" ",
"+",
"-",
".",
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"A",
"C",
"V",
"W",
"h",
"°",
]
display:
- platform: mipi_spi
id: lcd
model: M5CORE2
update_interval: 500ms
lambda: |-
owon_meter.render(
it,
id(meter_font),
id(atorch_value_font),
id(display_page),
id(atorch_connected),
id(atorch_voltage).has_state() ? id(atorch_voltage).state : NAN,
id(atorch_current).has_state() ? id(atorch_current).state : NAN,
id(atorch_power).has_state() ? id(atorch_power).state : NAN,
id(atorch_capacity).has_state() ? id(atorch_capacity).state : NAN,
id(atorch_energy_calculated).has_state() ? id(atorch_energy_calculated).state : NAN,
id(atorch_temperature).has_state() ? id(atorch_temperature).state : NAN
);
touchscreen:
- platform: ft63x6
id: touch
display: lcd
on_touch:
then:
- script.execute: wake_backlight
- if:
condition:
lambda: |-
return !owon_meter.connected && !id(atorch_connected);
then:
- script.execute: backlight_idle
binary_sensor:
- platform: touchscreen
touchscreen_id: touch
id: btn_toggle_page
x_min: 70
x_max: 250
y_min: 45
y_max: 195
on_press:
then:
- lambda: |-
id(display_page) = 1 - id(display_page);
id(lcd).update();
- platform: touchscreen
id: button_a
touchscreen_id: touch
x_min: 34
x_max: 74
y_min: 212
y_max: 240
internal: true
on_press:
then:
- lambda: |-
owon_meter.previous_button();
- platform: touchscreen
id: button_b
touchscreen_id: touch
x_min: 108
x_max: 208
y_min: 212
y_max: 240
internal: true
on_click:
- min_length: 50ms
max_length: 1500ms
then:
- logger.log:
level: INFO
format: "OWON short press: %s"
args: ["owon_meter.selected_button_name()"]
- ble_client.ble_write:
id: owon_ble_client
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb"
value: !lambda |-
std::vector<uint8_t> data = {owon_meter.selected_button, 0x01};
return data;
- min_length: 1500ms
max_length: 5000ms
then:
- logger.log:
level: INFO
format: "OWON long press: %s"
args: ["owon_meter.selected_button_name()"]
- ble_client.ble_write:
id: owon_ble_client
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb"
value: !lambda |-
uint8_t press_type = (owon_meter.selected_button == 1 || owon_meter.selected_button == 5) ? 0x01 : 0x00;
std::vector<uint8_t> data = {owon_meter.selected_button, press_type};
return data;
- platform: touchscreen
id: button_c
touchscreen_id: touch
x_min: 242
x_max: 282
y_min: 212
y_max: 240
internal: true
on_press:
then:
- lambda: |-
owon_meter.next_button();
- platform: template
name: "${friendly_name} OWON Connected"
icon: "mdi:bluetooth-connect"
lambda: |-
return owon_meter.connected;
- platform: template
name: "${friendly_name} OWON Overload"
icon: "mdi:debug-step-over"
lambda: |-
return owon_meter.overload;
- platform: template
name: "${friendly_name} OWON Low Battery"
icon: "mdi:battery-low"
lambda: |-
return owon_meter.low_battery;
- platform: template
name: "${friendly_name} Atorch Connected"
device_class: connectivity
icon: "mdi:bluetooth-connect"
lambda: |-
return id(atorch_connected);
sensor:
- platform: ble_client
type: characteristic
ble_client_id: owon_ble_client
id: owon_notify_source
internal: true
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
characteristic_uuid: "0000fff4-0000-1000-8000-00805f9b34fb"
notify: true
update_interval: never
lambda: |-
owon_meter.handle_notify(x);
return owon_meter.value();
- platform: wifi_signal
name: "${friendly_name} WiFi Signal"
update_interval: 60s
- platform: atorch_dl24
atorch_dl24_id: atorch0
voltage:
name: "${friendly_name} Atorch Voltage"
id: atorch_voltage
current:
name: "${friendly_name} Atorch Current"
id: atorch_current
power:
name: "${friendly_name} Atorch Power"
id: atorch_power
capacity:
name: "${friendly_name} Atorch Capacity"
id: atorch_capacity
temperature:
name: "${friendly_name} Atorch Temperature"
id: atorch_temperature
dim_backlight:
name: "${friendly_name} Atorch Dim Backlight"
id: atorch_dim_backlight
- platform: integration
name: "${friendly_name} Atorch Energy"
id: atorch_energy_calculated
sensor: atorch_power
time_unit: h
unit_of_measurement: "Wh"
icon: "mdi:lightning-bolt"
device_class: energy
state_class: total_increasing
accuracy_decimals: 3
restore: true
integration_method: trapezoid
text_sensor:
- platform: template
name: "${friendly_name} OWON Reading"
update_interval: 1s
icon: "mdi:gauge"
lambda: |-
return owon_meter.reading_text();
- platform: template
name: "${friendly_name} OWON Unit"
update_interval: 1s
icon: "mdi:ruler"
lambda: |-
return std::string(owon_meter.scale()) + owon_meter.unit();
- platform: template
name: "${friendly_name} OWON Mode"
icon: "mdi:knob"
update_interval: 1s
lambda: |-
return owon_meter.mode_text();
button:
- platform: atorch_dl24
atorch_dl24_id: atorch0
reset_energy:
name: "${friendly_name} Atorch Reset Energy"
on_press:
- sensor.integration.reset: atorch_energy_calculated
reset_capacity:
name: "${friendly_name} Atorch Reset Capacity"
reset_runtime:
name: "${friendly_name} Atorch Reset Runtime"
reset_all:
name: "${friendly_name} Atorch Reset All"
on_press:
- sensor.integration.reset: atorch_energy_calculated
usb_plus:
name: "${friendly_name} Atorch Plus"
usb_minus:
name: "${friendly_name} Atorch Minus"
setup:
name: "${friendly_name} Atorch Setup"
enter:
name: "${friendly_name} Atorch Enter"
- platform: template
name: "OWON SELECT"
id: owon_btn_select
on_press:
- ble_client.ble_write:
id: owon_ble_client
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb"
value: !lambda "return std::vector<uint8_t>({1, 0x01});"
- platform: template
name: "OWON RANGE"
id: owon_btn_range
on_press:
- ble_client.ble_write:
id: owon_ble_client
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb"
value: !lambda "return std::vector<uint8_t>({2, 0x01});"
- platform: template
name: "OWON HOLD "
id: owon_btn_hold
on_press:
- ble_client.ble_write:
id: owon_ble_client
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb"
value: !lambda "return std::vector<uint8_t>({3, 0x01});"
- platform: template
name: "OWON LIGHT"
id: owon_btn_light
on_press:
- ble_client.ble_write:
id: owon_ble_client
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb"
value: !lambda "return std::vector<uint8_t>({3, 0x00});"
- platform: template
name: "OWON RELATIVE"
id: owon_btn_rel
on_press:
- ble_client.ble_write:
id: owon_ble_client
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb"
value: !lambda "return std::vector<uint8_t>({4, 0x01});"
- platform: template
name: "OWON BT"
id: owon_btn_bt
on_press:
- ble_client.ble_write:
id: owon_ble_client
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb"
value: !lambda "return std::vector<uint8_t>({4, 0x00});"
- platform: template
name: "OWON HZ | DUTY"
id: owon_btn_hz
on_press:
- ble_client.ble_write:
id: owon_ble_client
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb"
value: !lambda "return std::vector<uint8_t>({5, 0x01});"
- platform: template
name: "OWON MAX | MIN"
id: owon_btn_maxmin
on_press:
- ble_client.ble_write:
id: owon_ble_client
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb"
value: !lambda "return std::vector<uint8_t>({6, 0x01});"
+11 -4
View File
@@ -52,14 +52,20 @@ esphome:
esp32:
board: nodemcu-32s
framework:
type: arduino
type: esp-idf
advanced:
minimum_chip_revision: "3.0"
network:
enable_ipv6: true
wifi:
ssid: "Voltage-legacy"
password: !secret voltage_legacy_psk
use_address: ${name}.home
# use_address: ${name}.home
power_save_mode: high
fast_connect: on
min_auth_mode: WPA2
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
@@ -114,6 +120,7 @@ light:
# Initialize UART for Vindriktning sensor
uart:
id: uartPM
rx_pin: GPIO22
baud_rate: 9600
@@ -209,7 +216,7 @@ sensor:
ambient_pressure_compensation: 1
automatic_self_calibration: True
address: 0x61
update_interval: 60s
update_interval: 10s
# Vindriktning particulate matter sensor
- platform: pm1006
pm_2_5:
@@ -236,7 +243,7 @@ sensor:
- platform: adc
pin: 34
name: "Spannung Photodiode"
attenuation: 11db
attenuation: 12db
update_interval: 1s
internal: true
on_value_range:
+5 -1
View File
@@ -26,14 +26,18 @@ ota:
id: ota_esphome
password: !secret ota
network:
enable_ipv6: true
wifi:
on_connect:
- delay: 5s # Gives time for improv results to be transmitted
ssid: Voltage-legacy
password: !secret voltage_legacy_psk
use_address: ${name}.home
#use_address: ${name}.home
power_save_mode: high
fast_connect: on
min_auth_mode: WPA2
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "Raspiaudio Fallback Hotspot"
+5 -1
View File
@@ -50,12 +50,16 @@ esphome:
esp8266:
board: esp12e
network:
enable_ipv6: true
wifi:
ssid: "Voltage-legacy"
password: !secret voltage_legacy_psk
use_address: riden-labornetzteil-18a.home
#use_address: riden-labornetzteil-18a.home
power_save_mode: high
fast_connect: on
min_auth_mode: WPA2
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
+5 -1
View File
@@ -50,12 +50,16 @@ esphome:
esp8266:
board: esp12e
network:
enable_ipv6: true
wifi:
ssid: "Voltage-legacy"
password: !secret voltage_legacy_psk
use_address: riden-labornetzteil-6a.home
#use_address: riden-labornetzteil-6a.home
power_save_mode: high
fast_connect: on
min_auth_mode: WPA2
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
+6 -2
View File
@@ -14,14 +14,18 @@ esphome:
esp32:
board: esp32-c3-devkitm-1
framework:
type: arduino
type: esp-idf
network:
enable_ipv6: true
wifi:
ssid: "Voltage-legacy"
password: !secret voltage_legacy_psk
use_address: serverroom.home
#use_address: serverroom.home
power_save_mode: high
fast_connect: on
min_auth_mode: WPA2
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
-321
View File
@@ -1,321 +0,0 @@
##############################################################################
# Globals Glances parameters
##############################################################################
[global]
# Does Glances should check if a newer version is available on PyPI ?
check_update=false
# History size (maximum number of values)
# Default glances history size to 0
# https://github.com/hassio-addons/addon-glances/issues/61
# https://github.com/nicolargo/glances/issues/1341
history_size=0
##############################################################################
# User interface
##############################################################################
[outputs]
# Theme name for the Curses interface: black or white
curse_theme=black
# Limit the number of processes to display in the WebUI
max_processes_display=30
##############################################################################
# plugins
##############################################################################
[quicklook]
# Define CPU, MEM and SWAP thresholds in %
cpu_careful=50
cpu_warning=70
cpu_critical=90
mem_careful=50
mem_warning=70
mem_critical=90
swap_careful=50
swap_warning=70
swap_critical=90
[cpu]
# Default values if not defined: 50/70/90 (except for iowait)
user_careful=50
user_warning=70
user_critical=90
#user_log=False
#user_critical_action=echo {{user}} {{value}} {{max}} > /tmp/cpu.alert
system_careful=50
system_warning=70
system_critical=90
steal_careful=50
steal_warning=70
steal_critical=90
#steal_log=True
# I/O wait percentage should be lower than 1/# (Logical CPU cores)
# Leave commented to just use the default config (1/#-20% / 1/#-10% / 1/#)
#iowait_careful=30
#iowait_warning=40
#iowait_critical=50
# Context switch limit (core / second)
# Leave commented to just use the default config (critical is 50000*# (Logical CPU cores)
#ctx_switches_careful=10000
#ctx_switches_warning=12000
#ctx_switches_critical=14000
[percpu]
# Define CPU thresholds in %
# Default values if not defined: 50/70/90
user_careful=50
user_warning=70
user_critical=90
iowait_careful=50
iowait_warning=70
iowait_critical=90
system_careful=50
system_warning=70
system_critical=90
[gpu]
# Default processor values if not defined: 50/70/90
proc_careful=50
proc_warning=70
proc_critical=90
# Default memory values if not defined: 50/70/90
mem_careful=50
mem_warning=70
mem_critical=90
[mem]
# Define RAM thresholds in %
# Default values if not defined: 50/70/90
careful=50
#careful_action_repeat=echo {{percent}} >> /tmp/memory.alert
warning=70
critical=90
[memswap]
# Define SWAP thresholds in %
# Default values if not defined: 50/70/90
careful=50
warning=70
critical=90
[load]
# Define LOAD thresholds
# Value * number of cores
# Default values if not defined: 0.7/1.0/5.0 per number of cores
# Source: http://blog.scoutapp.com/articles/2009/07/31/understanding-load-averages
# http://www.linuxjournal.com/article/9001
careful=0.7
warning=1.0
critical=5.0
#log=False
[network]
# Default bitrate thresholds in % of the network interface speed
# Default values if not defined: 70/80/90
rx_careful=70
rx_warning=80
rx_critical=90
tx_careful=70
tx_warning=80
tx_critical=90
# Define the list of hidden network interfaces (comma-separated regexp)
#hide=docker.*,lo
# WLAN 0 alias
#wlan0_alias=Wireless IF
# It is possible to overwrite the bitrate thresholds per interface
# WLAN 0 Default limits (in bits per second aka bps) for interface bitrate
#wlan0_rx_careful=4000000
#wlan0_rx_warning=5000000
#wlan0_rx_critical=6000000
#wlan0_rx_log=True
#wlan0_tx_careful=700000
#wlan0_tx_warning=900000
#wlan0_tx_critical=1000000
#wlan0_tx_log=True
[wifi]
# Define the list of hidden wireless network interfaces (comma-separated regexp)
hide=lo,docker.*
# Define SIGNAL thresholds in db (lower is better...)
# Based on: http://serverfault.com/questions/501025/industry-standard-for-minimum-wifi-signal-strength
careful=-65
warning=-75
critical=-85
[diskio]
# Define the list of hidden disks (comma-separated regexp)
#hide=sda2,sda5,loop.*
hide=loop.*
# Alias for sda1
#sda1_alias=IntDisk
[fs]
# Define the list of hidden file system (comma-separated regexp)
#hide=/boot.*
# Define filesystem space thresholds in %
# Default values if not defined: 50/70/90
# It is also possible to define per mount point value
# Example: /_careful=40
careful=50
warning=70
critical=90
# Allow additional file system types (comma-separated FS type)
#allow=zfs
[folders]
# Define a folder list to monitor
# The list is composed of items (list_#nb <= 10)
# An item is defined by:
# * path: absolute path
# * careful: optional careful threshold (in MB)
# * warning: optional warning threshold (in MB)
# * critical: optional critical threshold (in MB)
folder_1_path=/config
folder_1_careful=4096
folder_1_warning=8196
folder_1_critical=12228
folder_2_path=/share
folder_2_careful=4096
folder_2_warning=8196
folder_2_critical=12228
folder_3_path=/backup
folder_3_careful=4096
folder_3_warning=8196
folder_3_critical=12228
folder_4_path=/media
folder_4_careful=4096
folder_4_warning=8196
folder_4_critical=12228
[sensors]
# Sensors core thresholds (in Celsius...)
# Default values if not defined: 60/70/80
temperature_core_careful=60
temperature_core_warning=70
temperature_core_critical=80
# Temperatures threshold in °C for hddtemp
# Default values if not defined: 45/52/60
temperature_hdd_careful=45
temperature_hdd_warning=52
temperature_hdd_critical=60
# Battery threshold in %
battery_careful=80
battery_warning=90
battery_critical=95
# Sensors alias
#temp1_alias=Motherboard 0
#temp2_alias=Motherboard 1
#core 0_alias=CPU Core 0
#core 1_alias=CPU Core 1
[processlist]
# Define CPU/MEM (per process) thresholds in %
# Default values if not defined: 50/70/90
cpu_careful=50
cpu_warning=70
cpu_critical=90
mem_careful=50
mem_warning=70
mem_critical=90
#
# Nice priorities range from -20 to 19.
# Configure nice levels using a comma separated list.
#
# Nice: Example 1, non-zero is warning (default behavior)
nice_warning=-20,-19,-18,-17,-16,-15,-14,-13,-12,-11,-10,-9,-8,-7,-6,-5,-4,-3,-2,-1,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19
#
# Nice: Example 2, low priority processes escalate from careful to critical
#nice_careful=1,2,3,4,5,6,7,8,9
#nice_warning=10,11,12,13,14
#nice_critical=15,16,17,18,19
[ports]
# Ports scanner plugin configuration
# Interval in second between two scans
refresh=30
# Set the default timeout (in second) for a scan (can be overwritten in the scan list)
timeout=3
# If port_default_gateway is True, add the default gateway on top of the scan list
port_default_gateway=True
#
# Define the scan list (1 < x < 255)
# port_x_host (name or IP) is mandatory
# port_x_port (TCP port number) is optional (if not set, use ICMP)
# port_x_description is optional (if not set, define to host:port)
# port_x_timeout is optional and overwrite the default timeout value
# port_x_rtt_warning is optional and defines the warning threshold in ms
#
#port_1_host=192.168.0.1
#port_1_port=80
#port_1_description=Home Box
#port_1_timeout=1
#port_2_host=www.free.fr
#port_2_description=My ISP
#port_3_host=www.google.com
#port_3_description=Internet ICMP
#port_3_rtt_warning=1000
#port_4_description=Internet Web
#port_4_host=www.google.com
#port_4_port=80
#port_4_rtt_warning=1000
#
# Define Web (URL) monitoring list (1 < x < 255)
# web_x_url is the URL to monitor (example: http://my.site.com/folder)
# web_x_description is optional (if not set, define to URL)
# web_x_timeout is optional and overwrite the default timeout value
# web_x_rtt_warning is optional and defines the warning respond time in ms (approximatively)
#
#web_1_url=https://blog.nicolargo.com
#web_1_description=My Blog
#web_1_rtt_warning=3000
#web_2_url=https://github.com
#web_3_url=http://www.google.fr
#web_3_description=Google Fr
#web_4_url=https://blog.nicolargo.com/nonexist
#web_4_description=Intranet
[docker]
# Thresholds for CPU and MEM (in %)
cpu_careful=50
cpu_warning=70
cpu_critical=90
mem_careful=20
mem_warning=50
mem_critical=70
#
# Per container thresholds
#containername_cpu_careful=10
#containername_cpu_warning=20
#containername_cpu_critical=30
#
# By default, Glances only display running containers
# Set the following key to True to display all containers
all=False
##############################################################################
# Client/server
##############################################################################
[serverlist]
# Define the static servers list
#server_1_name=localhost
#server_1_alias=My local PC
#server_1_port=61209
#server_2_name=localhost
#server_2_port=61235
#server_3_name=192.168.0.17
#server_3_alias=Another PC on my network
#server_3_port=61209
#server_4_name=pasbon
#server_4_port=61237
[passwords]
# Define the passwords list
# Syntax: host=password
# Where: host is the hostname
# password is the clear password
# Additionally (and optionally) a default password could be defined
#localhost=abc
#default=defaultpassword
+30 -28
View File
@@ -1,9 +1,9 @@
db_url: !secret ha_recorder_dburl
# Commit to db only every X seconds
commit_interval: 60
# Delete events and states older than 1 week
auto_purge: true
purge_keep_days: 7
db_url: !secret ha_recorder_dburl
# Commit to db only every X seconds
commit_interval: 60
# Delete events and states older than 1 week
auto_purge: true
purge_keep_days: 7
# include:
# entity_globs:
# - sensor.rd6018_controller_*
@@ -15,27 +15,29 @@
# - sensor
# - switch
# - media_player
exclude:
# domains:
# - automation
# - updater
entity_globs:
- weather.zuhause_*
- sensor.*_power_factor
- sensor.dwd*
- binary_sensor.*firmware_update
- binary_sensor.*_overpowering
- binary_sensor.*_overheating
- sensor.*_wi_fi_signal
- sensor.*_wifi_strenght
- sensor.*_uptime
- sensor.sun*
entities:
- sun.sun # Don't record sun data
- sensor.fritzbox_device_uptime
- sensor.time
- sensor.awtrix_kitchen_current_app
- sensor.awtrix_desk_current_app
exclude:
# domains:
# - automation
# - updater
entity_globs:
- weather.zuhause_*
- sensor.*_power_factor
- sensor.dwd*
- binary_sensor.*firmware_update
- binary_sensor.*_overpowering
- binary_sensor.*_overheating
- sensor.*_wi_fi_signal
- sensor.*_wifi_strenght
- sensor.*_uptime
- sensor.sun*
entities:
- sun.sun # Don't record sun data
- sensor.fritzbox_device_uptime
- sensor.time
- sensor.awtrix_kitchen_current_app
- sensor.awtrix_desk_current_app
- sensor.owon_b35t_multimeter_core2_owon_b35t_multimeter_core2_reading
- sensor.owon_b35t_multimeter_core2_owon_b35t_multimeter_core2_mode
- sensor.owon_b35t_multimeter_core2_owon_b35t_multimeter_core2_unit
# event_types:
# - call_service # Don't record service calls
+821
View File
@@ -2419,3 +2419,824 @@
entity_only: true
select.wohnzimmer_hinten_color_palette:
entity_only: true
- id: '1771105514417'
name: Lavalampe
entities:
light.wohnzimmer_hinten:
effect_list:
- Solid
- Blink
- Breathe
- Wipe
- Wipe Random
- Random Colors
- Sweep
- Dynamic
- Colorloop
- Rainbow
- Scan
- Scan Dual
- Fade
- Theater
- Theater Rainbow
- Running
- Saw
- Twinkle
- Dissolve
- Dissolve Rnd
- Sparkle
- Sparkle Dark
- Sparkle+
- Strobe
- Strobe Rainbow
- Strobe Mega
- Blink Rainbow
- Android
- Chase
- Chase Random
- Chase Rainbow
- Chase Flash
- Chase Flash Rnd
- Rainbow Runner
- Colorful
- Traffic Light
- Sweep Random
- Chase 2
- Aurora
- Stream
- Scanner
- Lighthouse
- Fireworks
- Rain
- Tetrix
- Fire Flicker
- Gradient
- Loading
- Rolling Balls
- Fairy
- Two Dots
- Fairytwinkle
- Running Dual
- RSVD
- Chase 3
- Tri Wipe
- Tri Fade
- Lightning
- ICU
- Multi Comet
- Scanner Dual
- Stream 2
- Oscillate
- Pride 2015
- Juggle
- Palette
- Fire 2012
- Colorwaves
- Bpm
- Fill Noise
- Noise 1
- Noise 2
- Noise 3
- Noise 4
- Colortwinkles
- Lake
- Meteor
- Meteor Smooth
- Railway
- Ripple
- Twinklefox
- Twinklecat
- Halloween Eyes
- Solid Pattern
- Solid Pattern Tri
- Spots
- Spots Fade
- Glitter
- Candle
- Fireworks Starburst
- Fireworks 1D
- Bouncing Balls
- Sinelon
- Sinelon Dual
- Sinelon Rainbow
- Popcorn
- Drip
- Plasma
- Percent
- Ripple Rainbow
- Heartbeat
- Pacifica
- Candle Multi
- Solid Glitter
- Sunrise
- Phased
- Twinkleup
- Noise Pal
- Sine
- Phased Noise
- Flow
- Chunchun
- Dancing Shadows
- Washing Machine
- Rotozoomer
- Blends
- TV Simulator
- Dynamic Smooth
- Spaceships
- Crazy Bees
- Ghost Rider
- Blobs
- Scrolling Text
- Drift Rose
- Distortion Waves
- Soap
- Octopus
- Waving Cell
- Pixels
- Pixelwave
- Juggles
- Matripix
- Gravimeter
- Plasmoid
- Puddles
- Midnoise
- Noisemeter
- Freqwave
- Freqmatrix
- GEQ
- Waterfall
- Freqpixels
- RSVD
- Noisefire
- Puddlepeak
- Noisemove
- Noise2D
- Perlin Move
- Ripple Peak
- Firenoise
- Squared Swirl
- RSVD
- DNA
- Matrix
- Metaballs
- Freqmap
- Gravcenter
- Gravcentric
- Gravfreq
- DJ Light
- Funky Plank
- RSVD
- Pulser
- Blurz
- Drift
- Waverly
- Sun Radiation
- Colored Bursts
- Julia
- RSVD
- RSVD
- RSVD
- Game Of Life
- Tartan
- Polar Lights
- Swirl
- Lissajous
- Frizzles
- Plasma Ball
- Flow Stripe
- Hiphotic
- Sindots
- DNA Spiral
- Black Hole
- Wavesins
- Rocktaves
- Akemi
supported_color_modes:
- rgbw
effect: Plasma
color_mode: rgbw
brightness: 51
hs_color:
- 105.829
- 78.039
rgb_color:
- 103
- 255
- 56
xy_color:
- 0.234
- 0.662
rgbw_color:
- 103
- 255
- 56
- 0
friendly_name: Wohnzimmer hinten
supported_features: 36
state: 'on'
select.wohnzimmer_hinten_preset:
options: []
friendly_name: Wohnzimmer hinten Voreinstellung
state: unavailable
select.wohnzimmer_hinten_playlist:
options: []
friendly_name: Wohnzimmer hinten Wiedergabeliste
state: unavailable
light.wohnzimmer_vorne:
effect_list:
- Solid
- Blink
- Breathe
- Wipe
- Wipe Random
- Random Colors
- Sweep
- Dynamic
- Colorloop
- Rainbow
- Scan
- Scan Dual
- Fade
- Theater
- Theater Rainbow
- Running
- Saw
- Twinkle
- Dissolve
- Dissolve Rnd
- Sparkle
- Sparkle Dark
- Sparkle+
- Strobe
- Strobe Rainbow
- Strobe Mega
- Blink Rainbow
- Android
- Chase
- Chase Random
- Chase Rainbow
- Chase Flash
- Chase Flash Rnd
- Rainbow Runner
- Colorful
- Traffic Light
- Sweep Random
- Chase 2
- Aurora
- Stream
- Scanner
- Lighthouse
- Fireworks
- Rain
- Tetrix
- Fire Flicker
- Gradient
- Loading
- Rolling Balls
- Fairy
- Two Dots
- Fairytwinkle
- Running Dual
- RSVD
- Chase 3
- Tri Wipe
- Tri Fade
- Lightning
- ICU
- Multi Comet
- Scanner Dual
- Stream 2
- Oscillate
- Pride 2015
- Juggle
- Palette
- Fire 2012
- Colorwaves
- Bpm
- Fill Noise
- Noise 1
- Noise 2
- Noise 3
- Noise 4
- Colortwinkles
- Lake
- Meteor
- Meteor Smooth
- Railway
- Ripple
- Twinklefox
- Twinklecat
- Halloween Eyes
- Solid Pattern
- Solid Pattern Tri
- Spots
- Spots Fade
- Glitter
- Candle
- Fireworks Starburst
- Fireworks 1D
- Bouncing Balls
- Sinelon
- Sinelon Dual
- Sinelon Rainbow
- Popcorn
- Drip
- Plasma
- Percent
- Ripple Rainbow
- Heartbeat
- Pacifica
- Candle Multi
- Solid Glitter
- Sunrise
- Phased
- Twinkleup
- Noise Pal
- Sine
- Phased Noise
- Flow
- Chunchun
- Dancing Shadows
- Washing Machine
- Rotozoomer
- Blends
- TV Simulator
- Dynamic Smooth
- Spaceships
- Crazy Bees
- Ghost Rider
- Blobs
- Scrolling Text
- Drift Rose
- Distortion Waves
- Soap
- Octopus
- Waving Cell
- Pixels
- Pixelwave
- Juggles
- Matripix
- Gravimeter
- Plasmoid
- Puddles
- Midnoise
- Noisemeter
- Freqwave
- Freqmatrix
- GEQ
- Waterfall
- Freqpixels
- RSVD
- Noisefire
- Puddlepeak
- Noisemove
- Noise2D
- Perlin Move
- Ripple Peak
- Firenoise
- Squared Swirl
- RSVD
- DNA
- Matrix
- Metaballs
- Freqmap
- Gravcenter
- Gravcentric
- Gravfreq
- DJ Light
- Funky Plank
- RSVD
- Pulser
- Blurz
- Drift
- Waverly
- Sun Radiation
- Colored Bursts
- Julia
- RSVD
- RSVD
- RSVD
- Game Of Life
- Tartan
- Polar Lights
- Swirl
- Lissajous
- Frizzles
- Plasma Ball
- Flow Stripe
- Hiphotic
- Sindots
- DNA Spiral
- Black Hole
- Wavesins
- Rocktaves
- Akemi
supported_color_modes:
- rgbw
effect: Plasma
color_mode: rgbw
brightness: 51
hs_color:
- 105.829
- 78.039
rgb_color:
- 103
- 255
- 56
xy_color:
- 0.234
- 0.662
rgbw_color:
- 103
- 255
- 56
- 0
friendly_name: Wohnzimmer vorne
supported_features: 36
state: 'on'
select.wohnzimmer_vorne_preset:
options: []
friendly_name: Wohnzimmer vorne Voreinstellung
state: unavailable
select.wohnzimmer_vorne_playlist:
options: []
friendly_name: Wohnzimmer vorne Wiedergabeliste
state: unavailable
light.kajplats_e14_cws_globe_806lm:
min_color_temp_kelvin: 1801
max_color_temp_kelvin: 6535
min_mireds: 153
max_mireds: 555
supported_color_modes:
- color_temp
- hs
- xy
color_mode: hs
brightness: 192
color_temp_kelvin:
color_temp:
hs_color:
- 65.197
- 100
rgb_color:
- 233
- 255
- 0
xy_color:
- 0.417
- 0.539
friendly_name: Högvind
supported_features: 32
state: 'on'
select.all_wled_effects:
restored: true
options:
- Solid
- Blink
- Breathe
- Wipe
- Wipe Random
- Random Colors
- Sweep
- Dynamic
- Colorloop
- Rainbow
- Scan
- Scan Dual
- Fade
- Theater
- Theater Rainbow
- Running
- Saw
- Twinkle
- Dissolve
- Dissolve Rnd
- Sparkle
- Sparkle Dark
- Sparkle+
- Strobe
- Strobe Rainbow
- Strobe Mega
- Blink Rainbow
- Android
- Chase
- Chase Random
- Chase Rainbow
- Chase Flash
- Chase Flash Rnd
- Rainbow Runner
- Colorful
- Traffic Light
- Sweep Random
- Chase 2
- Aurora
- Stream
- Scanner
- Lighthouse
- Fireworks
- Rain
- Tetrix
- Fire Flicker
- Gradient
- Loading
- Rolling Balls
- Fairy
- Two Dots
- Fairytwinkle
- Running Dual
- RSVD
- Chase 3
- Tri Wipe
- Tri Fade
- Lightning
- ICU
- Multi Comet
- Scanner Dual
- Stream 2
- Oscillate
- Pride 2015
- Juggle
- Palette
- Fire 2012
- Colorwaves
- Bpm
- Fill Noise
- Noise 1
- Noise 2
- Noise 3
- Noise 4
- Colortwinkles
- Lake
- Meteor
- Meteor Smooth
- Railway
- Ripple
- Twinklefox
- Twinklecat
- Halloween Eyes
- Solid Pattern
- Solid Pattern Tri
- Spots
- Spots Fade
- Glitter
- Candle
- Fireworks Starburst
- Fireworks 1D
- Bouncing Balls
- Sinelon
- Sinelon Dual
- Sinelon Rainbow
- Popcorn
- Drip
- Plasma
- Percent
- Ripple Rainbow
- Heartbeat
- Pacifica
- Candle Multi
- Solid Glitter
- Sunrise
- Phased
- Twinkleup
- Noise Pal
- Sine
- Phased Noise
- Flow
- Chunchun
- Dancing Shadows
- Washing Machine
- RSVD
- Blends
- TV Simulator
- Dynamic Smooth
- Spaceships
- Crazy Bees
- Ghost Rider
- Blobs
- Scrolling Text
- Drift Rose
- Distortion Waves
- Soap
- Octopus
- Waving Cell
- Pixels
- Pixelwave
- Juggles
- Matripix
- Gravimeter
- Plasmoid
- Puddles
- Midnoise
- Noisemeter
- Freqwave
- Freqmatrix
- GEQ
- Waterfall
- Freqpixels
- RSVD
- Noisefire
- Puddlepeak
- Noisemove
- Noise2D
- Perlin Move
- Ripple Peak
- Firenoise
- Squared Swirl
- RSVD
- DNA
- Matrix
- Metaballs
- Freqmap
- Gravcenter
- Gravcentric
- Gravfreq
- DJ Light
- Funky Plank
- RSVD
- Pulser
- Blurz
- Drift
- Waverly
- Sun Radiation
- Colored Bursts
- Julia
- RSVD
- RSVD
- RSVD
- Game Of Life
- Tartan
- Polar Lights
- Swirl
- Lissajous
- Frizzles
- Plasma Ball
- Flow Stripe
- Hiphotic
- Sindots
- DNA Spiral
- Black Hole
- Wavesins
- Rocktaves
- Akemi
icon: mdi:firework
friendly_name: Wohnzimmer Effekt
supported_features: 0
state: unavailable
select.wohnzimmer_hinten_color_palette:
options:
- '* Color 1'
- '* Color Gradient'
- '* Colors 1&2'
- '* Colors Only'
- '* Random Cycle'
- Analogous
- April Night
- Aqua Flash
- Atlantica
- Aurora
- Aurora 2
- Autumn
- Beach
- Beech
- Blink Red
- Breeze
- C9
- C9 2
- C9 New
- Candy
- Candy2
- Cloud
- Cyane
- Default
- Departure
- Drywet
- Fairy Reaf
- Fire
- Forest
- Grintage
- Hult
- Hult 64
- Icefire
- Jul
- Landscape
- Lava
- Light Pink
- Lite Light
- Magenta
- Magred
- Ocean
- Orange & Teal
- Orangery
- Party
- Pastel
- Pink Candy
- Rainbow
- Rainbow Bands
- Red & Blue
- Red Flash
- Red Reaf
- Red Shift
- Red Tide
- Retro Clown
- Rewhi
- Rivendell
- Sakura
- Semi Blue
- Sherbet
- Splash
- Sunset
- Sunset 2
- Temperature
- Tertiary
- Tiamat
- Toxy Reaf
- Vintage
- Yelblu
- Yelblu Hot
- Yellowout
- Yelmag
friendly_name: Wohnzimmer hinten Farbpalette
state: '* Colors 1&2'
select.wohnzimmer_vorne_color_palette:
options:
- '* Color 1'
- '* Color Gradient'
- '* Colors 1&2'
- '* Colors Only'
- '* Random Cycle'
- Analogous
- April Night
- Aqua Flash
- Atlantica
- Aurora
- Aurora 2
- Autumn
- Beach
- Beech
- Blink Red
- Breeze
- C9
- C9 2
- C9 New
- Candy
- Candy2
- Cloud
- Cyane
- Default
- Departure
- Drywet
- Fairy Reaf
- Fire
- Forest
- Grintage
- Hult
- Hult 64
- Icefire
- Jul
- Landscape
- Lava
- Light Pink
- Lite Light
- Magenta
- Magred
- Ocean
- Orange & Teal
- Orangery
- Party
- Pastel
- Pink Candy
- Rainbow
- Rainbow Bands
- Red & Blue
- Red Flash
- Red Reaf
- Red Shift
- Red Tide
- Retro Clown
- Rewhi
- Rivendell
- Sakura
- Semi Blue
- Sherbet
- Splash
- Sunset
- Sunset 2
- Temperature
- Tertiary
- Tiamat
- Toxy Reaf
- Vintage
- Yelblu
- Yelblu Hot
- Yellowout
- Yelmag
friendly_name: Wohnzimmer vorne Farbpalette
state: '* Colors 1&2'
metadata:
select.all_wled_effects:
entity_only: true
select.wohnzimmer_hinten_color_palette:
entity_only: true
select.wohnzimmer_vorne_color_palette:
entity_only: true
icon: mdi:lava-lamp
+55
View File
@@ -114,3 +114,58 @@ wled_wohnzimmer_nachster_effekt:
}}'
mode: single
icon: mdi:firework
light_color_temp_helper:
sequence:
- action: light.turn_on
target:
entity_id: '{{ target_light }}'
data:
color_temp_kelvin: "{% if state_attr(target_light, 'color_temp_kelvin') is none
%} \n {{ max_color_temp }} \n{% else %} \n {% if mode == \"cycle\" and
state_attr(target_light, 'color_temp_kelvin')\n == max_color_temp %}\n {{min_color_temp}}\n
\ {% else %} \n {% set new_value = state_attr(target_light, 'color_temp_kelvin')+iif(mode
== \"down\", -1, +1) * color_temp_step %}\n {{ [([new_value, max_color_temp]
| min), min_color_temp] | max}} \n {% endif %} \n{% endif %} "
alias: light_color_temp_helper
fields:
target_light:
name: Light
description: Light entity
selector:
entity:
filter:
domain: light
required: true
min_color_temp:
name: Minimal color temperature
default: 2200
selector:
number:
mode: box
required: true
max_color_temp:
name: Maximal color temperature
default: 4000
selector:
number:
mode: box
required: true
color_temp_step:
name: Change of color temperature per step
default: 300
selector:
number:
mode: box
required: true
mode:
selector:
select:
options:
- up
- down
- cycle
required: true
default: cycle
name: Mode
description: Select switch mode
description: Switch color temperature of light in steps
-22
View File
@@ -3,14 +3,6 @@
display_options:
- 'time'
- 'date'
# Database size sensor
- platform: sql
db_url: !secret ha_recorder_dburl
queries:
- name: "Home Assistant Datenbankgröße"
query: 'SELECT table_schema "database", Round(Sum(data_length + index_length) / 1048576, 2) "value" FROM information_schema.tables WHERE table_schema="homeassistant" GROUP BY table_schema;'
column: 'value'
unit_of_measurement: MB
# SNMP (Juniper) router traffic sensor
- platform: snmp
@@ -54,20 +46,6 @@
entity_id: sensor.internet_speed_out
sampling_size: 10
- platform: template
sensors:
sun_rising_template:
friendly_name: "Sun Rising Template"
unique_id: '0680294616247'
value_template: "{{ as_timestamp(states.sun.sun.attributes.next_rising) | timestamp_custom ('%H:%M') }}"
- platform: template
sensors:
sun_setting_template:
friendly_name: "Sun Setting Template"
unique_id: '8298170865533'
value_template: "{{ as_timestamp(states.sun.sun.attributes.next_setting) | timestamp_custom ('%H:%M') }}"
# Sensor for Riemann sum of energy import (W -> Wh)
- platform: integration
source: sensor.power_import
+41 -2
View File
@@ -39,6 +39,16 @@
data:
value: "{{ option }}"
- sensor:
- unique_id: 0680294616247
default_entity_id: sensor.sun_rising_template
name: Sun Rising Template
state: '{{ as_timestamp(states.sun.sun.attributes.next_rising) | timestamp_custom
(''%H:%M'') }}'
- unique_id: '8298170865533'
default_entity_id: sensor.sun_setting_template
name: Sun Setting Template
state: '{{ as_timestamp(states.sun.sun.attributes.next_setting) | timestamp_custom
(''%H:%M'') }}'
- name: "power_other"
unique_id: '5579422933393'
unit_of_measurement: "W"
@@ -59,10 +69,11 @@
{% set spieleschrank = states('sensor.wohnzimmer_spieleschrank_power') | float %}
{% set kuehlschrank = states('sensor.tz3000_ww6drja5_ts011f_leistung') | float %}
{% set kaffeemaschine = states('sensor.kaffeemaschine_leistung_2') | float %}
{% set waeschetrockner = states('sensor.waschetrockner_leistung') | float %}
{% set waeschetrockner = states('sensor.ikea_of_sweden_inspelning_smart_plug_leistung') | float %}
{% set waschmaschine = states('sensor.waschmaschine_leistung') | float %}
{% set arcade = states('sensor.arcade_automat_leistung') | float %}
{{ (total + solar - raumduft - keller - musik - bett - heimkino_sz - deko - schreibtisch - serverraum - heimkino_wz - spieleschrank - kuehlschrank - kaffeemaschine - waeschetrockner - waschmaschine - arcade) | round(1) }}
{% set spuelmaschine = states('sensor.spulmaschine_leistung') | float %}
{{ (total + solar - raumduft - keller - musik - bett - heimkino_sz - deko - schreibtisch - serverraum - heimkino_wz - spieleschrank - kuehlschrank - kaffeemaschine - waeschetrockner - waschmaschine - arcade - spuelmaschine) | round(1) }}
device_class: power
state_class: measurement
attributes:
@@ -128,8 +139,36 @@
attributes:
last_reset: '1970-01-01T00:00:00+00:00'
- name: internet_speed_out
unique_id: '9519483670667'
state: >
{{ (( states('sensor.wan_out_derivative') | float * 8 / 1000000 ) | round(2)) }}
unit_of_measurement: 'Mbps'
attributes:
last_reset: '1970-01-01T00:00:00+00:00'
# ── Kalender-Ereignisse für epaperframe ──
# Alle Events werden per Automation (automations.yaml) in input_text Helfer geschrieben.
# Diese template sensors lesen nur noch daraus. Das verhindert doppelte Logik und garantiert,
# dass sowohl "heute" als auch "morgen" Events mit Titeln, Uhrzeiten und Beschreibungen verfügbar sind.
- sensor:
# ── HEUTE ──
- name: "Kalender heute Privat"
unique_id: kalender_heute_privat
state: "{{ states('input_text.kalender_heute_privat') }}"
icon: mdi:calendar-account
- name: "Kalender heute Arbeit"
unique_id: kalender_heute_arbeit
state: "{{ states('input_text.kalender_heute_arbeit') }}"
icon: mdi:calendar-account
# ── MORGEN ──
- name: "Kalender morgen Privat"
unique_id: kalender_morgen_privat
state: "{{ states('input_text.kalender_morgen_privat') }}"
icon: mdi:calendar-calendar-today
- name: "Kalender morgen Arbeit"
unique_id: kalender_morgen_arbeit
state: "{{ states('input_text.kalender_morgen_arbeit') }}"
icon: mdi:calendar-calendar-today
+20
View File
@@ -0,0 +1,20 @@
kalender_heute_privat:
name: "Kalender heute Privat"
initial: "-"
icon: mdi:calendar-account
mode: text
kalender_heute_arbeit:
name: "Kalender heute Arbeit"
initial: "-"
icon: mdi:calendar-account
mode: text
kalender_morgen_privat:
name: "Kalender morgen Privat"
initial: "-"
icon: mdi:calendar-calendar-today
mode: text
kalender_morgen_arbeit:
name: "Kalender morgen Arbeit"
initial: "-"
icon: mdi:calendar-calendar-today
mode: text
+1
Submodule themes/blackened added at 4393cc34a9