Compare commits
77 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2cbb15a425 | |||
| 6a04638f59 | |||
| a4a86272a9 | |||
| 4ae08e0400 | |||
| 2b93cf16dc | |||
| 2879dbeaa0 | |||
| e4a4aacc63 | |||
| 0d12317089 | |||
| 746dd833c8 | |||
| 3eb482e40b | |||
| e9a6064b4b | |||
| 38616556cb | |||
| bef1cfb1d0 | |||
| 0a8364d7ed | |||
| 3eb9eb3a2f | |||
| 7407955465 | |||
| 2ff8b5cafe | |||
| 7e08c4a8b9 | |||
| 855f013095 | |||
| a68b527c48 | |||
| cecec14be1 | |||
| 4e5df1ad0c | |||
| 2c16ba1679 | |||
| c21f2a5c7b | |||
| 2a1902ccf1 | |||
| 31848bf26c | |||
| d7337fd634 | |||
| 5b287303f1 | |||
| 816cc35861 | |||
| 058c186d63 | |||
| 22e5f9d767 | |||
| f9b32d06a4 | |||
| 3f890ae02f | |||
| f0cb74cc27 | |||
| 6038075e9e | |||
| 2dbdfd213c | |||
| e2eb886eef | |||
| f4f19eb847 | |||
| b3e307dcba | |||
| 1b3b4870e7 | |||
| 69c2166414 | |||
| 2fcf71d8ce | |||
| 676829a988 | |||
| 08201f1bc1 | |||
| 23cbd520a6 | |||
| 6f4e2b9319 | |||
| e5104fc005 | |||
| 91f3f69bd9 | |||
| 4bd62f539c | |||
| a3bd9ff213 | |||
| 24fc4bf9f4 | |||
| 0bfc2eff34 | |||
| 8f18a043af | |||
| 5b0a992686 | |||
| ef233296e9 | |||
| c0af2b734b | |||
| a94386babe | |||
| f23166b765 | |||
| 8438785241 | |||
| b0b6093619 | |||
| c463a5e28f | |||
| a3e06a85d6 | |||
| 6795c31d43 | |||
| e504cdf90c | |||
| c9807a7bf0 | |||
| fdbbbb5300 | |||
| fda6e6abff | |||
| ee01b56fb6 | |||
| db1edfaf24 | |||
| e07c66563c | |||
| 2ff40a0da7 | |||
| 24b4d07228 | |||
| 8b6f3012f4 | |||
| 792fc87166 | |||
| 67b2c2edde | |||
| db9b8a73d3 | |||
| 093be6f2d2 |
+1
-1
@@ -1 +1 @@
|
||||
2025.12.0
|
||||
2026.7.1
|
||||
+1
-1
@@ -13,4 +13,4 @@ home-assistant.log*
|
||||
zigbee.db*
|
||||
*__pycache__*
|
||||
.ha_run.lock
|
||||
|
||||
.cache
|
||||
|
||||
+332
-151
@@ -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!
|
||||
@@ -94,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.
|
||||
@@ -132,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
|
||||
@@ -162,9 +128,8 @@
|
||||
target:
|
||||
device_id:
|
||||
- 1f3c4b5de4aea99bac83688ceb22293b
|
||||
- 48dafb7f4a8ed6ccbb046758cb660c23
|
||||
- c4ead7f6227e2ee4c43c4b0df829cd84
|
||||
enabled: false
|
||||
enabled: true
|
||||
mode: single
|
||||
- id: '1623868115464'
|
||||
alias: 420!
|
||||
@@ -184,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
|
||||
@@ -239,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
|
||||
@@ -251,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
|
||||
@@ -511,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
|
||||
@@ -571,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}}
|
||||
|
||||
'
|
||||
@@ -614,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
|
||||
@@ -874,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
|
||||
@@ -884,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:
|
||||
@@ -913,7 +880,7 @@
|
||||
- 98a9eef603f8c8ae6200c4834a3dbf4f
|
||||
- 32d15eec63fe68d5770813d47da53657
|
||||
- 9f42805af5b7e423023595390342b9ac
|
||||
- 063e41329df7b29bbaa8df2f508ee6cb
|
||||
- 5abbb43e5d015ee75c7c900240b4329e
|
||||
data:
|
||||
transition: 2
|
||||
action: light.turn_off
|
||||
@@ -923,7 +890,6 @@
|
||||
target:
|
||||
device_id:
|
||||
- 1f3c4b5de4aea99bac83688ceb22293b
|
||||
- 48dafb7f4a8ed6ccbb046758cb660c23
|
||||
mode: single
|
||||
- id: '1701774106609'
|
||||
alias: IKEA STYRBAR Wohnzimmer
|
||||
@@ -1085,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
|
||||
@@ -1163,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:
|
||||
@@ -1188,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
|
||||
@@ -1200,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
|
||||
@@ -1215,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
|
||||
@@ -1227,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
|
||||
@@ -1240,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
|
||||
@@ -1273,8 +1247,8 @@
|
||||
sequence:
|
||||
- data:
|
||||
data:
|
||||
push:
|
||||
interruption-level: time-sensitive
|
||||
ttl: 0
|
||||
priority: high
|
||||
message: Jemand hat an der Haustür geklingelt
|
||||
title: Türklingel (außen)
|
||||
action: notify.alle_mobilen_gerate
|
||||
@@ -1285,18 +1259,225 @@
|
||||
sequence:
|
||||
- data:
|
||||
data:
|
||||
push:
|
||||
interruption-level: time-sensitive
|
||||
ttl: 0
|
||||
priority: high
|
||||
message: Jemand hat an der Wohnungstür geklingelt
|
||||
title: Türklingel (innen)
|
||||
action: notify.alle_mobilen_gerate
|
||||
mode: single
|
||||
- id: '1757574193367'
|
||||
alias: Zone Notification
|
||||
- id: '1767373636810'
|
||||
alias: IKEA Bilresa Scroll Wheel Kinderzimmer
|
||||
description: ''
|
||||
use_blueprint:
|
||||
path: homeassistant/notify_leaving_zone.yaml
|
||||
path: jhol-byte/Ikea_bilresa_scroll_wheel.yaml
|
||||
input:
|
||||
person_entity: person.marcus_scholz
|
||||
zone_entity: zone.home
|
||||
notify_device: 773450cae5e93524731940ad081846d9
|
||||
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 %}"
|
||||
@@ -49,6 +49,9 @@ sql:
|
||||
# calendar integration
|
||||
calendar: !include calendars.yaml
|
||||
|
||||
# Helpers for epaperframe calendar display
|
||||
input_text: !include text_helpers.yaml
|
||||
|
||||
# DB-recorder configuration
|
||||
recorder: !include recorder.yaml
|
||||
|
||||
@@ -82,3 +85,8 @@ generic_hygrostat:
|
||||
|
||||
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
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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) == "-":
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
"""Constants for ics_calendar platform."""
|
||||
|
||||
VERSION = "5.1.3"
|
||||
VERSION = "5.1.7"
|
||||
DOMAIN = "ics_calendar"
|
||||
|
||||
CONF_DEVICE_ID = "device_id"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,10 +38,13 @@ ota:
|
||||
platform: esphome
|
||||
password: !secret ota
|
||||
|
||||
network:
|
||||
enable_ipv6: true
|
||||
|
||||
wifi:
|
||||
ssid: "Voltage-legacy"
|
||||
password: !secret voltage_legacy_psk
|
||||
use_address: atorch-dc-load.home
|
||||
#use_address: atorch-dc-load.home
|
||||
power_save_mode: high
|
||||
fast_connect: on
|
||||
min_auth_mode: WPA2
|
||||
@@ -0,0 +1,293 @@
|
||||
substitutions:
|
||||
name: "owon-b35t"
|
||||
friendly_name: "OWON B35T Multimeter"
|
||||
device_description: "M5Stack Core 1 BLE client for OWON B35T/B35T+ multimeter with local graphical display"
|
||||
owon_mac_address: !secret owon_b35t_mac_address
|
||||
|
||||
esphome:
|
||||
name: ${name}
|
||||
friendly_name: ${friendly_name}
|
||||
comment: ${device_description}
|
||||
min_version: 2024.6.0
|
||||
includes:
|
||||
- owon_b35t-m5stack-core1.h
|
||||
project:
|
||||
name: "custom.owon-b35t-m5stack"
|
||||
version: "1.0"
|
||||
|
||||
esp32:
|
||||
board: m5stack-core-esp32
|
||||
framework:
|
||||
type: esp-idf
|
||||
advanced:
|
||||
minimum_chip_revision: "3.1"
|
||||
|
||||
logger:
|
||||
level: INFO
|
||||
|
||||
api:
|
||||
encryption:
|
||||
key: !secret apikey
|
||||
|
||||
ota:
|
||||
platform: esphome
|
||||
password: !secret ota
|
||||
|
||||
wifi:
|
||||
ssid: "Voltage-legacy"
|
||||
password: !secret voltage_legacy_psk
|
||||
#use_address: ${name}.home
|
||||
power_save_mode: none
|
||||
fast_connect: on
|
||||
min_auth_mode: WPA2
|
||||
ap:
|
||||
ssid: "OWON B35T Fallback Hotspot"
|
||||
password: !secret fallback_psk
|
||||
|
||||
# Disabled to save RAM on the M5Stack Core 1. Re-enable temporarily if Wi-Fi recovery is needed.
|
||||
# captive_portal:
|
||||
|
||||
interval:
|
||||
- interval: 10s
|
||||
then:
|
||||
- lambda: |-
|
||||
ESP_LOGI("mem", "heap free=%u min_free=%u internal_free=%u internal_largest=%u dma_free=%u dma_largest=%u",
|
||||
static_cast<unsigned>(esp_get_free_heap_size()),
|
||||
static_cast<unsigned>(esp_get_minimum_free_heap_size()),
|
||||
static_cast<unsigned>(heap_caps_get_free_size(MALLOC_CAP_INTERNAL)),
|
||||
static_cast<unsigned>(heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL)),
|
||||
static_cast<unsigned>(heap_caps_get_free_size(MALLOC_CAP_DMA)),
|
||||
static_cast<unsigned>(heap_caps_get_largest_free_block(MALLOC_CAP_DMA)));
|
||||
|
||||
esp32_ble_tracker:
|
||||
scan_parameters:
|
||||
active: true
|
||||
continuous: true
|
||||
|
||||
ble_client:
|
||||
- mac_address: ${owon_mac_address}
|
||||
id: owon_ble_client
|
||||
on_connect:
|
||||
then:
|
||||
- lambda: |-
|
||||
owon_meter.on_connect();
|
||||
on_disconnect:
|
||||
then:
|
||||
- lambda: |-
|
||||
owon_meter.on_disconnect();
|
||||
- logger.log:
|
||||
level: WARN
|
||||
format: "OWON BLE meter disconnected; restarting M5Stack to reclaim heap"
|
||||
- delay: 1s
|
||||
- lambda: |-
|
||||
esp_restart();
|
||||
|
||||
spi:
|
||||
clk_pin: GPIO18
|
||||
mosi_pin: GPIO23
|
||||
miso_pin: GPIO19
|
||||
|
||||
output:
|
||||
- platform: ledc
|
||||
pin: GPIO32
|
||||
id: lcd_backlight
|
||||
|
||||
light:
|
||||
- platform: monochromatic
|
||||
output: lcd_backlight
|
||||
name: "${friendly_name} Backlight"
|
||||
id: backlight
|
||||
restore_mode: ALWAYS_ON
|
||||
|
||||
font:
|
||||
- file: "fonts/Roboto-Regular.ttf"
|
||||
id: meter_font
|
||||
size: 15
|
||||
glyphs:
|
||||
[
|
||||
" ",
|
||||
"!",
|
||||
"%",
|
||||
"+",
|
||||
"-",
|
||||
".",
|
||||
"/",
|
||||
"0",
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5",
|
||||
"6",
|
||||
"7",
|
||||
"8",
|
||||
"9",
|
||||
"<",
|
||||
">",
|
||||
"A",
|
||||
"B",
|
||||
"C",
|
||||
"D",
|
||||
"E",
|
||||
"F",
|
||||
"G",
|
||||
"H",
|
||||
"I",
|
||||
"L",
|
||||
"M",
|
||||
"N",
|
||||
"O",
|
||||
"P",
|
||||
"R",
|
||||
"S",
|
||||
"T",
|
||||
"U",
|
||||
"V",
|
||||
"W",
|
||||
"X",
|
||||
"Y",
|
||||
"Z",
|
||||
"a",
|
||||
"c",
|
||||
"d",
|
||||
"e",
|
||||
"f",
|
||||
"g",
|
||||
"h",
|
||||
"i",
|
||||
"k",
|
||||
"m",
|
||||
"n",
|
||||
"o",
|
||||
"r",
|
||||
"s",
|
||||
"t",
|
||||
"u",
|
||||
"v",
|
||||
"w",
|
||||
"y",
|
||||
"z",
|
||||
"°",
|
||||
"µ",
|
||||
"Ω",
|
||||
]
|
||||
|
||||
display:
|
||||
- platform: ili9xxx
|
||||
id: lcd
|
||||
model: M5STACK
|
||||
cs_pin: GPIO14
|
||||
dc_pin: GPIO27
|
||||
reset_pin: GPIO33
|
||||
invert_colors: true
|
||||
color_palette: 8BIT
|
||||
rotation: 0
|
||||
update_interval: 500ms
|
||||
lambda: |-
|
||||
owon_meter.render(it, id(meter_font));
|
||||
|
||||
binary_sensor:
|
||||
- platform: gpio
|
||||
id: button_a
|
||||
pin:
|
||||
number: GPIO39
|
||||
inverted: true
|
||||
internal: true
|
||||
on_press:
|
||||
then:
|
||||
- lambda: |-
|
||||
owon_meter.previous_button();
|
||||
- platform: gpio
|
||||
id: button_b
|
||||
pin:
|
||||
number: GPIO38
|
||||
inverted: true
|
||||
internal: true
|
||||
on_click:
|
||||
- min_length: 50ms
|
||||
max_length: 1500ms
|
||||
then:
|
||||
- logger.log:
|
||||
level: INFO
|
||||
format: "OWON short press: %s"
|
||||
args: ["owon_meter.selected_button_name()"]
|
||||
- ble_client.ble_write:
|
||||
id: owon_ble_client
|
||||
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
|
||||
characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb"
|
||||
value: !lambda |-
|
||||
std::vector<uint8_t> data = {owon_meter.selected_button, 0x01};
|
||||
return data;
|
||||
- min_length: 1500ms
|
||||
max_length: 5000ms
|
||||
then:
|
||||
- logger.log:
|
||||
level: INFO
|
||||
format: "OWON long press: %s"
|
||||
args: ["owon_meter.selected_button_name()"]
|
||||
- ble_client.ble_write:
|
||||
id: owon_ble_client
|
||||
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
|
||||
characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb"
|
||||
value: !lambda |-
|
||||
uint8_t press_type = (owon_meter.selected_button == 1 || owon_meter.selected_button == 5) ? 0x01 : 0x00;
|
||||
std::vector<uint8_t> data = {owon_meter.selected_button, press_type};
|
||||
return data;
|
||||
- platform: gpio
|
||||
id: button_c
|
||||
pin:
|
||||
number: GPIO37
|
||||
inverted: true
|
||||
internal: true
|
||||
on_press:
|
||||
then:
|
||||
- lambda: |-
|
||||
owon_meter.next_button();
|
||||
|
||||
- platform: template
|
||||
name: "${friendly_name} Connected"
|
||||
lambda: |-
|
||||
return owon_meter.connected;
|
||||
- platform: template
|
||||
name: "${friendly_name} Overload"
|
||||
lambda: |-
|
||||
return owon_meter.overload;
|
||||
- platform: template
|
||||
name: "${friendly_name} Low Battery"
|
||||
lambda: |-
|
||||
return owon_meter.low_battery;
|
||||
|
||||
sensor:
|
||||
- platform: ble_client
|
||||
type: characteristic
|
||||
ble_client_id: owon_ble_client
|
||||
id: owon_notify_source
|
||||
internal: true
|
||||
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
|
||||
characteristic_uuid: "0000fff4-0000-1000-8000-00805f9b34fb"
|
||||
notify: true
|
||||
update_interval: never
|
||||
lambda: |-
|
||||
owon_meter.handle_notify(x);
|
||||
return owon_meter.value();
|
||||
|
||||
- platform: wifi_signal
|
||||
name: "${friendly_name} WiFi Signal"
|
||||
update_interval: 60s
|
||||
|
||||
text_sensor:
|
||||
- platform: template
|
||||
name: "${friendly_name} Reading"
|
||||
update_interval: 2s
|
||||
lambda: |-
|
||||
return owon_meter.reading_text();
|
||||
- platform: template
|
||||
name: "${friendly_name} Unit"
|
||||
update_interval: 2s
|
||||
lambda: |-
|
||||
return std::string(owon_meter.scale()) + owon_meter.unit();
|
||||
- platform: template
|
||||
name: "${friendly_name} Mode"
|
||||
update_interval: 2s
|
||||
lambda: |-
|
||||
return owon_meter.mode_text();
|
||||
@@ -0,0 +1,658 @@
|
||||
/*
|
||||
* ESPHome helper for OWON B35T/B35T+ BLE meter on M5Stack Core 1.
|
||||
* Parser is based on the standalone Arduino sketch by Reaper7
|
||||
* (Beerware license, Revision 42) and Dean Cording's owonb35 notes.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include "esp_heap_caps.h"
|
||||
#include "esp_system.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/components/display/display.h"
|
||||
|
||||
namespace owon_b35t {
|
||||
|
||||
using esphome::Color;
|
||||
using esphome::display::Display;
|
||||
|
||||
static const char *const TAG = "owon_b35t";
|
||||
static const char *const POWER_TAG = "core2_power";
|
||||
static constexpr uint8_t AXP192_ADDR = 0x34;
|
||||
static esphome::i2c::I2CDevice axp192;
|
||||
static bool axp192_ready = false;
|
||||
|
||||
static bool axp_write(uint8_t reg, uint8_t value) {
|
||||
if (!axp192_ready) return false;
|
||||
bool ok = axp192.write_byte(reg, value);
|
||||
if (!ok) ESP_LOGW(POWER_TAG, "AXP192 write reg 0x%02X failed", reg);
|
||||
return ok;
|
||||
}
|
||||
|
||||
static bool axp_read(uint8_t reg, uint8_t *value) {
|
||||
if (!axp192_ready) return false;
|
||||
bool ok = axp192.read_byte(reg, value);
|
||||
if (!ok) ESP_LOGW(POWER_TAG, "AXP192 read reg 0x%02X failed", reg);
|
||||
return ok;
|
||||
}
|
||||
|
||||
static void axp_update(uint8_t reg, uint8_t clear_mask, uint8_t set_mask) {
|
||||
uint8_t value = 0;
|
||||
if (!axp_read(reg, &value)) return;
|
||||
value = (value & ~clear_mask) | set_mask;
|
||||
axp_write(reg, value);
|
||||
}
|
||||
|
||||
static uint8_t axp_dc_voltage_data(uint16_t millivolts) {
|
||||
if (millivolts < 700) millivolts = 700;
|
||||
if (millivolts > 3500) millivolts = 3500;
|
||||
return static_cast<uint8_t>((millivolts - 700) / 25) & 0x7F;
|
||||
}
|
||||
|
||||
static uint8_t axp_ldo_voltage_data(uint16_t millivolts) {
|
||||
if (millivolts < 1800) millivolts = 1800;
|
||||
if (millivolts > 3300) millivolts = 3300;
|
||||
return static_cast<uint8_t>((millivolts - 1800) / 100) & 0x0F;
|
||||
}
|
||||
|
||||
static void core2_axp192_set_lcd_voltage(uint16_t millivolts) {
|
||||
uint8_t value = 0;
|
||||
axp_read(0x27, &value);
|
||||
axp_write(0x27, (value & 0x80) | axp_dc_voltage_data(millivolts)); // DCDC3, LCD backlight
|
||||
}
|
||||
|
||||
static void core2_axp192_set_backlight(float brightness) {
|
||||
if (brightness <= 0.0f) {
|
||||
axp_update(0x12, 0x02, 0x00); // DCDC3 off
|
||||
return;
|
||||
}
|
||||
if (brightness > 1.0f) brightness = 1.0f;
|
||||
uint16_t millivolts = static_cast<uint16_t>(2400 + brightness * 900);
|
||||
core2_axp192_set_lcd_voltage(millivolts);
|
||||
axp_update(0x12, 0x00, 0x02); // DCDC3 on
|
||||
}
|
||||
|
||||
static void core2_axp192_init(esphome::i2c::I2CBus *bus) {
|
||||
axp192.set_i2c_bus(bus);
|
||||
axp192.set_i2c_address(AXP192_ADDR);
|
||||
axp192_ready = true;
|
||||
ESP_LOGI(POWER_TAG, "Initializing M5Stack Core2 AXP192 LCD power");
|
||||
axp_update(0x30, 0xF9, 0x02); // Disable VBUS current limit, preserve bit 2.
|
||||
axp_update(0x92, 0x07, 0x00); // GPIO1 open-drain output.
|
||||
axp_update(0x93, 0x07, 0x00); // GPIO2 open-drain output.
|
||||
axp_write(0x35, 0xA2); // RTC battery charging.
|
||||
|
||||
uint8_t value = 0;
|
||||
axp_read(0x26, &value);
|
||||
axp_write(0x26, (value & 0x80) | axp_dc_voltage_data(3350)); // DCDC1 ESP32 VDD.
|
||||
core2_axp192_set_lcd_voltage(2800);
|
||||
|
||||
uint8_t ldo2 = axp_ldo_voltage_data(3300);
|
||||
uint8_t ldo3 = axp_ldo_voltage_data(2000);
|
||||
axp_write(0x28, (ldo2 << 4) | ldo3); // LDO2 LCD logic/SD, LDO3 vibrator.
|
||||
axp_update(0x12, 0x00, 0x07); // Enable DCDC1, DCDC3, LDO2.
|
||||
axp_write(0x82, 0xFF); // ADCs on.
|
||||
axp_update(0x95, 0x8D, 0x84); // GPIO4 setup, as M5Core2 library does.
|
||||
axp_write(0x36, 0x4C); // Power key timing.
|
||||
|
||||
// LCD reset through AXP192 GPIO4.
|
||||
axp_update(0x96, 0x02, 0x00);
|
||||
delay(100);
|
||||
axp_update(0x96, 0x00, 0x02);
|
||||
delay(100);
|
||||
|
||||
core2_axp192_set_backlight(1.0f);
|
||||
}
|
||||
|
||||
static const uint8_t ACCU_BMP[32] = {
|
||||
0b00000000, 0b00000000,
|
||||
0b00000000, 0b00000000,
|
||||
0b00000000, 0b00000000,
|
||||
0b11111111, 0b11111110,
|
||||
0b10000000, 0b00000010,
|
||||
0b10000000, 0b00000011,
|
||||
0b10000000, 0b00000011,
|
||||
0b10000000, 0b00000011,
|
||||
0b10000000, 0b00000011,
|
||||
0b10000000, 0b00000011,
|
||||
0b10000000, 0b00000011,
|
||||
0b10000000, 0b00000010,
|
||||
0b11111111, 0b11111110,
|
||||
0b00000000, 0b00000000,
|
||||
0b00000000, 0b00000000,
|
||||
0b00000000, 0b00000000,
|
||||
};
|
||||
|
||||
static const uint8_t BLE_BMP[32] = {
|
||||
0b00000001, 0b10000000,
|
||||
0b00000001, 0b11000000,
|
||||
0b00010001, 0b01100000,
|
||||
0b00011001, 0b00110000,
|
||||
0b00001101, 0b00011000,
|
||||
0b00000111, 0b00110000,
|
||||
0b00000011, 0b01100000,
|
||||
0b00000001, 0b11000000,
|
||||
0b00000001, 0b11000000,
|
||||
0b00000011, 0b01100000,
|
||||
0b00000111, 0b00110000,
|
||||
0b00001101, 0b00011000,
|
||||
0b00011001, 0b00110000,
|
||||
0b00010001, 0b01100000,
|
||||
0b00000001, 0b11000000,
|
||||
0b00000001, 0b10000000,
|
||||
};
|
||||
|
||||
static const uint8_t DIODE_BMP[32] = {
|
||||
0b00001000, 0b00011000,
|
||||
0b00001100, 0b00011000,
|
||||
0b00001110, 0b00011000,
|
||||
0b00001111, 0b00011000,
|
||||
0b00001111, 0b10011000,
|
||||
0b00001111, 0b11011000,
|
||||
0b00001111, 0b11111000,
|
||||
0b11111111, 0b11111111,
|
||||
0b11111111, 0b11111111,
|
||||
0b00001111, 0b11111000,
|
||||
0b00001111, 0b11011000,
|
||||
0b00001111, 0b10011000,
|
||||
0b00001111, 0b00011000,
|
||||
0b00001110, 0b00011000,
|
||||
0b00001100, 0b00011000,
|
||||
0b00001000, 0b00011000,
|
||||
};
|
||||
|
||||
static const uint8_t BUZZ_BMP[32] = {
|
||||
0b00000000, 0b11000000,
|
||||
0b00000001, 0b11000000,
|
||||
0b00000011, 0b11000001,
|
||||
0b00000111, 0b11000001,
|
||||
0b00001111, 0b11000101,
|
||||
0b11111111, 0b11000101,
|
||||
0b11111111, 0b11010101,
|
||||
0b11111111, 0b11010101,
|
||||
0b11111111, 0b11010101,
|
||||
0b11111111, 0b11010101,
|
||||
0b11111111, 0b11000101,
|
||||
0b00001111, 0b11000101,
|
||||
0b00000111, 0b11000001,
|
||||
0b00000011, 0b11000001,
|
||||
0b00000001, 0b11000000,
|
||||
0b00000000, 0b11000000,
|
||||
};
|
||||
|
||||
class Meter {
|
||||
public:
|
||||
static constexpr uint8_t REGPLUSMINUS = 0x00;
|
||||
static constexpr uint8_t FLAGPLUS = 0b00101011;
|
||||
static constexpr uint8_t FLAGMINUS = 0b00101101;
|
||||
static constexpr uint8_t REGDIG1 = 0x01;
|
||||
static constexpr uint8_t REGDIG2 = 0x02;
|
||||
static constexpr uint8_t REGDIG3 = 0x03;
|
||||
static constexpr uint8_t REGDIG4 = 0x04;
|
||||
static constexpr uint8_t REGPOINT = 0x06;
|
||||
static constexpr uint8_t FLAGPOINT0 = 0b00110000;
|
||||
static constexpr uint8_t FLAGPOINT1 = 0b00110001;
|
||||
static constexpr uint8_t FLAGPOINT2 = 0b00110010;
|
||||
static constexpr uint8_t FLAGPOINT3 = 0b00110100;
|
||||
static constexpr uint8_t REGMODE = 0x07;
|
||||
static constexpr uint8_t FLAGMODEHOLD = 0b00000010;
|
||||
static constexpr uint8_t FLAGMODEREL = 0b00000100;
|
||||
static constexpr uint8_t FLAGMODEAC = 0b00001000;
|
||||
static constexpr uint8_t FLAGMODEDC = 0b00010000;
|
||||
static constexpr uint8_t FLAGMODEAUTO = 0b00100000;
|
||||
static constexpr uint8_t REGMINMAX = 0x08;
|
||||
static constexpr uint8_t FLAGMIN = 0b00010000;
|
||||
static constexpr uint8_t FLAGMAX = 0b00100000;
|
||||
static constexpr uint8_t REGSCALE = 0x09;
|
||||
static constexpr uint8_t FLAGSCALEDUTY = 0b00000010;
|
||||
static constexpr uint8_t FLAGSCALEDIODE = 0b00000100;
|
||||
static constexpr uint8_t FLAGSCALEBUZZ = 0b00001000;
|
||||
static constexpr uint8_t FLAGSCALEMEGA = 0b00010000;
|
||||
static constexpr uint8_t FLAGSCALEKILO = 0b00100000;
|
||||
static constexpr uint8_t FLAGSCALEMILLI = 0b01000000;
|
||||
static constexpr uint8_t FLAGSCALEMICRO = 0b10000000;
|
||||
static constexpr uint8_t REGUNIT = 0x0a;
|
||||
static constexpr uint8_t FLAGUNITFAHR = 0b00000001;
|
||||
static constexpr uint8_t FLAGUNITGRAD = 0b00000010;
|
||||
static constexpr uint8_t FLAGUNITNF = 0b00000100;
|
||||
static constexpr uint8_t FLAGUNITHZ = 0b00001000;
|
||||
static constexpr uint8_t FLAGUNITHFE = 0b00010000;
|
||||
static constexpr uint8_t FLAGUNITOHM = 0b00100000;
|
||||
static constexpr uint8_t FLAGUNITAMP = 0b01000000;
|
||||
static constexpr uint8_t FLAGUNITVOLT = 0b10000000;
|
||||
|
||||
bool connected{false};
|
||||
bool write_available{false};
|
||||
bool is_plus{false};
|
||||
bool low_battery{false};
|
||||
bool overload{false};
|
||||
bool has_reading{false};
|
||||
uint8_t selected_button{1};
|
||||
uint32_t last_notify_ms{0};
|
||||
|
||||
bool handle_notify(const std::vector<uint8_t> &data) {
|
||||
if (data.size() > sizeof(this->raw_))
|
||||
return false;
|
||||
if (data.size() == 6 && data[1] >= 0xF0) {
|
||||
memset(this->raw_, 0, sizeof(this->raw_));
|
||||
memcpy(this->raw_, data.data(), data.size());
|
||||
this->is_plus = true;
|
||||
this->parse_plus_();
|
||||
} else if (data.size() == 14 && data[12] == 0x0D && data[13] == 0x0A) {
|
||||
memset(this->value_, 0, sizeof(this->value_));
|
||||
memcpy(this->value_, data.data(), data.size());
|
||||
this->is_plus = false;
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Ignoring unexpected OWON frame length=%u", static_cast<unsigned>(data.size()));
|
||||
return false;
|
||||
}
|
||||
|
||||
this->overload = memcmp(this->value_, OVERLOAD_FRAME, sizeof(OVERLOAD_FRAME)) == 0;
|
||||
this->display_value = this->calc_display_value_();
|
||||
this->base_value = this->calc_base_value_();
|
||||
this->has_reading = true;
|
||||
this->last_notify_ms = millis();
|
||||
return true;
|
||||
}
|
||||
|
||||
void on_connect() {
|
||||
this->connected = true;
|
||||
this->write_available = true;
|
||||
}
|
||||
|
||||
void on_disconnect() {
|
||||
this->connected = false;
|
||||
this->write_available = false;
|
||||
}
|
||||
|
||||
float value() const { return this->display_value; }
|
||||
float value_base() const { return this->base_value; }
|
||||
bool negative() const { return (this->value_[REGPLUSMINUS] & FLAGMINUS) == FLAGMINUS; }
|
||||
bool auto_range() const { return (this->value_[REGMODE] & FLAGMODEAUTO) == FLAGMODEAUTO; }
|
||||
bool hold() const { return (this->value_[REGMODE] & FLAGMODEHOLD) == FLAGMODEHOLD; }
|
||||
bool relative() const { return (this->value_[REGMODE] & FLAGMODEREL) == FLAGMODEREL; }
|
||||
bool ac() const { return (this->value_[REGMODE] & FLAGMODEAC) == FLAGMODEAC; }
|
||||
bool dc() const { return (this->value_[REGMODE] & FLAGMODEDC) == FLAGMODEDC; }
|
||||
bool min_mode() const { return (this->value_[REGMINMAX] & FLAGMIN) == FLAGMIN; }
|
||||
bool max_mode() const { return (this->value_[REGMINMAX] & FLAGMAX) == FLAGMAX; }
|
||||
bool diode() const { return (this->value_[REGSCALE] & FLAGSCALEDIODE) == FLAGSCALEDIODE; }
|
||||
bool continuity() const { return (this->value_[REGSCALE] & FLAGSCALEBUZZ) == FLAGSCALEBUZZ; }
|
||||
|
||||
const char *unit() const {
|
||||
switch (this->value_[REGUNIT]) {
|
||||
case FLAGUNITFAHR: return "°F";
|
||||
case FLAGUNITGRAD: return "°C";
|
||||
case FLAGUNITNF: return "nF";
|
||||
case FLAGUNITHZ: return "Hz";
|
||||
case FLAGUNITHFE: return "hFE";
|
||||
case FLAGUNITOHM: return "Ω";
|
||||
case FLAGUNITAMP: return "A";
|
||||
case FLAGUNITVOLT: return "V";
|
||||
default: return "";
|
||||
}
|
||||
}
|
||||
|
||||
const char *scale() const {
|
||||
if ((this->value_[REGSCALE] & FLAGSCALEDUTY) == FLAGSCALEDUTY) return "%";
|
||||
if ((this->value_[REGSCALE] & FLAGSCALEMEGA) == FLAGSCALEMEGA) return "M";
|
||||
if ((this->value_[REGSCALE] & FLAGSCALEKILO) == FLAGSCALEKILO) return "k";
|
||||
if ((this->value_[REGSCALE] & FLAGSCALEMILLI) == FLAGSCALEMILLI) return "m";
|
||||
if ((this->value_[REGSCALE] & FLAGSCALEMICRO) == FLAGSCALEMICRO) return "µ";
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string mode_text() const {
|
||||
std::string out;
|
||||
if (this->dc()) out += "DC ";
|
||||
if (this->ac()) out += "AC ";
|
||||
if (this->auto_range()) out += "AUTO ";
|
||||
if (this->hold()) out += "HOLD ";
|
||||
if (this->relative()) out += "REL ";
|
||||
if (this->min_mode()) out += "MIN ";
|
||||
if (this->max_mode()) out += "MAX ";
|
||||
if (this->diode()) out += "DIODE ";
|
||||
if (this->continuity()) out += "CONT ";
|
||||
if (!out.empty()) out.pop_back();
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string reading_text() const {
|
||||
if (!this->connected) return "Disconnected";
|
||||
if (!this->has_reading) return "Waiting for data";
|
||||
if (this->overload) return "OL " + std::string(this->scale()) + this->unit();
|
||||
char buf[48];
|
||||
snprintf(buf, sizeof(buf), "%s%.4g %s%s", this->negative() ? "-" : "", std::fabs(this->display_value), this->scale(), this->unit());
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
enum Kind { KIND_OTHER, KIND_VOLTAGE, KIND_CURRENT, KIND_RESISTANCE, KIND_FREQUENCY, KIND_CAPACITANCE, KIND_TEMP_C, KIND_TEMP_F, KIND_DUTY };
|
||||
Kind kind() const {
|
||||
if ((this->value_[REGSCALE] & FLAGSCALEDUTY) == FLAGSCALEDUTY) return KIND_DUTY;
|
||||
switch (this->value_[REGUNIT]) {
|
||||
case FLAGUNITVOLT: return KIND_VOLTAGE;
|
||||
case FLAGUNITAMP: return KIND_CURRENT;
|
||||
case FLAGUNITOHM: return KIND_RESISTANCE;
|
||||
case FLAGUNITHZ: return KIND_FREQUENCY;
|
||||
case FLAGUNITNF: return KIND_CAPACITANCE;
|
||||
case FLAGUNITGRAD: return KIND_TEMP_C;
|
||||
case FLAGUNITFAHR: return KIND_TEMP_F;
|
||||
default: return KIND_OTHER;
|
||||
}
|
||||
}
|
||||
|
||||
const char *selected_button_name() const {
|
||||
static const char *const names[] = {"SELECT", "RANGE", "HLD/LIG", "REL/BT", "HZ/DUTY", "MAX/MIN"};
|
||||
uint8_t index = this->selected_button;
|
||||
if (index < 1) index = 1;
|
||||
if (index > 6) index = 6;
|
||||
return names[index - 1];
|
||||
}
|
||||
|
||||
void previous_button() {
|
||||
if (this->selected_button > 1) this->selected_button--;
|
||||
}
|
||||
void next_button() {
|
||||
if (this->selected_button < 6) this->selected_button++;
|
||||
}
|
||||
|
||||
void render(Display &it, esphome::display::BaseFont *font) {
|
||||
const Color bg(0, 0, 0);
|
||||
const Color fg(210, 210, 210);
|
||||
// Chosen to map to a neutral dark gray in the RGB332 8-bit display palette.
|
||||
const Color inactive(80, 80, 80);
|
||||
const Color yellow(255, 220, 0);
|
||||
const Color blue(0, 80, 255);
|
||||
const Color cyan(0, 255, 255);
|
||||
const Color magenta(255, 0, 255);
|
||||
const Color red(255, 0, 0);
|
||||
const Color green(0, 220, 0);
|
||||
const Color orange(255, 165, 0);
|
||||
|
||||
it.fill(bg);
|
||||
bool status_active = this->connected && this->has_reading;
|
||||
this->draw_icon_(it, 12, 8, 16, 16, ACCU_BMP, status_active ? (this->low_battery ? red : green) : inactive);
|
||||
this->draw_icon_(it, 46, 8, 16, 16, BLE_BMP, this->connected ? blue : inactive);
|
||||
this->label_(it, font, 86, 8, "AUTO", status_active && this->auto_range() ? fg : inactive);
|
||||
this->label_(it, font, 138, 8, "MAX", status_active && this->max_mode() ? red : inactive);
|
||||
this->label_(it, font, 178, 8, "MIN", status_active && this->min_mode() ? green : inactive);
|
||||
this->label_(it, font, 218, 8, "HOLD", status_active && this->hold() ? blue : inactive);
|
||||
this->label_(it, font, 270, 8, "REL", status_active && this->relative() ? Color(128, 128, 0) : inactive);
|
||||
|
||||
this->label_(it, font, 8, 66, "DC", status_active && this->dc() ? cyan : inactive);
|
||||
this->label_(it, font, 8, 102, "AC", status_active && this->ac() ? magenta : inactive);
|
||||
|
||||
if (!this->connected) {
|
||||
this->draw_digits_(it, "----", false, inactive);
|
||||
it.print(160, 148, font, inactive, esphome::display::TextAlign::CENTER, "scan/connect");
|
||||
} else if (!this->has_reading) {
|
||||
this->draw_digits_(it, "8888", false, inactive);
|
||||
it.print(160, 148, font, inactive, esphome::display::TextAlign::CENTER, "waiting");
|
||||
} else if (this->overload) {
|
||||
this->draw_digits_(it, " OL ", false, fg);
|
||||
} else {
|
||||
char d[5];
|
||||
d[0] = this->digit_char_(REGDIG1);
|
||||
d[1] = this->digit_char_(REGDIG2);
|
||||
d[2] = this->digit_char_(REGDIG3);
|
||||
d[3] = this->digit_char_(REGDIG4);
|
||||
d[4] = 0;
|
||||
this->draw_digits_(it, d, this->negative(), fg);
|
||||
this->draw_decimal_points_(it, fg);
|
||||
}
|
||||
|
||||
if (status_active) {
|
||||
std::string unit_line = std::string(this->scale()) + this->unit();
|
||||
it.print(270, 140, font, yellow, esphome::display::TextAlign::CENTER, unit_line.c_str());
|
||||
}
|
||||
|
||||
bool bargraph_active = status_active && !this->overload;
|
||||
this->draw_bargraph_(it, bargraph_active ? this->digits_from_buffer_() : 0, bargraph_active);
|
||||
this->draw_icon_(it, 300, 148, 16, 16, DIODE_BMP, status_active && this->diode() ? magenta : inactive);
|
||||
this->draw_icon_(it, 300, 174, 16, 16, BUZZ_BMP, status_active && this->continuity() ? orange : inactive);
|
||||
|
||||
it.filled_rectangle(34, 212, 40, 24, this->write_available ? fg : inactive);
|
||||
it.filled_rectangle(108, 212, 100, 24, this->write_available ? fg : inactive);
|
||||
it.filled_rectangle(242, 212, 40, 24, this->write_available ? fg : inactive);
|
||||
it.print(54, 216, font, bg, esphome::display::TextAlign::TOP_CENTER, "<");
|
||||
it.print(158, 216, font, bg, esphome::display::TextAlign::TOP_CENTER, this->selected_button_name());
|
||||
it.print(262, 216, font, bg, esphome::display::TextAlign::TOP_CENTER, ">");
|
||||
}
|
||||
|
||||
private:
|
||||
uint8_t raw_[14]{};
|
||||
uint8_t value_[14]{};
|
||||
float display_value{NAN};
|
||||
float base_value{NAN};
|
||||
static constexpr uint8_t OVERLOAD_FRAME[5] = {0x2B, 0x3F, 0x30, 0x3A, 0x3F};
|
||||
|
||||
uint16_t digits_from_buffer_() const {
|
||||
uint16_t out = 0;
|
||||
if (this->value_[REGDIG1] >= '0' && this->value_[REGDIG1] <= '9') out += (this->value_[REGDIG1] - '0') * 1000;
|
||||
if (this->value_[REGDIG2] >= '0' && this->value_[REGDIG2] <= '9') out += (this->value_[REGDIG2] - '0') * 100;
|
||||
if (this->value_[REGDIG3] >= '0' && this->value_[REGDIG3] <= '9') out += (this->value_[REGDIG3] - '0') * 10;
|
||||
if (this->value_[REGDIG4] >= '0' && this->value_[REGDIG4] <= '9') out += (this->value_[REGDIG4] - '0');
|
||||
return out;
|
||||
}
|
||||
|
||||
float calc_display_value_() const {
|
||||
if (this->overload) return NAN;
|
||||
|
||||
if (this->is_plus) {
|
||||
uint16_t pair1 = static_cast<uint16_t>(this->raw_[0]) | (static_cast<uint16_t>(this->raw_[1]) << 8);
|
||||
uint8_t decimal = pair1 & 0x07;
|
||||
if (decimal >= 7) return NAN;
|
||||
|
||||
uint16_t pair3 = static_cast<uint16_t>(this->raw_[4]) | (static_cast<uint16_t>(this->raw_[5]) << 8);
|
||||
bool negative = pair3 >= 0x7FFF;
|
||||
uint16_t digits = negative ? (pair3 & 0x7FFF) : pair3;
|
||||
float v = static_cast<float>(digits) / std::pow(10.0f, decimal);
|
||||
return negative ? -v : v;
|
||||
}
|
||||
|
||||
uint8_t decimal = 0;
|
||||
switch (this->value_[REGPOINT] & 0x07) {
|
||||
case 0b001: decimal = 1; break;
|
||||
case 0b010: decimal = 2; break;
|
||||
case 0b100: decimal = 3; break;
|
||||
default: break;
|
||||
}
|
||||
float v = static_cast<float>(this->digits_from_buffer_()) / std::pow(10.0f, decimal);
|
||||
return this->negative() ? -v : v;
|
||||
}
|
||||
|
||||
float calc_base_value_() const {
|
||||
if (std::isnan(this->display_value)) return NAN;
|
||||
if (this->value_[REGUNIT] == FLAGUNITNF) return this->display_value * 1e-9f;
|
||||
if ((this->value_[REGSCALE] & FLAGSCALEMEGA) == FLAGSCALEMEGA) return this->display_value * 1e6f;
|
||||
if ((this->value_[REGSCALE] & FLAGSCALEKILO) == FLAGSCALEKILO) return this->display_value * 1e3f;
|
||||
if ((this->value_[REGSCALE] & FLAGSCALEMILLI) == FLAGSCALEMILLI) return this->display_value * 1e-3f;
|
||||
if ((this->value_[REGSCALE] & FLAGSCALEMICRO) == FLAGSCALEMICRO) return this->display_value * 1e-6f;
|
||||
return this->display_value;
|
||||
}
|
||||
|
||||
void parse_plus_() {
|
||||
memset(this->value_, 0, sizeof(this->value_));
|
||||
this->value_[5] = 0x20;
|
||||
this->value_[12] = 0x0D;
|
||||
this->value_[13] = 0x0A;
|
||||
|
||||
uint16_t pair1 = static_cast<uint16_t>(this->raw_[0]) | (static_cast<uint16_t>(this->raw_[1]) << 8);
|
||||
uint8_t function = (pair1 >> 6) & 0x0F;
|
||||
uint8_t scale = (pair1 >> 3) & 0x07;
|
||||
uint8_t decimal = pair1 & 0x07;
|
||||
|
||||
switch (decimal) {
|
||||
case 0: this->value_[REGPOINT] = FLAGPOINT0; break;
|
||||
case 1: this->value_[REGPOINT] = FLAGPOINT3; break;
|
||||
case 2: this->value_[REGPOINT] = FLAGPOINT2; break;
|
||||
case 3: this->value_[REGPOINT] = FLAGPOINT1; break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
switch (function) {
|
||||
case 0: this->value_[REGUNIT] |= FLAGUNITVOLT; this->value_[REGMODE] |= FLAGMODEDC; break;
|
||||
case 1: this->value_[REGUNIT] |= FLAGUNITVOLT; this->value_[REGMODE] |= FLAGMODEAC; break;
|
||||
case 2: this->value_[REGUNIT] |= FLAGUNITAMP; this->value_[REGMODE] |= FLAGMODEDC; break;
|
||||
case 3: this->value_[REGUNIT] |= FLAGUNITAMP; this->value_[REGMODE] |= FLAGMODEAC; break;
|
||||
case 4: this->value_[REGUNIT] |= FLAGUNITOHM; break;
|
||||
case 5: this->value_[REGUNIT] |= FLAGUNITNF; break;
|
||||
case 6: this->value_[REGUNIT] |= FLAGUNITHZ; break;
|
||||
case 7: this->value_[REGSCALE] |= FLAGSCALEDUTY; break;
|
||||
case 8: this->value_[REGUNIT] |= FLAGUNITGRAD; break;
|
||||
case 9: this->value_[REGUNIT] |= FLAGUNITFAHR; break;
|
||||
case 10: this->value_[REGSCALE] |= FLAGSCALEDIODE; break;
|
||||
case 11: this->value_[REGSCALE] |= FLAGSCALEBUZZ; break;
|
||||
case 12: this->value_[REGUNIT] |= FLAGUNITHFE; break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
switch (scale) {
|
||||
case 2: this->value_[REGSCALE] |= FLAGSCALEMICRO; break;
|
||||
case 3: this->value_[REGSCALE] |= FLAGSCALEMILLI; break;
|
||||
case 5: this->value_[REGSCALE] |= FLAGSCALEKILO; break;
|
||||
case 6: this->value_[REGSCALE] |= FLAGSCALEMEGA; break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
uint16_t pair2 = static_cast<uint16_t>(this->raw_[2]) | (static_cast<uint16_t>(this->raw_[3]) << 8);
|
||||
if (pair2 & (1 << 0)) this->value_[REGMODE] |= FLAGMODEHOLD;
|
||||
if (pair2 & (1 << 1)) this->value_[REGMODE] |= FLAGMODEREL;
|
||||
if (pair2 & (1 << 2)) this->value_[REGMODE] |= FLAGMODEAUTO;
|
||||
this->low_battery = (pair2 & (1 << 3)) != 0;
|
||||
if (pair2 & (1 << 4)) this->value_[REGMINMAX] |= FLAGMIN;
|
||||
if (pair2 & (1 << 5)) this->value_[REGMINMAX] |= FLAGMAX;
|
||||
|
||||
uint16_t pair3 = static_cast<uint16_t>(this->raw_[4]) | (static_cast<uint16_t>(this->raw_[5]) << 8);
|
||||
if (decimal < 7) {
|
||||
uint16_t digits = pair3;
|
||||
if (pair3 < 0x7FFF) {
|
||||
this->value_[REGPLUSMINUS] = FLAGPLUS;
|
||||
} else {
|
||||
this->value_[REGPLUSMINUS] = FLAGMINUS;
|
||||
digits = pair3 & 0x7FFF;
|
||||
}
|
||||
this->value_[REGDIG1] = '0' + ((digits / 1000) % 10);
|
||||
this->value_[REGDIG2] = '0' + ((digits / 100) % 10);
|
||||
this->value_[REGDIG3] = '0' + ((digits / 10) % 10);
|
||||
this->value_[REGDIG4] = '0' + (digits % 10);
|
||||
} else {
|
||||
memcpy(this->value_, OVERLOAD_FRAME, sizeof(OVERLOAD_FRAME));
|
||||
}
|
||||
}
|
||||
|
||||
char digit_char_(uint8_t reg) const {
|
||||
uint8_t c = this->value_[reg];
|
||||
return (c >= '0' && c <= '9') ? static_cast<char>(c) : ' ';
|
||||
}
|
||||
|
||||
void label_(Display &it, esphome::display::BaseFont *font, int x, int y, const char *text, Color color) {
|
||||
it.print(x, y, font, color, esphome::display::TextAlign::TOP_LEFT, text);
|
||||
}
|
||||
|
||||
void draw_digits_(Display &it, const char *text, bool negative, Color color) {
|
||||
if (negative) this->draw_segment_(it, 8, 88, 26, 9, true, color);
|
||||
constexpr int digit_x = 40;
|
||||
constexpr int digit_y = 35;
|
||||
constexpr int digit_w = 50;
|
||||
constexpr int digit_h = 88;
|
||||
constexpr int digit_distance = 64;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
this->draw_seven_segment_(it, digit_x + i * digit_distance, digit_y, digit_w, digit_h, text[i], color);
|
||||
}
|
||||
}
|
||||
|
||||
void draw_decimal_points_(Display &it, Color color) {
|
||||
uint8_t p = this->value_[REGPOINT];
|
||||
if ((p & FLAGPOINT1) == FLAGPOINT1) it.filled_rectangle(95, 117, 8, 10, color);
|
||||
if ((p & FLAGPOINT2) == FLAGPOINT2) it.filled_rectangle(159, 117, 8, 10, color);
|
||||
if ((p & FLAGPOINT3) == FLAGPOINT3) it.filled_rectangle(223, 117, 8, 10, color);
|
||||
}
|
||||
|
||||
void draw_segment_(Display &it, int x, int y, int w, int h, bool horizontal, Color color) {
|
||||
if (horizontal) {
|
||||
int cap = h / 2;
|
||||
it.filled_rectangle(x + cap, y, w - 2 * cap, h +1, color);
|
||||
it.filled_triangle(x, y + cap, x + cap, y, x + cap, y + h, color);
|
||||
it.filled_triangle(x + w, y + cap, x + w - cap, y, x + w - cap, y + h, color);
|
||||
} else {
|
||||
int cap = w / 2;
|
||||
it.filled_rectangle(x, y + cap, w +1, h - 2 * cap, color);
|
||||
it.filled_triangle(x + cap, y, x, y + cap, x + w, y + cap, color);
|
||||
it.filled_triangle(x + cap, y + h, x, y + h - cap, x + w, y + h - cap, color);
|
||||
}
|
||||
}
|
||||
|
||||
void draw_icon_(Display &it, int x, int y, int w, int h, const uint8_t *data, Color color) {
|
||||
for (int row = 0; row < h; row++) {
|
||||
for (int col = 0; col < w; col++) {
|
||||
uint8_t byte = data[row * ((w + 7) / 8) + (col / 8)];
|
||||
if ((byte & (0x80 >> (col % 8))) != 0) {
|
||||
it.draw_pixel_at(x + col, y + row, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void draw_seven_segment_(Display &it, int x, int y, int w, int h, char ch, Color color) {
|
||||
bool a=false,b=false,c=false,d=false,e=false,f=false,g=false;
|
||||
switch (ch) {
|
||||
case '0': a=b=c=d=e=f=true; break;
|
||||
case '1': b=c=true; break;
|
||||
case '2': a=b=d=e=g=true; break;
|
||||
case '3': a=b=c=d=g=true; break;
|
||||
case '4': b=c=f=g=true; break;
|
||||
case '5': a=c=d=f=g=true; break;
|
||||
case '6': a=c=d=e=f=g=true; break;
|
||||
case '7': a=b=c=true; break;
|
||||
case '8': a=b=c=d=e=f=g=true; break;
|
||||
case '9': a=b=c=d=f=g=true; break;
|
||||
case 'O': a=b=c=d=e=f=true; break;
|
||||
case 'L': d=e=f=true; break;
|
||||
case '-': g=true; break;
|
||||
default: break;
|
||||
}
|
||||
int t = 10;
|
||||
int half = h / 2;
|
||||
int gap = 1;
|
||||
|
||||
int top_y = y;
|
||||
int mid_y = y + half - t / 2;
|
||||
int bot_y = y + h - t;
|
||||
|
||||
int upper_v_y = top_y + t + gap;
|
||||
int upper_v_h = mid_y - gap - upper_v_y;
|
||||
int lower_v_y = mid_y + t + gap;
|
||||
int lower_v_h = bot_y - gap - lower_v_y;
|
||||
|
||||
if (a) this->draw_segment_(it, x + t / 2, top_y, w - t, t, true, color);
|
||||
if (b && upper_v_h > 0) this->draw_segment_(it, x + w - t, upper_v_y, t, upper_v_h, false, color);
|
||||
if (c && lower_v_h > 0) this->draw_segment_(it, x + w - t, lower_v_y, t, lower_v_h, false, color);
|
||||
if (d) this->draw_segment_(it, x + t / 2, bot_y, w - t, t, true, color);
|
||||
if (e && lower_v_h > 0) this->draw_segment_(it, x, lower_v_y, t, lower_v_h, false, color);
|
||||
if (f && upper_v_h > 0) this->draw_segment_(it, x, upper_v_y, t, upper_v_h, false, color);
|
||||
if (g) this->draw_segment_(it, x + t / 2, mid_y, w - t, t, true, color);
|
||||
}
|
||||
|
||||
void draw_bargraph_(Display &it, uint16_t digits, bool active) {
|
||||
const Color fg(255, 255, 255);
|
||||
const Color inactive(80, 80, 80);
|
||||
uint16_t mapped = active ? static_cast<uint16_t>(digits * 240 / 6000) : 0;
|
||||
if (mapped > 240) mapped = 240;
|
||||
for (uint16_t i = 0; i <= 240; i += 4) {
|
||||
Color col = (active && i <= mapped) ? fg : inactive;
|
||||
int h = (i % 40 == 0) ? 20 : ((i % 20 == 0) ? 15 : 10);
|
||||
it.vertical_line(40 + i, 185 - h, h, col);
|
||||
}
|
||||
it.horizontal_line(35, 185, 250, inactive);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace owon_b35t
|
||||
|
||||
static owon_b35t::Meter owon_meter;
|
||||
@@ -0,0 +1,316 @@
|
||||
# Derived work based on https://github.com/reaper7/M5Stack_BLE_client_Owon_B35T by reaper7.
|
||||
# AI (ChatGPT) has been used to adopt the Arduino sketch to ESPHome.
|
||||
# Ported to M5Stack Core2 due to memory constraints.
|
||||
|
||||
substitutions:
|
||||
name: "owon-b35t-m5stack-core2"
|
||||
friendly_name: "OWON B35T Multimeter Core2"
|
||||
device_description: "M5Stack Core2 BLE client for OWON B35T/B35T+ multimeter with local graphical display"
|
||||
owon_mac_address: !secret owon_b35t_mac_address
|
||||
|
||||
esphome:
|
||||
name: ${name}
|
||||
friendly_name: ${friendly_name}
|
||||
comment: ${device_description}
|
||||
min_version: 2024.6.0
|
||||
includes:
|
||||
- owon-b35t-m5stack-core2.h
|
||||
on_boot:
|
||||
priority: 850
|
||||
then:
|
||||
- lambda: |-
|
||||
owon_b35t::core2_axp192_init(id(core2_i2c));
|
||||
project:
|
||||
name: "custom.owon-b35t-m5stack-core2"
|
||||
version: "1.0"
|
||||
|
||||
esp32:
|
||||
board: m5stack-core2
|
||||
flash_size: 16MB
|
||||
framework:
|
||||
type: esp-idf
|
||||
advanced:
|
||||
minimum_chip_revision: "3.1"
|
||||
sram1_as_iram: true
|
||||
|
||||
psram:
|
||||
mode: quad
|
||||
speed: 80MHz
|
||||
|
||||
logger:
|
||||
level: INFO
|
||||
|
||||
api:
|
||||
encryption:
|
||||
key: !secret apikey
|
||||
|
||||
ota:
|
||||
platform: esphome
|
||||
password: !secret ota
|
||||
|
||||
wifi:
|
||||
ssid: "Voltage-legacy"
|
||||
password: !secret voltage_legacy_psk
|
||||
#use_address: ${name}.home
|
||||
power_save_mode: none
|
||||
fast_connect: on
|
||||
min_auth_mode: WPA2
|
||||
ap:
|
||||
ssid: "OWON B35T Fallback Hotspot"
|
||||
password: !secret fallback_psk
|
||||
|
||||
captive_portal:
|
||||
|
||||
interval:
|
||||
- interval: 10s
|
||||
then:
|
||||
- lambda: |-
|
||||
ESP_LOGI("mem", "heap free=%u min_free=%u internal_free=%u internal_largest=%u dma_free=%u dma_largest=%u psram_free=%u psram_largest=%u",
|
||||
static_cast<unsigned>(esp_get_free_heap_size()),
|
||||
static_cast<unsigned>(esp_get_minimum_free_heap_size()),
|
||||
static_cast<unsigned>(heap_caps_get_free_size(MALLOC_CAP_INTERNAL)),
|
||||
static_cast<unsigned>(heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL)),
|
||||
static_cast<unsigned>(heap_caps_get_free_size(MALLOC_CAP_DMA)),
|
||||
static_cast<unsigned>(heap_caps_get_largest_free_block(MALLOC_CAP_DMA)),
|
||||
static_cast<unsigned>(heap_caps_get_free_size(MALLOC_CAP_SPIRAM)),
|
||||
static_cast<unsigned>(heap_caps_get_largest_free_block(MALLOC_CAP_SPIRAM)));
|
||||
|
||||
esp32_ble_tracker:
|
||||
scan_parameters:
|
||||
active: true
|
||||
continuous: true
|
||||
|
||||
ble_client:
|
||||
- mac_address: ${owon_mac_address}
|
||||
id: owon_ble_client
|
||||
on_connect:
|
||||
then:
|
||||
- lambda: |-
|
||||
owon_meter.on_connect();
|
||||
on_disconnect:
|
||||
then:
|
||||
- lambda: |-
|
||||
owon_meter.on_disconnect();
|
||||
|
||||
spi:
|
||||
clk_pin: GPIO18
|
||||
mosi_pin: GPIO23
|
||||
|
||||
i2c:
|
||||
id: core2_i2c
|
||||
sda: GPIO21
|
||||
scl: GPIO22
|
||||
scan: true
|
||||
|
||||
output:
|
||||
- platform: template
|
||||
type: float
|
||||
id: lcd_backlight
|
||||
write_action:
|
||||
- lambda: |-
|
||||
owon_b35t::core2_axp192_set_backlight(state);
|
||||
|
||||
light:
|
||||
- platform: monochromatic
|
||||
output: lcd_backlight
|
||||
name: "${friendly_name} Backlight"
|
||||
id: backlight
|
||||
restore_mode: ALWAYS_ON
|
||||
|
||||
font:
|
||||
- file: "fonts/Roboto-Regular.ttf"
|
||||
id: meter_font
|
||||
size: 15
|
||||
glyphs:
|
||||
[
|
||||
" ",
|
||||
"!",
|
||||
"%",
|
||||
"+",
|
||||
"-",
|
||||
".",
|
||||
"/",
|
||||
"0",
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
"4",
|
||||
"5",
|
||||
"6",
|
||||
"7",
|
||||
"8",
|
||||
"9",
|
||||
"<",
|
||||
">",
|
||||
"A",
|
||||
"B",
|
||||
"C",
|
||||
"D",
|
||||
"E",
|
||||
"F",
|
||||
"G",
|
||||
"H",
|
||||
"I",
|
||||
"L",
|
||||
"M",
|
||||
"N",
|
||||
"O",
|
||||
"P",
|
||||
"R",
|
||||
"S",
|
||||
"T",
|
||||
"U",
|
||||
"V",
|
||||
"W",
|
||||
"X",
|
||||
"Y",
|
||||
"Z",
|
||||
"a",
|
||||
"c",
|
||||
"d",
|
||||
"e",
|
||||
"f",
|
||||
"g",
|
||||
"h",
|
||||
"i",
|
||||
"k",
|
||||
"m",
|
||||
"n",
|
||||
"o",
|
||||
"r",
|
||||
"s",
|
||||
"t",
|
||||
"u",
|
||||
"v",
|
||||
"w",
|
||||
"y",
|
||||
"z",
|
||||
"°",
|
||||
"µ",
|
||||
"Ω",
|
||||
]
|
||||
|
||||
display:
|
||||
- platform: mipi_spi
|
||||
id: lcd
|
||||
model: M5CORE2
|
||||
update_interval: 500ms
|
||||
lambda: |-
|
||||
owon_meter.render(it, id(meter_font));
|
||||
|
||||
touchscreen:
|
||||
- platform: ft63x6
|
||||
id: touch
|
||||
display: lcd
|
||||
|
||||
binary_sensor:
|
||||
- platform: touchscreen
|
||||
id: button_a
|
||||
touchscreen_id: touch
|
||||
x_min: 34
|
||||
x_max: 74
|
||||
y_min: 212
|
||||
y_max: 240
|
||||
internal: true
|
||||
on_press:
|
||||
then:
|
||||
- lambda: |-
|
||||
owon_meter.previous_button();
|
||||
- platform: touchscreen
|
||||
id: button_b
|
||||
touchscreen_id: touch
|
||||
x_min: 108
|
||||
x_max: 208
|
||||
y_min: 212
|
||||
y_max: 240
|
||||
internal: true
|
||||
on_click:
|
||||
- min_length: 50ms
|
||||
max_length: 1500ms
|
||||
then:
|
||||
- logger.log:
|
||||
level: INFO
|
||||
format: "OWON short press: %s"
|
||||
args: ["owon_meter.selected_button_name()"]
|
||||
- ble_client.ble_write:
|
||||
id: owon_ble_client
|
||||
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
|
||||
characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb"
|
||||
value: !lambda |-
|
||||
std::vector<uint8_t> data = {owon_meter.selected_button, 0x01};
|
||||
return data;
|
||||
- min_length: 1500ms
|
||||
max_length: 5000ms
|
||||
then:
|
||||
- logger.log:
|
||||
level: INFO
|
||||
format: "OWON long press: %s"
|
||||
args: ["owon_meter.selected_button_name()"]
|
||||
- ble_client.ble_write:
|
||||
id: owon_ble_client
|
||||
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
|
||||
characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb"
|
||||
value: !lambda |-
|
||||
uint8_t press_type = (owon_meter.selected_button == 1 || owon_meter.selected_button == 5) ? 0x01 : 0x00;
|
||||
std::vector<uint8_t> data = {owon_meter.selected_button, press_type};
|
||||
return data;
|
||||
- platform: touchscreen
|
||||
id: button_c
|
||||
touchscreen_id: touch
|
||||
x_min: 242
|
||||
x_max: 282
|
||||
y_min: 212
|
||||
y_max: 240
|
||||
internal: true
|
||||
on_press:
|
||||
then:
|
||||
- lambda: |-
|
||||
owon_meter.next_button();
|
||||
|
||||
- platform: template
|
||||
name: "${friendly_name} Connected"
|
||||
lambda: |-
|
||||
return owon_meter.connected;
|
||||
- platform: template
|
||||
name: "${friendly_name} Overload"
|
||||
lambda: |-
|
||||
return owon_meter.overload;
|
||||
- platform: template
|
||||
name: "${friendly_name} Low Battery"
|
||||
lambda: |-
|
||||
return owon_meter.low_battery;
|
||||
|
||||
sensor:
|
||||
- platform: ble_client
|
||||
type: characteristic
|
||||
ble_client_id: owon_ble_client
|
||||
id: owon_notify_source
|
||||
internal: true
|
||||
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
|
||||
characteristic_uuid: "0000fff4-0000-1000-8000-00805f9b34fb"
|
||||
notify: true
|
||||
update_interval: never
|
||||
lambda: |-
|
||||
owon_meter.handle_notify(x);
|
||||
return owon_meter.value();
|
||||
|
||||
- platform: wifi_signal
|
||||
name: "${friendly_name} WiFi Signal"
|
||||
update_interval: 60s
|
||||
|
||||
text_sensor:
|
||||
- platform: template
|
||||
name: "${friendly_name} Reading"
|
||||
update_interval: 1s
|
||||
lambda: |-
|
||||
return owon_meter.reading_text();
|
||||
- platform: template
|
||||
name: "${friendly_name} Unit"
|
||||
update_interval: 1s
|
||||
lambda: |-
|
||||
return std::string(owon_meter.scale()) + owon_meter.unit();
|
||||
- platform: template
|
||||
name: "${friendly_name} Mode"
|
||||
update_interval: 1s
|
||||
lambda: |-
|
||||
return owon_meter.mode_text();
|
||||
@@ -0,0 +1,567 @@
|
||||
/*
|
||||
* ESPHome helper for OWON B35T/B35T+ BLE meter on M5Stack Core 1.
|
||||
* Parser is based on the standalone Arduino sketch by Reaper7
|
||||
* (Beerware license, Revision 42) and Dean Cording's owonb35 notes.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include "esp_heap_caps.h"
|
||||
#include "esp_system.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/components/display/display.h"
|
||||
|
||||
namespace owon_b35t {
|
||||
|
||||
using esphome::Color;
|
||||
using esphome::display::Display;
|
||||
|
||||
static const char *const TAG = "owon_b35t";
|
||||
|
||||
static const uint8_t ACCU_BMP[32] = {
|
||||
0b00000000, 0b00000000,
|
||||
0b00000000, 0b00000000,
|
||||
0b00000000, 0b00000000,
|
||||
0b11111111, 0b11111110,
|
||||
0b10000000, 0b00000010,
|
||||
0b10000000, 0b00000011,
|
||||
0b10000000, 0b00000011,
|
||||
0b10000000, 0b00000011,
|
||||
0b10000000, 0b00000011,
|
||||
0b10000000, 0b00000011,
|
||||
0b10000000, 0b00000011,
|
||||
0b10000000, 0b00000010,
|
||||
0b11111111, 0b11111110,
|
||||
0b00000000, 0b00000000,
|
||||
0b00000000, 0b00000000,
|
||||
0b00000000, 0b00000000,
|
||||
};
|
||||
|
||||
static const uint8_t BLE_BMP[32] = {
|
||||
0b00000001, 0b10000000,
|
||||
0b00000001, 0b11000000,
|
||||
0b00010001, 0b01100000,
|
||||
0b00011001, 0b00110000,
|
||||
0b00001101, 0b00011000,
|
||||
0b00000111, 0b00110000,
|
||||
0b00000011, 0b01100000,
|
||||
0b00000001, 0b11000000,
|
||||
0b00000001, 0b11000000,
|
||||
0b00000011, 0b01100000,
|
||||
0b00000111, 0b00110000,
|
||||
0b00001101, 0b00011000,
|
||||
0b00011001, 0b00110000,
|
||||
0b00010001, 0b01100000,
|
||||
0b00000001, 0b11000000,
|
||||
0b00000001, 0b10000000,
|
||||
};
|
||||
|
||||
static const uint8_t DIODE_BMP[32] = {
|
||||
0b00001000, 0b00011000,
|
||||
0b00001100, 0b00011000,
|
||||
0b00001110, 0b00011000,
|
||||
0b00001111, 0b00011000,
|
||||
0b00001111, 0b10011000,
|
||||
0b00001111, 0b11011000,
|
||||
0b00001111, 0b11111000,
|
||||
0b11111111, 0b11111111,
|
||||
0b11111111, 0b11111111,
|
||||
0b00001111, 0b11111000,
|
||||
0b00001111, 0b11011000,
|
||||
0b00001111, 0b10011000,
|
||||
0b00001111, 0b00011000,
|
||||
0b00001110, 0b00011000,
|
||||
0b00001100, 0b00011000,
|
||||
0b00001000, 0b00011000,
|
||||
};
|
||||
|
||||
static const uint8_t BUZZ_BMP[32] = {
|
||||
0b00000000, 0b11000000,
|
||||
0b00000001, 0b11000000,
|
||||
0b00000011, 0b11000001,
|
||||
0b00000111, 0b11000001,
|
||||
0b00001111, 0b11000101,
|
||||
0b11111111, 0b11000101,
|
||||
0b11111111, 0b11010101,
|
||||
0b11111111, 0b11010101,
|
||||
0b11111111, 0b11010101,
|
||||
0b11111111, 0b11010101,
|
||||
0b11111111, 0b11000101,
|
||||
0b00001111, 0b11000101,
|
||||
0b00000111, 0b11000001,
|
||||
0b00000011, 0b11000001,
|
||||
0b00000001, 0b11000000,
|
||||
0b00000000, 0b11000000,
|
||||
};
|
||||
|
||||
class Meter {
|
||||
public:
|
||||
static constexpr uint8_t REGPLUSMINUS = 0x00;
|
||||
static constexpr uint8_t FLAGPLUS = 0b00101011;
|
||||
static constexpr uint8_t FLAGMINUS = 0b00101101;
|
||||
static constexpr uint8_t REGDIG1 = 0x01;
|
||||
static constexpr uint8_t REGDIG2 = 0x02;
|
||||
static constexpr uint8_t REGDIG3 = 0x03;
|
||||
static constexpr uint8_t REGDIG4 = 0x04;
|
||||
static constexpr uint8_t REGPOINT = 0x06;
|
||||
static constexpr uint8_t FLAGPOINT0 = 0b00110000;
|
||||
static constexpr uint8_t FLAGPOINT1 = 0b00110001;
|
||||
static constexpr uint8_t FLAGPOINT2 = 0b00110010;
|
||||
static constexpr uint8_t FLAGPOINT3 = 0b00110100;
|
||||
static constexpr uint8_t REGMODE = 0x07;
|
||||
static constexpr uint8_t FLAGMODEHOLD = 0b00000010;
|
||||
static constexpr uint8_t FLAGMODEREL = 0b00000100;
|
||||
static constexpr uint8_t FLAGMODEAC = 0b00001000;
|
||||
static constexpr uint8_t FLAGMODEDC = 0b00010000;
|
||||
static constexpr uint8_t FLAGMODEAUTO = 0b00100000;
|
||||
static constexpr uint8_t REGMINMAX = 0x08;
|
||||
static constexpr uint8_t FLAGMIN = 0b00010000;
|
||||
static constexpr uint8_t FLAGMAX = 0b00100000;
|
||||
static constexpr uint8_t REGSCALE = 0x09;
|
||||
static constexpr uint8_t FLAGSCALEDUTY = 0b00000010;
|
||||
static constexpr uint8_t FLAGSCALEDIODE = 0b00000100;
|
||||
static constexpr uint8_t FLAGSCALEBUZZ = 0b00001000;
|
||||
static constexpr uint8_t FLAGSCALEMEGA = 0b00010000;
|
||||
static constexpr uint8_t FLAGSCALEKILO = 0b00100000;
|
||||
static constexpr uint8_t FLAGSCALEMILLI = 0b01000000;
|
||||
static constexpr uint8_t FLAGSCALEMICRO = 0b10000000;
|
||||
static constexpr uint8_t REGUNIT = 0x0a;
|
||||
static constexpr uint8_t FLAGUNITFAHR = 0b00000001;
|
||||
static constexpr uint8_t FLAGUNITGRAD = 0b00000010;
|
||||
static constexpr uint8_t FLAGUNITNF = 0b00000100;
|
||||
static constexpr uint8_t FLAGUNITHZ = 0b00001000;
|
||||
static constexpr uint8_t FLAGUNITHFE = 0b00010000;
|
||||
static constexpr uint8_t FLAGUNITOHM = 0b00100000;
|
||||
static constexpr uint8_t FLAGUNITAMP = 0b01000000;
|
||||
static constexpr uint8_t FLAGUNITVOLT = 0b10000000;
|
||||
|
||||
bool connected{false};
|
||||
bool write_available{false};
|
||||
bool is_plus{false};
|
||||
bool low_battery{false};
|
||||
bool overload{false};
|
||||
bool has_reading{false};
|
||||
uint8_t selected_button{1};
|
||||
uint32_t last_notify_ms{0};
|
||||
|
||||
bool handle_notify(const std::vector<uint8_t> &data) {
|
||||
if (data.size() > sizeof(this->raw_))
|
||||
return false;
|
||||
if (data.size() == 6 && data[1] >= 0xF0) {
|
||||
memset(this->raw_, 0, sizeof(this->raw_));
|
||||
memcpy(this->raw_, data.data(), data.size());
|
||||
this->is_plus = true;
|
||||
this->parse_plus_();
|
||||
} else if (data.size() == 14 && data[12] == 0x0D && data[13] == 0x0A) {
|
||||
memset(this->value_, 0, sizeof(this->value_));
|
||||
memcpy(this->value_, data.data(), data.size());
|
||||
this->is_plus = false;
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Ignoring unexpected OWON frame length=%u", static_cast<unsigned>(data.size()));
|
||||
return false;
|
||||
}
|
||||
|
||||
this->overload = memcmp(this->value_, OVERLOAD_FRAME, sizeof(OVERLOAD_FRAME)) == 0;
|
||||
this->display_value = this->calc_display_value_();
|
||||
this->base_value = this->calc_base_value_();
|
||||
this->has_reading = true;
|
||||
this->last_notify_ms = millis();
|
||||
return true;
|
||||
}
|
||||
|
||||
void on_connect() {
|
||||
this->connected = true;
|
||||
this->write_available = true;
|
||||
}
|
||||
|
||||
void on_disconnect() {
|
||||
this->connected = false;
|
||||
this->write_available = false;
|
||||
}
|
||||
|
||||
float value() const { return this->display_value; }
|
||||
float value_base() const { return this->base_value; }
|
||||
bool negative() const { return (this->value_[REGPLUSMINUS] & FLAGMINUS) == FLAGMINUS; }
|
||||
bool auto_range() const { return (this->value_[REGMODE] & FLAGMODEAUTO) == FLAGMODEAUTO; }
|
||||
bool hold() const { return (this->value_[REGMODE] & FLAGMODEHOLD) == FLAGMODEHOLD; }
|
||||
bool relative() const { return (this->value_[REGMODE] & FLAGMODEREL) == FLAGMODEREL; }
|
||||
bool ac() const { return (this->value_[REGMODE] & FLAGMODEAC) == FLAGMODEAC; }
|
||||
bool dc() const { return (this->value_[REGMODE] & FLAGMODEDC) == FLAGMODEDC; }
|
||||
bool min_mode() const { return (this->value_[REGMINMAX] & FLAGMIN) == FLAGMIN; }
|
||||
bool max_mode() const { return (this->value_[REGMINMAX] & FLAGMAX) == FLAGMAX; }
|
||||
bool diode() const { return (this->value_[REGSCALE] & FLAGSCALEDIODE) == FLAGSCALEDIODE; }
|
||||
bool continuity() const { return (this->value_[REGSCALE] & FLAGSCALEBUZZ) == FLAGSCALEBUZZ; }
|
||||
|
||||
const char *unit() const {
|
||||
switch (this->value_[REGUNIT]) {
|
||||
case FLAGUNITFAHR: return "°F";
|
||||
case FLAGUNITGRAD: return "°C";
|
||||
case FLAGUNITNF: return "nF";
|
||||
case FLAGUNITHZ: return "Hz";
|
||||
case FLAGUNITHFE: return "hFE";
|
||||
case FLAGUNITOHM: return "Ω";
|
||||
case FLAGUNITAMP: return "A";
|
||||
case FLAGUNITVOLT: return "V";
|
||||
default: return "";
|
||||
}
|
||||
}
|
||||
|
||||
const char *scale() const {
|
||||
if ((this->value_[REGSCALE] & FLAGSCALEDUTY) == FLAGSCALEDUTY) return "%";
|
||||
if ((this->value_[REGSCALE] & FLAGSCALEMEGA) == FLAGSCALEMEGA) return "M";
|
||||
if ((this->value_[REGSCALE] & FLAGSCALEKILO) == FLAGSCALEKILO) return "k";
|
||||
if ((this->value_[REGSCALE] & FLAGSCALEMILLI) == FLAGSCALEMILLI) return "m";
|
||||
if ((this->value_[REGSCALE] & FLAGSCALEMICRO) == FLAGSCALEMICRO) return "µ";
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string mode_text() const {
|
||||
std::string out;
|
||||
if (this->dc()) out += "DC ";
|
||||
if (this->ac()) out += "AC ";
|
||||
if (this->auto_range()) out += "AUTO ";
|
||||
if (this->hold()) out += "HOLD ";
|
||||
if (this->relative()) out += "REL ";
|
||||
if (this->min_mode()) out += "MIN ";
|
||||
if (this->max_mode()) out += "MAX ";
|
||||
if (this->diode()) out += "DIODE ";
|
||||
if (this->continuity()) out += "CONT ";
|
||||
if (!out.empty()) out.pop_back();
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string reading_text() const {
|
||||
if (!this->connected) return "Disconnected";
|
||||
if (!this->has_reading) return "Waiting for data";
|
||||
if (this->overload) return "OL " + std::string(this->scale()) + this->unit();
|
||||
char buf[48];
|
||||
snprintf(buf, sizeof(buf), "%s%.4g %s%s", this->negative() ? "-" : "", std::fabs(this->display_value), this->scale(), this->unit());
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
enum Kind { KIND_OTHER, KIND_VOLTAGE, KIND_CURRENT, KIND_RESISTANCE, KIND_FREQUENCY, KIND_CAPACITANCE, KIND_TEMP_C, KIND_TEMP_F, KIND_DUTY };
|
||||
Kind kind() const {
|
||||
if ((this->value_[REGSCALE] & FLAGSCALEDUTY) == FLAGSCALEDUTY) return KIND_DUTY;
|
||||
switch (this->value_[REGUNIT]) {
|
||||
case FLAGUNITVOLT: return KIND_VOLTAGE;
|
||||
case FLAGUNITAMP: return KIND_CURRENT;
|
||||
case FLAGUNITOHM: return KIND_RESISTANCE;
|
||||
case FLAGUNITHZ: return KIND_FREQUENCY;
|
||||
case FLAGUNITNF: return KIND_CAPACITANCE;
|
||||
case FLAGUNITGRAD: return KIND_TEMP_C;
|
||||
case FLAGUNITFAHR: return KIND_TEMP_F;
|
||||
default: return KIND_OTHER;
|
||||
}
|
||||
}
|
||||
|
||||
const char *selected_button_name() const {
|
||||
static const char *const names[] = {"SELECT", "RANGE", "HLD/LIG", "REL/BT", "HZ/DUTY", "MAX/MIN"};
|
||||
uint8_t index = this->selected_button;
|
||||
if (index < 1) index = 1;
|
||||
if (index > 6) index = 6;
|
||||
return names[index - 1];
|
||||
}
|
||||
|
||||
void previous_button() {
|
||||
if (this->selected_button > 1) this->selected_button--;
|
||||
}
|
||||
void next_button() {
|
||||
if (this->selected_button < 6) this->selected_button++;
|
||||
}
|
||||
|
||||
void render(Display &it, esphome::display::BaseFont *font) {
|
||||
const Color bg(0, 0, 0);
|
||||
const Color fg(210, 210, 210);
|
||||
// Chosen to map to a neutral dark gray in the RGB332 8-bit display palette.
|
||||
const Color inactive(80, 80, 80);
|
||||
const Color yellow(255, 220, 0);
|
||||
const Color blue(0, 80, 255);
|
||||
const Color cyan(0, 255, 255);
|
||||
const Color magenta(255, 0, 255);
|
||||
const Color red(255, 0, 0);
|
||||
const Color green(0, 220, 0);
|
||||
const Color orange(255, 165, 0);
|
||||
|
||||
it.fill(bg);
|
||||
this->draw_icon_(it, 12, 8, 16, 16, ACCU_BMP, this->low_battery ? red : green);
|
||||
this->draw_icon_(it, 46, 8, 16, 16, BLE_BMP, this->connected ? blue : inactive);
|
||||
this->label_(it, font, 86, 8, "AUTO", this->auto_range() ? fg : inactive);
|
||||
this->label_(it, font, 138, 8, "MAX", this->max_mode() ? red : inactive);
|
||||
this->label_(it, font, 178, 8, "MIN", this->min_mode() ? green : inactive);
|
||||
this->label_(it, font, 218, 8, "HOLD", this->hold() ? blue : inactive);
|
||||
this->label_(it, font, 270, 8, "REL", this->relative() ? Color(128, 128, 0) : inactive);
|
||||
|
||||
this->label_(it, font, 8, 66, "DC", this->dc() ? cyan : inactive);
|
||||
this->label_(it, font, 8, 102, "AC", this->ac() ? magenta : inactive);
|
||||
|
||||
if (!this->connected) {
|
||||
this->draw_digits_(it, "----", false, inactive);
|
||||
it.print(160, 148, font, inactive, esphome::display::TextAlign::CENTER, "scan/connect");
|
||||
} else if (!this->has_reading) {
|
||||
this->draw_digits_(it, "8888", false, inactive);
|
||||
it.print(160, 148, font, inactive, esphome::display::TextAlign::CENTER, "waiting");
|
||||
} else if (this->overload) {
|
||||
this->draw_digits_(it, " OL ", false, fg);
|
||||
} else {
|
||||
char d[5];
|
||||
d[0] = this->digit_char_(REGDIG1);
|
||||
d[1] = this->digit_char_(REGDIG2);
|
||||
d[2] = this->digit_char_(REGDIG3);
|
||||
d[3] = this->digit_char_(REGDIG4);
|
||||
d[4] = 0;
|
||||
this->draw_digits_(it, d, this->negative(), fg);
|
||||
this->draw_decimal_points_(it, fg);
|
||||
}
|
||||
|
||||
std::string unit_line = std::string(this->scale()) + this->unit();
|
||||
it.print(270, 140, font, yellow, esphome::display::TextAlign::CENTER, unit_line.c_str());
|
||||
|
||||
this->draw_bargraph_(it, this->has_reading && !this->overload ? this->digits_from_buffer_() : 0, this->has_reading && !this->overload);
|
||||
this->draw_icon_(it, 300, 148, 16, 16, DIODE_BMP, this->diode() ? magenta : inactive);
|
||||
this->draw_icon_(it, 300, 174, 16, 16, BUZZ_BMP, this->continuity() ? orange : inactive);
|
||||
|
||||
it.filled_rectangle(34, 212, 40, 24, this->write_available ? fg : inactive);
|
||||
it.filled_rectangle(108, 212, 100, 24, this->write_available ? fg : inactive);
|
||||
it.filled_rectangle(242, 212, 40, 24, this->write_available ? fg : inactive);
|
||||
it.print(54, 216, font, bg, esphome::display::TextAlign::TOP_CENTER, "<");
|
||||
it.print(158, 216, font, bg, esphome::display::TextAlign::TOP_CENTER, this->selected_button_name());
|
||||
it.print(262, 216, font, bg, esphome::display::TextAlign::TOP_CENTER, ">");
|
||||
}
|
||||
|
||||
private:
|
||||
uint8_t raw_[14]{};
|
||||
uint8_t value_[14]{};
|
||||
float display_value{NAN};
|
||||
float base_value{NAN};
|
||||
static constexpr uint8_t OVERLOAD_FRAME[5] = {0x2B, 0x3F, 0x30, 0x3A, 0x3F};
|
||||
|
||||
uint16_t digits_from_buffer_() const {
|
||||
uint16_t out = 0;
|
||||
if (this->value_[REGDIG1] >= '0' && this->value_[REGDIG1] <= '9') out += (this->value_[REGDIG1] - '0') * 1000;
|
||||
if (this->value_[REGDIG2] >= '0' && this->value_[REGDIG2] <= '9') out += (this->value_[REGDIG2] - '0') * 100;
|
||||
if (this->value_[REGDIG3] >= '0' && this->value_[REGDIG3] <= '9') out += (this->value_[REGDIG3] - '0') * 10;
|
||||
if (this->value_[REGDIG4] >= '0' && this->value_[REGDIG4] <= '9') out += (this->value_[REGDIG4] - '0');
|
||||
return out;
|
||||
}
|
||||
|
||||
float calc_display_value_() const {
|
||||
if (this->overload) return NAN;
|
||||
|
||||
if (this->is_plus) {
|
||||
uint16_t pair1 = static_cast<uint16_t>(this->raw_[0]) | (static_cast<uint16_t>(this->raw_[1]) << 8);
|
||||
uint8_t decimal = pair1 & 0x07;
|
||||
if (decimal >= 7) return NAN;
|
||||
|
||||
uint16_t pair3 = static_cast<uint16_t>(this->raw_[4]) | (static_cast<uint16_t>(this->raw_[5]) << 8);
|
||||
bool negative = pair3 >= 0x7FFF;
|
||||
uint16_t digits = negative ? (pair3 & 0x7FFF) : pair3;
|
||||
float v = static_cast<float>(digits) / std::pow(10.0f, decimal);
|
||||
return negative ? -v : v;
|
||||
}
|
||||
|
||||
uint8_t decimal = 0;
|
||||
switch (this->value_[REGPOINT] & 0x07) {
|
||||
case 0b001: decimal = 1; break;
|
||||
case 0b010: decimal = 2; break;
|
||||
case 0b100: decimal = 3; break;
|
||||
default: break;
|
||||
}
|
||||
float v = static_cast<float>(this->digits_from_buffer_()) / std::pow(10.0f, decimal);
|
||||
return this->negative() ? -v : v;
|
||||
}
|
||||
|
||||
float calc_base_value_() const {
|
||||
if (std::isnan(this->display_value)) return NAN;
|
||||
if (this->value_[REGUNIT] == FLAGUNITNF) return this->display_value * 1e-9f;
|
||||
if ((this->value_[REGSCALE] & FLAGSCALEMEGA) == FLAGSCALEMEGA) return this->display_value * 1e6f;
|
||||
if ((this->value_[REGSCALE] & FLAGSCALEKILO) == FLAGSCALEKILO) return this->display_value * 1e3f;
|
||||
if ((this->value_[REGSCALE] & FLAGSCALEMILLI) == FLAGSCALEMILLI) return this->display_value * 1e-3f;
|
||||
if ((this->value_[REGSCALE] & FLAGSCALEMICRO) == FLAGSCALEMICRO) return this->display_value * 1e-6f;
|
||||
return this->display_value;
|
||||
}
|
||||
|
||||
void parse_plus_() {
|
||||
memset(this->value_, 0, sizeof(this->value_));
|
||||
this->value_[5] = 0x20;
|
||||
this->value_[12] = 0x0D;
|
||||
this->value_[13] = 0x0A;
|
||||
|
||||
uint16_t pair1 = static_cast<uint16_t>(this->raw_[0]) | (static_cast<uint16_t>(this->raw_[1]) << 8);
|
||||
uint8_t function = (pair1 >> 6) & 0x0F;
|
||||
uint8_t scale = (pair1 >> 3) & 0x07;
|
||||
uint8_t decimal = pair1 & 0x07;
|
||||
|
||||
switch (decimal) {
|
||||
case 0: this->value_[REGPOINT] = FLAGPOINT0; break;
|
||||
case 1: this->value_[REGPOINT] = FLAGPOINT3; break;
|
||||
case 2: this->value_[REGPOINT] = FLAGPOINT2; break;
|
||||
case 3: this->value_[REGPOINT] = FLAGPOINT1; break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
switch (function) {
|
||||
case 0: this->value_[REGUNIT] |= FLAGUNITVOLT; this->value_[REGMODE] |= FLAGMODEDC; break;
|
||||
case 1: this->value_[REGUNIT] |= FLAGUNITVOLT; this->value_[REGMODE] |= FLAGMODEAC; break;
|
||||
case 2: this->value_[REGUNIT] |= FLAGUNITAMP; this->value_[REGMODE] |= FLAGMODEDC; break;
|
||||
case 3: this->value_[REGUNIT] |= FLAGUNITAMP; this->value_[REGMODE] |= FLAGMODEAC; break;
|
||||
case 4: this->value_[REGUNIT] |= FLAGUNITOHM; break;
|
||||
case 5: this->value_[REGUNIT] |= FLAGUNITNF; break;
|
||||
case 6: this->value_[REGUNIT] |= FLAGUNITHZ; break;
|
||||
case 7: this->value_[REGSCALE] |= FLAGSCALEDUTY; break;
|
||||
case 8: this->value_[REGUNIT] |= FLAGUNITGRAD; break;
|
||||
case 9: this->value_[REGUNIT] |= FLAGUNITFAHR; break;
|
||||
case 10: this->value_[REGSCALE] |= FLAGSCALEDIODE; break;
|
||||
case 11: this->value_[REGSCALE] |= FLAGSCALEBUZZ; break;
|
||||
case 12: this->value_[REGUNIT] |= FLAGUNITHFE; break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
switch (scale) {
|
||||
case 2: this->value_[REGSCALE] |= FLAGSCALEMICRO; break;
|
||||
case 3: this->value_[REGSCALE] |= FLAGSCALEMILLI; break;
|
||||
case 5: this->value_[REGSCALE] |= FLAGSCALEKILO; break;
|
||||
case 6: this->value_[REGSCALE] |= FLAGSCALEMEGA; break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
uint16_t pair2 = static_cast<uint16_t>(this->raw_[2]) | (static_cast<uint16_t>(this->raw_[3]) << 8);
|
||||
if (pair2 & (1 << 0)) this->value_[REGMODE] |= FLAGMODEHOLD;
|
||||
if (pair2 & (1 << 1)) this->value_[REGMODE] |= FLAGMODEREL;
|
||||
if (pair2 & (1 << 2)) this->value_[REGMODE] |= FLAGMODEAUTO;
|
||||
this->low_battery = (pair2 & (1 << 3)) != 0;
|
||||
if (pair2 & (1 << 4)) this->value_[REGMINMAX] |= FLAGMIN;
|
||||
if (pair2 & (1 << 5)) this->value_[REGMINMAX] |= FLAGMAX;
|
||||
|
||||
uint16_t pair3 = static_cast<uint16_t>(this->raw_[4]) | (static_cast<uint16_t>(this->raw_[5]) << 8);
|
||||
if (decimal < 7) {
|
||||
uint16_t digits = pair3;
|
||||
if (pair3 < 0x7FFF) {
|
||||
this->value_[REGPLUSMINUS] = FLAGPLUS;
|
||||
} else {
|
||||
this->value_[REGPLUSMINUS] = FLAGMINUS;
|
||||
digits = pair3 & 0x7FFF;
|
||||
}
|
||||
this->value_[REGDIG1] = '0' + ((digits / 1000) % 10);
|
||||
this->value_[REGDIG2] = '0' + ((digits / 100) % 10);
|
||||
this->value_[REGDIG3] = '0' + ((digits / 10) % 10);
|
||||
this->value_[REGDIG4] = '0' + (digits % 10);
|
||||
} else {
|
||||
memcpy(this->value_, OVERLOAD_FRAME, sizeof(OVERLOAD_FRAME));
|
||||
}
|
||||
}
|
||||
|
||||
char digit_char_(uint8_t reg) const {
|
||||
uint8_t c = this->value_[reg];
|
||||
return (c >= '0' && c <= '9') ? static_cast<char>(c) : ' ';
|
||||
}
|
||||
|
||||
void label_(Display &it, esphome::display::BaseFont *font, int x, int y, const char *text, Color color) {
|
||||
it.print(x, y, font, color, esphome::display::TextAlign::TOP_LEFT, text);
|
||||
}
|
||||
|
||||
void draw_digits_(Display &it, const char *text, bool negative, Color color) {
|
||||
if (negative) this->draw_segment_(it, 8, 88, 26, 9, true, color);
|
||||
constexpr int digit_x = 40;
|
||||
constexpr int digit_y = 35;
|
||||
constexpr int digit_w = 50;
|
||||
constexpr int digit_h = 88;
|
||||
constexpr int digit_distance = 64;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
this->draw_seven_segment_(it, digit_x + i * digit_distance, digit_y, digit_w, digit_h, text[i], color);
|
||||
}
|
||||
}
|
||||
|
||||
void draw_decimal_points_(Display &it, Color color) {
|
||||
uint8_t p = this->value_[REGPOINT];
|
||||
if ((p & FLAGPOINT1) == FLAGPOINT1) it.filled_rectangle(95, 117, 8, 10, color);
|
||||
if ((p & FLAGPOINT2) == FLAGPOINT2) it.filled_rectangle(159, 117, 8, 10, color);
|
||||
if ((p & FLAGPOINT3) == FLAGPOINT3) it.filled_rectangle(223, 117, 8, 10, color);
|
||||
}
|
||||
|
||||
void draw_segment_(Display &it, int x, int y, int w, int h, bool horizontal, Color color) {
|
||||
if (horizontal) {
|
||||
int cap = h / 2;
|
||||
it.filled_rectangle(x + cap, y, w - 2 * cap, h, color);
|
||||
it.filled_triangle(x, y + cap, x + cap, y, x + cap, y + h, color);
|
||||
it.filled_triangle(x + w, y + cap, x + w - cap, y, x + w - cap, y + h, color);
|
||||
} else {
|
||||
int cap = w / 2;
|
||||
it.filled_rectangle(x, y + cap, w, h - 2 * cap, color);
|
||||
it.filled_triangle(x + cap, y, x, y + cap, x + w, y + cap, color);
|
||||
it.filled_triangle(x + cap, y + h, x, y + h - cap, x + w, y + h - cap, color);
|
||||
}
|
||||
}
|
||||
|
||||
void draw_icon_(Display &it, int x, int y, int w, int h, const uint8_t *data, Color color) {
|
||||
for (int row = 0; row < h; row++) {
|
||||
for (int col = 0; col < w; col++) {
|
||||
uint8_t byte = data[row * ((w + 7) / 8) + (col / 8)];
|
||||
if ((byte & (0x80 >> (col % 8))) != 0) {
|
||||
it.draw_pixel_at(x + col, y + row, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void draw_seven_segment_(Display &it, int x, int y, int w, int h, char ch, Color color) {
|
||||
bool a=false,b=false,c=false,d=false,e=false,f=false,g=false;
|
||||
switch (ch) {
|
||||
case '0': a=b=c=d=e=f=true; break;
|
||||
case '1': b=c=true; break;
|
||||
case '2': a=b=d=e=g=true; break;
|
||||
case '3': a=b=c=d=g=true; break;
|
||||
case '4': b=c=f=g=true; break;
|
||||
case '5': a=c=d=f=g=true; break;
|
||||
case '6': a=c=d=e=f=g=true; break;
|
||||
case '7': a=b=c=true; break;
|
||||
case '8': a=b=c=d=e=f=g=true; break;
|
||||
case '9': a=b=c=d=f=g=true; break;
|
||||
case 'O': a=b=c=d=e=f=true; break;
|
||||
case 'L': d=e=f=true; break;
|
||||
case '-': g=true; break;
|
||||
default: break;
|
||||
}
|
||||
int t = 10;
|
||||
int half = h / 2;
|
||||
int gap = 1;
|
||||
|
||||
int top_y = y;
|
||||
int mid_y = y + half - t / 2;
|
||||
int bot_y = y + h - t;
|
||||
|
||||
int upper_v_y = top_y + t + gap;
|
||||
int upper_v_h = mid_y - gap - upper_v_y;
|
||||
int lower_v_y = mid_y + t + gap;
|
||||
int lower_v_h = bot_y - gap - lower_v_y;
|
||||
|
||||
if (a) this->draw_segment_(it, x + t / 2, top_y, w - t, t, true, color);
|
||||
if (b && upper_v_h > 0) this->draw_segment_(it, x + w - t, upper_v_y, t, upper_v_h, false, color);
|
||||
if (c && lower_v_h > 0) this->draw_segment_(it, x + w - t, lower_v_y, t, lower_v_h, false, color);
|
||||
if (d) this->draw_segment_(it, x + t / 2, bot_y, w - t, t, true, color);
|
||||
if (e && lower_v_h > 0) this->draw_segment_(it, x, lower_v_y, t, lower_v_h, false, color);
|
||||
if (f && upper_v_h > 0) this->draw_segment_(it, x, upper_v_y, t, upper_v_h, false, color);
|
||||
if (g) this->draw_segment_(it, x + t / 2, mid_y, w - t, t, true, color);
|
||||
}
|
||||
|
||||
void draw_bargraph_(Display &it, uint16_t digits, bool active) {
|
||||
const Color fg(255, 255, 255);
|
||||
const Color inactive(80, 80, 80);
|
||||
uint16_t mapped = active ? static_cast<uint16_t>(digits * 240 / 6000) : 0;
|
||||
if (mapped > 240) mapped = 240;
|
||||
for (uint16_t i = 0; i <= 240; i += 4) {
|
||||
Color col = (active && i <= mapped) ? fg : inactive;
|
||||
int h = (i % 40 == 0) ? 20 : ((i % 20 == 0) ? 15 : 10);
|
||||
it.vertical_line(40 + i, 185 - h, h, col);
|
||||
}
|
||||
it.horizontal_line(35, 185, 250, inactive);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace owon_b35t
|
||||
|
||||
static owon_b35t::Meter owon_meter;
|
||||
@@ -25,10 +25,13 @@ external_components:
|
||||
components: [es8388]
|
||||
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
|
||||
@@ -51,7 +51,7 @@ esphome:
|
||||
esp32:
|
||||
board: nodemcu-32s
|
||||
framework:
|
||||
type: arduino
|
||||
type: esp-idf
|
||||
|
||||
wifi:
|
||||
ssid: "Voltage-legacy"
|
||||
@@ -114,6 +114,7 @@ light:
|
||||
|
||||
# Initialize UART for Vindriktning sensor
|
||||
uart:
|
||||
id: uartPM
|
||||
rx_pin: GPIO26
|
||||
baud_rate: 9600
|
||||
|
||||
@@ -209,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:
|
||||
@@ -16,10 +16,13 @@ esp32:
|
||||
framework:
|
||||
type: arduino
|
||||
|
||||
network:
|
||||
enable_ipv6: true
|
||||
|
||||
wifi:
|
||||
ssid: "Voltage-legacy"
|
||||
password: !secret voltage_legacy_psk
|
||||
use_address: bathroom.home
|
||||
#use_address: bathroom.home
|
||||
power_save_mode: high
|
||||
fast_connect: on
|
||||
min_auth_mode: WPA2
|
||||
|
||||
+694
-183
File diff suppressed because it is too large
Load Diff
@@ -14,12 +14,15 @@ 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
|
||||
|
||||
@@ -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:
|
||||
@@ -29,4 +29,5 @@ api:
|
||||
ota:
|
||||
platform: esphome
|
||||
password: !secret ota
|
||||
|
||||
|
||||
http_request:
|
||||
|
||||
@@ -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:
|
||||
@@ -14,7 +14,10 @@ esphome:
|
||||
esp32:
|
||||
board: esp32cam
|
||||
framework:
|
||||
type: arduino
|
||||
type: esp-idf
|
||||
|
||||
network:
|
||||
enable_ipv6: true
|
||||
|
||||
wifi:
|
||||
ssid: "Voltage-legacy"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -0,0 +1,785 @@
|
||||
/*
|
||||
* ESPHome helper for OWON B35T/B35T+ BLE meter on M5Stack Core 2.
|
||||
* Parser is based on the standalone Arduino sketch by Reaper7
|
||||
* (Beerware license, Revision 42) and Dean Cording's owonb35 notes.
|
||||
* Rendering code and some functionality modified to suppport the
|
||||
* Atorch DL24 dc load.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include "esp_heap_caps.h"
|
||||
#include "esp_system.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/components/display/display.h"
|
||||
|
||||
namespace owon_b35t {
|
||||
|
||||
using esphome::Color;
|
||||
using esphome::display::Display;
|
||||
|
||||
static const char *const TAG = "owon_b35t";
|
||||
static const char *const POWER_TAG = "core2_power";
|
||||
static constexpr uint8_t AXP192_ADDR = 0x34;
|
||||
static esphome::i2c::I2CDevice axp192;
|
||||
static bool axp192_ready = false;
|
||||
|
||||
static bool axp_write(uint8_t reg, uint8_t value) {
|
||||
if (!axp192_ready) return false;
|
||||
bool ok = axp192.write_byte(reg, value);
|
||||
if (!ok) ESP_LOGW(POWER_TAG, "AXP192 write reg 0x%02X failed", reg);
|
||||
return ok;
|
||||
}
|
||||
|
||||
static bool axp_read(uint8_t reg, uint8_t *value) {
|
||||
if (!axp192_ready) return false;
|
||||
bool ok = axp192.read_byte(reg, value);
|
||||
if (!ok) ESP_LOGW(POWER_TAG, "AXP192 read reg 0x%02X failed", reg);
|
||||
return ok;
|
||||
}
|
||||
|
||||
static void axp_update(uint8_t reg, uint8_t clear_mask, uint8_t set_mask) {
|
||||
uint8_t value = 0;
|
||||
if (!axp_read(reg, &value)) return;
|
||||
value = (value & ~clear_mask) | set_mask;
|
||||
axp_write(reg, value);
|
||||
}
|
||||
|
||||
static uint8_t axp_dc_voltage_data(uint16_t millivolts) {
|
||||
if (millivolts < 700) millivolts = 700;
|
||||
if (millivolts > 3500) millivolts = 3500;
|
||||
return static_cast<uint8_t>((millivolts - 700) / 25) & 0x7F;
|
||||
}
|
||||
|
||||
static uint8_t axp_ldo_voltage_data(uint16_t millivolts) {
|
||||
if (millivolts < 1800) millivolts = 1800;
|
||||
if (millivolts > 3300) millivolts = 3300;
|
||||
return static_cast<uint8_t>((millivolts - 1800) / 100) & 0x0F;
|
||||
}
|
||||
|
||||
static void core2_axp192_set_lcd_voltage(uint16_t millivolts) {
|
||||
uint8_t value = 0;
|
||||
axp_read(0x27, &value);
|
||||
axp_write(0x27, (value & 0x80) | axp_dc_voltage_data(millivolts)); // DCDC3, LCD backlight
|
||||
}
|
||||
|
||||
static void core2_axp192_set_backlight(float brightness) {
|
||||
if (brightness <= 0.0f) {
|
||||
axp_update(0x12, 0x02, 0x00); // DCDC3 off
|
||||
return;
|
||||
}
|
||||
if (brightness > 1.0f) brightness = 1.0f;
|
||||
uint16_t millivolts = static_cast<uint16_t>(2400 + brightness * 900);
|
||||
core2_axp192_set_lcd_voltage(millivolts);
|
||||
axp_update(0x12, 0x00, 0x02); // DCDC3 on
|
||||
}
|
||||
|
||||
static void core2_axp192_init(esphome::i2c::I2CBus *bus) {
|
||||
axp192.set_i2c_bus(bus);
|
||||
axp192.set_i2c_address(AXP192_ADDR);
|
||||
axp192_ready = true;
|
||||
ESP_LOGI(POWER_TAG, "Initializing M5Stack Core2 AXP192 LCD power");
|
||||
axp_update(0x30, 0xF9, 0x02); // Disable VBUS current limit, preserve bit 2.
|
||||
axp_update(0x92, 0x07, 0x00); // GPIO1 open-drain output.
|
||||
axp_update(0x93, 0x07, 0x00); // GPIO2 open-drain output.
|
||||
axp_write(0x35, 0xA2); // RTC battery charging.
|
||||
|
||||
uint8_t value = 0;
|
||||
axp_read(0x26, &value);
|
||||
axp_write(0x26, (value & 0x80) | axp_dc_voltage_data(3350)); // DCDC1 ESP32 VDD.
|
||||
core2_axp192_set_lcd_voltage(2800);
|
||||
|
||||
uint8_t ldo2 = axp_ldo_voltage_data(3300);
|
||||
uint8_t ldo3 = axp_ldo_voltage_data(2000);
|
||||
axp_write(0x28, (ldo2 << 4) | ldo3); // LDO2 LCD logic/SD, LDO3 vibrator.
|
||||
axp_update(0x12, 0x00, 0x07); // Enable DCDC1, DCDC3, LDO2.
|
||||
axp_write(0x82, 0xFF); // ADCs on.
|
||||
axp_update(0x95, 0x8D, 0x84); // GPIO4 setup, as M5Core2 library does.
|
||||
axp_write(0x36, 0x4C); // Power key timing.
|
||||
|
||||
// LCD reset through AXP192 GPIO4.
|
||||
axp_update(0x96, 0x02, 0x00);
|
||||
delay(100);
|
||||
axp_update(0x96, 0x00, 0x02);
|
||||
delay(100);
|
||||
|
||||
core2_axp192_set_backlight(1.0f);
|
||||
}
|
||||
|
||||
static const uint8_t ACCU_BMP[32] = {
|
||||
0b00000000, 0b00000000,
|
||||
0b00000000, 0b00000000,
|
||||
0b00000000, 0b00000000,
|
||||
0b11111111, 0b11111110,
|
||||
0b10000000, 0b00000010,
|
||||
0b10000000, 0b00000011,
|
||||
0b10000000, 0b00000011,
|
||||
0b10000000, 0b00000011,
|
||||
0b10000000, 0b00000011,
|
||||
0b10000000, 0b00000011,
|
||||
0b10000000, 0b00000011,
|
||||
0b10000000, 0b00000010,
|
||||
0b11111111, 0b11111110,
|
||||
0b00000000, 0b00000000,
|
||||
0b00000000, 0b00000000,
|
||||
0b00000000, 0b00000000,
|
||||
};
|
||||
|
||||
static const uint8_t BLE_BMP[32] = {
|
||||
0b00000001, 0b10000000,
|
||||
0b00000001, 0b11000000,
|
||||
0b00010001, 0b01100000,
|
||||
0b00011001, 0b00110000,
|
||||
0b00001101, 0b00011000,
|
||||
0b00000111, 0b00110000,
|
||||
0b00000011, 0b01100000,
|
||||
0b00000001, 0b11000000,
|
||||
0b00000001, 0b11000000,
|
||||
0b00000011, 0b01100000,
|
||||
0b00000111, 0b00110000,
|
||||
0b00001101, 0b00011000,
|
||||
0b00011001, 0b00110000,
|
||||
0b00010001, 0b01100000,
|
||||
0b00000001, 0b11000000,
|
||||
0b00000001, 0b10000000,
|
||||
};
|
||||
|
||||
static const uint8_t HOLD_BMP[32] = {
|
||||
0b01111111, 0b11111110,
|
||||
0b11111111, 0b11111111,
|
||||
0b11100011, 0b11000111,
|
||||
0b11100011, 0b11000111,
|
||||
0b11100011, 0b11000111,
|
||||
0b11100011, 0b11000111,
|
||||
0b11100011, 0b11000111,
|
||||
0b11100000, 0b00000111,
|
||||
0b11100000, 0b00000111,
|
||||
0b11100011, 0b11000111,
|
||||
0b11100011, 0b11000111,
|
||||
0b11100011, 0b11000111,
|
||||
0b11100011, 0b11000111,
|
||||
0b11100011, 0b11000111,
|
||||
0b11111111, 0b11111111,
|
||||
0b01111111, 0b11111110,
|
||||
};
|
||||
|
||||
static const uint8_t REL_BMP[32] = {
|
||||
0b00000001, 0b00000000,
|
||||
0b00000001, 0b00000000,
|
||||
0b00000011, 0b10000000,
|
||||
0b00000011, 0b10000000,
|
||||
0b00000110, 0b11000000,
|
||||
0b00000110, 0b11000000,
|
||||
0b00001100, 0b01100000,
|
||||
0b00001100, 0b01100000,
|
||||
0b00011000, 0b00110000,
|
||||
0b00011000, 0b00110000,
|
||||
0b00110000, 0b00011000,
|
||||
0b00110000, 0b00011000,
|
||||
0b01100000, 0b00001100,
|
||||
0b01100000, 0b00001100,
|
||||
0b11111111, 0b11111110,
|
||||
0b11111111, 0b11111110,
|
||||
};
|
||||
|
||||
static const uint8_t DIODE_BMP[32] = {
|
||||
0b00001000, 0b00011000,
|
||||
0b00001100, 0b00011000,
|
||||
0b00001110, 0b00011000,
|
||||
0b00001111, 0b00011000,
|
||||
0b00001111, 0b10011000,
|
||||
0b00001111, 0b11011000,
|
||||
0b00001111, 0b11111000,
|
||||
0b11111111, 0b11111111,
|
||||
0b11111111, 0b11111111,
|
||||
0b00001111, 0b11111000,
|
||||
0b00001111, 0b11011000,
|
||||
0b00001111, 0b10011000,
|
||||
0b00001111, 0b00011000,
|
||||
0b00001110, 0b00011000,
|
||||
0b00001100, 0b00011000,
|
||||
0b00001000, 0b00011000,
|
||||
};
|
||||
|
||||
static const uint8_t BUZZ_BMP[32] = {
|
||||
0b00000000, 0b11000000,
|
||||
0b00000001, 0b11000000,
|
||||
0b00000011, 0b11000001,
|
||||
0b00000111, 0b11000001,
|
||||
0b00001111, 0b11000101,
|
||||
0b11111111, 0b11000101,
|
||||
0b11111111, 0b11010101,
|
||||
0b11111111, 0b11010101,
|
||||
0b11111111, 0b11010101,
|
||||
0b11111111, 0b11010101,
|
||||
0b11111111, 0b11000101,
|
||||
0b00001111, 0b11000101,
|
||||
0b00000111, 0b11000001,
|
||||
0b00000011, 0b11000001,
|
||||
0b00000001, 0b11000000,
|
||||
0b00000000, 0b11000000,
|
||||
};
|
||||
|
||||
class Meter {
|
||||
public:
|
||||
static constexpr uint8_t REGPLUSMINUS = 0x00;
|
||||
static constexpr uint8_t FLAGPLUS = 0b00101011;
|
||||
static constexpr uint8_t FLAGMINUS = 0b00101101;
|
||||
static constexpr uint8_t REGDIG1 = 0x01;
|
||||
static constexpr uint8_t REGDIG2 = 0x02;
|
||||
static constexpr uint8_t REGDIG3 = 0x03;
|
||||
static constexpr uint8_t REGDIG4 = 0x04;
|
||||
static constexpr uint8_t REGPOINT = 0x06;
|
||||
static constexpr uint8_t FLAGPOINT0 = 0b00110000;
|
||||
static constexpr uint8_t FLAGPOINT1 = 0b00110001;
|
||||
static constexpr uint8_t FLAGPOINT2 = 0b00110010;
|
||||
static constexpr uint8_t FLAGPOINT3 = 0b00110100;
|
||||
static constexpr uint8_t REGMODE = 0x07;
|
||||
static constexpr uint8_t FLAGMODEHOLD = 0b00000010;
|
||||
static constexpr uint8_t FLAGMODEREL = 0b00000100;
|
||||
static constexpr uint8_t FLAGMODEAC = 0b00001000;
|
||||
static constexpr uint8_t FLAGMODEDC = 0b00010000;
|
||||
static constexpr uint8_t FLAGMODEAUTO = 0b00100000;
|
||||
static constexpr uint8_t REGMINMAX = 0x08;
|
||||
static constexpr uint8_t FLAGMIN = 0b00010000;
|
||||
static constexpr uint8_t FLAGMAX = 0b00100000;
|
||||
static constexpr uint8_t REGSCALE = 0x09;
|
||||
static constexpr uint8_t FLAGSCALEDUTY = 0b00000010;
|
||||
static constexpr uint8_t FLAGSCALEDIODE = 0b00000100;
|
||||
static constexpr uint8_t FLAGSCALEBUZZ = 0b00001000;
|
||||
static constexpr uint8_t FLAGSCALEMEGA = 0b00010000;
|
||||
static constexpr uint8_t FLAGSCALEKILO = 0b00100000;
|
||||
static constexpr uint8_t FLAGSCALEMILLI = 0b01000000;
|
||||
static constexpr uint8_t FLAGSCALEMICRO = 0b10000000;
|
||||
static constexpr uint8_t REGUNIT = 0x0a;
|
||||
static constexpr uint8_t FLAGUNITFAHR = 0b00000001;
|
||||
static constexpr uint8_t FLAGUNITGRAD = 0b00000010;
|
||||
static constexpr uint8_t FLAGUNITNF = 0b00000100;
|
||||
static constexpr uint8_t FLAGUNITHZ = 0b00001000;
|
||||
static constexpr uint8_t FLAGUNITHFE = 0b00010000;
|
||||
static constexpr uint8_t FLAGUNITOHM = 0b00100000;
|
||||
static constexpr uint8_t FLAGUNITAMP = 0b01000000;
|
||||
static constexpr uint8_t FLAGUNITVOLT = 0b10000000;
|
||||
|
||||
bool connected{false};
|
||||
bool write_available{false};
|
||||
bool is_plus{false};
|
||||
bool low_battery{false};
|
||||
bool overload{false};
|
||||
bool has_reading{false};
|
||||
uint8_t selected_button{1};
|
||||
uint32_t last_notify_ms{0};
|
||||
|
||||
bool handle_notify(const std::vector<uint8_t> &data) {
|
||||
if (data.size() > sizeof(this->raw_))
|
||||
return false;
|
||||
if (data.size() == 6 && data[1] >= 0xF0) {
|
||||
memset(this->raw_, 0, sizeof(this->raw_));
|
||||
memcpy(this->raw_, data.data(), data.size());
|
||||
this->is_plus = true;
|
||||
this->parse_plus_();
|
||||
} else if (data.size() == 14 && data[12] == 0x0D && data[13] == 0x0A) {
|
||||
memset(this->value_, 0, sizeof(this->value_));
|
||||
memcpy(this->value_, data.data(), data.size());
|
||||
this->is_plus = false;
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Ignoring unexpected OWON frame length=%u", static_cast<unsigned>(data.size()));
|
||||
return false;
|
||||
}
|
||||
|
||||
this->overload = memcmp(this->value_, OVERLOAD_FRAME, sizeof(OVERLOAD_FRAME)) == 0;
|
||||
this->display_value = this->calc_display_value_();
|
||||
this->base_value = this->calc_base_value_();
|
||||
this->has_reading = true;
|
||||
this->last_notify_ms = millis();
|
||||
return true;
|
||||
}
|
||||
|
||||
void on_connect() {
|
||||
this->connected = true;
|
||||
this->write_available = true;
|
||||
}
|
||||
|
||||
void on_disconnect() {
|
||||
this->connected = false;
|
||||
this->write_available = false;
|
||||
}
|
||||
|
||||
float value() const { return this->display_value; }
|
||||
float value_base() const { return this->base_value; }
|
||||
bool negative() const { return (this->value_[REGPLUSMINUS] & FLAGMINUS) == FLAGMINUS; }
|
||||
bool auto_range() const { return (this->value_[REGMODE] & FLAGMODEAUTO) == FLAGMODEAUTO; }
|
||||
bool hold() const { return (this->value_[REGMODE] & FLAGMODEHOLD) == FLAGMODEHOLD; }
|
||||
bool relative() const { return (this->value_[REGMODE] & FLAGMODEREL) == FLAGMODEREL; }
|
||||
bool ac() const { return (this->value_[REGMODE] & FLAGMODEAC) == FLAGMODEAC; }
|
||||
bool dc() const { return (this->value_[REGMODE] & FLAGMODEDC) == FLAGMODEDC; }
|
||||
bool min_mode() const { return (this->value_[REGMINMAX] & FLAGMIN) == FLAGMIN; }
|
||||
bool max_mode() const { return (this->value_[REGMINMAX] & FLAGMAX) == FLAGMAX; }
|
||||
bool diode() const { return (this->value_[REGSCALE] & FLAGSCALEDIODE) == FLAGSCALEDIODE; }
|
||||
bool continuity() const { return (this->value_[REGSCALE] & FLAGSCALEBUZZ) == FLAGSCALEBUZZ; }
|
||||
|
||||
const char *unit() const {
|
||||
switch (this->value_[REGUNIT]) {
|
||||
case FLAGUNITFAHR: return "°F";
|
||||
case FLAGUNITGRAD: return "°C";
|
||||
case FLAGUNITNF: return "nF";
|
||||
case FLAGUNITHZ: return "Hz";
|
||||
case FLAGUNITHFE: return "hFE";
|
||||
case FLAGUNITOHM: return "Ω";
|
||||
case FLAGUNITAMP: return "A";
|
||||
case FLAGUNITVOLT: return "V";
|
||||
default: return "";
|
||||
}
|
||||
}
|
||||
|
||||
const char *scale() const {
|
||||
if ((this->value_[REGSCALE] & FLAGSCALEDUTY) == FLAGSCALEDUTY) return "%";
|
||||
if ((this->value_[REGSCALE] & FLAGSCALEMEGA) == FLAGSCALEMEGA) return "M";
|
||||
if ((this->value_[REGSCALE] & FLAGSCALEKILO) == FLAGSCALEKILO) return "k";
|
||||
if ((this->value_[REGSCALE] & FLAGSCALEMILLI) == FLAGSCALEMILLI) return "m";
|
||||
if ((this->value_[REGSCALE] & FLAGSCALEMICRO) == FLAGSCALEMICRO) return "µ";
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string mode_text() const {
|
||||
std::string out;
|
||||
if (this->dc()) out += "DC ";
|
||||
if (this->ac()) out += "AC ";
|
||||
if (this->auto_range()) out += "AUTO ";
|
||||
if (this->hold()) out += "HOLD ";
|
||||
if (this->relative()) out += "REL ";
|
||||
if (this->min_mode()) out += "MIN ";
|
||||
if (this->max_mode()) out += "MAX ";
|
||||
if (this->diode()) out += "DIODE ";
|
||||
if (this->continuity()) out += "CONT ";
|
||||
if (!out.empty()) out.pop_back();
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string reading_text() const {
|
||||
if (!this->connected) return "Disconnected";
|
||||
if (!this->has_reading) return "Waiting for data";
|
||||
if (this->overload) return "OL " + std::string(this->scale()) + this->unit();
|
||||
char buf[48];
|
||||
snprintf(buf, sizeof(buf), "%s%.4g %s%s", this->negative() ? "-" : "", std::fabs(this->display_value), this->scale(), this->unit());
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
enum Kind { KIND_OTHER, KIND_VOLTAGE, KIND_CURRENT, KIND_RESISTANCE, KIND_FREQUENCY, KIND_CAPACITANCE, KIND_TEMP_C, KIND_TEMP_F, KIND_DUTY };
|
||||
Kind kind() const {
|
||||
if ((this->value_[REGSCALE] & FLAGSCALEDUTY) == FLAGSCALEDUTY) return KIND_DUTY;
|
||||
switch (this->value_[REGUNIT]) {
|
||||
case FLAGUNITVOLT: return KIND_VOLTAGE;
|
||||
case FLAGUNITAMP: return KIND_CURRENT;
|
||||
case FLAGUNITOHM: return KIND_RESISTANCE;
|
||||
case FLAGUNITHZ: return KIND_FREQUENCY;
|
||||
case FLAGUNITNF: return KIND_CAPACITANCE;
|
||||
case FLAGUNITGRAD: return KIND_TEMP_C;
|
||||
case FLAGUNITFAHR: return KIND_TEMP_F;
|
||||
default: return KIND_OTHER;
|
||||
}
|
||||
}
|
||||
|
||||
const char *selected_button_name() const {
|
||||
static const char *const names[] = {"SELECT", "RANGE", "HLD/LIG", "REL/BT", "HZ/DUTY", "MAX/MIN"};
|
||||
uint8_t index = this->selected_button;
|
||||
if (index < 1) index = 1;
|
||||
if (index > 6) index = 6;
|
||||
return names[index - 1];
|
||||
}
|
||||
|
||||
void previous_button() {
|
||||
if (this->selected_button > 1) this->selected_button--;
|
||||
}
|
||||
void next_button() {
|
||||
if (this->selected_button < 6) this->selected_button++;
|
||||
}
|
||||
|
||||
void render(esphome::display::Display &it, esphome::display::BaseFont *font,
|
||||
esphome::display::BaseFont *value_font, int display_page = 0,
|
||||
bool atorch_connected = false,
|
||||
float atorch_voltage = NAN, float atorch_current = NAN, float atorch_power = NAN,
|
||||
float atorch_capacity = NAN, float atorch_energy = NAN, float atorch_temperature = NAN) {
|
||||
const Color bg(0, 0, 0);
|
||||
const Color fg(210, 210, 210);
|
||||
// Chosen to map to a neutral dark gray in the RGB332 8-bit display palette.
|
||||
const Color inactive(80, 80, 80);
|
||||
const Color yellow(255, 220, 0);
|
||||
const Color blue(0, 80, 255);
|
||||
const Color cyan(0, 255, 255);
|
||||
const Color magenta(255, 0, 255);
|
||||
const Color red(255, 0, 0);
|
||||
const Color green(0, 220, 0);
|
||||
const Color orange(255, 165, 0);
|
||||
|
||||
if (display_page == 0) {
|
||||
// --- PAGE 1: OWON Multimeter ---
|
||||
it.fill(bg);
|
||||
bool status_active = this->connected && this->has_reading;
|
||||
this->draw_icon_(it, 12, 8, 16, 16, ACCU_BMP, status_active ? (this->low_battery ? red : green) : inactive);
|
||||
this->draw_icon_(it, 46, 8, 16, 16, BLE_BMP, this->connected ? blue : inactive);
|
||||
this->label_(it, font, 86, 8, "AUTO", status_active && this->auto_range() ? fg : inactive);
|
||||
this->label_(it, font, 138, 8, "MAX", status_active && this->max_mode() ? red : inactive);
|
||||
this->label_(it, font, 178, 8, "MIN", status_active && this->min_mode() ? green : inactive);
|
||||
this->draw_icon_(it, 218, 8, 16, 16, HOLD_BMP, status_active && this->hold() ? blue : inactive);
|
||||
this->draw_icon_(it, 244, 8, 16, 16, REL_BMP, status_active && this->relative() ? Color(128, 128, 0) : inactive);
|
||||
this->draw_icon_(it, 270, 8, 16, 16, DIODE_BMP, status_active && this->diode() ? magenta : inactive);
|
||||
this->draw_icon_(it, 296, 8, 16, 16, BUZZ_BMP, status_active && this->continuity() ? orange : inactive);
|
||||
|
||||
this->label_(it, font, 8, 104, "DC", status_active && this->dc() ? cyan : inactive);
|
||||
this->label_(it, font, 8, 124, "AC", status_active && this->ac() ? magenta : inactive);
|
||||
|
||||
if (!this->connected) {
|
||||
this->draw_digits_(it, "----", false, inactive);
|
||||
it.print(160, 148, font, inactive, esphome::display::TextAlign::CENTER, "scan/connect");
|
||||
} else if (!this->has_reading) {
|
||||
this->draw_digits_(it, "8888", false, inactive);
|
||||
it.print(160, 148, font, inactive, esphome::display::TextAlign::CENTER, "waiting");
|
||||
} else if (this->overload) {
|
||||
this->draw_digits_(it, " OL ", false, fg);
|
||||
} else {
|
||||
char d[5];
|
||||
d[0] = this->digit_char_(REGDIG1);
|
||||
d[1] = this->digit_char_(REGDIG2);
|
||||
d[2] = this->digit_char_(REGDIG3);
|
||||
d[3] = this->digit_char_(REGDIG4);
|
||||
d[4] = 0;
|
||||
this->draw_digits_(it, d, this->negative(), fg);
|
||||
this->draw_decimal_points_(it, fg);
|
||||
}
|
||||
|
||||
if (status_active) {
|
||||
std::string unit_line = std::string(this->scale()) + this->unit();
|
||||
it.print(316, 141, font, yellow, esphome::display::TextAlign::BOTTOM_RIGHT, unit_line.c_str());
|
||||
}
|
||||
|
||||
bool bargraph_active = status_active && !this->overload;
|
||||
this->draw_bargraph_(it, bargraph_active ? this->digits_from_buffer_() : 0, bargraph_active);
|
||||
it.filled_rectangle(34, 212, 40, 24, this->write_available ? fg : inactive);
|
||||
it.filled_rectangle(108, 212, 100, 24, this->write_available ? fg : inactive);
|
||||
it.filled_rectangle(242, 212, 40, 24, this->write_available ? fg : inactive);
|
||||
it.print(54, 216, font, bg, esphome::display::TextAlign::TOP_CENTER, "<");
|
||||
it.print(158, 216, font, bg, esphome::display::TextAlign::TOP_CENTER, this->selected_button_name());
|
||||
it.print(262, 216, font, bg, esphome::display::TextAlign::TOP_CENTER, ">");
|
||||
} else {
|
||||
// --- PAGE 2: Atorch DL24 DC load ---
|
||||
it.fill(bg);
|
||||
const Color panel_active(18, 24, 32);
|
||||
const Color panel_inactive(12, 12, 12);
|
||||
const Color panel_dim(10, 14, 20);
|
||||
const Color border_active(55, 70, 86);
|
||||
const Color border_inactive(42, 42, 42);
|
||||
const Color white(245, 245, 245);
|
||||
const Color dim_value(110, 110, 110);
|
||||
const Color dim_accent(65, 65, 65);
|
||||
const Color panel = atorch_connected ? panel_active : panel_inactive;
|
||||
const Color border = atorch_connected ? border_active : border_inactive;
|
||||
const Color value_color = atorch_connected ? white : dim_value;
|
||||
const Color voltage_color = atorch_connected ? cyan : dim_accent;
|
||||
const Color current_color = atorch_connected ? orange : dim_accent;
|
||||
const Color power_color = atorch_connected ? yellow : dim_accent;
|
||||
const Color temp_color = atorch_connected ? magenta : dim_accent;
|
||||
const Color header_color = atorch_connected ? cyan : dim_value;
|
||||
|
||||
char voltage_text[24];
|
||||
char current_text[24];
|
||||
char power_text[24];
|
||||
char capacity_text[24];
|
||||
char energy_text[24];
|
||||
char temperature_text[24];
|
||||
this->format_metric_(voltage_text, sizeof(voltage_text), atorch_voltage, "V", 2);
|
||||
this->format_metric_(current_text, sizeof(current_text), atorch_current, "A", 3);
|
||||
this->format_metric_(power_text, sizeof(power_text), atorch_power, "W", 2);
|
||||
this->format_metric_(capacity_text, sizeof(capacity_text), atorch_capacity, "Ah", 3);
|
||||
this->format_metric_(energy_text, sizeof(energy_text), atorch_energy, "Wh", 3);
|
||||
this->format_metric_(temperature_text, sizeof(temperature_text), atorch_temperature, "°C", 1);
|
||||
|
||||
it.filled_rectangle(0, 0, 320, 30, atorch_connected ? panel_dim : panel_inactive);
|
||||
it.print(10, 7, font, header_color, esphome::display::TextAlign::TOP_LEFT, "ATORCH DL24");
|
||||
if (atorch_connected) {
|
||||
this->draw_icon_(it, 152, 7, 16, 16, BLE_BMP, blue);
|
||||
}
|
||||
it.print(310, 7, font, inactive, esphome::display::TextAlign::TOP_RIGHT, "DC LOAD");
|
||||
|
||||
this->draw_metric_card_(it, font, value_font, 10, 42, 145, 76, "VOLTAGE", voltage_text, voltage_color, panel, border, value_color);
|
||||
this->draw_metric_card_(it, font, value_font, 165, 42, 145, 76, "CURRENT", current_text, current_color, panel, border, value_color);
|
||||
this->draw_metric_card_(it, font, value_font, 10, 128, 145, 76, "POWER", power_text, power_color, panel, border, value_color);
|
||||
this->draw_metric_card_(it, font, value_font, 165, 128, 145, 76, "TEMP", temperature_text, temp_color, panel, border, value_color);
|
||||
|
||||
it.filled_rectangle(10, 212, 300, 22, atorch_connected ? panel_dim : panel_inactive);
|
||||
it.filled_rectangle(10, 212, 300, 1, border);
|
||||
it.filled_rectangle(10, 233, 300, 1, border);
|
||||
it.filled_rectangle(10, 212, 1, 22, border);
|
||||
it.filled_rectangle(309, 212, 1, 22, border);
|
||||
it.print(20, 216, font, inactive, esphome::display::TextAlign::TOP_LEFT, "CAP");
|
||||
it.print(64, 216, font, value_color, esphome::display::TextAlign::TOP_LEFT, capacity_text);
|
||||
it.print(170, 216, font, inactive, esphome::display::TextAlign::TOP_LEFT, "ENERGY");
|
||||
it.print(306, 216, font, value_color, esphome::display::TextAlign::TOP_RIGHT, energy_text);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
uint8_t raw_[14]{};
|
||||
uint8_t value_[14]{};
|
||||
float display_value{NAN};
|
||||
float base_value{NAN};
|
||||
static constexpr uint8_t OVERLOAD_FRAME[5] = {0x2B, 0x3F, 0x30, 0x3A, 0x3F};
|
||||
|
||||
uint16_t digits_from_buffer_() const {
|
||||
uint16_t out = 0;
|
||||
if (this->value_[REGDIG1] >= '0' && this->value_[REGDIG1] <= '9') out += (this->value_[REGDIG1] - '0') * 1000;
|
||||
if (this->value_[REGDIG2] >= '0' && this->value_[REGDIG2] <= '9') out += (this->value_[REGDIG2] - '0') * 100;
|
||||
if (this->value_[REGDIG3] >= '0' && this->value_[REGDIG3] <= '9') out += (this->value_[REGDIG3] - '0') * 10;
|
||||
if (this->value_[REGDIG4] >= '0' && this->value_[REGDIG4] <= '9') out += (this->value_[REGDIG4] - '0');
|
||||
return out;
|
||||
}
|
||||
|
||||
float calc_display_value_() const {
|
||||
if (this->overload) return NAN;
|
||||
|
||||
if (this->is_plus) {
|
||||
uint16_t pair1 = static_cast<uint16_t>(this->raw_[0]) | (static_cast<uint16_t>(this->raw_[1]) << 8);
|
||||
uint8_t decimal = pair1 & 0x07;
|
||||
if (decimal >= 7) return NAN;
|
||||
|
||||
uint16_t pair3 = static_cast<uint16_t>(this->raw_[4]) | (static_cast<uint16_t>(this->raw_[5]) << 8);
|
||||
bool negative = pair3 >= 0x7FFF;
|
||||
uint16_t digits = negative ? (pair3 & 0x7FFF) : pair3;
|
||||
float v = static_cast<float>(digits) / std::pow(10.0f, decimal);
|
||||
return negative ? -v : v;
|
||||
}
|
||||
|
||||
uint8_t decimal = 0;
|
||||
switch (this->value_[REGPOINT] & 0x07) {
|
||||
case 0b001: decimal = 1; break;
|
||||
case 0b010: decimal = 2; break;
|
||||
case 0b100: decimal = 3; break;
|
||||
default: break;
|
||||
}
|
||||
float v = static_cast<float>(this->digits_from_buffer_()) / std::pow(10.0f, decimal);
|
||||
return this->negative() ? -v : v;
|
||||
}
|
||||
|
||||
float calc_base_value_() const {
|
||||
if (std::isnan(this->display_value)) return NAN;
|
||||
if (this->value_[REGUNIT] == FLAGUNITNF) return this->display_value * 1e-9f;
|
||||
if ((this->value_[REGSCALE] & FLAGSCALEMEGA) == FLAGSCALEMEGA) return this->display_value * 1e6f;
|
||||
if ((this->value_[REGSCALE] & FLAGSCALEKILO) == FLAGSCALEKILO) return this->display_value * 1e3f;
|
||||
if ((this->value_[REGSCALE] & FLAGSCALEMILLI) == FLAGSCALEMILLI) return this->display_value * 1e-3f;
|
||||
if ((this->value_[REGSCALE] & FLAGSCALEMICRO) == FLAGSCALEMICRO) return this->display_value * 1e-6f;
|
||||
return this->display_value;
|
||||
}
|
||||
|
||||
void parse_plus_() {
|
||||
memset(this->value_, 0, sizeof(this->value_));
|
||||
this->value_[5] = 0x20;
|
||||
this->value_[12] = 0x0D;
|
||||
this->value_[13] = 0x0A;
|
||||
|
||||
uint16_t pair1 = static_cast<uint16_t>(this->raw_[0]) | (static_cast<uint16_t>(this->raw_[1]) << 8);
|
||||
uint8_t function = (pair1 >> 6) & 0x0F;
|
||||
uint8_t scale = (pair1 >> 3) & 0x07;
|
||||
uint8_t decimal = pair1 & 0x07;
|
||||
|
||||
switch (decimal) {
|
||||
case 0: this->value_[REGPOINT] = FLAGPOINT0; break;
|
||||
case 1: this->value_[REGPOINT] = FLAGPOINT3; break;
|
||||
case 2: this->value_[REGPOINT] = FLAGPOINT2; break;
|
||||
case 3: this->value_[REGPOINT] = FLAGPOINT1; break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
switch (function) {
|
||||
case 0: this->value_[REGUNIT] |= FLAGUNITVOLT; this->value_[REGMODE] |= FLAGMODEDC; break;
|
||||
case 1: this->value_[REGUNIT] |= FLAGUNITVOLT; this->value_[REGMODE] |= FLAGMODEAC; break;
|
||||
case 2: this->value_[REGUNIT] |= FLAGUNITAMP; this->value_[REGMODE] |= FLAGMODEDC; break;
|
||||
case 3: this->value_[REGUNIT] |= FLAGUNITAMP; this->value_[REGMODE] |= FLAGMODEAC; break;
|
||||
case 4: this->value_[REGUNIT] |= FLAGUNITOHM; break;
|
||||
case 5: this->value_[REGUNIT] |= FLAGUNITNF; break;
|
||||
case 6: this->value_[REGUNIT] |= FLAGUNITHZ; break;
|
||||
case 7: this->value_[REGSCALE] |= FLAGSCALEDUTY; break;
|
||||
case 8: this->value_[REGUNIT] |= FLAGUNITGRAD; break;
|
||||
case 9: this->value_[REGUNIT] |= FLAGUNITFAHR; break;
|
||||
case 10: this->value_[REGSCALE] |= FLAGSCALEDIODE; break;
|
||||
case 11: this->value_[REGSCALE] |= FLAGSCALEBUZZ; break;
|
||||
case 12: this->value_[REGUNIT] |= FLAGUNITHFE; break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
switch (scale) {
|
||||
case 2: this->value_[REGSCALE] |= FLAGSCALEMICRO; break;
|
||||
case 3: this->value_[REGSCALE] |= FLAGSCALEMILLI; break;
|
||||
case 5: this->value_[REGSCALE] |= FLAGSCALEKILO; break;
|
||||
case 6: this->value_[REGSCALE] |= FLAGSCALEMEGA; break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
uint16_t pair2 = static_cast<uint16_t>(this->raw_[2]) | (static_cast<uint16_t>(this->raw_[3]) << 8);
|
||||
if (pair2 & (1 << 0)) this->value_[REGMODE] |= FLAGMODEHOLD;
|
||||
if (pair2 & (1 << 1)) this->value_[REGMODE] |= FLAGMODEREL;
|
||||
if (pair2 & (1 << 2)) this->value_[REGMODE] |= FLAGMODEAUTO;
|
||||
this->low_battery = (pair2 & (1 << 3)) != 0;
|
||||
if (pair2 & (1 << 4)) this->value_[REGMINMAX] |= FLAGMIN;
|
||||
if (pair2 & (1 << 5)) this->value_[REGMINMAX] |= FLAGMAX;
|
||||
|
||||
uint16_t pair3 = static_cast<uint16_t>(this->raw_[4]) | (static_cast<uint16_t>(this->raw_[5]) << 8);
|
||||
if (decimal < 7) {
|
||||
uint16_t digits = pair3;
|
||||
if (pair3 < 0x7FFF) {
|
||||
this->value_[REGPLUSMINUS] = FLAGPLUS;
|
||||
} else {
|
||||
this->value_[REGPLUSMINUS] = FLAGMINUS;
|
||||
digits = pair3 & 0x7FFF;
|
||||
}
|
||||
this->value_[REGDIG1] = '0' + ((digits / 1000) % 10);
|
||||
this->value_[REGDIG2] = '0' + ((digits / 100) % 10);
|
||||
this->value_[REGDIG3] = '0' + ((digits / 10) % 10);
|
||||
this->value_[REGDIG4] = '0' + (digits % 10);
|
||||
} else {
|
||||
memcpy(this->value_, OVERLOAD_FRAME, sizeof(OVERLOAD_FRAME));
|
||||
}
|
||||
}
|
||||
|
||||
char digit_char_(uint8_t reg) const {
|
||||
uint8_t c = this->value_[reg];
|
||||
return (c >= '0' && c <= '9') ? static_cast<char>(c) : ' ';
|
||||
}
|
||||
|
||||
void format_metric_(char *buffer, size_t size, float value, const char *unit, uint8_t decimals) const {
|
||||
if (!std::isfinite(value)) {
|
||||
snprintf(buffer, size, "-- %s", unit);
|
||||
return;
|
||||
}
|
||||
char format[12];
|
||||
snprintf(format, sizeof(format), "%%.%uf %%s", decimals);
|
||||
snprintf(buffer, size, format, value, unit);
|
||||
}
|
||||
|
||||
void draw_metric_card_(Display &it, esphome::display::BaseFont *label_font, esphome::display::BaseFont *value_font,
|
||||
int x, int y, int w, int h, const char *title, const char *value,
|
||||
Color accent, Color fill, Color border, Color value_color) {
|
||||
const Color bg(0, 0, 0);
|
||||
const Color inactive(90, 100, 110);
|
||||
it.filled_rectangle(x, y, w, h, fill);
|
||||
it.filled_rectangle(x, y, w, 2, accent);
|
||||
it.filled_rectangle(x, y + h - 1, w, 1, border);
|
||||
it.filled_rectangle(x, y, 1, h, border);
|
||||
it.filled_rectangle(x + w - 1, y, 1, h, border);
|
||||
it.print(x + 10, y + 9, label_font, inactive, esphome::display::TextAlign::TOP_LEFT, title);
|
||||
it.print(x + w / 2, y + 43, value_font, value_color, esphome::display::TextAlign::CENTER, value);
|
||||
it.filled_rectangle(x + 10, y + h - 11, w - 20, 3, bg);
|
||||
it.filled_rectangle(x + 10, y + h - 11, w - 20, 1, accent);
|
||||
}
|
||||
|
||||
void label_(Display &it, esphome::display::BaseFont *font, int x, int y, const char *text, Color color) {
|
||||
it.print(x, y, font, color, esphome::display::TextAlign::TOP_LEFT, text);
|
||||
}
|
||||
|
||||
void draw_digits_(Display &it, const char *text, bool negative, Color color) {
|
||||
if (negative) this->draw_segment_(it, 8, 83, 26, 9, true, color);
|
||||
constexpr int digit_x = 40;
|
||||
constexpr int digit_y = 35;
|
||||
constexpr int digit_w = 50;
|
||||
constexpr int digit_h = 106;
|
||||
constexpr int digit_distance = 64;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
this->draw_seven_segment_(it, digit_x + i * digit_distance, digit_y, digit_w, digit_h, text[i], color);
|
||||
}
|
||||
}
|
||||
|
||||
void draw_decimal_points_(Display &it, Color color) {
|
||||
uint8_t p = this->value_[REGPOINT];
|
||||
if ((p & FLAGPOINT1) == FLAGPOINT1) it.filled_rectangle(95, 135, 8, 10, color);
|
||||
if ((p & FLAGPOINT2) == FLAGPOINT2) it.filled_rectangle(159, 135, 8, 10, color);
|
||||
if ((p & FLAGPOINT3) == FLAGPOINT3) it.filled_rectangle(223, 135, 8, 10, color);
|
||||
}
|
||||
|
||||
void draw_segment_(Display &it, int x, int y, int w, int h, bool horizontal, Color color) {
|
||||
if (horizontal) {
|
||||
int cap = h / 2;
|
||||
it.filled_rectangle(x + cap, y, w - 2 * cap, h +1, color);
|
||||
it.filled_triangle(x, y + cap, x + cap, y, x + cap, y + h, color);
|
||||
it.filled_triangle(x + w, y + cap, x + w - cap, y, x + w - cap, y + h, color);
|
||||
} else {
|
||||
int cap = w / 2;
|
||||
it.filled_rectangle(x, y + cap, w +1, h - 2 * cap, color);
|
||||
it.filled_triangle(x + cap, y, x, y + cap, x + w, y + cap, color);
|
||||
it.filled_triangle(x + cap, y + h, x, y + h - cap, x + w, y + h - cap, color);
|
||||
}
|
||||
}
|
||||
|
||||
void draw_icon_(Display &it, int x, int y, int w, int h, const uint8_t *data, Color color) {
|
||||
for (int row = 0; row < h; row++) {
|
||||
for (int col = 0; col < w; col++) {
|
||||
uint8_t byte = data[row * ((w + 7) / 8) + (col / 8)];
|
||||
if ((byte & (0x80 >> (col % 8))) != 0) {
|
||||
it.draw_pixel_at(x + col, y + row, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void draw_seven_segment_(Display &it, int x, int y, int w, int h, char ch, Color color) {
|
||||
bool a=false,b=false,c=false,d=false,e=false,f=false,g=false;
|
||||
switch (ch) {
|
||||
case '0': a=b=c=d=e=f=true; break;
|
||||
case '1': b=c=true; break;
|
||||
case '2': a=b=d=e=g=true; break;
|
||||
case '3': a=b=c=d=g=true; break;
|
||||
case '4': b=c=f=g=true; break;
|
||||
case '5': a=c=d=f=g=true; break;
|
||||
case '6': a=c=d=e=f=g=true; break;
|
||||
case '7': a=b=c=true; break;
|
||||
case '8': a=b=c=d=e=f=g=true; break;
|
||||
case '9': a=b=c=d=f=g=true; break;
|
||||
case 'O': a=b=c=d=e=f=true; break;
|
||||
case 'L': d=e=f=true; break;
|
||||
case '-': g=true; break;
|
||||
default: break;
|
||||
}
|
||||
int t = 10;
|
||||
int half = h / 2;
|
||||
int gap = 1;
|
||||
|
||||
int top_y = y;
|
||||
int mid_y = y + half - t / 2;
|
||||
int bot_y = y + h - t;
|
||||
|
||||
int upper_v_y = top_y + t + gap;
|
||||
int upper_v_h = mid_y - gap - upper_v_y;
|
||||
int lower_v_y = mid_y + t + gap;
|
||||
int lower_v_h = bot_y - gap - lower_v_y;
|
||||
|
||||
if (a) this->draw_segment_(it, x + t / 2, top_y, w - t, t, true, color);
|
||||
if (b && upper_v_h > 0) this->draw_segment_(it, x + w - t, upper_v_y, t, upper_v_h, false, color);
|
||||
if (c && lower_v_h > 0) this->draw_segment_(it, x + w - t, lower_v_y, t, lower_v_h, false, color);
|
||||
if (d) this->draw_segment_(it, x + t / 2, bot_y, w - t, t, true, color);
|
||||
if (e && lower_v_h > 0) this->draw_segment_(it, x, lower_v_y, t, lower_v_h, false, color);
|
||||
if (f && upper_v_h > 0) this->draw_segment_(it, x, upper_v_y, t, upper_v_h, false, color);
|
||||
if (g) this->draw_segment_(it, x + t / 2, mid_y, w - t, t, true, color);
|
||||
}
|
||||
|
||||
void draw_bargraph_(Display &it, uint16_t digits, bool active) {
|
||||
const Color fg(255, 255, 255);
|
||||
const Color inactive(80, 80, 80);
|
||||
uint16_t mapped = active ? static_cast<uint16_t>(digits * 240 / 6000) : 0;
|
||||
if (mapped > 240) mapped = 240;
|
||||
for (uint16_t i = 0; i <= 240; i += 4) {
|
||||
Color col = (active && i <= mapped) ? fg : inactive;
|
||||
int h = (i % 40 == 0) ? 20 : ((i % 20 == 0) ? 15 : 10);
|
||||
it.vertical_line(40 + i, 185 - h, h, col);
|
||||
}
|
||||
it.horizontal_line(35, 185, 250, inactive);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace owon_b35t
|
||||
|
||||
static owon_b35t::Meter owon_meter;
|
||||
@@ -0,0 +1,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});"
|
||||
@@ -52,12 +52,17 @@ 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
|
||||
@@ -115,6 +120,7 @@ light:
|
||||
|
||||
# Initialize UART for Vindriktning sensor
|
||||
uart:
|
||||
id: uartPM
|
||||
rx_pin: GPIO22
|
||||
baud_rate: 9600
|
||||
|
||||
@@ -210,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:
|
||||
|
||||
@@ -26,12 +26,15 @@ ota:
|
||||
id: ota_esphome
|
||||
password: !secret ota
|
||||
|
||||
network:
|
||||
enable_ipv6: true
|
||||
|
||||
wifi:
|
||||
on_connect:
|
||||
- delay: 5s # Gives time for improv results to be transmitted
|
||||
ssid: Voltage-legacy
|
||||
password: !secret voltage_legacy_psk
|
||||
use_address: ${name}.home
|
||||
#use_address: ${name}.home
|
||||
power_save_mode: high
|
||||
fast_connect: on
|
||||
min_auth_mode: WPA2
|
||||
|
||||
@@ -50,10 +50,13 @@ esphome:
|
||||
esp8266:
|
||||
board: esp12e
|
||||
|
||||
network:
|
||||
enable_ipv6: true
|
||||
|
||||
wifi:
|
||||
ssid: "Voltage-legacy"
|
||||
password: !secret voltage_legacy_psk
|
||||
use_address: riden-labornetzteil-18a.home
|
||||
#use_address: riden-labornetzteil-18a.home
|
||||
power_save_mode: high
|
||||
fast_connect: on
|
||||
min_auth_mode: WPA2
|
||||
|
||||
@@ -50,10 +50,13 @@ esphome:
|
||||
esp8266:
|
||||
board: esp12e
|
||||
|
||||
network:
|
||||
enable_ipv6: true
|
||||
|
||||
wifi:
|
||||
ssid: "Voltage-legacy"
|
||||
password: !secret voltage_legacy_psk
|
||||
use_address: riden-labornetzteil-6a.home
|
||||
#use_address: riden-labornetzteil-6a.home
|
||||
power_save_mode: high
|
||||
fast_connect: on
|
||||
min_auth_mode: WPA2
|
||||
|
||||
@@ -14,12 +14,15 @@ 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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+31
-2
@@ -69,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:
|
||||
@@ -138,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
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user