Compare commits
33 Commits
ff59c494a7
...
main
Author | SHA1 | Date | |
---|---|---|---|
a947806618 | |||
439ead1047 | |||
20c86831f1 | |||
61b018270b | |||
911477d381 | |||
1ee475ee4a | |||
cae28341fe | |||
17cfa429ea | |||
791d7d504f | |||
631cdae751 | |||
06321730e0 | |||
5ac8ceacfe | |||
4981cbfa2e | |||
e5ddf02c34 | |||
ebf11021e2 | |||
72ce055810 | |||
8461f43bca | |||
ca599eab7a | |||
fef90d5a78 | |||
1f151a804e | |||
31da165db1 | |||
dd390001ee | |||
7993a9174a | |||
fe40743fd1 | |||
01bad587b8 | |||
4888393a12 | |||
05f8cc1af8 | |||
5c98baa24b | |||
490b00301b | |||
c3e813551b | |||
474805328b | |||
fd18934e97 | |||
5e1d197e7a |
@@ -1 +1 @@
|
|||||||
2025.1.0
|
2025.9.0
|
511
automations.yaml
@@ -38,7 +38,7 @@
|
|||||||
input:
|
input:
|
||||||
person_entity: person.marcus_scholz
|
person_entity: person.marcus_scholz
|
||||||
zone_entity: zone.home
|
zone_entity: zone.home
|
||||||
notify_device: 6adad2de67b26c864cfcb1a91bd12e48
|
notify_device: 773450cae5e93524731940ad081846d9
|
||||||
- id: '1623526683767'
|
- id: '1623526683767'
|
||||||
alias: Licht bei Sonnenaufgang ausschalten
|
alias: Licht bei Sonnenaufgang ausschalten
|
||||||
description: ''
|
description: ''
|
||||||
@@ -69,9 +69,11 @@
|
|||||||
alias: Gute Nacht!
|
alias: Gute Nacht!
|
||||||
description: Schalte alles (außer Schlafzimmer) aus, sobald das Schlaftracking startet.
|
description: Schalte alles (außer Schlafzimmer) aus, sobald das Schlaftracking startet.
|
||||||
triggers:
|
triggers:
|
||||||
- entity_id: input_text.sleep_as_android
|
- trigger: state
|
||||||
to: sleep_tracking_started
|
entity_id:
|
||||||
trigger: state
|
- event.sleep_as_android_schlaf_tracking
|
||||||
|
attribute: event_type
|
||||||
|
to: started
|
||||||
conditions: []
|
conditions: []
|
||||||
actions:
|
actions:
|
||||||
- target:
|
- target:
|
||||||
@@ -89,23 +91,63 @@
|
|||||||
- data:
|
- data:
|
||||||
message: Gute Nacht!
|
message: Gute Nacht!
|
||||||
action: notify.mobile_app_le2123
|
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.
|
||||||
|
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.m5stack_atom_echo
|
||||||
- target:
|
- target:
|
||||||
entity_id: media_player.raspiaudio_muse_luxe
|
entity_id: media_player.raspiaudio_muse_luxe
|
||||||
data:
|
data:
|
||||||
media_content_id: media-source://tts/tts.piper?message=Gute+Nacht%2C+schlaf+gut.
|
media:
|
||||||
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.
|
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
|
action: media_player.play_media
|
||||||
|
enabled: true
|
||||||
- target:
|
- target:
|
||||||
area_id:
|
area_id:
|
||||||
- wohnzimmer
|
- wohnzimmer
|
||||||
@@ -119,32 +161,50 @@
|
|||||||
- 1f3c4b5de4aea99bac83688ceb22293b
|
- 1f3c4b5de4aea99bac83688ceb22293b
|
||||||
- 48dafb7f4a8ed6ccbb046758cb660c23
|
- 48dafb7f4a8ed6ccbb046758cb660c23
|
||||||
- c4ead7f6227e2ee4c43c4b0df829cd84
|
- c4ead7f6227e2ee4c43c4b0df829cd84
|
||||||
|
enabled: false
|
||||||
mode: single
|
mode: single
|
||||||
- id: '1623868115464'
|
- id: '1623868115464'
|
||||||
alias: 420!
|
alias: 420!
|
||||||
description: ''
|
description: ''
|
||||||
trigger:
|
triggers:
|
||||||
- platform: time
|
- at: '16:20'
|
||||||
at: '16:20'
|
trigger: time
|
||||||
condition: []
|
conditions: []
|
||||||
action:
|
actions:
|
||||||
- service: notify.mobile_app_le2123
|
- action: notify.mobile_app_apollo
|
||||||
data:
|
data:
|
||||||
|
message: Lodere es, Lustknabe.
|
||||||
|
title: 420!
|
||||||
|
- data:
|
||||||
title: 420!
|
title: 420!
|
||||||
message: Lodere es, Lustknabe.
|
message: Lodere es, Lustknabe.
|
||||||
- service: tts.speak
|
action: notify.mobile_app_le2123
|
||||||
data:
|
- data:
|
||||||
cache: true
|
cache: true
|
||||||
media_player_entity_id: media_player.raspiaudio_muse_luxe
|
media_player_entity_id: media_player.raspiaudio_muse_luxe
|
||||||
message: 4 20 lodere es, Lustknabe
|
message: 4 20 lodere es, Lustknabe
|
||||||
target:
|
target:
|
||||||
entity_id: tts.piper
|
entity_id: tts.piper
|
||||||
- service: mqtt.publish
|
action: tts.speak
|
||||||
data:
|
- data:
|
||||||
|
cache: true
|
||||||
|
media_player_entity_id: media_player.home_assistant_voice_09c0e7_media_player
|
||||||
|
message: 4 20 lodere es, Lustknabe
|
||||||
|
target:
|
||||||
|
entity_id: tts.piper
|
||||||
|
action: tts.speak
|
||||||
|
- data:
|
||||||
qos: 0
|
qos: 0
|
||||||
retain: false
|
retain: false
|
||||||
topic: awtrix_b8658c/notify
|
topic: awtrix_kitchen/notify
|
||||||
payload: '{"text": "420, lodere es, Lustknabe!"}'
|
payload: '{"text": "420, lodere es, Lustknabe!"}'
|
||||||
|
action: mqtt.publish
|
||||||
|
- data:
|
||||||
|
qos: 0
|
||||||
|
retain: false
|
||||||
|
topic: awtrix_desk/notify
|
||||||
|
payload: '{"text": "420, lodere es, Lustknabe!"}'
|
||||||
|
action: mqtt.publish
|
||||||
mode: single
|
mode: single
|
||||||
- id: '1623911524804'
|
- id: '1623911524804'
|
||||||
alias: TV Anti-Reflexion (undo)
|
alias: TV Anti-Reflexion (undo)
|
||||||
@@ -171,70 +231,91 @@
|
|||||||
- id: '1623941937228'
|
- id: '1623941937228'
|
||||||
alias: Licht im Schlafzimmer zur Schlafenszeit einschalten
|
alias: Licht im Schlafzimmer zur Schlafenszeit einschalten
|
||||||
description: Bei Beginn der empfohlenen Schlafenszeit.
|
description: Bei Beginn der empfohlenen Schlafenszeit.
|
||||||
trigger:
|
triggers:
|
||||||
- platform: state
|
- trigger: state
|
||||||
entity_id: input_text.sleep_as_android
|
entity_id:
|
||||||
|
- event.sleep_as_android_benutzerbenachrichtigung
|
||||||
|
attribute: event_type
|
||||||
to: time_to_bed_alarm_alert
|
to: time_to_bed_alarm_alert
|
||||||
for: 00:05:00
|
conditions:
|
||||||
condition:
|
|
||||||
- condition: state
|
- condition: state
|
||||||
entity_id: person.marcus_scholz
|
entity_id: person.marcus_scholz
|
||||||
state: home
|
state: home
|
||||||
action:
|
actions:
|
||||||
- service: light.turn_on
|
- target:
|
||||||
target:
|
|
||||||
device_id: 68868390eda35e969ec60a13020f2407
|
device_id: 68868390eda35e969ec60a13020f2407
|
||||||
data: {}
|
data: {}
|
||||||
- service: tts.speak
|
action: light.turn_on
|
||||||
data:
|
- data:
|
||||||
cache: true
|
cache: true
|
||||||
media_player_entity_id: media_player.raspiaudio_muse_luxe
|
media_player_entity_id: media_player.raspiaudio_muse_luxe
|
||||||
message: Ab ins Bett, Schlafenszeit.
|
message: Ab ins Bett, Schlafenszeit.
|
||||||
target:
|
target:
|
||||||
entity_id: tts.piper
|
entity_id: tts.piper
|
||||||
|
action: tts.speak
|
||||||
|
enabled: true
|
||||||
|
- data:
|
||||||
|
cache: true
|
||||||
|
media_player_entity_id: media_player.home_assistant_voice_09c0e7_media_player_2
|
||||||
|
message: Ab ins Bett, Schlafenszeit.
|
||||||
|
target:
|
||||||
|
entity_id: tts.piper
|
||||||
|
action: tts.speak
|
||||||
|
enabled: true
|
||||||
|
- data:
|
||||||
|
cache: true
|
||||||
|
media_player_entity_id: media_player.m5stack_atom_echo
|
||||||
|
message: Ab ins Bett, Schlafenszeit.
|
||||||
|
target:
|
||||||
|
entity_id: tts.piper
|
||||||
|
action: tts.speak
|
||||||
|
enabled: true
|
||||||
mode: single
|
mode: single
|
||||||
- id: '1623954512941'
|
- id: '1623954512941'
|
||||||
alias: Licht im Schlafzimmer zum Aufwachen einschalten
|
alias: Licht im Schlafzimmer zum Aufwachen einschalten
|
||||||
description: Nach der Alarmquittierung
|
description: Nach der Alarmquittierung
|
||||||
trigger:
|
triggers:
|
||||||
- platform: state
|
- trigger: state
|
||||||
entity_id: input_text.sleep_as_android
|
entity_id:
|
||||||
to: alarm_alert_start
|
- event.sleep_as_android_schlaf_tracking
|
||||||
condition:
|
attribute: event_type
|
||||||
|
to: stopped
|
||||||
|
conditions:
|
||||||
- condition: state
|
- condition: state
|
||||||
entity_id: person.marcus_scholz
|
entity_id: person.marcus_scholz
|
||||||
state: home
|
state: home
|
||||||
action:
|
actions:
|
||||||
- delay:
|
- delay:
|
||||||
hours: 0
|
hours: 0
|
||||||
minutes: 0
|
minutes: 1
|
||||||
seconds: 30
|
seconds: 0
|
||||||
milliseconds: 0
|
milliseconds: 0
|
||||||
- service: light.turn_on
|
- target:
|
||||||
target:
|
|
||||||
device_id: 68868390eda35e969ec60a13020f2407
|
device_id: 68868390eda35e969ec60a13020f2407
|
||||||
data: {}
|
data: {}
|
||||||
|
action: light.turn_on
|
||||||
mode: single
|
mode: single
|
||||||
- id: '1624820688449'
|
- id: '1624820688449'
|
||||||
alias: 'Anruf: Beim Klingeln grün blinken'
|
alias: 'Anruf: Beim Klingeln grün blinken'
|
||||||
description: ''
|
description: ''
|
||||||
trigger:
|
triggers:
|
||||||
- platform: state
|
- entity_id: sensor.fritz_box_7490_call_monitor_dem_commander1024_seine_cloud
|
||||||
entity_id: sensor.fritz_box_7490_call_monitor_dem_commander1024_seine_cloud
|
|
||||||
to: ringing
|
to: ringing
|
||||||
- platform: state
|
trigger: state
|
||||||
entity_id: sensor.le2123_phone_state
|
- entity_id:
|
||||||
|
- sensor.le2123_phone_state_2
|
||||||
to: ringing
|
to: ringing
|
||||||
- platform: state
|
trigger: state
|
||||||
to: ringing
|
- to: ringing
|
||||||
entity_id: sensor.moto_g_100_phone_state
|
entity_id:
|
||||||
condition:
|
- sensor.xt2125_4_phone_state
|
||||||
|
trigger: state
|
||||||
|
conditions:
|
||||||
- condition: state
|
- condition: state
|
||||||
entity_id: person.marcus_scholz
|
entity_id: person.marcus_scholz
|
||||||
state: home
|
state: home
|
||||||
action:
|
actions:
|
||||||
- service: light.turn_on
|
- data:
|
||||||
data:
|
|
||||||
rgb_color:
|
rgb_color:
|
||||||
- 9
|
- 9
|
||||||
- 255
|
- 255
|
||||||
@@ -243,8 +324,8 @@
|
|||||||
entity_id:
|
entity_id:
|
||||||
- light.awtrix_desk_indicator_1
|
- light.awtrix_desk_indicator_1
|
||||||
- light.awtrix_kitchen_indicator_1
|
- light.awtrix_kitchen_indicator_1
|
||||||
- service: scene.create
|
action: light.turn_on
|
||||||
data:
|
- data:
|
||||||
scene_id: wled_last_state
|
scene_id: wled_last_state
|
||||||
snapshot_entities:
|
snapshot_entities:
|
||||||
- light.kuche
|
- light.kuche
|
||||||
@@ -253,51 +334,53 @@
|
|||||||
- select.kuche_color_palette
|
- select.kuche_color_palette
|
||||||
- select.wohnzimmer_vorne_color_palette
|
- select.wohnzimmer_vorne_color_palette
|
||||||
- select.wohnzimmer_hinten_color_palette
|
- select.wohnzimmer_hinten_color_palette
|
||||||
- service: scene.turn_on
|
action: scene.create
|
||||||
target:
|
- target:
|
||||||
entity_id: scene.grun_blinken
|
entity_id: scene.grun_blinken
|
||||||
data: {}
|
data: {}
|
||||||
|
action: scene.turn_on
|
||||||
- delay:
|
- delay:
|
||||||
hours: 0
|
hours: 0
|
||||||
minutes: 0
|
minutes: 0
|
||||||
seconds: 5
|
seconds: 5
|
||||||
milliseconds: 0
|
milliseconds: 0
|
||||||
- service: scene.turn_on
|
- target:
|
||||||
target:
|
|
||||||
entity_id: scene.wled_last_state
|
entity_id: scene.wled_last_state
|
||||||
data: {}
|
data: {}
|
||||||
|
action: scene.turn_on
|
||||||
- delay:
|
- delay:
|
||||||
hours: 0
|
hours: 0
|
||||||
minutes: 0
|
minutes: 0
|
||||||
seconds: 15
|
seconds: 15
|
||||||
milliseconds: 0
|
milliseconds: 0
|
||||||
- service: light.turn_off
|
- data: {}
|
||||||
data: {}
|
|
||||||
target:
|
target:
|
||||||
entity_id:
|
entity_id:
|
||||||
- light.awtrix_desk_indicator_1
|
- light.awtrix_desk_indicator_1
|
||||||
- light.awtrix_kitchen_indicator_1
|
- light.awtrix_kitchen_indicator_1
|
||||||
|
action: light.turn_off
|
||||||
mode: single
|
mode: single
|
||||||
- id: '1625481640348'
|
- id: '1625481640348'
|
||||||
alias: 'Anruf: Beim Telefonieren Musik pausieren'
|
alias: 'Anruf: Beim Telefonieren Musik pausieren'
|
||||||
description: ''
|
description: ''
|
||||||
trigger:
|
triggers:
|
||||||
- platform: state
|
- entity_id: sensor.fritz_box_7490_call_monitor_telefonbuch
|
||||||
entity_id: sensor.fritz_box_7490_call_monitor_telefonbuch
|
|
||||||
to: talking
|
to: talking
|
||||||
- platform: state
|
trigger: state
|
||||||
entity_id: sensor.le2123_phone_state
|
- entity_id:
|
||||||
|
- sensor.le2123_phone_state_2
|
||||||
to: talking
|
to: talking
|
||||||
- platform: state
|
trigger: state
|
||||||
entity_id: sensor.moto_g_100_phone_state
|
- entity_id:
|
||||||
|
- sensor.xt2125_4_phone_state
|
||||||
to: talking
|
to: talking
|
||||||
condition:
|
trigger: state
|
||||||
|
conditions:
|
||||||
- condition: state
|
- condition: state
|
||||||
entity_id: person.marcus_scholz
|
entity_id: person.marcus_scholz
|
||||||
state: home
|
state: home
|
||||||
action:
|
actions:
|
||||||
- service: scene.create
|
- data:
|
||||||
data:
|
|
||||||
scene_id: media_last_state
|
scene_id: media_last_state
|
||||||
snapshot_entities:
|
snapshot_entities:
|
||||||
- media_player.wohnzimmer_main
|
- media_player.wohnzimmer_main
|
||||||
@@ -305,24 +388,27 @@
|
|||||||
- media_player.ccze
|
- media_player.ccze
|
||||||
- media_player.spotify_marcus_scholz
|
- media_player.spotify_marcus_scholz
|
||||||
- media_player.xboxonex
|
- media_player.xboxonex
|
||||||
- service: media_player.media_pause
|
action: scene.create
|
||||||
target:
|
- target:
|
||||||
area_id:
|
area_id:
|
||||||
- schlafzimmer
|
- schlafzimmer
|
||||||
- wohnzimmer
|
- wohnzimmer
|
||||||
|
action: media_player.media_pause
|
||||||
|
data: {}
|
||||||
- wait_for_trigger:
|
- wait_for_trigger:
|
||||||
- platform: state
|
- entity_id: sensor.j9110_phone_state
|
||||||
entity_id: sensor.j9110_phone_state
|
|
||||||
to: idle
|
to: idle
|
||||||
from: talking
|
from: talking
|
||||||
- platform: state
|
trigger: state
|
||||||
entity_id: sensor.fritz_box_7490_call_monitor_telefonbuch
|
- entity_id: sensor.fritz_box_7490_call_monitor_telefonbuch
|
||||||
to: idle
|
to: idle
|
||||||
from: talking
|
from: talking
|
||||||
- service: scene.turn_on
|
trigger: state
|
||||||
target:
|
- target:
|
||||||
entity_id:
|
entity_id:
|
||||||
- scene.media_last_state
|
- scene.media_last_state
|
||||||
|
action: scene.turn_on
|
||||||
|
data: {}
|
||||||
mode: single
|
mode: single
|
||||||
- id: '1628972104416'
|
- id: '1628972104416'
|
||||||
alias: Raumduft nach einer Stunde wieder ausschalten
|
alias: Raumduft nach einer Stunde wieder ausschalten
|
||||||
@@ -348,25 +434,27 @@
|
|||||||
- id: '1628972885682'
|
- id: '1628972885682'
|
||||||
alias: Raumduft zum Aufstehen einschalten
|
alias: Raumduft zum Aufstehen einschalten
|
||||||
description: ''
|
description: ''
|
||||||
trigger:
|
triggers:
|
||||||
- platform: state
|
- trigger: state
|
||||||
entity_id: input_text.sleep_as_android
|
entity_id:
|
||||||
to: sleep_tracking_stopped
|
- event.sleep_as_android_schlaf_tracking
|
||||||
condition:
|
attribute: event_type
|
||||||
|
to: stopped
|
||||||
|
conditions:
|
||||||
- condition: state
|
- condition: state
|
||||||
entity_id: person.marcus_scholz
|
entity_id: person.marcus_scholz
|
||||||
state: home
|
state: home
|
||||||
action:
|
actions:
|
||||||
- type: turn_on
|
- type: turn_on
|
||||||
device_id: 5a08ac4c3b3893b540a9934fa92dccfa
|
device_id: 5a08ac4c3b3893b540a9934fa92dccfa
|
||||||
entity_id: switch.flur_raumduft
|
entity_id: switch.flur_raumduft
|
||||||
domain: switch
|
domain: switch
|
||||||
- service: light.turn_on
|
- data: {}
|
||||||
data: {}
|
|
||||||
target:
|
target:
|
||||||
entity_id:
|
entity_id:
|
||||||
- light.awtrix_desk_matrix
|
- light.awtrix_528bd4_matrix
|
||||||
- light.awtrix_kitchen_matrix
|
- light.awtrix_b8658c_matrix
|
||||||
|
action: light.turn_on
|
||||||
mode: single
|
mode: single
|
||||||
- id: '1630914505161'
|
- id: '1630914505161'
|
||||||
alias: Beim Verlassen der Wohnung alles abschalten
|
alias: Beim Verlassen der Wohnung alles abschalten
|
||||||
@@ -455,26 +543,23 @@
|
|||||||
- id: '1675778284738'
|
- id: '1675778284738'
|
||||||
alias: Stündliche Zeitansage
|
alias: Stündliche Zeitansage
|
||||||
description: ''
|
description: ''
|
||||||
trigger:
|
triggers:
|
||||||
- platform: time_pattern
|
- minutes: '0'
|
||||||
minutes: '0'
|
|
||||||
seconds: '0'
|
seconds: '0'
|
||||||
hours: '*'
|
hours: '*'
|
||||||
condition:
|
trigger: time_pattern
|
||||||
|
conditions:
|
||||||
- condition: state
|
- condition: state
|
||||||
entity_id: person.marcus_scholz
|
entity_id: person.marcus_scholz
|
||||||
state: home
|
state: home
|
||||||
- condition: or
|
- condition: or
|
||||||
conditions:
|
conditions:
|
||||||
- condition: state
|
- condition: state
|
||||||
entity_id: input_text.sleep_as_android
|
entity_id: event.sleep_as_android_schlaf_tracking
|
||||||
state: sleep_tracking_stopped
|
attribute: event_type
|
||||||
- condition: state
|
state: stopped
|
||||||
entity_id: input_text.sleep_as_android
|
actions:
|
||||||
state: alarm_alert_dismiss
|
- data:
|
||||||
action:
|
|
||||||
- service: tts.speak
|
|
||||||
data:
|
|
||||||
cache: true
|
cache: true
|
||||||
media_player_entity_id: media_player.raspiaudio_muse_luxe
|
media_player_entity_id: media_player.raspiaudio_muse_luxe
|
||||||
message: '{{message}}
|
message: '{{message}}
|
||||||
@@ -483,14 +568,35 @@
|
|||||||
target:
|
target:
|
||||||
entity_id: tts.piper
|
entity_id: tts.piper
|
||||||
enabled: true
|
enabled: true
|
||||||
- service: mqtt.publish
|
action: tts.speak
|
||||||
data:
|
- data:
|
||||||
|
cache: true
|
||||||
|
media_player_entity_id: media_player.home_assistant_voice_09c0e7_media_player
|
||||||
|
message: '{{message}}
|
||||||
|
|
||||||
|
'
|
||||||
|
target:
|
||||||
|
entity_id: tts.piper
|
||||||
|
enabled: true
|
||||||
|
action: tts.speak
|
||||||
|
- data:
|
||||||
|
cache: true
|
||||||
|
media_player_entity_id: media_player.m5stack_atom_echo
|
||||||
|
message: '{{message}}
|
||||||
|
|
||||||
|
'
|
||||||
|
target:
|
||||||
|
entity_id: tts.piper
|
||||||
|
enabled: true
|
||||||
|
action: tts.speak
|
||||||
|
- data:
|
||||||
topic: awtrix_desk/notify
|
topic: awtrix_desk/notify
|
||||||
payload: '{"text": "{{message}}", "icon": "clockcolor", "duration": 16}'
|
payload: '{"text": "{{message}}", "icon": "clockcolor", "duration": 16}'
|
||||||
- service: mqtt.publish
|
action: mqtt.publish
|
||||||
data:
|
- data:
|
||||||
topic: awtrix_kitchen/notify
|
topic: awtrix_kitchen/notify
|
||||||
payload: '{"text": "{{message}}", "icon": "clockcolor", "duration": 16}'
|
payload: '{"text": "{{message}}", "icon": "clockcolor", "duration": 16}'
|
||||||
|
action: mqtt.publish
|
||||||
variables:
|
variables:
|
||||||
message: '{% set t = now().hour %} {% set m = ''Morgen'' if t < 12 else ''Nachmittag''
|
message: '{% set t = now().hour %} {% set m = ''Morgen'' if t < 12 else ''Nachmittag''
|
||||||
if t < 18 else ''Abend'' %} Guten {{m}} Marcus. Draußen sind es {{states(''sensor.aussentemperatur'')}}
|
if t < 18 else ''Abend'' %} Guten {{m}} Marcus. Draußen sind es {{states(''sensor.aussentemperatur'')}}
|
||||||
@@ -514,16 +620,16 @@
|
|||||||
alias: Fenster schließen, wenn es warm wird
|
alias: Fenster schließen, wenn es warm wird
|
||||||
description: Tagsüber im Sommer, wenn die Außentemperatur sich der Innentemperatur
|
description: Tagsüber im Sommer, wenn die Außentemperatur sich der Innentemperatur
|
||||||
annähert.
|
annähert.
|
||||||
trigger:
|
triggers:
|
||||||
- platform: numeric_state
|
- entity_id:
|
||||||
entity_id:
|
|
||||||
- sensor.aussentemperatur
|
- sensor.aussentemperatur
|
||||||
above: sensor.wohnungstemperatur
|
above: sensor.wohnungstemperatur
|
||||||
for:
|
for:
|
||||||
hours: 0
|
hours: 0
|
||||||
minutes: 5
|
minutes: 5
|
||||||
seconds: 0
|
seconds: 0
|
||||||
condition:
|
trigger: numeric_state
|
||||||
|
conditions:
|
||||||
- condition: state
|
- condition: state
|
||||||
entity_id: person.marcus_scholz
|
entity_id: person.marcus_scholz
|
||||||
state: home
|
state: home
|
||||||
@@ -533,16 +639,15 @@
|
|||||||
- condition: sun
|
- condition: sun
|
||||||
after: sunrise
|
after: sunrise
|
||||||
before: sunset
|
before: sunset
|
||||||
action:
|
actions:
|
||||||
- service: notify.mobile_app_le2123
|
- data:
|
||||||
data:
|
|
||||||
title: Schlaues lüften
|
title: Schlaues lüften
|
||||||
message: '{{message}}
|
message: '{{message}}
|
||||||
|
|
||||||
'
|
'
|
||||||
enabled: true
|
enabled: true
|
||||||
- service: tts.speak
|
action: notify.mobile_app_le2123
|
||||||
data:
|
- data:
|
||||||
cache: true
|
cache: true
|
||||||
media_player_entity_id: media_player.raspiaudio_muse_luxe
|
media_player_entity_id: media_player.raspiaudio_muse_luxe
|
||||||
message: '{{message}}
|
message: '{{message}}
|
||||||
@@ -550,6 +655,7 @@
|
|||||||
'
|
'
|
||||||
target:
|
target:
|
||||||
entity_id: tts.piper
|
entity_id: tts.piper
|
||||||
|
action: tts.speak
|
||||||
variables:
|
variables:
|
||||||
message: 'Die Außentemperatur ist mit {{states(''sensor.aussentemperatur'')}}
|
message: 'Die Außentemperatur ist mit {{states(''sensor.aussentemperatur'')}}
|
||||||
° Celsius {{((states(''sensor.aussentemperatur'')|float)-(states(''sensor.wohnungstemperatur'')|float))
|
° Celsius {{((states(''sensor.aussentemperatur'')|float)-(states(''sensor.wohnungstemperatur'')|float))
|
||||||
@@ -561,40 +667,37 @@
|
|||||||
- id: '1686327239749'
|
- id: '1686327239749'
|
||||||
alias: Fenster öffnen wenn es kühler wird
|
alias: Fenster öffnen wenn es kühler wird
|
||||||
description: Im Sommer, wenn die Außentemperatur sich der Innentemperatur annähert.
|
description: Im Sommer, wenn die Außentemperatur sich der Innentemperatur annähert.
|
||||||
trigger:
|
triggers:
|
||||||
- platform: numeric_state
|
- entity_id:
|
||||||
entity_id:
|
|
||||||
- sensor.aussentemperatur
|
- sensor.aussentemperatur
|
||||||
for:
|
for:
|
||||||
hours: 0
|
hours: 0
|
||||||
minutes: 5
|
minutes: 5
|
||||||
seconds: 0
|
seconds: 0
|
||||||
below: sensor.wohnungstemperatur
|
below: sensor.wohnungstemperatur
|
||||||
condition:
|
trigger: numeric_state
|
||||||
|
conditions:
|
||||||
- condition: state
|
- condition: state
|
||||||
entity_id: person.marcus_scholz
|
entity_id: person.marcus_scholz
|
||||||
state: home
|
state: home
|
||||||
- condition: or
|
- condition: or
|
||||||
conditions:
|
conditions:
|
||||||
- condition: state
|
- condition: state
|
||||||
entity_id: input_text.sleep_as_android
|
entity_id: event.sleep_as_android_schlaf_tracking
|
||||||
state: sleep_tracking_stopped
|
state: sleep_tracking_stopped
|
||||||
- condition: state
|
attribute: event_type
|
||||||
entity_id: input_text.sleep_as_android
|
|
||||||
state: alarm_alert_dismiss
|
|
||||||
- condition: state
|
- condition: state
|
||||||
entity_id: sensor.season
|
entity_id: sensor.season
|
||||||
state: summer
|
state: summer
|
||||||
action:
|
actions:
|
||||||
- service: notify.mobile_app_le2123
|
- data:
|
||||||
data:
|
|
||||||
title: Schlaues lüften
|
title: Schlaues lüften
|
||||||
message: '{{message}}
|
message: '{{message}}
|
||||||
|
|
||||||
'
|
'
|
||||||
enabled: true
|
enabled: true
|
||||||
- service: tts.speak
|
action: notify.mobile_app_le2123
|
||||||
data:
|
- data:
|
||||||
cache: true
|
cache: true
|
||||||
media_player_entity_id: media_player.raspiaudio_muse_luxe
|
media_player_entity_id: media_player.raspiaudio_muse_luxe
|
||||||
message: '{{message}}
|
message: '{{message}}
|
||||||
@@ -602,6 +705,7 @@
|
|||||||
'
|
'
|
||||||
target:
|
target:
|
||||||
entity_id: tts.piper
|
entity_id: tts.piper
|
||||||
|
action: tts.speak
|
||||||
mode: single
|
mode: single
|
||||||
variables:
|
variables:
|
||||||
message: 'Die Außentemperatur ist mit {{states(''sensor.aussentemperatur'')}}
|
message: 'Die Außentemperatur ist mit {{states(''sensor.aussentemperatur'')}}
|
||||||
@@ -669,14 +773,13 @@
|
|||||||
- id: '1698954553138'
|
- id: '1698954553138'
|
||||||
alias: 'Awtrix: Jahresfortschirtt'
|
alias: 'Awtrix: Jahresfortschirtt'
|
||||||
description: Jahresfortschritt in %
|
description: Jahresfortschritt in %
|
||||||
trigger:
|
triggers:
|
||||||
- platform: time_pattern
|
- hours: '*'
|
||||||
hours: '*'
|
|
||||||
minutes: 0
|
minutes: 0
|
||||||
condition: []
|
trigger: time_pattern
|
||||||
action:
|
conditions: []
|
||||||
- service: mqtt.publish
|
actions:
|
||||||
data:
|
- data:
|
||||||
payload: '{# Get current timestamp #} {%- set now = now() -%} {# Start of current
|
payload: '{# Get current timestamp #} {%- set now = now() -%} {# Start of current
|
||||||
year #} {%- set startOfYear = now.replace(year=now.year, month=1, day=1, hour=0,
|
year #} {%- set startOfYear = now.replace(year=now.year, month=1, day=1, hour=0,
|
||||||
minute=0, second=0, microsecond=0) -%} {# Determine end of current year #}
|
minute=0, second=0, microsecond=0) -%} {# Determine end of current year #}
|
||||||
@@ -686,9 +789,24 @@
|
|||||||
= as_timestamp(now) - as_timestamp(startOfYear) -%} {%- set progress = ( current
|
= as_timestamp(now) - as_timestamp(startOfYear) -%} {%- set progress = ( current
|
||||||
/ total * 100 ) | round(0, "floor", 0) -%} {# Output #} { "text": "{{ progress
|
/ total * 100 ) | round(0, "floor", 0) -%} {# Output #} { "text": "{{ progress
|
||||||
}} %", "icon": "y2023"}'
|
}} %", "icon": "y2023"}'
|
||||||
topic: awtrix_b8658c/custom/yearprogress
|
topic: awtrix_desk/custom/yearprogress
|
||||||
qos: 0
|
qos: 0
|
||||||
retain: false
|
retain: false
|
||||||
|
action: mqtt.publish
|
||||||
|
- data:
|
||||||
|
payload: '{# Get current timestamp #} {%- set now = now() -%} {# Start of current
|
||||||
|
year #} {%- set startOfYear = now.replace(year=now.year, month=1, day=1, hour=0,
|
||||||
|
minute=0, second=0, microsecond=0) -%} {# Determine end of current year #}
|
||||||
|
{%- set endOfYear = startOfYear.replace(month=12, day=31, hour=23, minute=59,
|
||||||
|
second=59, microsecond=999999) -%} {# Calculate progress #} {%- set total
|
||||||
|
= as_timestamp(endOfYear) - as_timestamp(startOfYear) -%} {%- set current
|
||||||
|
= as_timestamp(now) - as_timestamp(startOfYear) -%} {%- set progress = ( current
|
||||||
|
/ total * 100 ) | round(0, "floor", 0) -%} {# Output #} { "text": "{{ progress
|
||||||
|
}} %", "icon": "y2023"}'
|
||||||
|
topic: awtrix_kitchen/custom/yearprogress
|
||||||
|
qos: 0
|
||||||
|
retain: false
|
||||||
|
action: mqtt.publish
|
||||||
mode: single
|
mode: single
|
||||||
- id: '1699955800413'
|
- id: '1699955800413'
|
||||||
alias: 'Awtrix: Laufender Spotify Song'
|
alias: 'Awtrix: Laufender Spotify Song'
|
||||||
@@ -728,19 +846,19 @@
|
|||||||
- id: '1699969052661'
|
- id: '1699969052661'
|
||||||
alias: 'Awtrix: Matrix einschalten'
|
alias: 'Awtrix: Matrix einschalten'
|
||||||
description: ''
|
description: ''
|
||||||
trigger:
|
triggers:
|
||||||
- platform: state
|
- entity_id: person.marcus_scholz
|
||||||
entity_id: person.marcus_scholz
|
|
||||||
to: home
|
to: home
|
||||||
from: not_home
|
from: not_home
|
||||||
condition: []
|
trigger: state
|
||||||
action:
|
conditions: []
|
||||||
- service: light.turn_on
|
actions:
|
||||||
data: {}
|
- data: {}
|
||||||
target:
|
target:
|
||||||
entity_id:
|
entity_id:
|
||||||
- light.awtrix_desk_matrix
|
- light.awtrix_b8658c_matrix
|
||||||
- light.awtrix_kitchen_matrix
|
- light.awtrix_528bd4_matrix
|
||||||
|
action: light.turn_on
|
||||||
mode: single
|
mode: single
|
||||||
- id: '1700482951854'
|
- id: '1700482951854'
|
||||||
alias: Licht bei Sonnenuntergang einschalten (XMas)
|
alias: Licht bei Sonnenuntergang einschalten (XMas)
|
||||||
@@ -807,44 +925,42 @@
|
|||||||
controller_device: f3b032ad1f3ccc658a7d4588cc0e5c0c
|
controller_device: f3b032ad1f3ccc658a7d4588cc0e5c0c
|
||||||
helper_last_controller_event: input_text.styrbar_wohnzimmer_moodlight_letztes_event
|
helper_last_controller_event: input_text.styrbar_wohnzimmer_moodlight_letztes_event
|
||||||
action_button_up_short:
|
action_button_up_short:
|
||||||
- service: light.turn_on
|
- data: {}
|
||||||
data: {}
|
|
||||||
target:
|
target:
|
||||||
device_id:
|
device_id:
|
||||||
- 4edd9b9df7d1f6f2fe7dcc2e5c0eb968
|
- 4edd9b9df7d1f6f2fe7dcc2e5c0eb968
|
||||||
- 6dcbd87b459412144bddc42af3ae8b83
|
- 6dcbd87b459412144bddc42af3ae8b83
|
||||||
|
action: light.turn_on
|
||||||
action_button_down_short:
|
action_button_down_short:
|
||||||
- service: light.turn_off
|
- data: {}
|
||||||
data: {}
|
|
||||||
target:
|
target:
|
||||||
device_id:
|
device_id:
|
||||||
- 6dcbd87b459412144bddc42af3ae8b83
|
- 6dcbd87b459412144bddc42af3ae8b83
|
||||||
- 4edd9b9df7d1f6f2fe7dcc2e5c0eb968
|
- 4edd9b9df7d1f6f2fe7dcc2e5c0eb968
|
||||||
|
action: light.turn_off
|
||||||
action_button_left_short:
|
action_button_left_short:
|
||||||
- service: script.wled_random_effect
|
- data: {}
|
||||||
data: {}
|
action: script.wled_random_effect
|
||||||
action_button_right_short:
|
action_button_right_short:
|
||||||
- service: script.wled_random_palette
|
- data: {}
|
||||||
data: {}
|
action: script.wled_random_palette
|
||||||
action_button_up_long:
|
action_button_up_long:
|
||||||
- service: light.turn_on
|
- action: script.moodlight_orange_plasma
|
||||||
metadata: {}
|
data: {}
|
||||||
data:
|
action_button_down_long:
|
||||||
brightness_pct: 10
|
- action: script.moodlight_neutral
|
||||||
target:
|
data: {}
|
||||||
entity_id: light.wohnzimmer_moodlight
|
|
||||||
action_button_down_long: []
|
|
||||||
action_button_down_double:
|
action_button_down_double:
|
||||||
- service: light.turn_off
|
- target:
|
||||||
target:
|
|
||||||
device_id: 9f42805af5b7e423023595390342b9ac
|
device_id: 9f42805af5b7e423023595390342b9ac
|
||||||
data: {}
|
data: {}
|
||||||
|
action: light.turn_off
|
||||||
action_button_up_double:
|
action_button_up_double:
|
||||||
- service: light.turn_on
|
- metadata: {}
|
||||||
metadata: {}
|
|
||||||
data: {}
|
data: {}
|
||||||
target:
|
target:
|
||||||
device_id: 9f42805af5b7e423023595390342b9ac
|
device_id: 9f42805af5b7e423023595390342b9ac
|
||||||
|
action: light.turn_on
|
||||||
button_up_double_press: true
|
button_up_double_press: true
|
||||||
button_down_double_press: true
|
button_down_double_press: true
|
||||||
- id: '1702588441751'
|
- id: '1702588441751'
|
||||||
@@ -867,41 +983,41 @@
|
|||||||
input:
|
input:
|
||||||
remote: bd97db2ae9b0104d50dc6a343315608b
|
remote: bd97db2ae9b0104d50dc6a343315608b
|
||||||
double_dot_single_press:
|
double_dot_single_press:
|
||||||
- service: light.turn_off
|
- target:
|
||||||
target:
|
|
||||||
device_id:
|
device_id:
|
||||||
- 68868390eda35e969ec60a13020f2407
|
- 68868390eda35e969ec60a13020f2407
|
||||||
data: {}
|
data: {}
|
||||||
|
action: light.turn_off
|
||||||
double_dot_double_press:
|
double_dot_double_press:
|
||||||
- service: light.turn_off
|
- metadata: {}
|
||||||
metadata: {}
|
|
||||||
data: {}
|
data: {}
|
||||||
target:
|
target:
|
||||||
device_id: 3c86ddd39979139f29645308815c0271
|
device_id: 3c86ddd39979139f29645308815c0271
|
||||||
|
action: light.turn_off
|
||||||
single_dot_single_press:
|
single_dot_single_press:
|
||||||
- service: light.turn_on
|
- metadata: {}
|
||||||
metadata: {}
|
|
||||||
data: {}
|
data: {}
|
||||||
target:
|
target:
|
||||||
device_id: 68868390eda35e969ec60a13020f2407
|
device_id: 68868390eda35e969ec60a13020f2407
|
||||||
|
action: light.turn_on
|
||||||
single_dot_double_press:
|
single_dot_double_press:
|
||||||
- service: light.turn_on
|
- metadata: {}
|
||||||
metadata: {}
|
|
||||||
data: {}
|
data: {}
|
||||||
target:
|
target:
|
||||||
device_id: 3c86ddd39979139f29645308815c0271
|
device_id: 3c86ddd39979139f29645308815c0271
|
||||||
|
action: light.turn_on
|
||||||
double_dot_long_press:
|
double_dot_long_press:
|
||||||
- service: switch.turn_off
|
- metadata: {}
|
||||||
metadata: {}
|
|
||||||
data: {}
|
data: {}
|
||||||
target:
|
target:
|
||||||
device_id: b7c1c80b21406c5608e38aa0c7e7e439
|
device_id: b7c1c80b21406c5608e38aa0c7e7e439
|
||||||
|
action: switch.turn_off
|
||||||
single_dot_long_press:
|
single_dot_long_press:
|
||||||
- service: switch.turn_on
|
- metadata: {}
|
||||||
metadata: {}
|
|
||||||
data: {}
|
data: {}
|
||||||
target:
|
target:
|
||||||
device_id: b7c1c80b21406c5608e38aa0c7e7e439
|
device_id: b7c1c80b21406c5608e38aa0c7e7e439
|
||||||
|
action: switch.turn_on
|
||||||
- id: '1705488817426'
|
- id: '1705488817426'
|
||||||
alias: Let's Encrypt
|
alias: Let's Encrypt
|
||||||
description: Zertifikat prüfen und verlängern via Addon
|
description: Zertifikat prüfen und verlängern via Addon
|
||||||
@@ -1131,3 +1247,34 @@
|
|||||||
target:
|
target:
|
||||||
entity_id: input_boolean.waschetrockner_aktiv
|
entity_id: input_boolean.waschetrockner_aktiv
|
||||||
mode: single
|
mode: single
|
||||||
|
- id: '1756756139646'
|
||||||
|
alias: Türklingel
|
||||||
|
description: Benachrichtigung über Wohnungs- oder Haustürklingeln
|
||||||
|
triggers:
|
||||||
|
- trigger: mqtt
|
||||||
|
topic: gdoor/bus_rx
|
||||||
|
actions:
|
||||||
|
- choose:
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ trigger.payload_json.action == ''BUTTON_RING'' and trigger.payload_json.parameters
|
||||||
|
== ''0560'' }}'
|
||||||
|
sequence:
|
||||||
|
- data:
|
||||||
|
data:
|
||||||
|
push:
|
||||||
|
interruption-level: time-sensitive
|
||||||
|
message: Türklingel (außen)
|
||||||
|
action: notify.alle_mobilen_gerate
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ trigger.payload_json.action == ''BUTTON_FLOOR'' and trigger.payload_json.parameters
|
||||||
|
== ''FF6F'' and trigger.payload_json.busdata == ''0110139A59A6FF6FA1CC''}}'
|
||||||
|
sequence:
|
||||||
|
- data:
|
||||||
|
data:
|
||||||
|
push:
|
||||||
|
interruption-level: time-sensitive
|
||||||
|
message: Türklingel (innen)
|
||||||
|
action: notify.alle_mobilen_gerate
|
||||||
|
mode: single
|
||||||
|
@@ -1,7 +0,0 @@
|
|||||||
- sensor:
|
|
||||||
# Raspberry Pi CPU temp
|
|
||||||
name: "CPU Temp"
|
|
||||||
unique_id: '4144237916258'
|
|
||||||
command: "cat /sys/class/thermal/thermal_zone0/temp"
|
|
||||||
unit_of_measurement: "°C"
|
|
||||||
value_template: "{{ value | multiply(0.001) | round(1) }}"
|
|
@@ -11,15 +11,13 @@ http:
|
|||||||
tts:
|
tts:
|
||||||
- platform: picotts_remote
|
- platform: picotts_remote
|
||||||
language: "de-DE"
|
language: "de-DE"
|
||||||
- platform: google_translate
|
|
||||||
language: "de"
|
|
||||||
|
|
||||||
# Include modules
|
# Include modules
|
||||||
group: !include groups.yaml
|
group: !include groups.yaml
|
||||||
automation: !include automations.yaml
|
automation: !include automations.yaml
|
||||||
automation webhooks: !include automations_webhooks.yaml
|
|
||||||
script: !include scripts.yaml
|
script: !include scripts.yaml
|
||||||
scene: !include scenes.yaml
|
scene: !include scenes.yaml
|
||||||
|
notify: !include notify.yaml
|
||||||
|
|
||||||
# Enable additional integrations
|
# Enable additional integrations
|
||||||
# Enable 'wake_on_lan' intrgration
|
# Enable 'wake_on_lan' intrgration
|
||||||
@@ -31,18 +29,17 @@ homeassistant:
|
|||||||
|
|
||||||
# Additional sensors
|
# Additional sensors
|
||||||
sensor: !include sensors.yaml
|
sensor: !include sensors.yaml
|
||||||
command_line: !include commandline.yaml
|
|
||||||
utility_meter: !include utility_meters.yaml
|
utility_meter: !include utility_meters.yaml
|
||||||
|
|
||||||
# MQTT sensors
|
# MQTT sensors
|
||||||
mqtt: !include mqtt.yaml
|
mqtt: !include mqtt.yaml
|
||||||
|
mqtt_statestream: !include mqtt_statestream.yaml
|
||||||
|
|
||||||
# Template sensors
|
# Template sensors
|
||||||
template: !include template.yaml
|
template: !include template.yaml
|
||||||
|
|
||||||
# calendar integration
|
# calendar integration
|
||||||
calendar: !include calendars.yaml
|
calendar: !include calendars.yaml
|
||||||
ics_calendar: !include ics_calendars.yaml
|
|
||||||
|
|
||||||
# DB-recorder configuration
|
# DB-recorder configuration
|
||||||
recorder: !include recorder.yaml
|
recorder: !include recorder.yaml
|
||||||
@@ -53,7 +50,7 @@ bluetooth:
|
|||||||
# Bluetooth Low Energy tracker
|
# Bluetooth Low Energy tracker
|
||||||
device_tracker:
|
device_tracker:
|
||||||
- platform: bluetooth_le_tracker
|
- platform: bluetooth_le_tracker
|
||||||
track_new_devices: true
|
track_new_devices: false
|
||||||
|
|
||||||
generic_hygrostat:
|
generic_hygrostat:
|
||||||
- name: Badezimmer
|
- name: Badezimmer
|
||||||
@@ -74,4 +71,3 @@ generic_hygrostat:
|
|||||||
away_humidity: 60
|
away_humidity: 60
|
||||||
away_fixed: true
|
away_fixed: true
|
||||||
sensor_stale_duration: 00:15:00
|
sensor_stale_duration: 00:15:00
|
||||||
|
|
||||||
|
@@ -4,12 +4,18 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from lhpapi import HochwasserPortalAPI, LHPError
|
from lhpapi import HochwasserPortalAPI, LHPError, get_all_stations
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigFlow
|
from homeassistant.config_entries import ConfigFlow
|
||||||
from homeassistant.data_entry_flow import FlowResult
|
from homeassistant.data_entry_flow import FlowResult
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.helpers.selector import (
|
||||||
|
SelectOptionDict,
|
||||||
|
SelectSelector,
|
||||||
|
SelectSelectorConfig,
|
||||||
|
SelectSelectorMode,
|
||||||
|
)
|
||||||
|
|
||||||
from .const import CONF_ADD_UNAVAILABLE, CONF_PEGEL_IDENTIFIER, DOMAIN, LOGGER
|
from .const import CONF_ADD_UNAVAILABLE, CONF_PEGEL_IDENTIFIER, DOMAIN, LOGGER
|
||||||
|
|
||||||
@@ -50,12 +56,29 @@ class HochwasserPortalConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
return self.async_create_entry(title=f"{api.name}", data=user_input)
|
return self.async_create_entry(title=f"{api.name}", data=user_input)
|
||||||
|
|
||||||
|
stations_dict = await self.hass.async_add_executor_job(get_all_stations)
|
||||||
|
LOGGER.debug(
|
||||||
|
"%i stations found on Github",
|
||||||
|
len(stations_dict),
|
||||||
|
)
|
||||||
|
stations = [SelectOptionDict(value="---", label="")]
|
||||||
|
stations.extend(
|
||||||
|
SelectOptionDict(value=k, label=f"{v} ({k})")
|
||||||
|
for k, v in stations_dict.items()
|
||||||
|
)
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="user",
|
step_id="user",
|
||||||
errors=errors,
|
errors=errors,
|
||||||
data_schema=vol.Schema(
|
data_schema=vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Required(CONF_PEGEL_IDENTIFIER): cv.string,
|
vol.Required(CONF_PEGEL_IDENTIFIER): SelectSelector(
|
||||||
|
SelectSelectorConfig(
|
||||||
|
options=stations,
|
||||||
|
mode=SelectSelectorMode.DROPDOWN,
|
||||||
|
sort=True,
|
||||||
|
custom_value=True,
|
||||||
|
)
|
||||||
|
),
|
||||||
vol.Required(CONF_ADD_UNAVAILABLE, default=False): cv.boolean,
|
vol.Required(CONF_ADD_UNAVAILABLE, default=False): cv.boolean,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
@@ -9,6 +9,6 @@
|
|||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"issue_tracker": "https://github.com/stephan192/hochwasserportal/issues",
|
"issue_tracker": "https://github.com/stephan192/hochwasserportal/issues",
|
||||||
"loggers": ["hochwasserportal"],
|
"loggers": ["hochwasserportal"],
|
||||||
"requirements": ["lhpapi==1.0.3"],
|
"requirements": ["lhpapi==1.0.5"],
|
||||||
"version": "1.0.1"
|
"version": "1.0.3"
|
||||||
}
|
}
|
@@ -4,6 +4,7 @@ import logging
|
|||||||
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_EXCLUDE,
|
CONF_EXCLUDE,
|
||||||
CONF_INCLUDE,
|
CONF_INCLUDE,
|
||||||
@@ -14,24 +15,35 @@ from homeassistant.const import (
|
|||||||
CONF_USERNAME,
|
CONF_USERNAME,
|
||||||
Platform,
|
Platform,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.helpers import discovery
|
||||||
|
from homeassistant.helpers.issue_registry import (
|
||||||
|
IssueSeverity,
|
||||||
|
async_create_issue,
|
||||||
|
)
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
from .const import DOMAIN, UPGRADE_URL
|
from .const import (
|
||||||
|
CONF_ACCEPT_HEADER,
|
||||||
|
CONF_ADV_CONNECT_OPTS,
|
||||||
|
CONF_CALENDARS,
|
||||||
|
CONF_CONNECTION_TIMEOUT,
|
||||||
|
CONF_DAYS,
|
||||||
|
CONF_DOWNLOAD_INTERVAL,
|
||||||
|
CONF_INCLUDE_ALL_DAY,
|
||||||
|
CONF_OFFSET_HOURS,
|
||||||
|
CONF_PARSER,
|
||||||
|
CONF_REQUIRES_AUTH,
|
||||||
|
CONF_SET_TIMEOUT,
|
||||||
|
CONF_SUMMARY_DEFAULT,
|
||||||
|
CONF_SUMMARY_DEFAULT_DEFAULT,
|
||||||
|
CONF_USER_AGENT,
|
||||||
|
DOMAIN,
|
||||||
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
PLATFORMS: list[Platform] = [Platform.CALENDAR]
|
PLATFORMS: list[Platform] = [Platform.CALENDAR]
|
||||||
|
|
||||||
CONF_DEVICE_ID = "device_id"
|
|
||||||
CONF_CALENDARS = "calendars"
|
|
||||||
CONF_DAYS = "days"
|
|
||||||
CONF_INCLUDE_ALL_DAY = "include_all_day"
|
|
||||||
CONF_PARSER = "parser"
|
|
||||||
CONF_DOWNLOAD_INTERVAL = "download_interval"
|
|
||||||
CONF_USER_AGENT = "user_agent"
|
|
||||||
CONF_OFFSET_HOURS = "offset_hours"
|
|
||||||
CONF_ACCEPT_HEADER = "accept_header"
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema(
|
CONFIG_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
DOMAIN: vol.Schema(
|
DOMAIN: vol.Schema(
|
||||||
@@ -81,6 +93,13 @@ CONFIG_SCHEMA = vol.Schema(
|
|||||||
vol.Optional(
|
vol.Optional(
|
||||||
CONF_ACCEPT_HEADER, default=""
|
CONF_ACCEPT_HEADER, default=""
|
||||||
): cv.string,
|
): cv.string,
|
||||||
|
vol.Optional(
|
||||||
|
CONF_CONNECTION_TIMEOUT, default=300
|
||||||
|
): cv.positive_float,
|
||||||
|
vol.Optional(
|
||||||
|
CONF_SUMMARY_DEFAULT,
|
||||||
|
default=CONF_SUMMARY_DEFAULT_DEFAULT,
|
||||||
|
): cv.string,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
@@ -92,22 +111,150 @@ CONFIG_SCHEMA = vol.Schema(
|
|||||||
extra=vol.ALLOW_EXTRA,
|
extra=vol.ALLOW_EXTRA,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
STORAGE_KEY = DOMAIN
|
||||||
|
STORAGE_VERSION_MAJOR = 1
|
||||||
|
STORAGE_VERSION_MINOR = 0
|
||||||
|
|
||||||
def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|
||||||
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
"""Set up calendars."""
|
"""Set up calendars."""
|
||||||
_LOGGER.debug("Setting up ics_calendar component")
|
_LOGGER.debug("Setting up ics_calendar component")
|
||||||
hass.data.setdefault(DOMAIN, {})
|
hass.data.setdefault(DOMAIN, {})
|
||||||
|
|
||||||
if DOMAIN in config and config[DOMAIN]:
|
if DOMAIN in config and config[DOMAIN]:
|
||||||
hass.helpers.discovery.load_platform(
|
_LOGGER.debug("discovery.load_platform called")
|
||||||
PLATFORMS[0], DOMAIN, config[DOMAIN], config
|
discovery.load_platform(
|
||||||
|
hass=hass,
|
||||||
|
component=PLATFORMS[0],
|
||||||
|
platform=DOMAIN,
|
||||||
|
discovered=config[DOMAIN],
|
||||||
|
hass_config=config,
|
||||||
)
|
)
|
||||||
else:
|
async_create_issue(
|
||||||
_LOGGER.error(
|
hass,
|
||||||
"No configuration found! If you upgraded from ics_calendar v3.2.0 "
|
DOMAIN,
|
||||||
"or older, you need to update your configuration! See "
|
"deprecated_yaml_configuration",
|
||||||
"%s for more information.",
|
is_fixable=False,
|
||||||
UPGRADE_URL,
|
issue_domain=DOMAIN,
|
||||||
|
severity=IssueSeverity.WARNING,
|
||||||
|
translation_key="YAML_Warning",
|
||||||
|
)
|
||||||
|
_LOGGER.warning(
|
||||||
|
"YAML configuration of ics_calendar is deprecated and will be "
|
||||||
|
"removed in ics_calendar v5.0.0. Your configuration items have "
|
||||||
|
"been imported. Please remove them from your configuration.yaml "
|
||||||
|
"file."
|
||||||
|
)
|
||||||
|
|
||||||
|
config_entry = _async_find_matching_config_entry(hass)
|
||||||
|
if not config_entry:
|
||||||
|
if config[DOMAIN].get("calendars"):
|
||||||
|
for calendar in config[DOMAIN].get("calendars"):
|
||||||
|
hass.async_create_task(
|
||||||
|
hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": SOURCE_IMPORT},
|
||||||
|
data=dict(calendar),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
# update entry with any changes
|
||||||
|
if config[DOMAIN].get("calendars"):
|
||||||
|
for calendar in config[DOMAIN].get("calendars"):
|
||||||
|
hass.config_entries.async_update_entry(
|
||||||
|
config_entry, data=dict(calendar)
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def _async_find_matching_config_entry(hass):
|
||||||
|
for entry in hass.config_entries.async_entries(DOMAIN):
|
||||||
|
if entry.source == SOURCE_IMPORT:
|
||||||
|
return entry
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def async_migrate_entry(hass, entry: ConfigEntry):
|
||||||
|
"""Migrate old config entry."""
|
||||||
|
# Don't downgrade entries
|
||||||
|
if entry.version > STORAGE_VERSION_MAJOR:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if entry.version == STORAGE_VERSION_MAJOR:
|
||||||
|
new_data = {**entry.data}
|
||||||
|
|
||||||
|
hass.config_entries.async_update_entry(
|
||||||
|
entry,
|
||||||
|
data=new_data,
|
||||||
|
minor_version=STORAGE_VERSION_MINOR,
|
||||||
|
version=STORAGE_VERSION_MAJOR,
|
||||||
)
|
)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
"""Implement async_setup_entry."""
|
||||||
|
full_data: dict = add_missing_defaults(entry)
|
||||||
|
hass.config_entries.async_update_entry(entry=entry, data=full_data)
|
||||||
|
|
||||||
|
hass.data.setdefault(DOMAIN, {})
|
||||||
|
hass.data[DOMAIN][entry.entry_id] = full_data
|
||||||
|
await hass.config_entries.async_forward_entry_setups(entry, ["calendar"])
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def add_missing_defaults(
|
||||||
|
entry: ConfigEntry,
|
||||||
|
) -> dict:
|
||||||
|
"""Initialize missing data."""
|
||||||
|
data = {
|
||||||
|
CONF_NAME: "",
|
||||||
|
CONF_URL: "",
|
||||||
|
CONF_ADV_CONNECT_OPTS: False,
|
||||||
|
CONF_SET_TIMEOUT: False,
|
||||||
|
CONF_REQUIRES_AUTH: False,
|
||||||
|
CONF_INCLUDE_ALL_DAY: False,
|
||||||
|
CONF_REQUIRES_AUTH: False,
|
||||||
|
CONF_USERNAME: "",
|
||||||
|
CONF_PASSWORD: "",
|
||||||
|
CONF_PARSER: "rie",
|
||||||
|
CONF_PREFIX: "",
|
||||||
|
CONF_DAYS: 1,
|
||||||
|
CONF_DOWNLOAD_INTERVAL: 15,
|
||||||
|
CONF_USER_AGENT: "",
|
||||||
|
CONF_EXCLUDE: "",
|
||||||
|
CONF_INCLUDE: "",
|
||||||
|
CONF_OFFSET_HOURS: 0,
|
||||||
|
CONF_ACCEPT_HEADER: "",
|
||||||
|
CONF_CONNECTION_TIMEOUT: 300.0,
|
||||||
|
CONF_SUMMARY_DEFAULT: CONF_SUMMARY_DEFAULT_DEFAULT,
|
||||||
|
}
|
||||||
|
data.update(entry.data)
|
||||||
|
|
||||||
|
if CONF_USERNAME in entry.data or CONF_PASSWORD in entry.data:
|
||||||
|
data[CONF_REQUIRES_AUTH] = True
|
||||||
|
if (
|
||||||
|
CONF_USER_AGENT in entry.data
|
||||||
|
or CONF_ACCEPT_HEADER in entry.data
|
||||||
|
or CONF_CONNECTION_TIMEOUT in entry.data
|
||||||
|
):
|
||||||
|
data[CONF_ADV_CONNECT_OPTS] = True
|
||||||
|
if CONF_CONNECTION_TIMEOUT in entry.data:
|
||||||
|
data[CONF_SET_TIMEOUT] = True
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
"""Unload entry."""
|
||||||
|
unload_ok = await hass.config_entries.async_unload_platforms(
|
||||||
|
entry, PLATFORMS
|
||||||
|
)
|
||||||
|
if unload_ok:
|
||||||
|
hass.data[DOMAIN].pop(entry.entry_id)
|
||||||
|
|
||||||
|
return unload_ok
|
||||||
|
@@ -1,7 +1,8 @@
|
|||||||
"""Support for ICS Calendar."""
|
"""Support for ICS Calendar."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
# import homeassistant.helpers.config_validation as cv
|
# import homeassistant.helpers.config_validation as cv
|
||||||
# import voluptuous as vol
|
# import voluptuous as vol
|
||||||
@@ -12,6 +13,7 @@ from homeassistant.components.calendar import (
|
|||||||
extract_offset,
|
extract_offset,
|
||||||
is_offset_reached,
|
is_offset_reached,
|
||||||
)
|
)
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_EXCLUDE,
|
CONF_EXCLUDE,
|
||||||
CONF_INCLUDE,
|
CONF_INCLUDE,
|
||||||
@@ -24,23 +26,28 @@ from homeassistant.const import (
|
|||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity import generate_entity_id
|
from homeassistant.helpers.entity import generate_entity_id
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
from homeassistant.helpers.httpx_client import get_async_client
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||||
from homeassistant.util import Throttle
|
|
||||||
from homeassistant.util.dt import now as hanow
|
from homeassistant.util.dt import now as hanow
|
||||||
|
|
||||||
from . import (
|
from .calendardata import CalendarData
|
||||||
|
from .const import (
|
||||||
CONF_ACCEPT_HEADER,
|
CONF_ACCEPT_HEADER,
|
||||||
CONF_CALENDARS,
|
CONF_CALENDARS,
|
||||||
|
CONF_CONNECTION_TIMEOUT,
|
||||||
CONF_DAYS,
|
CONF_DAYS,
|
||||||
CONF_DOWNLOAD_INTERVAL,
|
CONF_DOWNLOAD_INTERVAL,
|
||||||
CONF_INCLUDE_ALL_DAY,
|
CONF_INCLUDE_ALL_DAY,
|
||||||
CONF_OFFSET_HOURS,
|
CONF_OFFSET_HOURS,
|
||||||
CONF_PARSER,
|
CONF_PARSER,
|
||||||
|
CONF_SET_TIMEOUT,
|
||||||
|
CONF_SUMMARY_DEFAULT,
|
||||||
CONF_USER_AGENT,
|
CONF_USER_AGENT,
|
||||||
|
DOMAIN,
|
||||||
)
|
)
|
||||||
from .calendardata import CalendarData
|
|
||||||
from .filter import Filter
|
from .filter import Filter
|
||||||
from .icalendarparser import ICalendarParser
|
from .getparser import GetParser
|
||||||
|
from .parserevent import ParserEvent
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -51,6 +58,34 @@ OFFSET = "!!"
|
|||||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
|
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: ConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up the calendar in background."""
|
||||||
|
hass.async_create_task(
|
||||||
|
_async_setup_entry_bg_task(hass, config_entry, async_add_entities)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _async_setup_entry_bg_task(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: ConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up the calendar."""
|
||||||
|
data = hass.data[DOMAIN][config_entry.entry_id]
|
||||||
|
device_id = f"{data[CONF_NAME]}"
|
||||||
|
entity = ICSCalendarEntity(
|
||||||
|
hass,
|
||||||
|
generate_entity_id(ENTITY_ID_FORMAT, device_id, hass=hass),
|
||||||
|
hass.data[DOMAIN][config_entry.entry_id],
|
||||||
|
config_entry.entry_id,
|
||||||
|
)
|
||||||
|
async_add_entities([entity])
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(
|
def setup_platform(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config: ConfigType,
|
config: ConfigType,
|
||||||
@@ -70,8 +105,13 @@ def setup_platform(
|
|||||||
"""
|
"""
|
||||||
_LOGGER.debug("Setting up ics calendars")
|
_LOGGER.debug("Setting up ics calendars")
|
||||||
if discovery_info is not None:
|
if discovery_info is not None:
|
||||||
calendars: list = discovery_info.get(CONF_CALENDARS)
|
_LOGGER.debug(
|
||||||
|
"setup_platform: ignoring discovery_info, already imported!"
|
||||||
|
)
|
||||||
|
# calendars: list = discovery_info.get(CONF_CALENDARS)
|
||||||
|
calendars = []
|
||||||
else:
|
else:
|
||||||
|
_LOGGER.debug("setup_platform: discovery_info is None")
|
||||||
calendars: list = config.get(CONF_CALENDARS)
|
calendars: list = config.get(CONF_CALENDARS)
|
||||||
|
|
||||||
calendar_devices = []
|
calendar_devices = []
|
||||||
@@ -91,10 +131,13 @@ def setup_platform(
|
|||||||
CONF_INCLUDE: calendar.get(CONF_INCLUDE),
|
CONF_INCLUDE: calendar.get(CONF_INCLUDE),
|
||||||
CONF_OFFSET_HOURS: calendar.get(CONF_OFFSET_HOURS),
|
CONF_OFFSET_HOURS: calendar.get(CONF_OFFSET_HOURS),
|
||||||
CONF_ACCEPT_HEADER: calendar.get(CONF_ACCEPT_HEADER),
|
CONF_ACCEPT_HEADER: calendar.get(CONF_ACCEPT_HEADER),
|
||||||
|
CONF_CONNECTION_TIMEOUT: calendar.get(CONF_CONNECTION_TIMEOUT),
|
||||||
}
|
}
|
||||||
device_id = f"{device_data[CONF_NAME]}"
|
device_id = f"{device_data[CONF_NAME]}"
|
||||||
entity_id = generate_entity_id(ENTITY_ID_FORMAT, device_id, hass=hass)
|
entity_id = generate_entity_id(ENTITY_ID_FORMAT, device_id, hass=hass)
|
||||||
calendar_devices.append(ICSCalendarEntity(entity_id, device_data))
|
calendar_devices.append(
|
||||||
|
ICSCalendarEntity(hass, entity_id, device_data)
|
||||||
|
)
|
||||||
|
|
||||||
add_entities(calendar_devices)
|
add_entities(calendar_devices)
|
||||||
|
|
||||||
@@ -102,7 +145,13 @@ def setup_platform(
|
|||||||
class ICSCalendarEntity(CalendarEntity):
|
class ICSCalendarEntity(CalendarEntity):
|
||||||
"""A CalendarEntity for an ICS Calendar."""
|
"""A CalendarEntity for an ICS Calendar."""
|
||||||
|
|
||||||
def __init__(self, entity_id: str, device_data):
|
def __init__(
|
||||||
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entity_id: str,
|
||||||
|
device_data,
|
||||||
|
unique_id: str = None,
|
||||||
|
):
|
||||||
"""Construct ICSCalendarEntity.
|
"""Construct ICSCalendarEntity.
|
||||||
|
|
||||||
:param entity_id: Entity id for the calendar
|
:param entity_id: Entity id for the calendar
|
||||||
@@ -111,14 +160,16 @@ class ICSCalendarEntity(CalendarEntity):
|
|||||||
:type device_data: dict
|
:type device_data: dict
|
||||||
"""
|
"""
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Initializing calendar: %s with URL: %s",
|
"Initializing calendar: %s with URL: %s, uniqueid: %s",
|
||||||
device_data[CONF_NAME],
|
device_data[CONF_NAME],
|
||||||
device_data[CONF_URL],
|
device_data[CONF_URL],
|
||||||
|
unique_id,
|
||||||
)
|
)
|
||||||
self.data = ICSCalendarData(device_data)
|
self.data = ICSCalendarData(hass, device_data)
|
||||||
self.entity_id = entity_id
|
self.entity_id = entity_id
|
||||||
|
self._attr_unique_id = f"ICSCalendar.{unique_id}"
|
||||||
self._event = None
|
self._event = None
|
||||||
self._name = device_data[CONF_NAME]
|
self._attr_name = device_data[CONF_NAME]
|
||||||
self._last_call = None
|
self._last_call = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -131,12 +182,7 @@ class ICSCalendarEntity(CalendarEntity):
|
|||||||
return self._event
|
return self._event
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def should_poll(self) -> bool:
|
||||||
"""Return the name of the calendar."""
|
|
||||||
return self._name
|
|
||||||
|
|
||||||
@property
|
|
||||||
def should_poll(self):
|
|
||||||
"""Indicate if the calendar should be polled.
|
"""Indicate if the calendar should be polled.
|
||||||
|
|
||||||
If the last call to update or get_api_events was not within the minimum
|
If the last call to update or get_api_events was not within the minimum
|
||||||
@@ -168,25 +214,50 @@ class ICSCalendarEntity(CalendarEntity):
|
|||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"%s: async_get_events called; calling internal.", self.name
|
"%s: async_get_events called; calling internal.", self.name
|
||||||
)
|
)
|
||||||
return await self.data.async_get_events(hass, start_date, end_date)
|
return await self.data.async_get_events(start_date, end_date)
|
||||||
|
|
||||||
def update(self):
|
async def async_update(self):
|
||||||
"""Get the current or next event."""
|
"""Get the current or next event."""
|
||||||
self.data.update()
|
await self.data.async_update()
|
||||||
self._event = self.data.event
|
self._event: CalendarEvent | None = self.data.event
|
||||||
self._attr_extra_state_attributes = {
|
self._attr_extra_state_attributes = {
|
||||||
"offset_reached": is_offset_reached(
|
"offset_reached": (
|
||||||
self._event.start_datetime_local, self.data.offset
|
is_offset_reached(
|
||||||
|
self._event.start_datetime_local, self.data.offset
|
||||||
|
)
|
||||||
|
if self._event
|
||||||
|
else False
|
||||||
)
|
)
|
||||||
if self._event
|
|
||||||
else False
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async def async_create_event(self, **kwargs: Any):
|
||||||
|
"""Raise error, this is a read-only calendar."""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
async def async_delete_event(
|
||||||
|
self,
|
||||||
|
uid: str,
|
||||||
|
recurrence_id: str | None = None,
|
||||||
|
recurrence_range: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""Raise error, this is a read-only calendar."""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
async def async_update_event(
|
||||||
|
self,
|
||||||
|
uid: str,
|
||||||
|
event: dict[str, Any],
|
||||||
|
recurrence_id: str | None = None,
|
||||||
|
recurrence_range: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""Raise error, this is a read-only calendar."""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
class ICSCalendarData: # pylint: disable=R0902
|
class ICSCalendarData: # pylint: disable=R0902
|
||||||
"""Class to use the calendar ICS client object to get next event."""
|
"""Class to use the calendar ICS client object to get next event."""
|
||||||
|
|
||||||
def __init__(self, device_data):
|
def __init__(self, hass: HomeAssistant, device_data):
|
||||||
"""Set up how we are going to connect to the URL.
|
"""Set up how we are going to connect to the URL.
|
||||||
|
|
||||||
:param device_data Information about the calendar
|
:param device_data Information about the calendar
|
||||||
@@ -196,18 +267,25 @@ class ICSCalendarData: # pylint: disable=R0902
|
|||||||
self._offset_hours = device_data[CONF_OFFSET_HOURS]
|
self._offset_hours = device_data[CONF_OFFSET_HOURS]
|
||||||
self.include_all_day = device_data[CONF_INCLUDE_ALL_DAY]
|
self.include_all_day = device_data[CONF_INCLUDE_ALL_DAY]
|
||||||
self._summary_prefix: str = device_data[CONF_PREFIX]
|
self._summary_prefix: str = device_data[CONF_PREFIX]
|
||||||
self.parser = ICalendarParser.get_instance(device_data[CONF_PARSER])
|
self._summary_default: str = device_data[CONF_SUMMARY_DEFAULT]
|
||||||
|
self.parser = GetParser.get_parser(device_data[CONF_PARSER])
|
||||||
self.parser.set_filter(
|
self.parser.set_filter(
|
||||||
Filter(device_data[CONF_EXCLUDE], device_data[CONF_INCLUDE])
|
Filter(device_data[CONF_EXCLUDE], device_data[CONF_INCLUDE])
|
||||||
)
|
)
|
||||||
self.offset = None
|
self.offset = None
|
||||||
self.event = None
|
self.event = None
|
||||||
|
self._hass = hass
|
||||||
|
|
||||||
self._calendar_data = CalendarData(
|
self._calendar_data = CalendarData(
|
||||||
|
get_async_client(hass),
|
||||||
_LOGGER,
|
_LOGGER,
|
||||||
self.name,
|
{
|
||||||
device_data[CONF_URL],
|
"name": self.name,
|
||||||
timedelta(minutes=device_data[CONF_DOWNLOAD_INTERVAL]),
|
"url": device_data[CONF_URL],
|
||||||
|
"min_update_time": timedelta(
|
||||||
|
minutes=device_data[CONF_DOWNLOAD_INTERVAL]
|
||||||
|
),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
self._calendar_data.set_headers(
|
self._calendar_data.set_headers(
|
||||||
@@ -217,22 +295,23 @@ class ICSCalendarData: # pylint: disable=R0902
|
|||||||
device_data[CONF_ACCEPT_HEADER],
|
device_data[CONF_ACCEPT_HEADER],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if device_data.get(CONF_SET_TIMEOUT):
|
||||||
|
self._calendar_data.set_timeout(
|
||||||
|
device_data[CONF_CONNECTION_TIMEOUT]
|
||||||
|
)
|
||||||
|
|
||||||
async def async_get_events(
|
async def async_get_events(
|
||||||
self, hass: HomeAssistant, start_date: datetime, end_date: datetime
|
self, start_date: datetime, end_date: datetime
|
||||||
) -> list[CalendarEvent]:
|
) -> list[CalendarEvent]:
|
||||||
"""Get all events in a specific time frame.
|
"""Get all events in a specific time frame.
|
||||||
|
|
||||||
:param hass: Home Assistant object
|
|
||||||
:type hass: HomeAssistant
|
|
||||||
:param start_date: The first starting date to consider
|
:param start_date: The first starting date to consider
|
||||||
:type start_date: datetime
|
:type start_date: datetime
|
||||||
:param end_date: The last starting date to consider
|
:param end_date: The last starting date to consider
|
||||||
:type end_date: datetime
|
:type end_date: datetime
|
||||||
"""
|
"""
|
||||||
event_list = []
|
event_list: list[ParserEvent] = []
|
||||||
if await hass.async_add_executor_job(
|
if await self._calendar_data.download_calendar():
|
||||||
self._calendar_data.download_calendar
|
|
||||||
):
|
|
||||||
_LOGGER.debug("%s: Setting calendar content", self.name)
|
_LOGGER.debug("%s: Setting calendar content", self.name)
|
||||||
self.parser.set_content(self._calendar_data.get())
|
self.parser.set_content(self._calendar_data.get())
|
||||||
try:
|
try:
|
||||||
@@ -248,22 +327,27 @@ class ICSCalendarData: # pylint: disable=R0902
|
|||||||
self.name,
|
self.name,
|
||||||
exc_info=True,
|
exc_info=True,
|
||||||
)
|
)
|
||||||
event_list = []
|
event_list: list[ParserEvent] = []
|
||||||
|
|
||||||
for event in event_list:
|
for event in event_list:
|
||||||
event.summary = self._summary_prefix + event.summary
|
event.summary = self._summary_prefix + event.summary
|
||||||
|
if not event.summary:
|
||||||
|
event.summary = self._summary_default
|
||||||
|
# Since we skipped the validation code earlier, invoke it now,
|
||||||
|
# before passing the object outside this component
|
||||||
|
event.validate()
|
||||||
|
|
||||||
return event_list
|
return event_list
|
||||||
|
|
||||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
async def async_update(self):
|
||||||
def update(self):
|
|
||||||
"""Get the current or next event."""
|
"""Get the current or next event."""
|
||||||
_LOGGER.debug("%s: Update was called", self.name)
|
_LOGGER.debug("%s: Update was called", self.name)
|
||||||
if self._calendar_data.download_calendar():
|
parser_event: ParserEvent | None = None
|
||||||
|
if await self._calendar_data.download_calendar():
|
||||||
_LOGGER.debug("%s: Setting calendar content", self.name)
|
_LOGGER.debug("%s: Setting calendar content", self.name)
|
||||||
self.parser.set_content(self._calendar_data.get())
|
self.parser.set_content(self._calendar_data.get())
|
||||||
try:
|
try:
|
||||||
self.event = self.parser.get_current_event(
|
parser_event: ParserEvent | None = self.parser.get_current_event(
|
||||||
include_all_day=self.include_all_day,
|
include_all_day=self.include_all_day,
|
||||||
now=hanow(),
|
now=hanow(),
|
||||||
days=self._days,
|
days=self._days,
|
||||||
@@ -273,18 +357,24 @@ class ICSCalendarData: # pylint: disable=R0902
|
|||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
"update: %s: Failed to parse ICS!", self.name, exc_info=True
|
"update: %s: Failed to parse ICS!", self.name, exc_info=True
|
||||||
)
|
)
|
||||||
if self.event is not None:
|
if parser_event is not None:
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"%s: got event: %s; start: %s; end: %s; all_day: %s",
|
"%s: got event: %s; start: %s; end: %s; all_day: %s",
|
||||||
self.name,
|
self.name,
|
||||||
self.event.summary,
|
parser_event.summary,
|
||||||
self.event.start,
|
parser_event.start,
|
||||||
self.event.end,
|
parser_event.end,
|
||||||
self.event.all_day,
|
parser_event.all_day,
|
||||||
)
|
)
|
||||||
(summary, offset) = extract_offset(self.event.summary, OFFSET)
|
(summary, offset) = extract_offset(parser_event.summary, OFFSET)
|
||||||
self.event.summary = self._summary_prefix + summary
|
parser_event.summary = self._summary_prefix + summary
|
||||||
|
if not parser_event.summary:
|
||||||
|
parser_event.summary = self._summary_default
|
||||||
self.offset = offset
|
self.offset = offset
|
||||||
|
# Invoke validation here, since it was skipped when creating the
|
||||||
|
# ParserEvent
|
||||||
|
parser_event.validate()
|
||||||
|
self.event: CalendarEvent = parser_event
|
||||||
return True
|
return True
|
||||||
|
|
||||||
_LOGGER.debug("%s: No event found!", self.name)
|
_LOGGER.debug("%s: No event found!", self.name)
|
||||||
|
@@ -1,23 +1,25 @@
|
|||||||
"""Provide CalendarData class."""
|
"""Provide CalendarData class."""
|
||||||
import zlib
|
|
||||||
from datetime import timedelta
|
|
||||||
from gzip import BadGzipFile, GzipFile
|
|
||||||
from logging import Logger
|
|
||||||
from threading import Lock
|
|
||||||
from urllib.error import ContentTooShortError, HTTPError, URLError
|
|
||||||
from urllib.request import (
|
|
||||||
HTTPBasicAuthHandler,
|
|
||||||
HTTPDigestAuthHandler,
|
|
||||||
HTTPPasswordMgrWithDefaultRealm,
|
|
||||||
build_opener,
|
|
||||||
install_opener,
|
|
||||||
urlopen,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
import re
|
||||||
|
from logging import Logger
|
||||||
|
from math import floor
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
import httpx_auth
|
||||||
from homeassistant.util.dt import now as hanow
|
from homeassistant.util.dt import now as hanow
|
||||||
|
|
||||||
|
# from urllib.error import ContentTooShortError, HTTPError, URLError
|
||||||
|
|
||||||
class CalendarData:
|
|
||||||
|
class DigestWithMultiAuth(httpx.DigestAuth, httpx_auth.SupportMultiAuth):
|
||||||
|
"""Describes a DigestAuth authentication."""
|
||||||
|
|
||||||
|
def __init__(self, username: str, password: str):
|
||||||
|
"""Construct Digest authentication that supports Multi Auth."""
|
||||||
|
httpx.DigestAuth.__init__(self, username, password)
|
||||||
|
|
||||||
|
|
||||||
|
class CalendarData: # pylint: disable=R0902
|
||||||
"""CalendarData class.
|
"""CalendarData class.
|
||||||
|
|
||||||
The CalendarData class is used to download and cache calendar data from a
|
The CalendarData class is used to download and cache calendar data from a
|
||||||
@@ -25,32 +27,33 @@ class CalendarData:
|
|||||||
instance.
|
instance.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
opener_lock = Lock()
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, logger: Logger, name: str, url: str, min_update_time: timedelta
|
self,
|
||||||
|
async_client: httpx.AsyncClient,
|
||||||
|
logger: Logger,
|
||||||
|
conf: dict,
|
||||||
):
|
):
|
||||||
"""Construct CalendarData object.
|
"""Construct CalendarData object.
|
||||||
|
|
||||||
|
:param async_client: An httpx.AsyncClient object for requests
|
||||||
|
:type httpx.AsyncClient
|
||||||
:param logger: The logger for reporting problems
|
:param logger: The logger for reporting problems
|
||||||
:type logger: Logger
|
:type logger: Logger
|
||||||
:param name: The name of the calendar (used for reporting problems)
|
:param conf: Configuration options
|
||||||
:type name: str
|
:type conf: dict
|
||||||
:param url: The URL of the calendar
|
|
||||||
:type url: str
|
|
||||||
:param min_update_time: The minimum time between downloading data from
|
|
||||||
the URL when requested
|
|
||||||
:type min_update_time: timedelta
|
|
||||||
"""
|
"""
|
||||||
|
self._auth = None
|
||||||
self._calendar_data = None
|
self._calendar_data = None
|
||||||
|
self._headers = []
|
||||||
self._last_download = None
|
self._last_download = None
|
||||||
self._min_update_time = min_update_time
|
self._min_update_time = conf["min_update_time"]
|
||||||
self._opener = None
|
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
self.name = name
|
self.name = conf["name"]
|
||||||
self.url = url
|
self.url = conf["url"]
|
||||||
|
self.connection_timeout = None
|
||||||
|
self._httpx = async_client
|
||||||
|
|
||||||
def download_calendar(self) -> bool:
|
async def download_calendar(self) -> bool:
|
||||||
"""Download the calendar data.
|
"""Download the calendar data.
|
||||||
|
|
||||||
This only downloads data if self.min_update_time has passed since the
|
This only downloads data if self.min_update_time has passed since the
|
||||||
@@ -59,20 +62,25 @@ class CalendarData:
|
|||||||
returns: True if data was downloaded, otherwise False.
|
returns: True if data was downloaded, otherwise False.
|
||||||
rtype: bool
|
rtype: bool
|
||||||
"""
|
"""
|
||||||
now = hanow()
|
self.logger.debug("%s: download_calendar start", self.name)
|
||||||
if (
|
if (
|
||||||
self._calendar_data is None
|
self._calendar_data is None
|
||||||
or self._last_download is None
|
or self._last_download is None
|
||||||
or (now - self._last_download) > self._min_update_time
|
or (hanow() - self._last_download) > self._min_update_time
|
||||||
):
|
):
|
||||||
self._last_download = now
|
|
||||||
self._calendar_data = None
|
self._calendar_data = None
|
||||||
|
next_url: str = self._make_url()
|
||||||
self.logger.debug(
|
self.logger.debug(
|
||||||
"%s: Downloading calendar data from: %s", self.name, self.url
|
"%s: Downloading calendar data from: %s",
|
||||||
|
self.name,
|
||||||
|
next_url,
|
||||||
)
|
)
|
||||||
self._download_data()
|
await self._download_data(next_url)
|
||||||
|
self._last_download = hanow()
|
||||||
|
self.logger.debug("%s: download_calendar done", self.name)
|
||||||
return self._calendar_data is not None
|
return self._calendar_data is not None
|
||||||
|
|
||||||
|
self.logger.debug("%s: download_calendar skipped download", self.name)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get(self) -> str:
|
def get(self) -> str:
|
||||||
@@ -92,10 +100,8 @@ class CalendarData:
|
|||||||
):
|
):
|
||||||
"""Set a user agent, accept header, and/or user name and password.
|
"""Set a user agent, accept header, and/or user name and password.
|
||||||
|
|
||||||
The user name and password will be set into an HTTPBasicAuthHandler an
|
The user name and password will be set into an auth object that
|
||||||
an HTTPDigestAuthHandler. Both are attached to a new urlopener, so
|
supports both Basic Auth and Digest Auth for httpx.
|
||||||
that HTTP Basic Auth and HTTP Digest Auth will be supported when
|
|
||||||
opening the URL.
|
|
||||||
|
|
||||||
If the user_agent parameter is not "", a User-agent header will be
|
If the user_agent parameter is not "", a User-agent header will be
|
||||||
added to the urlopener.
|
added to the urlopener.
|
||||||
@@ -110,81 +116,63 @@ class CalendarData:
|
|||||||
:type accept_header: str
|
:type accept_header: str
|
||||||
"""
|
"""
|
||||||
if user_name != "" and password != "":
|
if user_name != "" and password != "":
|
||||||
passman = HTTPPasswordMgrWithDefaultRealm()
|
self._auth = httpx_auth.Basic(
|
||||||
passman.add_password(None, self.url, user_name, password)
|
user_name, password
|
||||||
basic_auth_handler = HTTPBasicAuthHandler(passman)
|
) + DigestWithMultiAuth(user_name, password)
|
||||||
digest_auth_handler = HTTPDigestAuthHandler(passman)
|
|
||||||
self._opener = build_opener(
|
|
||||||
digest_auth_handler, basic_auth_handler
|
|
||||||
)
|
|
||||||
|
|
||||||
additional_headers = []
|
|
||||||
if user_agent != "":
|
if user_agent != "":
|
||||||
additional_headers.append(("User-agent", user_agent))
|
self._headers.append(("User-agent", user_agent))
|
||||||
if accept_header != "":
|
if accept_header != "":
|
||||||
additional_headers.append(("Accept", accept_header))
|
self._headers.append(("Accept", accept_header))
|
||||||
if len(additional_headers) > 0:
|
|
||||||
if self._opener is None:
|
|
||||||
self._opener = build_opener()
|
|
||||||
self._opener.addheaders = additional_headers
|
|
||||||
|
|
||||||
def _decode_data(self, conn):
|
def set_timeout(self, connection_timeout: float):
|
||||||
if (
|
"""Set the connection timeout.
|
||||||
"Content-Encoding" in conn.headers
|
|
||||||
and conn.headers["Content-Encoding"] == "gzip"
|
|
||||||
):
|
|
||||||
reader = GzipFile(fileobj=conn)
|
|
||||||
else:
|
|
||||||
reader = conn
|
|
||||||
try:
|
|
||||||
return self._decode_stream(reader.read()).replace("\0", "")
|
|
||||||
except zlib.error:
|
|
||||||
self.logger.error(
|
|
||||||
"%s: Failed to uncompress gzip data from url(%s): zlib",
|
|
||||||
self.name,
|
|
||||||
self.url,
|
|
||||||
)
|
|
||||||
except BadGzipFile as gzip_error:
|
|
||||||
self.logger.error(
|
|
||||||
"%s: Failed to uncompress gzip data from url(%s): %s",
|
|
||||||
self.name,
|
|
||||||
self.url,
|
|
||||||
gzip_error.strerror,
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _decode_stream(self, strm):
|
:param connection_timeout: The timeout value in seconds.
|
||||||
for encoding in "utf-8-sig", "utf-8", "utf-16":
|
:type connection_timeout: float
|
||||||
try:
|
"""
|
||||||
return strm.decode(encoding)
|
self.connection_timeout = connection_timeout
|
||||||
except UnicodeDecodeError:
|
|
||||||
continue
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _download_data(self):
|
def _decode_data(self, data):
|
||||||
|
return data.replace("\0", "")
|
||||||
|
|
||||||
|
async def _download_data(self, url): # noqa: C901
|
||||||
"""Download the calendar data."""
|
"""Download the calendar data."""
|
||||||
|
self.logger.debug("%s: _download_data start", self.name)
|
||||||
try:
|
try:
|
||||||
with CalendarData.opener_lock:
|
response = await self._httpx.get(
|
||||||
if self._opener is not None:
|
url,
|
||||||
install_opener(self._opener)
|
auth=self._auth,
|
||||||
with urlopen(self._make_url()) as conn:
|
headers=self._headers,
|
||||||
self._calendar_data = self._decode_data(conn)
|
follow_redirects=True,
|
||||||
except HTTPError as http_error:
|
timeout=self.connection_timeout,
|
||||||
|
)
|
||||||
|
if response.status_code >= 400:
|
||||||
|
raise httpx.HTTPStatusError(
|
||||||
|
"status error", request=None, response=response
|
||||||
|
)
|
||||||
|
self._calendar_data = self._decode_data(response.text)
|
||||||
|
self.logger.debug("%s: _download_data done", self.name)
|
||||||
|
except httpx.HTTPStatusError as http_status_error:
|
||||||
self.logger.error(
|
self.logger.error(
|
||||||
"%s: Failed to open url(%s): %s",
|
"%s: Failed to open url(%s): %s",
|
||||||
self.name,
|
self.name,
|
||||||
self.url,
|
self.url,
|
||||||
http_error.reason,
|
http_status_error.response.status_code,
|
||||||
)
|
)
|
||||||
except ContentTooShortError as content_too_short_error:
|
except httpx.TimeoutException:
|
||||||
self.logger.error(
|
self.logger.error(
|
||||||
"%s: Could not download calendar data: %s",
|
"%s: Timeout opening url: %s", self.name, self.url
|
||||||
self.name,
|
|
||||||
content_too_short_error.reason,
|
|
||||||
)
|
)
|
||||||
except URLError as url_error:
|
except httpx.DecodingError:
|
||||||
self.logger.error(
|
self.logger.error(
|
||||||
"%s: Failed to open url: %s", self.name, url_error.reason
|
"%s: Error decoding data from url: %s", self.name, self.url
|
||||||
|
)
|
||||||
|
except httpx.InvalidURL:
|
||||||
|
self.logger.error("%s: Invalid URL: %s", self.name, self.url)
|
||||||
|
except httpx.HTTPError:
|
||||||
|
self.logger.error(
|
||||||
|
"%s: Error decoding data from url: %s", self.name, self.url
|
||||||
)
|
)
|
||||||
except: # pylint: disable=W0702
|
except: # pylint: disable=W0702
|
||||||
self.logger.error(
|
self.logger.error(
|
||||||
@@ -192,7 +180,45 @@ class CalendarData:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _make_url(self):
|
def _make_url(self):
|
||||||
|
"""Replace templates in url and encode."""
|
||||||
now = hanow()
|
now = hanow()
|
||||||
return self.url.replace("{year}", f"{now.year:04}").replace(
|
year: int = now.year
|
||||||
"{month}", f"{now.month:02}"
|
month: int = now.month
|
||||||
|
url = self.url
|
||||||
|
(month, year, url) = self._get_month_year(url, month, year)
|
||||||
|
return url.replace("{year}", f"{year:04}").replace(
|
||||||
|
"{month}", f"{month:02}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _get_year_as_months(self, url: str, month: int) -> int:
|
||||||
|
year_match = re.search("\\{year([-+])([0-9]+)\\}", url)
|
||||||
|
if year_match:
|
||||||
|
if year_match.group(1) == "-":
|
||||||
|
month = month - (int(year_match.group(2)) * 12)
|
||||||
|
else:
|
||||||
|
month = month + (int(year_match.group(2)) * 12)
|
||||||
|
url = url.replace(year_match.group(0), "{year}")
|
||||||
|
return (month, url)
|
||||||
|
|
||||||
|
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) == "-":
|
||||||
|
month = month - int(month_match.group(2))
|
||||||
|
else:
|
||||||
|
month = month + int(month_match.group(2))
|
||||||
|
if month < 1:
|
||||||
|
year -= floor(abs(month) / 12) + 1
|
||||||
|
month = month % 12
|
||||||
|
if month == 0:
|
||||||
|
month = 12
|
||||||
|
elif month > 12:
|
||||||
|
year += abs(floor(month / 12))
|
||||||
|
month = month % 12
|
||||||
|
if month == 0:
|
||||||
|
month = 12
|
||||||
|
year -= 1
|
||||||
|
url = url.replace(month_match.group(0), "{month}")
|
||||||
|
return (month, year, url)
|
||||||
|
330
custom_components/ics_calendar/config_flow.py
Normal file
@@ -0,0 +1,330 @@
|
|||||||
|
"""Config Flow for ICS Calendar."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
from typing import Any, Dict, Optional, Self
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
import voluptuous as vol
|
||||||
|
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_EXCLUDE,
|
||||||
|
CONF_INCLUDE,
|
||||||
|
CONF_NAME,
|
||||||
|
CONF_PASSWORD,
|
||||||
|
CONF_PREFIX,
|
||||||
|
CONF_URL,
|
||||||
|
CONF_USERNAME,
|
||||||
|
)
|
||||||
|
from homeassistant.helpers.selector import selector
|
||||||
|
|
||||||
|
from . import (
|
||||||
|
CONF_ACCEPT_HEADER,
|
||||||
|
CONF_ADV_CONNECT_OPTS,
|
||||||
|
CONF_CONNECTION_TIMEOUT,
|
||||||
|
CONF_DAYS,
|
||||||
|
CONF_DOWNLOAD_INTERVAL,
|
||||||
|
CONF_INCLUDE_ALL_DAY,
|
||||||
|
CONF_OFFSET_HOURS,
|
||||||
|
CONF_PARSER,
|
||||||
|
CONF_REQUIRES_AUTH,
|
||||||
|
CONF_SET_TIMEOUT,
|
||||||
|
CONF_SUMMARY_DEFAULT,
|
||||||
|
CONF_USER_AGENT,
|
||||||
|
)
|
||||||
|
from .const import CONF_SUMMARY_DEFAULT_DEFAULT, DOMAIN
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
CALENDAR_NAME_SCHEMA = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_NAME): cv.string,
|
||||||
|
vol.Optional(CONF_DAYS, default=1): cv.positive_int,
|
||||||
|
vol.Optional(CONF_INCLUDE_ALL_DAY, default=False): cv.boolean,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
CALENDAR_OPTS_SCHEMA = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Optional(CONF_EXCLUDE, default=""): cv.string,
|
||||||
|
vol.Optional(CONF_INCLUDE, default=""): cv.string,
|
||||||
|
vol.Optional(CONF_PREFIX, default=""): cv.string,
|
||||||
|
vol.Optional(CONF_DOWNLOAD_INTERVAL, default=15): cv.positive_int,
|
||||||
|
vol.Optional(CONF_OFFSET_HOURS, default=0): int,
|
||||||
|
vol.Optional(CONF_PARSER, default="rie"): selector(
|
||||||
|
{"select": {"options": ["rie", "ics"], "mode": "dropdown"}}
|
||||||
|
),
|
||||||
|
vol.Optional(
|
||||||
|
CONF_SUMMARY_DEFAULT, default=CONF_SUMMARY_DEFAULT_DEFAULT
|
||||||
|
): cv.string,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
CONNECT_OPTS_SCHEMA = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_URL): cv.string,
|
||||||
|
vol.Optional(CONF_REQUIRES_AUTH, default=False): cv.boolean,
|
||||||
|
vol.Optional(CONF_ADV_CONNECT_OPTS, default=False): cv.boolean,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
AUTH_OPTS_SCHEMA = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Optional(CONF_USERNAME, default=""): cv.string,
|
||||||
|
vol.Optional(CONF_PASSWORD, default=""): cv.string,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
ADVANCED_CONNECT_OPTS_SCHEMA = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Optional(CONF_ACCEPT_HEADER, default=""): cv.string,
|
||||||
|
vol.Optional(CONF_USER_AGENT, default=""): cv.string,
|
||||||
|
vol.Optional(CONF_SET_TIMEOUT, default=False): cv.boolean,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
TIMEOUT_OPTS_SCHEMA = vol.Schema(
|
||||||
|
{vol.Optional(CONF_CONNECTION_TIMEOUT, default=None): cv.positive_float}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def is_array_string(arr_str: str) -> bool:
|
||||||
|
"""Return true if arr_str starts with [ and ends with ]."""
|
||||||
|
return arr_str.startswith("[") and arr_str.endswith("]")
|
||||||
|
|
||||||
|
|
||||||
|
def format_url(url: str) -> str:
|
||||||
|
"""Format a URL using quote() and ensure any templates are not quoted."""
|
||||||
|
is_quoted = bool(re.search("%[0-9A-Fa-f][0-9A-Fa-f]", url))
|
||||||
|
if not is_quoted:
|
||||||
|
year_match = re.search("\\{(year([-+][0-9]+)?)\\}", url)
|
||||||
|
month_match = re.search("\\{(month([-+][0-9]+)?)\\}", url)
|
||||||
|
has_template: bool = year_match or month_match
|
||||||
|
url = quote(url, safe=":/?&=")
|
||||||
|
if has_template:
|
||||||
|
year_template = year_match.group(1)
|
||||||
|
month_template = month_match.group(1)
|
||||||
|
year_template1 = year_template.replace("+", "%2[Bb]")
|
||||||
|
month_template1 = month_template.replace("+", "%2[Bb]")
|
||||||
|
url = re.sub(
|
||||||
|
f"%7[Bb]{year_template1}%7[Dd]",
|
||||||
|
f"{{{year_template}}}",
|
||||||
|
url,
|
||||||
|
)
|
||||||
|
url = re.sub(
|
||||||
|
f"%7[Bb]{month_template1}%7[Dd]",
|
||||||
|
f"{{{month_template}}}",
|
||||||
|
url,
|
||||||
|
)
|
||||||
|
if url.startswith("webcal://"):
|
||||||
|
url = re.sub("^webcal://", "https://", url)
|
||||||
|
|
||||||
|
return url
|
||||||
|
|
||||||
|
|
||||||
|
class ICSCalendarConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
|
"""Config Flow for ICS Calendar."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
MINOR_VERSION = 0
|
||||||
|
|
||||||
|
data: Optional[Dict[str, Any]]
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Construct ICSCalendarConfigFlow."""
|
||||||
|
self.data = {}
|
||||||
|
|
||||||
|
def is_matching(self, _other_flow: Self) -> bool:
|
||||||
|
"""Match discovery method.
|
||||||
|
|
||||||
|
This method doesn't do anything, because this integration has no
|
||||||
|
discoverable components.
|
||||||
|
"""
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def async_step_reauth(self, user_input=None):
|
||||||
|
"""Re-authenticateon auth error."""
|
||||||
|
# self.reauth_entry = self.hass.config_entries.async_get_entry(
|
||||||
|
# self.context["entry_id"]
|
||||||
|
# )
|
||||||
|
return await self.async_step_reauth_confirm(user_input)
|
||||||
|
|
||||||
|
async def async_step_reauth_confirm(
|
||||||
|
self, user_input=None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Dialog to inform user that reauthentication is required."""
|
||||||
|
if user_input is None:
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="reauth_confirm", data_schema=vol.Schema({})
|
||||||
|
)
|
||||||
|
return await self.async_step_user()
|
||||||
|
|
||||||
|
# Don't allow reconfigure for now!
|
||||||
|
# async def async_step_reconfigure(
|
||||||
|
# self, user_input: dict[str, Any] | None = None
|
||||||
|
# ) -> ConfigFlowResult:
|
||||||
|
# """Reconfigure entry."""
|
||||||
|
# return await self.async_step_user(user_input)
|
||||||
|
|
||||||
|
async def async_step_user(
|
||||||
|
self, user_input: Optional[Dict[str, Any]] = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Start of Config Flow."""
|
||||||
|
errors = {}
|
||||||
|
if user_input is not None:
|
||||||
|
user_input[CONF_NAME] = user_input[CONF_NAME].strip()
|
||||||
|
if not user_input[CONF_NAME]:
|
||||||
|
errors[CONF_NAME] = "empty_name"
|
||||||
|
else:
|
||||||
|
self._async_abort_entries_match(
|
||||||
|
{CONF_NAME: user_input[CONF_NAME]}
|
||||||
|
)
|
||||||
|
|
||||||
|
if not errors:
|
||||||
|
self.data = user_input
|
||||||
|
return await self.async_step_calendar_opts()
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user",
|
||||||
|
data_schema=CALENDAR_NAME_SCHEMA,
|
||||||
|
errors=errors,
|
||||||
|
last_step=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_calendar_opts( # noqa: R701,C901
|
||||||
|
self, user_input: Optional[Dict[str, Any]] = None
|
||||||
|
):
|
||||||
|
"""Calendar Options step for ConfigFlow."""
|
||||||
|
errors = {}
|
||||||
|
if user_input is not None:
|
||||||
|
user_input[CONF_EXCLUDE] = user_input[CONF_EXCLUDE].strip()
|
||||||
|
user_input[CONF_INCLUDE] = user_input[CONF_INCLUDE].strip()
|
||||||
|
if (
|
||||||
|
user_input[CONF_EXCLUDE]
|
||||||
|
and user_input[CONF_EXCLUDE] == user_input[CONF_INCLUDE]
|
||||||
|
):
|
||||||
|
errors[CONF_EXCLUDE] = "exclude_include_cannot_be_the_same"
|
||||||
|
else:
|
||||||
|
if user_input[CONF_EXCLUDE] and not is_array_string(
|
||||||
|
user_input[CONF_EXCLUDE]
|
||||||
|
):
|
||||||
|
errors[CONF_EXCLUDE] = "exclude_must_be_array"
|
||||||
|
if user_input[CONF_INCLUDE] and not is_array_string(
|
||||||
|
user_input[CONF_INCLUDE]
|
||||||
|
):
|
||||||
|
errors[CONF_INCLUDE] = "include_must_be_array"
|
||||||
|
|
||||||
|
if user_input[CONF_DOWNLOAD_INTERVAL] < 15:
|
||||||
|
_LOGGER.error("download_interval_too_small error")
|
||||||
|
errors[CONF_DOWNLOAD_INTERVAL] = "download_interval_too_small"
|
||||||
|
|
||||||
|
if not user_input[CONF_SUMMARY_DEFAULT]:
|
||||||
|
user_input[CONF_SUMMARY_DEFAULT] = CONF_SUMMARY_DEFAULT_DEFAULT
|
||||||
|
|
||||||
|
if not errors:
|
||||||
|
self.data.update(user_input)
|
||||||
|
return await self.async_step_connect_opts()
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="calendar_opts",
|
||||||
|
data_schema=CALENDAR_OPTS_SCHEMA,
|
||||||
|
errors=errors,
|
||||||
|
last_step=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_connect_opts(
|
||||||
|
self, user_input: Optional[Dict[str, Any]] = None
|
||||||
|
):
|
||||||
|
"""Connect Options step for ConfigFlow."""
|
||||||
|
errors = {}
|
||||||
|
if user_input is not None:
|
||||||
|
user_input[CONF_URL] = user_input[CONF_URL].strip()
|
||||||
|
if not user_input[CONF_URL]:
|
||||||
|
errors[CONF_URL] = "empty_url"
|
||||||
|
|
||||||
|
if not errors:
|
||||||
|
user_input[CONF_URL] = format_url(user_input[CONF_URL])
|
||||||
|
|
||||||
|
self.data.update(user_input)
|
||||||
|
if user_input.get(CONF_REQUIRES_AUTH, False):
|
||||||
|
return await self.async_step_auth_opts()
|
||||||
|
if user_input.get(CONF_ADV_CONNECT_OPTS, False):
|
||||||
|
return await self.async_step_adv_connect_opts()
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=self.data[CONF_NAME],
|
||||||
|
data=self.data,
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="connect_opts",
|
||||||
|
data_schema=CONNECT_OPTS_SCHEMA,
|
||||||
|
errors=errors,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_auth_opts(
|
||||||
|
self, user_input: Optional[Dict[str, Any]] = None
|
||||||
|
):
|
||||||
|
"""Auth Options step for ConfigFlow."""
|
||||||
|
if user_input is not None:
|
||||||
|
self.data.update(user_input)
|
||||||
|
if self.data.get(CONF_ADV_CONNECT_OPTS, False):
|
||||||
|
return await self.async_step_adv_connect_opts()
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=self.data[CONF_NAME],
|
||||||
|
data=self.data,
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="auth_opts", data_schema=AUTH_OPTS_SCHEMA
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_adv_connect_opts(
|
||||||
|
self, user_input: Optional[Dict[str, Any]] = None
|
||||||
|
):
|
||||||
|
"""Advanced Connection Options step for ConfigFlow."""
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="adv_connect_opts",
|
||||||
|
data_schema=ADVANCED_CONNECT_OPTS_SCHEMA,
|
||||||
|
errors=errors,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_timeout_opts(
|
||||||
|
self, user_input: Optional[Dict[str, Any]] = None
|
||||||
|
):
|
||||||
|
"""Timeout Options step for ConfigFlow."""
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="timeout_opts",
|
||||||
|
data_schema=TIMEOUT_OPTS_SCHEMA,
|
||||||
|
errors=errors,
|
||||||
|
last_step=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_import(self, import_data):
|
||||||
|
"""Import config from configuration.yaml."""
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=import_data[CONF_NAME],
|
||||||
|
data=import_data,
|
||||||
|
)
|
@@ -1,7 +1,24 @@
|
|||||||
"""Constants for ics_calendar platform."""
|
"""Constants for ics_calendar platform."""
|
||||||
VERSION = "4.1.0"
|
|
||||||
|
VERSION = "5.1.3"
|
||||||
DOMAIN = "ics_calendar"
|
DOMAIN = "ics_calendar"
|
||||||
UPGRADE_URL = (
|
|
||||||
"https://github.com/franc6/ics_calendar/blob/releases/"
|
CONF_DEVICE_ID = "device_id"
|
||||||
"UpgradeTo4.0AndLater.md"
|
CONF_CALENDARS = "calendars"
|
||||||
)
|
CONF_DAYS = "days"
|
||||||
|
CONF_INCLUDE_ALL_DAY = "include_all_day"
|
||||||
|
CONF_PARSER = "parser"
|
||||||
|
CONF_DOWNLOAD_INTERVAL = "download_interval"
|
||||||
|
CONF_USER_AGENT = "user_agent"
|
||||||
|
CONF_OFFSET_HOURS = "offset_hours"
|
||||||
|
CONF_ACCEPT_HEADER = "accept_header"
|
||||||
|
CONF_CONNECTION_TIMEOUT = "connection_timeout"
|
||||||
|
CONF_SET_TIMEOUT = "set_connection_timeout"
|
||||||
|
CONF_REQUIRES_AUTH = "requires_auth"
|
||||||
|
CONF_ADV_CONNECT_OPTS = "advanced_connection_options"
|
||||||
|
CONF_SUMMARY_DEFAULT = "summary_default"
|
||||||
|
# It'd be really nifty if this could be a translatable string, but it seems
|
||||||
|
# that's not supported, unless I want to roll my own interpretation of the
|
||||||
|
# translate/*.json files. :(
|
||||||
|
# See also https://github.com/home-assistant/core/issues/125075
|
||||||
|
CONF_SUMMARY_DEFAULT_DEFAULT = "No title"
|
||||||
|
@@ -1,9 +1,10 @@
|
|||||||
"""Provide Filter class."""
|
"""Provide Filter class."""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from ast import literal_eval
|
from ast import literal_eval
|
||||||
from typing import List, Optional, Pattern
|
from typing import List, Optional, Pattern
|
||||||
|
|
||||||
from homeassistant.components.calendar import CalendarEvent
|
from .parserevent import ParserEvent
|
||||||
|
|
||||||
|
|
||||||
class Filter:
|
class Filter:
|
||||||
@@ -113,11 +114,11 @@ class Filter:
|
|||||||
add_event = self._is_included(summary, description)
|
add_event = self._is_included(summary, description)
|
||||||
return add_event
|
return add_event
|
||||||
|
|
||||||
def filter_event(self, event: CalendarEvent) -> bool:
|
def filter_event(self, event: ParserEvent) -> bool:
|
||||||
"""Check if the event should be included or not.
|
"""Check if the event should be included or not.
|
||||||
|
|
||||||
:param event: The event to examine
|
:param event: The event to examine
|
||||||
:type event: CalendarEvent
|
:type event: ParserEvent
|
||||||
:return: true if the event should be included, otherwise false
|
:return: true if the event should be included, otherwise false
|
||||||
:rtype: bool
|
:rtype: bool
|
||||||
"""
|
"""
|
||||||
|
27
custom_components/ics_calendar/getparser.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
"""Provide GetParser class."""
|
||||||
|
|
||||||
|
from .icalendarparser import ICalendarParser
|
||||||
|
from .parsers.parser_ics import ParserICS
|
||||||
|
from .parsers.parser_rie import ParserRIE
|
||||||
|
|
||||||
|
|
||||||
|
class GetParser: # pylint: disable=R0903
|
||||||
|
"""Provide get_parser to return an instance of ICalendarParser.
|
||||||
|
|
||||||
|
The class provides a static method , get_instace, to get a parser instance.
|
||||||
|
The non static methods allow this class to act as an "interface" for the
|
||||||
|
parser classes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_parser(parser: str, *args) -> ICalendarParser | None:
|
||||||
|
"""Get an instance of the requested parser."""
|
||||||
|
# parser_cls = ICalendarParser.get_class(parser)
|
||||||
|
# if parser_cls is not None:
|
||||||
|
# return parser_cls(*args)
|
||||||
|
if parser == "rie":
|
||||||
|
return ParserRIE(*args)
|
||||||
|
if parser == "ics":
|
||||||
|
return ParserICS(*args)
|
||||||
|
|
||||||
|
return None
|
@@ -1,39 +1,14 @@
|
|||||||
"""Provide ICalendarParser class."""
|
"""Provide ICalendarParser class."""
|
||||||
import importlib
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from homeassistant.components.calendar import CalendarEvent
|
|
||||||
|
|
||||||
from .filter import Filter
|
from .filter import Filter
|
||||||
|
from .parserevent import ParserEvent
|
||||||
|
|
||||||
|
|
||||||
class ICalendarParser:
|
class ICalendarParser:
|
||||||
"""Provide interface for various parser classes.
|
"""Provide interface for various parser classes."""
|
||||||
|
|
||||||
The class provides a static method , get_instace, to get a parser instance.
|
|
||||||
The non static methods allow this class to act as an "interface" for the
|
|
||||||
parser classes.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_class(parser: str):
|
|
||||||
"""Get the class of the requested parser."""
|
|
||||||
parser_module_name = ".parsers.parser_" + parser
|
|
||||||
parser = "Parser" + parser.upper()
|
|
||||||
try:
|
|
||||||
module = importlib.import_module(parser_module_name, __package__)
|
|
||||||
return getattr(module, parser)
|
|
||||||
except ImportError:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_instance(parser: str, *args):
|
|
||||||
"""Get an instance of the requested parser."""
|
|
||||||
parser_cls = ICalendarParser.get_class(parser)
|
|
||||||
if parser_cls is not None:
|
|
||||||
return parser_cls(*args)
|
|
||||||
return None
|
|
||||||
|
|
||||||
def set_content(self, content: str):
|
def set_content(self, content: str):
|
||||||
"""Parse content into a calendar object.
|
"""Parse content into a calendar object.
|
||||||
@@ -57,7 +32,7 @@ class ICalendarParser:
|
|||||||
end: datetime,
|
end: datetime,
|
||||||
include_all_day: bool,
|
include_all_day: bool,
|
||||||
offset_hours: int = 0,
|
offset_hours: int = 0,
|
||||||
) -> list[CalendarEvent]:
|
) -> list[ParserEvent]:
|
||||||
"""Get a list of events.
|
"""Get a list of events.
|
||||||
|
|
||||||
Gets the events from start to end, including or excluding all day
|
Gets the events from start to end, including or excluding all day
|
||||||
@@ -71,7 +46,7 @@ class ICalendarParser:
|
|||||||
:param offset_hours the number of hours to offset the event
|
:param offset_hours the number of hours to offset the event
|
||||||
:type offset_hours int
|
:type offset_hours int
|
||||||
:returns a list of events, or an empty list
|
:returns a list of events, or an empty list
|
||||||
:rtype list[CalendarEvent]
|
:rtype list[ParserEvent]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def get_current_event(
|
def get_current_event(
|
||||||
@@ -80,7 +55,7 @@ class ICalendarParser:
|
|||||||
now: datetime,
|
now: datetime,
|
||||||
days: int,
|
days: int,
|
||||||
offset_hours: int = 0,
|
offset_hours: int = 0,
|
||||||
) -> Optional[CalendarEvent]:
|
) -> Optional[ParserEvent]:
|
||||||
"""Get the current or next event.
|
"""Get the current or next event.
|
||||||
|
|
||||||
Gets the current event, or the next upcoming event with in the
|
Gets the current event, or the next upcoming event with in the
|
||||||
@@ -93,5 +68,5 @@ class ICalendarParser:
|
|||||||
:type days int
|
:type days int
|
||||||
:param offset_hours the number of hours to offset the event
|
:param offset_hours the number of hours to offset the event
|
||||||
:type offset_hours int
|
:type offset_hours int
|
||||||
:returns a CalendarEvent or None
|
:returns a ParserEvent or None
|
||||||
"""
|
"""
|
||||||
|
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
|
|
||||||
"domain": "ics_calendar",
|
"domain": "ics_calendar",
|
||||||
"name": "ics Calendar",
|
"name": "ics Calendar",
|
||||||
"codeowners": ["@franc6"],
|
"codeowners": ["@franc6"],
|
||||||
|
"config_flow": true,
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
"documentation": "https://github.com/franc6/ics_calendar",
|
"documentation": "https://github.com/franc6/ics_calendar",
|
||||||
"integration_type": "service",
|
"integration_type": "service",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"issue_tracker": "https://github.com/franc6/ics_calendar/issues",
|
"issue_tracker": "https://github.com/franc6/ics_calendar/issues",
|
||||||
"requirements": ["ics>=0.7.2", "recurring_ical_events>=2.0.2", "icalendar>=5.0.4"],
|
"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": "4.1.0"
|
"version": "5.1.3"
|
||||||
}
|
}
|
||||||
|
20
custom_components/ics_calendar/parserevent.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
"""Provide ParserEvent class."""
|
||||||
|
|
||||||
|
import dataclasses
|
||||||
|
|
||||||
|
from homeassistant.components.calendar import CalendarEvent
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class ParserEvent(CalendarEvent):
|
||||||
|
"""Class to represent CalendarEvent without validation."""
|
||||||
|
|
||||||
|
def validate(self) -> None:
|
||||||
|
"""Invoke __post_init__ from CalendarEvent."""
|
||||||
|
return super().__post_init__()
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
"""Don't do validation steps for this class."""
|
||||||
|
# This is necessary to prevent problems when creating events that don't
|
||||||
|
# have a summary. We'll add a summary after the event is created, not
|
||||||
|
# before, to reduce code repitition.
|
@@ -1,14 +1,15 @@
|
|||||||
"""Support for ics parser."""
|
"""Support for ics parser."""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from datetime import date, datetime, timedelta
|
from datetime import date, datetime, timedelta
|
||||||
from typing import Optional, Union
|
from typing import Optional, Union
|
||||||
|
|
||||||
from arrow import Arrow, get as arrowget
|
from arrow import Arrow, get as arrowget
|
||||||
from homeassistant.components.calendar import CalendarEvent
|
|
||||||
from ics import Calendar
|
from ics import Calendar
|
||||||
|
|
||||||
from ..filter import Filter
|
from ..filter import Filter
|
||||||
from ..icalendarparser import ICalendarParser
|
from ..icalendarparser import ICalendarParser
|
||||||
|
from ..parserevent import ParserEvent
|
||||||
from ..utility import compare_event_dates
|
from ..utility import compare_event_dates
|
||||||
|
|
||||||
|
|
||||||
@@ -41,7 +42,7 @@ class ParserICS(ICalendarParser):
|
|||||||
|
|
||||||
def get_event_list(
|
def get_event_list(
|
||||||
self, start, end, include_all_day: bool, offset_hours: int = 0
|
self, start, end, include_all_day: bool, offset_hours: int = 0
|
||||||
) -> list[CalendarEvent]:
|
) -> list[ParserEvent]:
|
||||||
"""Get a list of events.
|
"""Get a list of events.
|
||||||
|
|
||||||
Gets the events from start to end, including or excluding all day
|
Gets the events from start to end, including or excluding all day
|
||||||
@@ -55,9 +56,9 @@ class ParserICS(ICalendarParser):
|
|||||||
:param offset_hours the number of hours to offset the event
|
:param offset_hours the number of hours to offset the event
|
||||||
:type offset_hours int
|
:type offset_hours int
|
||||||
:returns a list of events, or an empty list
|
:returns a list of events, or an empty list
|
||||||
:rtype list[CalendarEvent]
|
:rtype list[ParserEvent]
|
||||||
"""
|
"""
|
||||||
event_list: list[CalendarEvent] = []
|
event_list: list[ParserEvent] = []
|
||||||
|
|
||||||
if self._calendar is not None:
|
if self._calendar is not None:
|
||||||
# ics 0.8 takes datetime not Arrow objects
|
# ics 0.8 takes datetime not Arrow objects
|
||||||
@@ -75,7 +76,7 @@ class ParserICS(ICalendarParser):
|
|||||||
# summary = event.summary
|
# summary = event.summary
|
||||||
# elif hasattr(event, "name"):
|
# elif hasattr(event, "name"):
|
||||||
summary = event.name
|
summary = event.name
|
||||||
calendar_event: CalendarEvent = CalendarEvent(
|
calendar_event: ParserEvent = ParserEvent(
|
||||||
summary=summary,
|
summary=summary,
|
||||||
start=ParserICS.get_date(
|
start=ParserICS.get_date(
|
||||||
event.begin, event.all_day, offset_hours
|
event.begin, event.all_day, offset_hours
|
||||||
@@ -97,7 +98,7 @@ class ParserICS(ICalendarParser):
|
|||||||
now: datetime,
|
now: datetime,
|
||||||
days: int,
|
days: int,
|
||||||
offset_hours: int = 0,
|
offset_hours: int = 0,
|
||||||
) -> Optional[CalendarEvent]:
|
) -> Optional[ParserEvent]:
|
||||||
"""Get the current or next event.
|
"""Get the current or next event.
|
||||||
|
|
||||||
Gets the current event, or the next upcoming event with in the
|
Gets the current event, or the next upcoming event with in the
|
||||||
@@ -110,7 +111,7 @@ class ParserICS(ICalendarParser):
|
|||||||
:type int
|
:type int
|
||||||
:param offset_hours the number of hours to offset the event
|
:param offset_hours the number of hours to offset the event
|
||||||
:type int
|
:type int
|
||||||
:returns a CalendarEvent or None
|
:returns a ParserEvent or None
|
||||||
"""
|
"""
|
||||||
if self._calendar is None:
|
if self._calendar is None:
|
||||||
return None
|
return None
|
||||||
@@ -144,7 +145,7 @@ class ParserICS(ICalendarParser):
|
|||||||
# summary = temp_event.summary
|
# summary = temp_event.summary
|
||||||
# elif hasattr(event, "name"):
|
# elif hasattr(event, "name"):
|
||||||
summary = temp_event.name
|
summary = temp_event.name
|
||||||
return CalendarEvent(
|
return ParserEvent(
|
||||||
summary=summary,
|
summary=summary,
|
||||||
start=ParserICS.get_date(
|
start=ParserICS.get_date(
|
||||||
temp_event.begin, temp_event.all_day, offset_hours
|
temp_event.begin, temp_event.all_day, offset_hours
|
||||||
|
@@ -1,13 +1,14 @@
|
|||||||
"""Support for recurring_ical_events parser."""
|
"""Support for recurring_ical_events parser."""
|
||||||
|
|
||||||
from datetime import date, datetime, timedelta
|
from datetime import date, datetime, timedelta
|
||||||
from typing import Optional, Union
|
from typing import Optional, Union
|
||||||
|
|
||||||
import recurring_ical_events as rie
|
import recurring_ical_events as rie
|
||||||
from homeassistant.components.calendar import CalendarEvent
|
|
||||||
from icalendar import Calendar
|
from icalendar import Calendar
|
||||||
|
|
||||||
from ..filter import Filter
|
from ..filter import Filter
|
||||||
from ..icalendarparser import ICalendarParser
|
from ..icalendarparser import ICalendarParser
|
||||||
|
from ..parserevent import ParserEvent
|
||||||
from ..utility import compare_event_dates
|
from ..utility import compare_event_dates
|
||||||
|
|
||||||
|
|
||||||
@@ -45,7 +46,7 @@ class ParserRIE(ICalendarParser):
|
|||||||
end: datetime,
|
end: datetime,
|
||||||
include_all_day: bool,
|
include_all_day: bool,
|
||||||
offset_hours: int = 0,
|
offset_hours: int = 0,
|
||||||
) -> list[CalendarEvent]:
|
) -> list[ParserEvent]:
|
||||||
"""Get a list of events.
|
"""Get a list of events.
|
||||||
|
|
||||||
Gets the events from start to end, including or excluding all day
|
Gets the events from start to end, including or excluding all day
|
||||||
@@ -59,12 +60,12 @@ class ParserRIE(ICalendarParser):
|
|||||||
:param offset_hours the number of hours to offset the event
|
:param offset_hours the number of hours to offset the event
|
||||||
:type offset_hours int
|
:type offset_hours int
|
||||||
:returns a list of events, or an empty list
|
:returns a list of events, or an empty list
|
||||||
:rtype list[CalendarEvent]
|
:rtype list[ParserEvent]
|
||||||
"""
|
"""
|
||||||
event_list: list[CalendarEvent] = []
|
event_list: list[ParserEvent] = []
|
||||||
|
|
||||||
if self._calendar is not None:
|
if self._calendar is not None:
|
||||||
for event in rie.of(self._calendar).between(
|
for event in rie.of(self._calendar, skip_bad_series=True).between(
|
||||||
start - timedelta(hours=offset_hours),
|
start - timedelta(hours=offset_hours),
|
||||||
end - timedelta(hours=offset_hours),
|
end - timedelta(hours=offset_hours),
|
||||||
):
|
):
|
||||||
@@ -73,7 +74,7 @@ class ParserRIE(ICalendarParser):
|
|||||||
if all_day and not include_all_day:
|
if all_day and not include_all_day:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
calendar_event: CalendarEvent = CalendarEvent(
|
calendar_event: ParserEvent = ParserEvent(
|
||||||
summary=event.get("SUMMARY"),
|
summary=event.get("SUMMARY"),
|
||||||
start=start,
|
start=start,
|
||||||
end=end,
|
end=end,
|
||||||
@@ -91,7 +92,7 @@ class ParserRIE(ICalendarParser):
|
|||||||
now: datetime,
|
now: datetime,
|
||||||
days: int,
|
days: int,
|
||||||
offset_hours: int = 0,
|
offset_hours: int = 0,
|
||||||
) -> Optional[CalendarEvent]:
|
) -> Optional[ParserEvent]:
|
||||||
"""Get the current or next event.
|
"""Get the current or next event.
|
||||||
|
|
||||||
Gets the current event, or the next upcoming event with in the
|
Gets the current event, or the next upcoming event with in the
|
||||||
@@ -104,17 +105,17 @@ class ParserRIE(ICalendarParser):
|
|||||||
:type int
|
:type int
|
||||||
:param offset_hours the number of hours to offset the event
|
:param offset_hours the number of hours to offset the event
|
||||||
:type offset_hours int
|
:type offset_hours int
|
||||||
:returns a CalendarEvent or None
|
:returns a ParserEvent or None
|
||||||
"""
|
"""
|
||||||
if self._calendar is None:
|
if self._calendar is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
temp_event: CalendarEvent = None
|
temp_event = None
|
||||||
temp_start: date | datetime = None
|
temp_start: date | datetime = None
|
||||||
temp_end: date | datetime = None
|
temp_end: date | datetime = None
|
||||||
temp_all_day: bool = None
|
temp_all_day: bool = None
|
||||||
end: datetime = now + timedelta(days=days)
|
end: datetime = now + timedelta(days=days)
|
||||||
for event in rie.of(self._calendar).between(
|
for event in rie.of(self._calendar, skip_bad_series=True).between(
|
||||||
now - timedelta(hours=offset_hours),
|
now - timedelta(hours=offset_hours),
|
||||||
end - timedelta(hours=offset_hours),
|
end - timedelta(hours=offset_hours),
|
||||||
):
|
):
|
||||||
@@ -139,7 +140,7 @@ class ParserRIE(ICalendarParser):
|
|||||||
if temp_event is None:
|
if temp_event is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return CalendarEvent(
|
return ParserEvent(
|
||||||
summary=temp_event.get("SUMMARY"),
|
summary=temp_event.get("SUMMARY"),
|
||||||
start=temp_start,
|
start=temp_start,
|
||||||
end=temp_end,
|
end=temp_end,
|
||||||
|
77
custom_components/ics_calendar/strings.json
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
{
|
||||||
|
"issues": {
|
||||||
|
"YAML_Warning": {
|
||||||
|
"title": "YAML configuration is deprecated for ICS Calendar",
|
||||||
|
"description": "YAML configuration of ics_calendar is deprecated and will be removed in ics_calendar v5.0.0. Your configuration items have been imported. Please remove them from your configuration.yaml file."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "ICS Calendar",
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"name": "Name",
|
||||||
|
"days": "Days",
|
||||||
|
"include_all_day": "Include all day events?"
|
||||||
|
},
|
||||||
|
"title": "Add Calendar"
|
||||||
|
},
|
||||||
|
"calendar_opts": {
|
||||||
|
"data": {
|
||||||
|
"exclude": "Exclude filter",
|
||||||
|
"include": "Include filter",
|
||||||
|
"prefix": "String to prefix all event summaries",
|
||||||
|
"download_interval": "Download interval (minutes)",
|
||||||
|
"offset_hours": "Number of hours to offset event times",
|
||||||
|
"parser": "Parser (rie or ics)",
|
||||||
|
"summary_default": "Summary if event doesn't have one"
|
||||||
|
},
|
||||||
|
"title": "Calendar Options"
|
||||||
|
},
|
||||||
|
"connect_opts": {
|
||||||
|
"data": {
|
||||||
|
"url": "URL of ICS file",
|
||||||
|
"requires_auth": "Requires authentication?",
|
||||||
|
"advanced_connection_options": "Set advanced connection options?"
|
||||||
|
},
|
||||||
|
"title": "Connection Options"
|
||||||
|
},
|
||||||
|
"auth_opts": {
|
||||||
|
"data": {
|
||||||
|
"username": "Username",
|
||||||
|
"password": "Password"
|
||||||
|
},
|
||||||
|
"description": "Please note this component supports only HTTP Basic Auth and HTTP Digest Auth. More advanced authentication, like OAuth is not supported at this time.",
|
||||||
|
"title": "Authentication"
|
||||||
|
},
|
||||||
|
"adv_connect_opts": {
|
||||||
|
"data": {
|
||||||
|
"accept_header": "Custom Accept header for broken servers",
|
||||||
|
"user_agent": "Custom User-agent header",
|
||||||
|
"set_connection_timeout": "Change connection timeout?"
|
||||||
|
},
|
||||||
|
"title": "Advanced Connection Options"
|
||||||
|
},
|
||||||
|
"timeout_opts": {
|
||||||
|
"data": {
|
||||||
|
"connection_timeout": "Connection timeout in seconds"
|
||||||
|
},
|
||||||
|
"title": "Connection Timeout Options"
|
||||||
|
},
|
||||||
|
"reauth_confirm": {
|
||||||
|
"description": "Authorization failed for calendar. Please re-configured the calendar URL and/or authentication settings.",
|
||||||
|
"title": "Authorization Failure for ICS Calendar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"empty_name": "The calendar name must not be empty.",
|
||||||
|
"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."
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
76
custom_components/ics_calendar/translations/de.json
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
{
|
||||||
|
"issues": {
|
||||||
|
"YAML_Warning": {
|
||||||
|
"title": "YAML-Konfiguration für ICS-Kalender ist veraltet",
|
||||||
|
"description": "Die YAML-Konfiguration von ics_calendar ist veraltet und wird in ics_calendar v5.0.0 entfernt. Deine Konfigurationselemente wurden importiert. Bitte entferne sie aus deiner configuration.yaml-Datei."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "ICS-Kalender",
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"name": "Name",
|
||||||
|
"days": "Tage",
|
||||||
|
"include_all_day": "Ganztägige Ereignisse einbeziehen?"
|
||||||
|
},
|
||||||
|
"title": "Kalender hinzufügen"
|
||||||
|
},
|
||||||
|
"calendar_opts": {
|
||||||
|
"data": {
|
||||||
|
"exclude": "auszuschließende Ereignisse",
|
||||||
|
"include": "einzuschließende Ereignisse",
|
||||||
|
"prefix": "String, um allen Zusammenfassungen ein Präfix hinzuzufügen",
|
||||||
|
"download_interval": "Download-Intervall (Minuten)",
|
||||||
|
"offset_hours": "Anzahl der Stunden, um Ereigniszeiten zu versetzen",
|
||||||
|
"parser": "Parser (rie oder ics)"
|
||||||
|
},
|
||||||
|
"title": "Kalender-Optionen"
|
||||||
|
},
|
||||||
|
"connect_opts": {
|
||||||
|
"data": {
|
||||||
|
"url": "URL der ICS-Datei",
|
||||||
|
"requires_auth": "Erfordert Authentifizierung?",
|
||||||
|
"advanced_connection_options": "Erweiterte Verbindungsoptionen festlegen?"
|
||||||
|
},
|
||||||
|
"title": "Verbindungsoptionen"
|
||||||
|
},
|
||||||
|
"auth_opts": {
|
||||||
|
"data": {
|
||||||
|
"username": "Benutzername",
|
||||||
|
"password": "Passwort"
|
||||||
|
},
|
||||||
|
"description": "Bitte beachte, dass nur HTTP Basic Auth und HTTP Digest Auth unterstützt wird. Authentifizierungsmethoden wie OAuth werden derzeit nicht unterstützt.",
|
||||||
|
"title": "Authentifizierung"
|
||||||
|
},
|
||||||
|
"adv_connect_opts": {
|
||||||
|
"data": {
|
||||||
|
"accept_header": "Eigener Accept-Header für fehlerhafte Server",
|
||||||
|
"user_agent": "Eigener User-Agent-Header",
|
||||||
|
"set_connection_timeout": "Verbindungstimeout ändern?"
|
||||||
|
},
|
||||||
|
"title": "Erweiterte Verbindungsoptionen"
|
||||||
|
},
|
||||||
|
"timeout_opts": {
|
||||||
|
"data": {
|
||||||
|
"connection_timeout": "Verbindungstimeout in Sekunden"
|
||||||
|
},
|
||||||
|
"title": "Verbindungstimeout-Optionen"
|
||||||
|
},
|
||||||
|
"reauth_confirm": {
|
||||||
|
"description": "Die Autorisierung für den Kalender ist fehlgeschlagen. Bitte konfiguriere die Kalender-URL und/oder die Authentifizierungseinstellungen neu.",
|
||||||
|
"title": "Autorisierungsfehler für ICS-Kalender"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"empty_name": "Der Kalendername darf nicht leer sein.",
|
||||||
|
"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."
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
77
custom_components/ics_calendar/translations/en.json
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
{
|
||||||
|
"issues": {
|
||||||
|
"YAML_Warning": {
|
||||||
|
"title": "YAML configuration is deprecated for ICS Calendar",
|
||||||
|
"description": "YAML configuration of ics_calendar is deprecated and will be removed in ics_calendar v5.0.0. Your configuration items have been imported. Please remove them from your configuration.yaml file."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "ICS Calendar",
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"name": "Name",
|
||||||
|
"days": "Days",
|
||||||
|
"include_all_day": "Include all day events?"
|
||||||
|
},
|
||||||
|
"title": "Add Calendar"
|
||||||
|
},
|
||||||
|
"calendar_opts": {
|
||||||
|
"data": {
|
||||||
|
"exclude": "Exclude filter",
|
||||||
|
"include": "Include filter",
|
||||||
|
"prefix": "String to prefix all event summaries",
|
||||||
|
"download_interval": "Download interval (minutes)",
|
||||||
|
"offset_hours": "Number of hours to offset event times",
|
||||||
|
"parser": "Parser (rie or ics)",
|
||||||
|
"summary_default": "Summary if event doesn't have one"
|
||||||
|
},
|
||||||
|
"title": "Calendar Options"
|
||||||
|
},
|
||||||
|
"connect_opts": {
|
||||||
|
"data": {
|
||||||
|
"url": "URL of ICS file",
|
||||||
|
"requires_auth": "Requires authentication?",
|
||||||
|
"advanced_connection_options": "Set advanced connection options?"
|
||||||
|
},
|
||||||
|
"title": "Connection Options"
|
||||||
|
},
|
||||||
|
"auth_opts": {
|
||||||
|
"data": {
|
||||||
|
"username": "Username",
|
||||||
|
"password": "Password"
|
||||||
|
},
|
||||||
|
"description": "Please note this component supports only HTTP Basic Auth and HTTP Digest Auth. More advanced authentication, like OAuth is not supported at this time.",
|
||||||
|
"title": "Authentication"
|
||||||
|
},
|
||||||
|
"adv_connect_opts": {
|
||||||
|
"data": {
|
||||||
|
"accept_header": "Custom Accept header for broken servers",
|
||||||
|
"user_agent": "Custom User-agent header",
|
||||||
|
"set_connection_timeout": "Change connection timeout?"
|
||||||
|
},
|
||||||
|
"title": "Advanced Connection Options"
|
||||||
|
},
|
||||||
|
"timeout_opts": {
|
||||||
|
"data": {
|
||||||
|
"connection_timeout": "Connection timeout in seconds"
|
||||||
|
},
|
||||||
|
"title": "Connection Timeout Options"
|
||||||
|
},
|
||||||
|
"reauth_confirm": {
|
||||||
|
"description": "Authorization failed for calendar. Please re-configured the calendar URL and/or authentication settings.",
|
||||||
|
"title": "Authorization Failure for ICS Calendar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"empty_name": "The calendar name must not be empty.",
|
||||||
|
"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."
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
77
custom_components/ics_calendar/translations/es.json
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
{
|
||||||
|
"issues": {
|
||||||
|
"YAML_Warning": {
|
||||||
|
"title": "La configuración YAML está obsoleta para ICS Calendar",
|
||||||
|
"description": "La configuración YAML de ics_calendar está obsoleta y se eliminará en ics_calendar v5.0.0. Sus elementos de configuración se han importado. Elimínelos de su archivo configuration.yaml."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "ICS Calendar",
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"name": "Nombre",
|
||||||
|
"days": "Días",
|
||||||
|
"include_all_day": "¿Incluir eventos de todo el día?"
|
||||||
|
},
|
||||||
|
"title": "Agregar calendario"
|
||||||
|
},
|
||||||
|
"calendar_opts": {
|
||||||
|
"data": {
|
||||||
|
"exclude": "Excluir filtro",
|
||||||
|
"include": "Incluir filtro",
|
||||||
|
"prefix": "Cadena que precederá a todos los resúmenes de eventos",
|
||||||
|
"download_interval": "Intervalo de descarga (minutos)",
|
||||||
|
"offset_hours": "Número de horas para compensar los tiempos del evento",
|
||||||
|
"parser": "Parser (rie or ics)",
|
||||||
|
"summary_default": "Resumen si el evento no tiene uno"
|
||||||
|
},
|
||||||
|
"title": "Opciones de calendario"
|
||||||
|
},
|
||||||
|
"connect_opts": {
|
||||||
|
"data": {
|
||||||
|
"url": "URL del archivo ICS",
|
||||||
|
"requires_auth": "¿Requiere autentificación?",
|
||||||
|
"advanced_connection_options": "¿Establecer opciones de conexión avanzadas?"
|
||||||
|
},
|
||||||
|
"title": "Opciones de conexión"
|
||||||
|
},
|
||||||
|
"auth_opts": {
|
||||||
|
"data": {
|
||||||
|
"username": "Nombre de usuario",
|
||||||
|
"password": "Contraseña"
|
||||||
|
},
|
||||||
|
"description": "Tenga en cuenta que este componente solo admite la autenticación básica HTTP y la autenticación HTTP Digest. Actualmente, no se admiten autenticaciones más avanzadas, como OAuth.",
|
||||||
|
"title": "Autentificación"
|
||||||
|
},
|
||||||
|
"adv_connect_opts": {
|
||||||
|
"data": {
|
||||||
|
"accept_header": "Encabezado Accept personalizado para servidores rotos",
|
||||||
|
"user_agent": "Encabezado de agente de usuario personalizado",
|
||||||
|
"set_connection_timeout": "¿Cambiar el tiempo de espera de la conexión?"
|
||||||
|
},
|
||||||
|
"title": "Opciones avanzadas de conexión"
|
||||||
|
},
|
||||||
|
"timeout_opts": {
|
||||||
|
"data": {
|
||||||
|
"connection_timeout": "Tiempo de espera de la conexión en segundos"
|
||||||
|
},
|
||||||
|
"title": "Opciones de tiempo de espera de la conexión"
|
||||||
|
},
|
||||||
|
"reauth_confirm": {
|
||||||
|
"description": "Error de autorización para el calendario. Vuelva a configurar la URL del calendario y/o los ajustes de autenticación.",
|
||||||
|
"title": "Fallo de autorización para ICS Calendar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"empty_name": "El nombre del calendario no debe estar vacío.",
|
||||||
|
"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."
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
76
custom_components/ics_calendar/translations/fr.json
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
{
|
||||||
|
"issues": {
|
||||||
|
"YAML_Warning": {
|
||||||
|
"title": "La configuration YAML pour ICS Calendar est obsolète",
|
||||||
|
"description": "La configuration YAML d'ICS Calendar est obsolète et sera supprimée dans la version 5.0.0 d'ics_calendar. Les éléments de votre configuration ont été importés. Veuillez les supprimer de votre fichier configuration.yaml."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "ICS Calendar",
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"name": "Nom",
|
||||||
|
"days": "Jours",
|
||||||
|
"include_all_day": "Inclure les événements à la journée ?"
|
||||||
|
},
|
||||||
|
"title": "Ajouter un calendrier"
|
||||||
|
},
|
||||||
|
"calendar_opts": {
|
||||||
|
"data": {
|
||||||
|
"exclude": "Exclure les événements contenant",
|
||||||
|
"include": "Inclure les événements contenant",
|
||||||
|
"prefix": "Préfixer tous les résumés d'événements avec",
|
||||||
|
"download_interval": "Intervalle de téléchargement (minutes)",
|
||||||
|
"offset_hours": "Décalage à appliquer aux horaires des événements (heures)",
|
||||||
|
"parser": "Parseur (rie ou ics)"
|
||||||
|
},
|
||||||
|
"title": "Options du calendrier"
|
||||||
|
},
|
||||||
|
"connect_opts": {
|
||||||
|
"data": {
|
||||||
|
"url": "URL du fichier ICS",
|
||||||
|
"requires_auth": "Authentification requise ?",
|
||||||
|
"advanced_connection_options": "Définir les options avancées de la connexion ?"
|
||||||
|
},
|
||||||
|
"title": "Options de connexion"
|
||||||
|
},
|
||||||
|
"auth_opts": {
|
||||||
|
"data": {
|
||||||
|
"username": "Utilisateur",
|
||||||
|
"password": "Mot de passe"
|
||||||
|
},
|
||||||
|
"description": "Veuillez noter que cette intégration ne supporte que les modes d'authentification HTTP Basic et HTTP Digest. Les méthodes d'authentification plus avancées, telles que OAuth, ne sont pas supportées actuellement.",
|
||||||
|
"title": "Authentification"
|
||||||
|
},
|
||||||
|
"adv_connect_opts": {
|
||||||
|
"data": {
|
||||||
|
"accept_header": "Entête 'Accept' personnalisée pour les serveurs injoignables",
|
||||||
|
"user_agent": "Entête 'User-agent' personnalisée",
|
||||||
|
"set_connection_timeout": "Modifier le délai maximum autorisé pour la connexion ?"
|
||||||
|
},
|
||||||
|
"title": "Options avancées de connexion"
|
||||||
|
},
|
||||||
|
"timeout_opts": {
|
||||||
|
"data": {
|
||||||
|
"connection_timeout": "Délai maximum autorisé pour la connexion (secondes)"
|
||||||
|
},
|
||||||
|
"title": "Options de délai de connexion"
|
||||||
|
},
|
||||||
|
"reauth_confirm": {
|
||||||
|
"description": "L'autorisation a échoué pour le calendrier. Veuillez vérifier l'URL du calendrier et/ou les paramètres d'authentification.",
|
||||||
|
"title": "Échec d'autorisation pour ICS Calendar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"empty_name": "Le nom du calendrier doit être renseigné.",
|
||||||
|
"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."
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
77
custom_components/ics_calendar/translations/pt-br.json
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
{
|
||||||
|
"issues": {
|
||||||
|
"YAML_Warning": {
|
||||||
|
"title": "A configuração YAML está obsoleta para o ICS Calendar",
|
||||||
|
"description": "A configuração YAML do ics_calendar está obsoleta e será removida na versão 5.0.0 do ics_calendar. Seus itens de configuração foram importados. Por favor, remova-os do seu arquivo configuration.yaml."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "ICS Calendar",
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"name": "Nome",
|
||||||
|
"days": "Dias",
|
||||||
|
"include_all_day": "Incluir eventos de dia inteiro?"
|
||||||
|
},
|
||||||
|
"title": "Adicionar Calendário"
|
||||||
|
},
|
||||||
|
"calendar_opts": {
|
||||||
|
"data": {
|
||||||
|
"exclude": "Filtro de exclusão",
|
||||||
|
"include": "Filtro de inclusão",
|
||||||
|
"prefix": "Texto para prefixar todos os resumos de eventos",
|
||||||
|
"download_interval": "Intervalo de download (minutos)",
|
||||||
|
"offset_hours": "Número de horas para ajustar os horários dos eventos",
|
||||||
|
"parser": "Parser (rie ou ics)",
|
||||||
|
"summary_default": "Resumo padrão se o evento não tiver um"
|
||||||
|
},
|
||||||
|
"title": "Opções do Calendário"
|
||||||
|
},
|
||||||
|
"connect_opts": {
|
||||||
|
"data": {
|
||||||
|
"url": "URL do arquivo ICS",
|
||||||
|
"requires_auth": "Requer autenticação?",
|
||||||
|
"advanced_connection_options": "Definir opções de conexão avançadas?"
|
||||||
|
},
|
||||||
|
"title": "Opções de Conexão"
|
||||||
|
},
|
||||||
|
"auth_opts": {
|
||||||
|
"data": {
|
||||||
|
"username": "Usuário",
|
||||||
|
"password": "Senha"
|
||||||
|
},
|
||||||
|
"description": "Este componente oferece suporte apenas para HTTP Basic Auth e HTTP Digest Auth. Métodos de autenticação mais avançados, como OAuth, ainda não são suportados.",
|
||||||
|
"title": "Autenticação"
|
||||||
|
},
|
||||||
|
"adv_connect_opts": {
|
||||||
|
"data": {
|
||||||
|
"accept_header": "Cabeçalho Accept personalizado para servidores com problemas",
|
||||||
|
"user_agent": "Cabeçalho User-agent personalizado",
|
||||||
|
"set_connection_timeout": "Alterar tempo limite de conexão?"
|
||||||
|
},
|
||||||
|
"title": "Opções Avançadas de Conexão"
|
||||||
|
},
|
||||||
|
"timeout_opts": {
|
||||||
|
"data": {
|
||||||
|
"connection_timeout": "Tempo limite de conexão em segundos"
|
||||||
|
},
|
||||||
|
"title": "Opções de Tempo Limite de Conexão"
|
||||||
|
},
|
||||||
|
"reauth_confirm": {
|
||||||
|
"description": "A autorização falhou para o calendário. Por favor, reconfigure a URL do calendário e/ou as configurações de autenticação.",
|
||||||
|
"title": "Falha de Autorização para o ICS Calendar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"empty_name": "O nome do calendário não pode estar vazio.",
|
||||||
|
"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."
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,4 +1,5 @@
|
|||||||
"""Utility methods."""
|
"""Utility methods."""
|
||||||
|
|
||||||
from datetime import date, datetime
|
from datetime import date, datetime
|
||||||
|
|
||||||
|
|
||||||
@@ -9,7 +10,7 @@ def make_datetime(val):
|
|||||||
return val
|
return val
|
||||||
|
|
||||||
|
|
||||||
def compare_event_dates( # pylint: disable=R0913
|
def compare_event_dates( # pylint: disable=R0913,R0917
|
||||||
now, end2, start2, all_day2, end, start, all_day
|
now, end2, start2, all_day2, end, start, all_day
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Determine if end2 and start2 are newer than end and start."""
|
"""Determine if end2 and start2 are newer than end and start."""
|
||||||
|
BIN
custom_components/ics_calendar_old.tar.bz2
Normal file
141
esphome/atorch-ble-proxy.yaml
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
substitutions:
|
||||||
|
name: "dc-load"
|
||||||
|
friendly_name: Atorch programmable DC load
|
||||||
|
external_components_source: github://syssi/esphome-atorch-dl24@main
|
||||||
|
dl24_mac_address: !secret dl24_mac_address
|
||||||
|
project_version: 2.1.0
|
||||||
|
device_description: "Monitor and control a Atorch meter via bluetooth BLE"
|
||||||
|
|
||||||
|
esphome:
|
||||||
|
name: ${name}
|
||||||
|
friendly_name: ${friendly_name}
|
||||||
|
comment: ${device_description}
|
||||||
|
min_version: 2024.6.0
|
||||||
|
project:
|
||||||
|
name: "syssi.esphome-atorch-dl24"
|
||||||
|
version: ${project_version}
|
||||||
|
|
||||||
|
esp32:
|
||||||
|
board: esp32-s3-devkitc-1
|
||||||
|
framework:
|
||||||
|
type: esp-idf
|
||||||
|
|
||||||
|
external_components:
|
||||||
|
- source: ${external_components_source}
|
||||||
|
refresh: 0s
|
||||||
|
|
||||||
|
# Enable logging
|
||||||
|
logger:
|
||||||
|
level: DEBUG
|
||||||
|
|
||||||
|
# Enable Home Assistant API
|
||||||
|
api:
|
||||||
|
encryption:
|
||||||
|
key: !secret apikey
|
||||||
|
|
||||||
|
# Enable over-the-air updates
|
||||||
|
ota:
|
||||||
|
platform: esphome
|
||||||
|
password: !secret ota
|
||||||
|
|
||||||
|
wifi:
|
||||||
|
ssid: "Voltage-legacy"
|
||||||
|
password: !secret voltage_legacy_psk
|
||||||
|
use_address: atorch-dc-load.home
|
||||||
|
power_save_mode: high
|
||||||
|
fast_connect: on
|
||||||
|
|
||||||
|
# Enable fallback hotspot (captive portal) in case wifi connection fails
|
||||||
|
ap:
|
||||||
|
ssid: "DC-Load Fallback Hotspot"
|
||||||
|
password: !secret fallback_psk
|
||||||
|
|
||||||
|
captive_portal:
|
||||||
|
|
||||||
|
#esp32_ble_tracker:
|
||||||
|
# scan_parameters:
|
||||||
|
# active: true
|
||||||
|
# on_ble_advertise:
|
||||||
|
# then:
|
||||||
|
# - lambda: |-
|
||||||
|
# if (x.get_name().rfind("-BLE", 0) == 0) {
|
||||||
|
# ESP_LOGI("ble_adv", "New Atorch device found");
|
||||||
|
# ESP_LOGI("ble_adv", " Name: %s", x.get_name().c_str());
|
||||||
|
# ESP_LOGI("ble_adv", " MAC address: %s", x.address_str().c_str());
|
||||||
|
# ESP_LOGD("ble_adv", " Advertised service UUIDs:");
|
||||||
|
# for (auto uuid : x.get_service_uuids()) {
|
||||||
|
# ESP_LOGD("ble_adv", " - %s", uuid.to_string().c_str());
|
||||||
|
# }
|
||||||
|
# }
|
||||||
|
#
|
||||||
|
#text_sensor:
|
||||||
|
# - platform: ble_scanner
|
||||||
|
# name: "BLE Devices Scanner"
|
||||||
|
|
||||||
|
esp32_ble_tracker:
|
||||||
|
scan_parameters:
|
||||||
|
active: False
|
||||||
|
|
||||||
|
ble_client:
|
||||||
|
- mac_address: ${dl24_mac_address}
|
||||||
|
id: ble_client0
|
||||||
|
|
||||||
|
atorch_dl24:
|
||||||
|
- id: atorch0
|
||||||
|
ble_client_id: ble_client0
|
||||||
|
check_crc: false
|
||||||
|
# The meter publishes a status report per second via BLE notification. If you don't like this update interval
|
||||||
|
# you can use this setting to throttle the sensor updates by skipping some status reports.
|
||||||
|
throttle: 0s
|
||||||
|
|
||||||
|
binary_sensor:
|
||||||
|
- platform: atorch_dl24
|
||||||
|
atorch_dl24_id: atorch0
|
||||||
|
running:
|
||||||
|
name: "${name} running"
|
||||||
|
|
||||||
|
sensor:
|
||||||
|
- platform: atorch_dl24
|
||||||
|
atorch_dl24_id: atorch0
|
||||||
|
voltage:
|
||||||
|
name: "${name} voltage"
|
||||||
|
current:
|
||||||
|
name: "${name} current"
|
||||||
|
power:
|
||||||
|
name: "${name} power"
|
||||||
|
capacity:
|
||||||
|
name: "${name} capacity"
|
||||||
|
energy:
|
||||||
|
name: "${name} energy"
|
||||||
|
temperature:
|
||||||
|
name: "${name} temperature"
|
||||||
|
dim_backlight:
|
||||||
|
name: "${name} dim backlight"
|
||||||
|
runtime:
|
||||||
|
name: "${name} runtime"
|
||||||
|
|
||||||
|
text_sensor:
|
||||||
|
- platform: atorch_dl24
|
||||||
|
atorch_dl24_id: atorch0
|
||||||
|
runtime_formatted:
|
||||||
|
name: "${name} runtime formatted"
|
||||||
|
|
||||||
|
button:
|
||||||
|
- platform: atorch_dl24
|
||||||
|
atorch_dl24_id: atorch0
|
||||||
|
reset_energy:
|
||||||
|
name: "${name} reset energy"
|
||||||
|
reset_capacity:
|
||||||
|
name: "${name} reset capacity"
|
||||||
|
reset_runtime:
|
||||||
|
name: "${name} reset runtime"
|
||||||
|
reset_all:
|
||||||
|
name: "${name} reset all"
|
||||||
|
usb_plus:
|
||||||
|
name: "${name} plus"
|
||||||
|
usb_minus:
|
||||||
|
name: "${name} minus"
|
||||||
|
setup:
|
||||||
|
name: "${name} setup"
|
||||||
|
enter:
|
||||||
|
name: "${name} enter"
|
@@ -49,7 +49,7 @@ i2c:
|
|||||||
scl: 7
|
scl: 7
|
||||||
scan: true
|
scan: true
|
||||||
|
|
||||||
# DHT22 sensor
|
# Temp/humidity sensors
|
||||||
sensor:
|
sensor:
|
||||||
- platform: dht
|
- platform: dht
|
||||||
model: dht22
|
model: dht22
|
||||||
|
@@ -197,12 +197,14 @@ sensor:
|
|||||||
text_sensor:
|
text_sensor:
|
||||||
- platform: homeassistant
|
- platform: homeassistant
|
||||||
name: "Sun Rising ESP"
|
name: "Sun Rising ESP"
|
||||||
|
#entity_id: sensor.sun_next_rising
|
||||||
entity_id: sensor.sun_rising_template
|
entity_id: sensor.sun_rising_template
|
||||||
id: sun_rising
|
id: sun_rising
|
||||||
internal: true
|
internal: true
|
||||||
|
|
||||||
- platform: homeassistant
|
- platform: homeassistant
|
||||||
name: "Sun Setting ESP"
|
name: "Sun Setting ESP"
|
||||||
|
#entity_id: sensor.sun_next_setting
|
||||||
entity_id: sensor.sun_setting_template
|
entity_id: sensor.sun_setting_template
|
||||||
id: sun_setting
|
id: sun_setting
|
||||||
internal: true
|
internal: true
|
||||||
@@ -387,6 +389,7 @@ image:
|
|||||||
id: c1024_logo
|
id: c1024_logo
|
||||||
type: binary
|
type: binary
|
||||||
resize: 77x40
|
resize: 77x40
|
||||||
|
invert_alpha: True
|
||||||
|
|
||||||
spi:
|
spi:
|
||||||
clk_pin: 23
|
clk_pin: 23
|
||||||
|
31
esphome/home-assistant-voice-09c0e7.yaml
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
substitutions:
|
||||||
|
name: home-assistant-voice-09c0e7
|
||||||
|
friendly_name: Home Assistant Voice 09c0e7
|
||||||
|
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
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
114
esphome/kamera-balkon.yaml
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
substitutions:
|
||||||
|
name: kamera-balkon
|
||||||
|
friendly_name: Kamera Balkon
|
||||||
|
|
||||||
|
esphome:
|
||||||
|
name: ${name}
|
||||||
|
friendly_name: ${friendly_name}
|
||||||
|
name_add_mac_suffix: false
|
||||||
|
project:
|
||||||
|
name: sensor.camera
|
||||||
|
version: "1.0"
|
||||||
|
min_version: 2022.1.0
|
||||||
|
|
||||||
|
esp32:
|
||||||
|
board: esp32cam
|
||||||
|
framework:
|
||||||
|
type: arduino
|
||||||
|
|
||||||
|
wifi:
|
||||||
|
ssid: "Voltage-legacy"
|
||||||
|
password: !secret voltage_legacy_psk
|
||||||
|
# use_address: cam-balcony.home
|
||||||
|
power_save_mode: high
|
||||||
|
fast_connect: on
|
||||||
|
|
||||||
|
# Enable fallback hotspot (captive portal) in case wifi connection fails
|
||||||
|
ap:
|
||||||
|
ssid: "Cam Balkon Fallback Hotspot"
|
||||||
|
password: !secret fallback_psk
|
||||||
|
|
||||||
|
captive_portal:
|
||||||
|
|
||||||
|
# Enable logging
|
||||||
|
logger:
|
||||||
|
|
||||||
|
# Enable Home Assistant API
|
||||||
|
api:
|
||||||
|
encryption:
|
||||||
|
key: !secret apikey
|
||||||
|
|
||||||
|
ota:
|
||||||
|
platform: esphome
|
||||||
|
password: !secret ota
|
||||||
|
|
||||||
|
# Initialize I²C
|
||||||
|
i2c:
|
||||||
|
- id: bus_a
|
||||||
|
sda: GPIO13
|
||||||
|
scl: GPIO14
|
||||||
|
scan: true
|
||||||
|
- id: bus_c
|
||||||
|
sda: GPIO26
|
||||||
|
scl: GPIO27
|
||||||
|
|
||||||
|
# Camera
|
||||||
|
esp32_camera:
|
||||||
|
name: ${friendly_name}
|
||||||
|
external_clock:
|
||||||
|
pin: GPIO0
|
||||||
|
frequency: 20MHz
|
||||||
|
i2c_id:
|
||||||
|
bus_c
|
||||||
|
data_pins: [GPIO5, GPIO18, GPIO19, GPIO21, GPIO36, GPIO39, GPIO34, GPIO35]
|
||||||
|
vsync_pin: GPIO25
|
||||||
|
href_pin: GPIO23
|
||||||
|
pixel_clock_pin: GPIO22
|
||||||
|
power_down_pin: GPIO32
|
||||||
|
resolution: SVGA
|
||||||
|
max_framerate: 24 fps
|
||||||
|
idle_framerate: 0.2 fps
|
||||||
|
jpeg_quality: 30
|
||||||
|
agc_mode: auto
|
||||||
|
agc_gain_ceiling: 4x
|
||||||
|
wb_mode: auto
|
||||||
|
vertical_flip: true
|
||||||
|
horizontal_mirror: true
|
||||||
|
|
||||||
|
esp32_camera_web_server:
|
||||||
|
- port: 8080
|
||||||
|
mode: stream
|
||||||
|
- port: 8081
|
||||||
|
mode: snapshot
|
||||||
|
|
||||||
|
# Temp/humidity sensors
|
||||||
|
sensor:
|
||||||
|
- platform: wifi_signal
|
||||||
|
name: "WiFi Signalstärke"
|
||||||
|
update_interval: 60s
|
||||||
|
- platform: sht3xd
|
||||||
|
i2c_id:
|
||||||
|
bus_a
|
||||||
|
temperature:
|
||||||
|
name: "Balkon Temperatur"
|
||||||
|
filters:
|
||||||
|
- offset: -4.4
|
||||||
|
humidity:
|
||||||
|
name: "Balkon Luftfeuchtigkeit"
|
||||||
|
address: 0x44
|
||||||
|
heater_enabled: True
|
||||||
|
update_interval: 60s
|
||||||
|
|
||||||
|
# Flash LED
|
||||||
|
output:
|
||||||
|
- platform: ledc
|
||||||
|
pin: GPIO4
|
||||||
|
id: flash
|
||||||
|
channel: 2
|
||||||
|
|
||||||
|
# Define RGB mode for LED
|
||||||
|
light:
|
||||||
|
- platform: monochromatic
|
||||||
|
id: flashlight
|
||||||
|
name: "Blitzlicht"
|
||||||
|
output: flash
|
@@ -19,7 +19,7 @@ esp32:
|
|||||||
wifi:
|
wifi:
|
||||||
ssid: "Voltage-legacy"
|
ssid: "Voltage-legacy"
|
||||||
password: !secret voltage_legacy_psk
|
password: !secret voltage_legacy_psk
|
||||||
use_address: ${name}.home
|
use_address: cam-livingroom.home
|
||||||
power_save_mode: high
|
power_save_mode: high
|
||||||
fast_connect: on
|
fast_connect: on
|
||||||
|
|
||||||
@@ -60,9 +60,23 @@ esp32_camera:
|
|||||||
max_framerate: 24 fps
|
max_framerate: 24 fps
|
||||||
idle_framerate: 0.2 fps
|
idle_framerate: 0.2 fps
|
||||||
jpeg_quality: 30
|
jpeg_quality: 30
|
||||||
|
agc_mode: auto
|
||||||
|
agc_gain_ceiling: 4x
|
||||||
|
wb_mode: auto
|
||||||
vertical_flip: true
|
vertical_flip: true
|
||||||
horizontal_mirror: true
|
horizontal_mirror: true
|
||||||
|
|
||||||
|
esp32_camera_web_server:
|
||||||
|
- port: 8080
|
||||||
|
mode: stream
|
||||||
|
- port: 8081
|
||||||
|
mode: snapshot
|
||||||
|
|
||||||
|
sensor:
|
||||||
|
- platform: wifi_signal
|
||||||
|
name: "WiFi Signalstärke"
|
||||||
|
update_interval: 60s
|
||||||
|
|
||||||
# Flash LED
|
# Flash LED
|
||||||
output:
|
output:
|
||||||
- platform: ledc
|
- platform: ledc
|
||||||
|
@@ -1,15 +1,15 @@
|
|||||||
|
# Source: https://github.com/esphome/wake-word-voice-assistants/blob/main/m5stack-atom-echo/m5stack-atom-echo.yaml
|
||||||
|
|
||||||
substitutions:
|
substitutions:
|
||||||
name: "m5stack-atom-echo"
|
name: m5stack-atom-echo
|
||||||
friendly_name: "M5Stack Atom Echo"
|
friendly_name: M5Stack Atom Echo
|
||||||
|
micro_wake_word_model: okay_nabu # alexa, hey_jarvis, hey_mycroft are also supported
|
||||||
|
|
||||||
esphome:
|
esphome:
|
||||||
name: ${name}
|
name: ${name}
|
||||||
friendly_name: ${friendly_name}
|
|
||||||
name_add_mac_suffix: False
|
name_add_mac_suffix: False
|
||||||
project:
|
friendly_name: ${friendly_name}
|
||||||
name: m5stack.atom-echo-voice-assistant
|
min_version: 2025.2.0
|
||||||
version: "24.7.24"
|
|
||||||
min_version: 2024.9.0
|
|
||||||
|
|
||||||
esp32:
|
esp32:
|
||||||
board: m5stack-atom
|
board: m5stack-atom
|
||||||
@@ -23,25 +23,12 @@ api:
|
|||||||
|
|
||||||
ota:
|
ota:
|
||||||
- platform: esphome
|
- platform: esphome
|
||||||
password: !secret ota
|
|
||||||
id: ota_esphome
|
id: ota_esphome
|
||||||
- platform: http_request
|
password: !secret ota
|
||||||
id: ota_http_request
|
|
||||||
|
|
||||||
update:
|
|
||||||
- platform: http_request
|
|
||||||
id: update_http_request
|
|
||||||
name: Firmware
|
|
||||||
source: https://firmware.esphome.io/voice-assistant/m5stack-atom-echo/manifest.json
|
|
||||||
|
|
||||||
http_request:
|
|
||||||
|
|
||||||
wifi:
|
wifi:
|
||||||
on_connect:
|
on_connect:
|
||||||
- delay: 5s # Gives time for improv results to be transmitted
|
- delay: 5s # Gives time for improv results to be transmitted
|
||||||
- ble.disable:
|
|
||||||
on_disconnect:
|
|
||||||
- ble.enable:
|
|
||||||
ssid: Voltage-legacy
|
ssid: Voltage-legacy
|
||||||
password: !secret voltage_legacy_psk
|
password: !secret voltage_legacy_psk
|
||||||
use_address: ${name}.home
|
use_address: ${name}.home
|
||||||
@@ -52,22 +39,11 @@ wifi:
|
|||||||
ssid: "Raspiaudio Fallback Hotspot"
|
ssid: "Raspiaudio Fallback Hotspot"
|
||||||
password: !secret fallback_psk
|
password: !secret fallback_psk
|
||||||
|
|
||||||
improv_serial:
|
|
||||||
|
|
||||||
esp32_improv:
|
|
||||||
authorizer: none
|
|
||||||
|
|
||||||
logger:
|
logger:
|
||||||
|
|
||||||
dashboard_import:
|
|
||||||
package_import_url: github://esphome/firmware/voice-assistant/m5stack-atom-echo.adopted.yaml@main
|
|
||||||
|
|
||||||
captive_portal:
|
captive_portal:
|
||||||
|
|
||||||
button:
|
button:
|
||||||
- platform: safe_mode
|
|
||||||
id: button_safe_mode
|
|
||||||
name: Safe Mode Boot
|
|
||||||
- platform: factory_reset
|
- platform: factory_reset
|
||||||
id: factory_reset_btn
|
id: factory_reset_btn
|
||||||
name: Factory reset
|
name: Factory reset
|
||||||
@@ -89,16 +65,53 @@ speaker:
|
|||||||
id: echo_speaker
|
id: echo_speaker
|
||||||
i2s_dout_pin: GPIO22
|
i2s_dout_pin: GPIO22
|
||||||
dac_type: external
|
dac_type: external
|
||||||
channel: mono
|
bits_per_sample: 32bit
|
||||||
|
channel: right
|
||||||
|
buffer_duration: 60ms
|
||||||
|
|
||||||
|
media_player:
|
||||||
|
- platform: speaker
|
||||||
|
name: None
|
||||||
|
id: echo_media_player
|
||||||
|
announcement_pipeline:
|
||||||
|
speaker: echo_speaker
|
||||||
|
format: WAV
|
||||||
|
codec_support_enabled: false
|
||||||
|
buffer_size: 6000
|
||||||
|
volume_min: 0.4
|
||||||
|
files:
|
||||||
|
- id: timer_finished_wave_file
|
||||||
|
file: https://github.com/esphome/wake-word-voice-assistants/raw/main/sounds/timer_finished.wav
|
||||||
|
on_announcement:
|
||||||
|
- if:
|
||||||
|
condition:
|
||||||
|
- microphone.is_capturing:
|
||||||
|
then:
|
||||||
|
- if:
|
||||||
|
condition:
|
||||||
|
lambda: return id(wake_word_engine_location).state == "On device";
|
||||||
|
then:
|
||||||
|
- micro_wake_word.stop:
|
||||||
|
else:
|
||||||
|
- voice_assistant.stop:
|
||||||
|
- script.execute: reset_led
|
||||||
|
- light.turn_on:
|
||||||
|
id: led
|
||||||
|
blue: 100%
|
||||||
|
red: 0%
|
||||||
|
green: 0%
|
||||||
|
brightness: 100%
|
||||||
|
effect: none
|
||||||
|
on_idle:
|
||||||
|
- script.execute: start_wake_word
|
||||||
|
|
||||||
voice_assistant:
|
voice_assistant:
|
||||||
id: va
|
id: va
|
||||||
microphone: echo_microphone
|
microphone: echo_microphone
|
||||||
speaker: echo_speaker
|
media_player: echo_media_player
|
||||||
noise_suppression_level: 2
|
noise_suppression_level: 2
|
||||||
auto_gain: 31dBFS
|
auto_gain: 31dBFS
|
||||||
volume_multiplier: 2.0
|
volume_multiplier: 2.0
|
||||||
vad_threshold: 3
|
|
||||||
on_listening:
|
on_listening:
|
||||||
- light.turn_on:
|
- light.turn_on:
|
||||||
id: led
|
id: led
|
||||||
@@ -123,10 +136,7 @@ voice_assistant:
|
|||||||
effect: none
|
effect: none
|
||||||
on_end:
|
on_end:
|
||||||
- delay: 100ms
|
- delay: 100ms
|
||||||
- wait_until:
|
- script.execute: start_wake_word
|
||||||
not:
|
|
||||||
speaker.is_playing:
|
|
||||||
- script.execute: reset_led
|
|
||||||
on_error:
|
on_error:
|
||||||
- light.turn_on:
|
- light.turn_on:
|
||||||
id: led
|
id: led
|
||||||
@@ -135,28 +145,21 @@ voice_assistant:
|
|||||||
blue: 0%
|
blue: 0%
|
||||||
brightness: 100%
|
brightness: 100%
|
||||||
effect: none
|
effect: none
|
||||||
- delay: 1s
|
- delay: 2s
|
||||||
- script.execute: reset_led
|
- script.execute: reset_led
|
||||||
on_client_connected:
|
on_client_connected:
|
||||||
- if:
|
- delay: 2s # Give the api server time to settle
|
||||||
condition:
|
- script.execute: start_wake_word
|
||||||
switch.is_on: use_wake_word
|
|
||||||
then:
|
|
||||||
- voice_assistant.start_continuous:
|
|
||||||
- script.execute: reset_led
|
|
||||||
on_client_disconnected:
|
on_client_disconnected:
|
||||||
- if:
|
- voice_assistant.stop:
|
||||||
condition:
|
- micro_wake_word.stop:
|
||||||
switch.is_on: use_wake_word
|
|
||||||
then:
|
|
||||||
- voice_assistant.stop:
|
|
||||||
- light.turn_off: led
|
|
||||||
on_timer_finished:
|
on_timer_finished:
|
||||||
- voice_assistant.stop:
|
- voice_assistant.stop:
|
||||||
- switch.turn_on: timer_ringing
|
- micro_wake_word.stop:
|
||||||
- wait_until:
|
- wait_until:
|
||||||
not:
|
not:
|
||||||
microphone.is_capturing:
|
microphone.is_capturing:
|
||||||
|
- switch.turn_on: timer_ringing
|
||||||
- light.turn_on:
|
- light.turn_on:
|
||||||
id: led
|
id: led
|
||||||
red: 0%
|
red: 0%
|
||||||
@@ -164,25 +167,15 @@ voice_assistant:
|
|||||||
blue: 0%
|
blue: 0%
|
||||||
brightness: 100%
|
brightness: 100%
|
||||||
effect: "Fast Pulse"
|
effect: "Fast Pulse"
|
||||||
- while:
|
|
||||||
condition:
|
|
||||||
switch.is_on: timer_ringing
|
|
||||||
then:
|
|
||||||
- lambda: id(echo_speaker).play(id(timer_finished_wave_file), sizeof(id(timer_finished_wave_file)));
|
|
||||||
- delay: 1s
|
|
||||||
- wait_until:
|
- wait_until:
|
||||||
not:
|
- switch.is_off: timer_ringing
|
||||||
speaker.is_playing:
|
|
||||||
- light.turn_off: led
|
- light.turn_off: led
|
||||||
- switch.turn_off: timer_ringing
|
- switch.turn_off: timer_ringing
|
||||||
- if:
|
|
||||||
condition:
|
|
||||||
switch.is_on: use_wake_word
|
|
||||||
then:
|
|
||||||
- voice_assistant.start_continuous:
|
|
||||||
- script.execute: reset_led
|
|
||||||
|
|
||||||
binary_sensor:
|
binary_sensor:
|
||||||
|
# button does the following:
|
||||||
|
# short click - stop a timer
|
||||||
|
# if no timer then restart either microwakeword or voice assistant continuous
|
||||||
- platform: gpio
|
- platform: gpio
|
||||||
pin:
|
pin:
|
||||||
number: GPIO39
|
number: GPIO39
|
||||||
@@ -202,23 +195,7 @@ binary_sensor:
|
|||||||
then:
|
then:
|
||||||
- switch.turn_off: timer_ringing
|
- switch.turn_off: timer_ringing
|
||||||
else:
|
else:
|
||||||
- if:
|
- script.execute: start_wake_word
|
||||||
condition:
|
|
||||||
switch.is_off: use_wake_word
|
|
||||||
then:
|
|
||||||
- if:
|
|
||||||
condition: voice_assistant.is_running
|
|
||||||
then:
|
|
||||||
- voice_assistant.stop:
|
|
||||||
- script.execute: reset_led
|
|
||||||
else:
|
|
||||||
- voice_assistant.start:
|
|
||||||
else:
|
|
||||||
- voice_assistant.stop
|
|
||||||
- delay: 1s
|
|
||||||
- script.execute: reset_led
|
|
||||||
- script.wait: reset_led
|
|
||||||
- voice_assistant.start_continuous:
|
|
||||||
- timing:
|
- timing:
|
||||||
- ON for at least 10s
|
- ON for at least 10s
|
||||||
then:
|
then:
|
||||||
@@ -235,7 +212,6 @@ light:
|
|||||||
chipset: SK6812
|
chipset: SK6812
|
||||||
num_leds: 1
|
num_leds: 1
|
||||||
rgb_order: grb
|
rgb_order: grb
|
||||||
rmt_channel: 0
|
|
||||||
effects:
|
effects:
|
||||||
- pulse:
|
- pulse:
|
||||||
name: "Slow Pulse"
|
name: "Slow Pulse"
|
||||||
@@ -255,7 +231,7 @@ script:
|
|||||||
then:
|
then:
|
||||||
- if:
|
- if:
|
||||||
condition:
|
condition:
|
||||||
- switch.is_on: use_wake_word
|
- lambda: return id(wake_word_engine_location).state == "On device";
|
||||||
- switch.is_on: use_listen_light
|
- switch.is_on: use_listen_light
|
||||||
then:
|
then:
|
||||||
- light.turn_on:
|
- light.turn_on:
|
||||||
@@ -266,28 +242,45 @@ script:
|
|||||||
brightness: 60%
|
brightness: 60%
|
||||||
effect: none
|
effect: none
|
||||||
else:
|
else:
|
||||||
- light.turn_off: led
|
- if:
|
||||||
|
condition:
|
||||||
switch:
|
- lambda: return id(wake_word_engine_location).state != "On device";
|
||||||
- platform: template
|
- switch.is_on: use_listen_light
|
||||||
name: Use wake word
|
then:
|
||||||
id: use_wake_word
|
- light.turn_on:
|
||||||
optimistic: true
|
id: led
|
||||||
restore_mode: RESTORE_DEFAULT_ON
|
red: 0%
|
||||||
entity_category: config
|
green: 100%
|
||||||
on_turn_on:
|
blue: 100%
|
||||||
- lambda: id(va).set_use_wake_word(true);
|
brightness: 60%
|
||||||
|
effect: none
|
||||||
|
else:
|
||||||
|
- light.turn_off: led
|
||||||
|
- id: start_wake_word
|
||||||
|
then:
|
||||||
|
- wait_until:
|
||||||
|
and:
|
||||||
|
- media_player.is_idle:
|
||||||
|
- speaker.is_stopped:
|
||||||
- if:
|
- if:
|
||||||
condition:
|
condition:
|
||||||
not:
|
lambda: return id(wake_word_engine_location).state == "On device";
|
||||||
- voice_assistant.is_running
|
|
||||||
then:
|
then:
|
||||||
- voice_assistant.start_continuous
|
- voice_assistant.stop
|
||||||
- script.execute: reset_led
|
- micro_wake_word.stop:
|
||||||
on_turn_off:
|
- delay: 1s
|
||||||
- voice_assistant.stop
|
- script.execute: reset_led
|
||||||
- lambda: id(va).set_use_wake_word(false);
|
- script.wait: reset_led
|
||||||
- script.execute: reset_led
|
- micro_wake_word.start:
|
||||||
|
else:
|
||||||
|
- if:
|
||||||
|
condition: voice_assistant.is_running
|
||||||
|
then:
|
||||||
|
- voice_assistant.stop:
|
||||||
|
- script.execute: reset_led
|
||||||
|
- voice_assistant.start_continuous:
|
||||||
|
|
||||||
|
switch:
|
||||||
- platform: template
|
- platform: template
|
||||||
name: Use listen light
|
name: Use listen light
|
||||||
id: use_listen_light
|
id: use_listen_light
|
||||||
@@ -301,26 +294,67 @@ switch:
|
|||||||
- platform: template
|
- platform: template
|
||||||
id: timer_ringing
|
id: timer_ringing
|
||||||
optimistic: true
|
optimistic: true
|
||||||
internal: true
|
|
||||||
restore_mode: ALWAYS_OFF
|
restore_mode: ALWAYS_OFF
|
||||||
|
on_turn_off:
|
||||||
|
# Turn off the repeat mode and disable the pause between playlist items
|
||||||
|
- lambda: |-
|
||||||
|
id(echo_media_player)
|
||||||
|
->make_call()
|
||||||
|
.set_command(media_player::MediaPlayerCommand::MEDIA_PLAYER_COMMAND_REPEAT_OFF)
|
||||||
|
.set_announcement(true)
|
||||||
|
.perform();
|
||||||
|
id(echo_media_player)->set_playlist_delay_ms(speaker::AudioPipelineType::ANNOUNCEMENT, 0);
|
||||||
|
# Stop playing the alarm
|
||||||
|
- media_player.stop:
|
||||||
|
announcement: true
|
||||||
on_turn_on:
|
on_turn_on:
|
||||||
|
# Turn on the repeat mode and pause for 1000 ms between playlist items/repeats
|
||||||
|
- lambda: |-
|
||||||
|
id(echo_media_player)
|
||||||
|
->make_call()
|
||||||
|
.set_command(media_player::MediaPlayerCommand::MEDIA_PLAYER_COMMAND_REPEAT_ONE)
|
||||||
|
.set_announcement(true)
|
||||||
|
.perform();
|
||||||
|
id(echo_media_player)->set_playlist_delay_ms(speaker::AudioPipelineType::ANNOUNCEMENT, 1000);
|
||||||
|
- media_player.speaker.play_on_device_media_file:
|
||||||
|
media_file: timer_finished_wave_file
|
||||||
|
announcement: true
|
||||||
- delay: 15min
|
- delay: 15min
|
||||||
- switch.turn_off: timer_ringing
|
- switch.turn_off: timer_ringing
|
||||||
|
|
||||||
external_components:
|
select:
|
||||||
- source: github://pr#5230
|
- platform: template
|
||||||
components:
|
entity_category: config
|
||||||
- esp_adf
|
name: Wake word engine location
|
||||||
refresh: 0s
|
id: wake_word_engine_location
|
||||||
- source:
|
optimistic: true
|
||||||
type: git
|
restore_value: true
|
||||||
url: https://github.com/jesserockz/esphome-components
|
options:
|
||||||
ref: main
|
- In Home Assistant
|
||||||
components: [file]
|
- On device
|
||||||
refresh: 0s
|
initial_option: On device
|
||||||
|
on_value:
|
||||||
|
- if:
|
||||||
|
condition:
|
||||||
|
lambda: return x == "In Home Assistant";
|
||||||
|
then:
|
||||||
|
- micro_wake_word.stop
|
||||||
|
- delay: 500ms
|
||||||
|
- lambda: id(va).set_use_wake_word(true);
|
||||||
|
- voice_assistant.start_continuous:
|
||||||
|
- if:
|
||||||
|
condition:
|
||||||
|
lambda: return x == "On device";
|
||||||
|
then:
|
||||||
|
- lambda: id(va).set_use_wake_word(false);
|
||||||
|
- voice_assistant.stop
|
||||||
|
- delay: 500ms
|
||||||
|
- micro_wake_word.start
|
||||||
|
|
||||||
esp_adf:
|
micro_wake_word:
|
||||||
|
on_wake_word_detected:
|
||||||
file:
|
- voice_assistant.start:
|
||||||
- id: timer_finished_wave_file
|
wake_word: !lambda return wake_word;
|
||||||
file: https://github.com/esphome/firmware/raw/main/voice-assistant/sounds/timer_finished.wav
|
vad:
|
||||||
|
models:
|
||||||
|
- model: ${micro_wake_word_model}
|
||||||
|
@@ -1,6 +1,14 @@
|
|||||||
|
# Source: https://github.com/RASPIAUDIO/esphomeLuxe/blob/main/luxe_microWW.yaml
|
||||||
|
|
||||||
substitutions:
|
substitutions:
|
||||||
name: "raspiaudio-muse-luxe"
|
name: "raspiaudio-muse-luxe"
|
||||||
friendly_name: "RaspiAudio Muse Luxe"
|
friendly_name: "RaspiAudio Muse Luxe"
|
||||||
|
#States
|
||||||
|
P_starting: "0"
|
||||||
|
P_waiting: "1"
|
||||||
|
P_playing: "2"
|
||||||
|
P_listening: "3"
|
||||||
|
P_answering: "4"
|
||||||
|
|
||||||
# Enable Home Assistant API
|
# Enable Home Assistant API
|
||||||
api:
|
api:
|
||||||
@@ -12,6 +20,14 @@ ota:
|
|||||||
id: ota_esphome
|
id: ota_esphome
|
||||||
password: !secret ota
|
password: !secret ota
|
||||||
|
|
||||||
|
external_components:
|
||||||
|
- source: github://RASPIAUDIO/esphomeLuxe@main
|
||||||
|
# - source:
|
||||||
|
# type: local
|
||||||
|
# path: components
|
||||||
|
components: [es8388]
|
||||||
|
refresh: 0s
|
||||||
|
|
||||||
wifi:
|
wifi:
|
||||||
ssid: Voltage-legacy
|
ssid: Voltage-legacy
|
||||||
password: !secret voltage_legacy_psk
|
password: !secret voltage_legacy_psk
|
||||||
@@ -26,125 +42,175 @@ wifi:
|
|||||||
esphome:
|
esphome:
|
||||||
name: ${name}
|
name: ${name}
|
||||||
friendly_name: ${friendly_name}
|
friendly_name: ${friendly_name}
|
||||||
|
min_version: 2025.2.0
|
||||||
name_add_mac_suffix: false
|
name_add_mac_suffix: false
|
||||||
project:
|
platformio_options:
|
||||||
name: raspiaudio.muse-luxe
|
board_build.flash_mode: dio
|
||||||
version: "1.0"
|
board_build.arduino.memory_type: qio_opi
|
||||||
min_version: 2024.6.0
|
|
||||||
on_boot:
|
on_boot:
|
||||||
|
priority: -100.0
|
||||||
then:
|
then:
|
||||||
- media_player.volume_set:
|
- lambda: id(phase) = 0;
|
||||||
id: luxe_out
|
- script.execute: update_led
|
||||||
volume: 50%
|
|
||||||
|
|
||||||
esp32:
|
esp32:
|
||||||
board: esp-wrover-kit
|
board: esp-wrover-kit
|
||||||
|
flash_size: 4MB
|
||||||
framework:
|
framework:
|
||||||
type: arduino
|
type: esp-idf
|
||||||
|
version: recommended
|
||||||
|
sdkconfig_options:
|
||||||
|
CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240: "y"
|
||||||
|
CONFIG_ESP32S3_DATA_CACHE_64KB: "y"
|
||||||
|
CONFIG_ESP32S3_DATA_CACHE_LINE_64B: "y"
|
||||||
|
|
||||||
|
micro_wake_word:
|
||||||
|
id: mww
|
||||||
|
models:
|
||||||
|
- model: https://github.com/kahrendt/microWakeWord/releases/download/okay_nabu_20241226.3/okay_nabu.json
|
||||||
|
# - model: hey_jarvis
|
||||||
|
# - model: hey_mycroft
|
||||||
|
# - model: alexa
|
||||||
|
# vad:
|
||||||
|
microphone: luxe_mic
|
||||||
|
on_wake_word_detected:
|
||||||
|
- voice_assistant.start:
|
||||||
|
wake_word: !lambda return wake_word;
|
||||||
|
|
||||||
logger:
|
logger:
|
||||||
|
level: DEBUG
|
||||||
i2c:
|
|
||||||
- id: i2c_bus
|
|
||||||
sda: GPIO18
|
|
||||||
scl: GPIO23
|
|
||||||
|
|
||||||
dashboard_import:
|
|
||||||
package_import_url: github://esphome/media-players/raspiaudio-muse-luxe.yaml@main
|
|
||||||
|
|
||||||
captive_portal:
|
captive_portal:
|
||||||
|
|
||||||
external_components:
|
improv_serial:
|
||||||
- source: github://pr#3552 # DAC support https://github.com/esphome/esphome/pull/3552
|
|
||||||
components: [es8388]
|
|
||||||
refresh: 0s
|
|
||||||
|
|
||||||
|
##########
|
||||||
|
# Hardware Configuration
|
||||||
es8388:
|
es8388:
|
||||||
|
id: my_es8388
|
||||||
|
|
||||||
i2s_audio:
|
psram:
|
||||||
- id: i2s_audio_bus
|
mode: quad
|
||||||
i2s_lrclk_pin: GPIO25
|
speed: 80MHz
|
||||||
i2s_bclk_pin: GPIO5
|
|
||||||
|
|
||||||
media_player:
|
#######
|
||||||
- platform: i2s_audio
|
# Buses Configuration
|
||||||
name: None
|
i2c:
|
||||||
id: luxe_out
|
sda: GPIO18
|
||||||
dac_type: external
|
scl: GPIO23
|
||||||
i2s_dout_pin: GPIO26
|
|
||||||
mode: stereo
|
#####################
|
||||||
mute_pin:
|
# Internal Components
|
||||||
|
output:
|
||||||
|
- platform: gpio
|
||||||
|
id: dac_mute
|
||||||
|
pin:
|
||||||
number: GPIO21
|
number: GPIO21
|
||||||
inverted: true
|
inverted: true
|
||||||
|
mode:
|
||||||
|
output: true
|
||||||
|
|
||||||
microphone:
|
globals:
|
||||||
- platform: i2s_audio
|
- id: Vol
|
||||||
id: luxe_microphone
|
type: float
|
||||||
i2s_din_pin: GPIO35
|
initial_value: '0.6'
|
||||||
adc_type: external
|
- id: phase
|
||||||
pdm: false
|
type: int
|
||||||
|
initial_value: '0'
|
||||||
|
- id: mute
|
||||||
|
type: bool
|
||||||
|
initial_value: 'false'
|
||||||
|
- id: muteH
|
||||||
|
type: bool
|
||||||
|
initial_value: 'false'
|
||||||
|
|
||||||
voice_assistant:
|
interval:
|
||||||
microphone: luxe_microphone
|
- interval: 0.1sec
|
||||||
on_start:
|
then:
|
||||||
- light.turn_on:
|
- if:
|
||||||
id: top_led
|
condition:
|
||||||
blue: 100%
|
- speaker.is_stopped:
|
||||||
red: 0%
|
then:
|
||||||
green: 0%
|
- if:
|
||||||
effect: none
|
condition:
|
||||||
on_tts_start:
|
- not:
|
||||||
- light.turn_on:
|
- lambda: 'return(id(muteH));'
|
||||||
id: top_led
|
then:
|
||||||
blue: 60%
|
- output.turn_on: dac_mute
|
||||||
red: 20%
|
- lambda: id(muteH) = true;
|
||||||
green: 20%
|
- logger.log: "====> hardware mute"
|
||||||
effect: none
|
|
||||||
on_tts_end:
|
- if:
|
||||||
- media_player.play_media: !lambda return x;
|
condition:
|
||||||
- light.turn_on:
|
- speaker.is_playing:
|
||||||
id: top_led
|
then:
|
||||||
blue: 60%
|
- if:
|
||||||
red: 20%
|
condition:
|
||||||
green: 20%
|
- lambda: 'return(id(muteH));'
|
||||||
effect: pulse
|
then:
|
||||||
on_end:
|
- output.turn_off: dac_mute
|
||||||
- delay: 1s
|
- lambda: id(muteH) = false;
|
||||||
- wait_until:
|
- logger.log: "====> hardware unmute"
|
||||||
not:
|
|
||||||
media_player.is_playing: luxe_out
|
|
||||||
- light.turn_off: top_led
|
|
||||||
on_error:
|
|
||||||
- light.turn_on:
|
|
||||||
id: top_led
|
|
||||||
blue: 0%
|
|
||||||
red: 100%
|
|
||||||
green: 0%
|
|
||||||
effect: none
|
|
||||||
- delay: 1s
|
|
||||||
- light.turn_off: top_led
|
|
||||||
|
|
||||||
sensor:
|
sensor:
|
||||||
- platform: adc
|
- platform: adc
|
||||||
id: battery_sensor
|
|
||||||
pin: GPIO33
|
pin: GPIO33
|
||||||
name: Battery
|
name: Battery voltage
|
||||||
icon: "mdi:battery-outline"
|
|
||||||
device_class: voltage
|
device_class: voltage
|
||||||
|
unit_of_measurement: "V"
|
||||||
|
accuracy_decimals: 2
|
||||||
state_class: measurement
|
state_class: measurement
|
||||||
entity_category: diagnostic
|
entity_category: diagnostic
|
||||||
unit_of_measurement: V
|
|
||||||
update_interval: 15s
|
update_interval: 15s
|
||||||
accuracy_decimals: 3
|
attenuation: auto
|
||||||
attenuation: 11db
|
|
||||||
raw: true
|
|
||||||
filters:
|
filters:
|
||||||
- multiply: 0.00173913 # 2300 -> 4, for attenuation 11db, based on Olivier's code
|
- multiply: 2 # https://forum.raspiaudio.com/t/esp-muse-luxe-bluetooth-speaker/294/12
|
||||||
- exponential_moving_average:
|
- exponential_moving_average:
|
||||||
alpha: 0.2
|
alpha: 0.2
|
||||||
send_every: 2
|
send_every: 2
|
||||||
- delta: 0.002
|
- delta: 0.002
|
||||||
|
on_value:
|
||||||
|
then:
|
||||||
|
- sensor.template.publish:
|
||||||
|
id: battery_percent
|
||||||
|
state: !lambda "return x;"
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
name: Battery
|
||||||
|
id: battery_percent
|
||||||
|
device_class: battery
|
||||||
|
unit_of_measurement: "%"
|
||||||
|
accuracy_decimals: 0
|
||||||
|
state_class: measurement
|
||||||
|
entity_category: diagnostic
|
||||||
|
update_interval: 15s
|
||||||
|
filters:
|
||||||
|
- calibrate_polynomial:
|
||||||
|
degree: 3
|
||||||
|
datapoints:
|
||||||
|
- 4.58 -> 100.0
|
||||||
|
- 4.5 -> 97.1
|
||||||
|
- 4.47 -> 94.2
|
||||||
|
- 4.44 -> 88.4
|
||||||
|
- 4.42 -> 82.7
|
||||||
|
- 4.41 -> 76.9
|
||||||
|
- 4.41 -> 71.1
|
||||||
|
- 4.37 -> 65.3
|
||||||
|
- 4.35 -> 59.5
|
||||||
|
- 4.31 -> 53.8
|
||||||
|
- 4.28 -> 48.0
|
||||||
|
- 4.26 -> 42.2
|
||||||
|
- 4.23 -> 36.4
|
||||||
|
- 4.21 -> 30.6
|
||||||
|
- 4.19 -> 24.9
|
||||||
|
- 4.16 -> 19.1
|
||||||
|
- 4.1 -> 13.3
|
||||||
|
- 4.07 -> 10.4
|
||||||
|
- 4.03 -> 7.5
|
||||||
|
- 3.97 -> 4.6
|
||||||
|
- 3.82 -> 1.7
|
||||||
|
- 3.27 -> 0.0
|
||||||
|
- lambda: return clamp(x, 0.0f, 100.0f);
|
||||||
|
|
||||||
binary_sensor:
|
binary_sensor:
|
||||||
- platform: gpio
|
- platform: gpio
|
||||||
@@ -155,9 +221,14 @@ binary_sensor:
|
|||||||
input: true
|
input: true
|
||||||
pullup: true
|
pullup: true
|
||||||
name: Volume Up
|
name: Volume Up
|
||||||
id: volume_up
|
|
||||||
on_click:
|
on_click:
|
||||||
- media_player.volume_up: luxe_out
|
- lambda: |-
|
||||||
|
id(Vol) += 0.05;
|
||||||
|
if(id(Vol) > 1) id(Vol) = 1;
|
||||||
|
- media_player.volume_set:
|
||||||
|
id: luxe_media_player
|
||||||
|
volume: !lambda return id(Vol);
|
||||||
|
|
||||||
- platform: gpio
|
- platform: gpio
|
||||||
pin:
|
pin:
|
||||||
number: GPIO32
|
number: GPIO32
|
||||||
@@ -166,53 +237,251 @@ binary_sensor:
|
|||||||
input: true
|
input: true
|
||||||
pullup: true
|
pullup: true
|
||||||
name: Volume Down
|
name: Volume Down
|
||||||
id: volume_down
|
|
||||||
on_click:
|
on_click:
|
||||||
- media_player.volume_down: luxe_out
|
- lambda: |-
|
||||||
|
id(Vol) -= 0.05;
|
||||||
|
if(id(Vol) < 0) id(Vol) = 0;
|
||||||
|
- media_player.volume_set:
|
||||||
|
id: luxe_media_player
|
||||||
|
volume: !lambda return id(Vol);
|
||||||
|
|
||||||
- platform: gpio
|
- platform: gpio
|
||||||
pin:
|
pin:
|
||||||
number: GPIO12
|
number: GPIO12
|
||||||
inverted: true
|
inverted: true
|
||||||
mode:
|
mode:
|
||||||
input: true
|
input: true
|
||||||
pullup: true
|
pullup: true
|
||||||
name: Action
|
name: Mute
|
||||||
id: action_button
|
on_click:
|
||||||
on_multi_click:
|
- if:
|
||||||
- timing:
|
condition:
|
||||||
- ON FOR AT MOST 350ms
|
- lambda: 'return(id(mute));'
|
||||||
- OFF FOR AT LEAST 10ms
|
then:
|
||||||
then:
|
- script.execute: mute_off
|
||||||
- media_player.toggle: luxe_out
|
- lambda: id(mute) = false;
|
||||||
- timing:
|
else:
|
||||||
- ON FOR AT LEAST 350ms
|
- script.execute: mute_on
|
||||||
then:
|
- lambda: id(mute) = true;
|
||||||
- voice_assistant.start:
|
on_double_click:
|
||||||
- timing:
|
- if:
|
||||||
- ON FOR AT LEAST 350ms
|
condition:
|
||||||
- OFF FOR AT LEAST 10ms
|
- lambda: 'return(id(phase) == 2);'
|
||||||
then:
|
then:
|
||||||
- voice_assistant.stop:
|
- media_player.stop:
|
||||||
|
|
||||||
light:
|
light:
|
||||||
- platform: fastled_clockless
|
- platform: esp32_rmt_led_strip
|
||||||
name: None
|
name: None
|
||||||
id: top_led
|
id: top_led
|
||||||
pin: GPIO22
|
pin: GPIO22
|
||||||
chipset: SK6812
|
chipset: WS2812
|
||||||
num_leds: 1
|
num_leds: 1
|
||||||
rgb_order: grb
|
rgb_order: grb
|
||||||
|
# rmt_channel: 0
|
||||||
|
default_transition_length: 0s
|
||||||
gamma_correct: 2.8
|
gamma_correct: 2.8
|
||||||
effects:
|
effects:
|
||||||
- pulse:
|
- pulse:
|
||||||
|
name: pulse
|
||||||
transition_length: 250ms
|
transition_length: 250ms
|
||||||
update_interval: 250ms
|
update_interval: 250ms
|
||||||
|
- pulse:
|
||||||
|
name: slow_pulse
|
||||||
|
transition_length: 1s
|
||||||
|
update_interval: 2s
|
||||||
|
|
||||||
button:
|
i2s_audio:
|
||||||
- platform: safe_mode
|
i2s_lrclk_pin: GPIO25
|
||||||
id: button_safe_mode
|
i2s_bclk_pin: GPIO5
|
||||||
name: Safe Mode Boot
|
i2s_mclk_pin: GPIO0
|
||||||
|
|
||||||
- platform: factory_reset
|
microphone:
|
||||||
id: factory_reset_btn
|
- platform: i2s_audio
|
||||||
name: Factory reset
|
id: luxe_mic
|
||||||
|
sample_rate: 16000
|
||||||
|
i2s_din_pin: GPIO35
|
||||||
|
bits_per_sample: 16bit
|
||||||
|
channel: stereo
|
||||||
|
adc_type: external
|
||||||
|
|
||||||
|
speaker:
|
||||||
|
- platform: i2s_audio
|
||||||
|
id: luxe_speaker
|
||||||
|
i2s_dout_pin: GPIO26
|
||||||
|
dac_type: external
|
||||||
|
sample_rate: 48000
|
||||||
|
bits_per_sample: 16bit
|
||||||
|
channel: stereo
|
||||||
|
buffer_duration: 100ms
|
||||||
|
|
||||||
|
media_player:
|
||||||
|
- platform: speaker
|
||||||
|
name: None
|
||||||
|
id: luxe_media_player
|
||||||
|
# volume_min: 0.5
|
||||||
|
# volume_max: 0.8
|
||||||
|
announcement_pipeline:
|
||||||
|
speaker: luxe_speaker
|
||||||
|
format: FLAC
|
||||||
|
sample_rate: 48000
|
||||||
|
num_channels: 2
|
||||||
|
files:
|
||||||
|
- id: little_sound
|
||||||
|
file: https://github.com/esphome/home-assistant-voice-pe/raw/dev/sounds/timer_finished.flac
|
||||||
|
on_announcement:
|
||||||
|
- micro_wake_word.stop:
|
||||||
|
- if:
|
||||||
|
condition:
|
||||||
|
lambda: 'return(id(phase) != 2);'
|
||||||
|
then:
|
||||||
|
- lambda: |-
|
||||||
|
if(id(phase) == 1) id(phase) = 2;
|
||||||
|
- script.execute: mute_off
|
||||||
|
- script.execute: update_led
|
||||||
|
|
||||||
|
on_idle:
|
||||||
|
- wait_until:
|
||||||
|
and:
|
||||||
|
- not:
|
||||||
|
media_player.is_announcing:
|
||||||
|
- not:
|
||||||
|
voice_assistant.is_running:
|
||||||
|
- if:
|
||||||
|
condition:
|
||||||
|
lambda: 'return((id(phase) == 4) || (id(phase) == 2));'
|
||||||
|
then:
|
||||||
|
- lambda: |-
|
||||||
|
id(phase) = 1;
|
||||||
|
- micro_wake_word.start:
|
||||||
|
- script.execute: update_led
|
||||||
|
|
||||||
|
voice_assistant:
|
||||||
|
id: va
|
||||||
|
microphone: luxe_mic
|
||||||
|
media_player: luxe_media_player
|
||||||
|
use_wake_word: false
|
||||||
|
noise_suppression_level: 2
|
||||||
|
auto_gain: 31dBFS
|
||||||
|
volume_multiplier: 2.0
|
||||||
|
|
||||||
|
on_listening:
|
||||||
|
- logger.log: "listening 3 => phase"
|
||||||
|
- micro_wake_word.stop:
|
||||||
|
- lambda: |-
|
||||||
|
id(phase) = 3;
|
||||||
|
- script.execute: update_led
|
||||||
|
|
||||||
|
on_stt_end:
|
||||||
|
- media_player.play_media: !lambda return x;
|
||||||
|
- light.turn_on:
|
||||||
|
id: top_led
|
||||||
|
blue: 60%
|
||||||
|
red: 20%
|
||||||
|
green: 20%
|
||||||
|
effect: pulse
|
||||||
|
|
||||||
|
|
||||||
|
on_tts_start:
|
||||||
|
- logger.log: "answering 4 => phase"
|
||||||
|
- lambda: |-
|
||||||
|
id(phase) = 4;
|
||||||
|
- script.execute: update_led
|
||||||
|
|
||||||
|
on_error:
|
||||||
|
- logger.log: "ERROR!!!!!!!!!!!!!!!!"
|
||||||
|
- light.turn_on:
|
||||||
|
id: top_led
|
||||||
|
blue: 0%
|
||||||
|
red: 100%
|
||||||
|
green: 0%
|
||||||
|
effect: pulse
|
||||||
|
- delay: 3s
|
||||||
|
- lambda: id(phase) = 1;
|
||||||
|
- script.execute: update_led
|
||||||
|
|
||||||
|
|
||||||
|
#########
|
||||||
|
# Scripts
|
||||||
|
|
||||||
|
script:
|
||||||
|
- id: update_led
|
||||||
|
then:
|
||||||
|
- logger.log: "==>>>update_led"
|
||||||
|
- lambda: |-
|
||||||
|
if(id(phase) == 0)id(start).execute();
|
||||||
|
if(id(phase) == 1)id(waiting).execute();
|
||||||
|
if(id(phase) == 2)id(external_player).execute();
|
||||||
|
if(id(phase) == 3)id(listening).execute();
|
||||||
|
if(id(phase) == 4)id(answering).execute();
|
||||||
|
|
||||||
|
- id: start
|
||||||
|
then:
|
||||||
|
- light.turn_on:
|
||||||
|
id: top_led
|
||||||
|
effect: slow_pulse
|
||||||
|
red: 80%
|
||||||
|
green: 0%
|
||||||
|
blue: 80%
|
||||||
|
- delay: 5sec
|
||||||
|
- lambda: id(my_es8388).setup();
|
||||||
|
- output.turn_off: dac_mute
|
||||||
|
- lambda: id(phase) = 1;
|
||||||
|
- media_player.speaker.play_on_device_media_file:
|
||||||
|
media_file: little_sound
|
||||||
|
announcement: true
|
||||||
|
- script.execute: update_led
|
||||||
|
|
||||||
|
- id: waiting
|
||||||
|
then:
|
||||||
|
- light.turn_on:
|
||||||
|
id: top_led
|
||||||
|
effect: pulse
|
||||||
|
red: 0%
|
||||||
|
green: 0%
|
||||||
|
blue: 100%
|
||||||
|
brightness: 100%
|
||||||
|
- voice_assistant.stop:
|
||||||
|
- micro_wake_word.start:
|
||||||
|
|
||||||
|
|
||||||
|
- id: listening
|
||||||
|
then:
|
||||||
|
- light.turn_on:
|
||||||
|
id: top_led
|
||||||
|
effect: pulse
|
||||||
|
red: 0%
|
||||||
|
green: 100%
|
||||||
|
blue: 0%
|
||||||
|
brightness: 100%
|
||||||
|
|
||||||
|
- id: answering
|
||||||
|
then:
|
||||||
|
- light.turn_on:
|
||||||
|
id: top_led
|
||||||
|
effect: none
|
||||||
|
red: 100%
|
||||||
|
green: 100%
|
||||||
|
blue: 0%
|
||||||
|
brightness: 100%
|
||||||
|
|
||||||
|
- id: external_player
|
||||||
|
then:
|
||||||
|
- light.turn_on:
|
||||||
|
id: top_led
|
||||||
|
effect: none
|
||||||
|
red: 80%
|
||||||
|
green: 40%
|
||||||
|
blue: 0%
|
||||||
|
|
||||||
|
- id: mute_on
|
||||||
|
then:
|
||||||
|
- media_player.volume_set:
|
||||||
|
volume: '0'
|
||||||
|
- lambda: id(mute) = true;
|
||||||
|
|
||||||
|
- id: mute_off
|
||||||
|
then:
|
||||||
|
- media_player.volume_set:
|
||||||
|
volume: !lambda return(id(Vol));
|
||||||
|
- lambda: id(mute) = false;
|
||||||
|
@@ -1,12 +1,12 @@
|
|||||||
substitutions:
|
substitutions:
|
||||||
name: "riden-labornetzteil"
|
name: "riden-labornetzteil-18a"
|
||||||
friendly_name: "Riden Labornetzteil"
|
friendly_name: "Riden Labornetzteil 18A"
|
||||||
# Change this model to fit your particular one.
|
# Change this model to fit your particular one.
|
||||||
# You can find it in Home Assistant as the device diagnostic "Model Name".
|
# You can find it in Home Assistant as the device diagnostic "Model Name".
|
||||||
model: "RD6018"
|
model: "RD6018"
|
||||||
device_name: "rd6018-controller"
|
device_name: "rd6018-controller"
|
||||||
device_friendly_name: "Riden RD6018"
|
device_friendly_name: "Riden RD6018"
|
||||||
device_description: "Labornetzteil"
|
device_description: "Monitor and control a RD6018 PSU via WiFi"
|
||||||
time_timezone: "Europe/Berlin"
|
time_timezone: "Europe/Berlin"
|
||||||
|
|
||||||
# Model specific settings (Don't change these!)
|
# Model specific settings (Don't change these!)
|
||||||
@@ -53,7 +53,7 @@ esp8266:
|
|||||||
wifi:
|
wifi:
|
||||||
ssid: "Voltage-legacy"
|
ssid: "Voltage-legacy"
|
||||||
password: !secret voltage_legacy_psk
|
password: !secret voltage_legacy_psk
|
||||||
use_address: riden-labornetzteil.home
|
use_address: riden-labornetzteil-18a.home
|
||||||
power_save_mode: high
|
power_save_mode: high
|
||||||
fast_connect: on
|
fast_connect: on
|
||||||
|
|
506
esphome/riden-labornetzteil-6a.yaml
Normal file
@@ -0,0 +1,506 @@
|
|||||||
|
substitutions:
|
||||||
|
name: "riden-labornetzteil-6a"
|
||||||
|
friendly_name: "Riden Labornetzteil 6A"
|
||||||
|
# Change this model to fit your particular one.
|
||||||
|
# You can find it in Home Assistant as the device diagnostic "Model Name".
|
||||||
|
model: "RD6006"
|
||||||
|
device_name: "rd6006-controller"
|
||||||
|
device_friendly_name: "Riden RD6006"
|
||||||
|
device_description: "Monitor and control a RD6006 PSU via WiFi"
|
||||||
|
time_timezone: "Europe/Berlin"
|
||||||
|
|
||||||
|
# Model specific settings (Don't change these!)
|
||||||
|
RD6006_voltage_maximum: "60"
|
||||||
|
RD6006_voltage_accuracy: "2"
|
||||||
|
RD6006_voltage_multiplier: "0.01"
|
||||||
|
RD6006_current_maximum: "6"
|
||||||
|
RD6006_current_accuracy: "3"
|
||||||
|
RD6006_current_multiplier: "0.001"
|
||||||
|
|
||||||
|
RD6006P_voltage_maximum: "60"
|
||||||
|
RD6006P_voltage_accuracy: "3"
|
||||||
|
RD6006P_voltage_multiplier: "0.001"
|
||||||
|
RD6006P_current_maximum: "6"
|
||||||
|
RD6006P_current_accuracy: "4"
|
||||||
|
RD6006P_current_multiplier: "0.0001"
|
||||||
|
|
||||||
|
RD6012_voltage_maximum: "60"
|
||||||
|
RD6012_voltage_accuracy: "2"
|
||||||
|
RD6012_voltage_multiplier: "0.01"
|
||||||
|
RD6012_current_maximum: "12"
|
||||||
|
RD6012_current_accuracy: "2"
|
||||||
|
RD6012_current_multiplier: "0.01"
|
||||||
|
|
||||||
|
RD6018_voltage_maximum: "60"
|
||||||
|
RD6018_voltage_accuracy: "2"
|
||||||
|
RD6018_voltage_multiplier: "0.01"
|
||||||
|
RD6018_current_maximum: "18"
|
||||||
|
RD6018_current_accuracy: "2"
|
||||||
|
RD6018_current_multiplier: "0.01"
|
||||||
|
|
||||||
|
esphome:
|
||||||
|
name: $device_name
|
||||||
|
friendly_name: $device_friendly_name
|
||||||
|
comment: $device_description
|
||||||
|
name_add_mac_suffix: false
|
||||||
|
project:
|
||||||
|
name: "wildekek.rd6006-controller"
|
||||||
|
version: "1.4.1"
|
||||||
|
|
||||||
|
esp8266:
|
||||||
|
board: esp12e
|
||||||
|
|
||||||
|
wifi:
|
||||||
|
ssid: "Voltage-legacy"
|
||||||
|
password: !secret voltage_legacy_psk
|
||||||
|
use_address: riden-labornetzteil-6a.home
|
||||||
|
power_save_mode: high
|
||||||
|
fast_connect: on
|
||||||
|
|
||||||
|
# Enable fallback hotspot (captive portal) in case wifi connection fails
|
||||||
|
ap:
|
||||||
|
ssid: "RD6006 Fallback Hotspot"
|
||||||
|
password: !secret fallback_psk
|
||||||
|
|
||||||
|
captive_portal:
|
||||||
|
|
||||||
|
# Enable logging
|
||||||
|
logger:
|
||||||
|
level: INFO
|
||||||
|
# Disable logging via UART, since we're using this for modbus communication
|
||||||
|
baud_rate: 0
|
||||||
|
|
||||||
|
# Enable Home Assistant API
|
||||||
|
api:
|
||||||
|
encryption:
|
||||||
|
key: !secret apikey
|
||||||
|
|
||||||
|
ota:
|
||||||
|
platform: esphome
|
||||||
|
password: !secret ota
|
||||||
|
|
||||||
|
# Enable status LED
|
||||||
|
status_led:
|
||||||
|
pin:
|
||||||
|
number: GPIO2
|
||||||
|
inverted: true
|
||||||
|
|
||||||
|
uart:
|
||||||
|
id: mod_bus
|
||||||
|
tx_pin: GPIO1
|
||||||
|
rx_pin: GPIO3
|
||||||
|
baud_rate: 115200
|
||||||
|
data_bits: 8
|
||||||
|
stop_bits: 1
|
||||||
|
parity: none
|
||||||
|
|
||||||
|
modbus:
|
||||||
|
id: modbus1
|
||||||
|
|
||||||
|
modbus_controller:
|
||||||
|
- id: powersupply
|
||||||
|
## This address should be set to the "Address" value in the config menu
|
||||||
|
address: 0x01
|
||||||
|
modbus_id: modbus1
|
||||||
|
setup_priority: -10
|
||||||
|
update_interval: 5s
|
||||||
|
|
||||||
|
time:
|
||||||
|
# Get the time from HA, so we can use it for uptime
|
||||||
|
- platform: homeassistant
|
||||||
|
id: time_homeassistant
|
||||||
|
timezone: "${time_timezone}"
|
||||||
|
on_time_sync:
|
||||||
|
- logger.log: Time has been set and is valid!
|
||||||
|
- component.update: sensor_uptime_timestamp
|
||||||
|
- number.set:
|
||||||
|
id: date_year
|
||||||
|
value: !lambda return id(time_homeassistant).now().year;
|
||||||
|
- number.set:
|
||||||
|
id: date_month
|
||||||
|
value: !lambda return id(time_homeassistant).now().month;
|
||||||
|
- number.set:
|
||||||
|
id: date_day
|
||||||
|
value: !lambda return id(time_homeassistant).now().day_of_month;
|
||||||
|
- number.set:
|
||||||
|
id: date_hour
|
||||||
|
value: !lambda return id(time_homeassistant).now().hour;
|
||||||
|
- number.set:
|
||||||
|
id: date_minute
|
||||||
|
value: !lambda return id(time_homeassistant).now().minute;
|
||||||
|
- number.set:
|
||||||
|
id: date_second
|
||||||
|
value: !lambda return id(time_homeassistant).now().second;
|
||||||
|
|
||||||
|
sensor:
|
||||||
|
- platform: modbus_controller
|
||||||
|
id: model_number
|
||||||
|
name: "Model Number"
|
||||||
|
entity_category: diagnostic
|
||||||
|
disabled_by_default: True
|
||||||
|
modbus_controller_id: powersupply
|
||||||
|
address: 0
|
||||||
|
skip_updates: 10
|
||||||
|
unit_of_measurement: ""
|
||||||
|
register_type: holding
|
||||||
|
value_type: U_WORD
|
||||||
|
accuracy_decimals: 0
|
||||||
|
on_value:
|
||||||
|
then:
|
||||||
|
- lambda: |-
|
||||||
|
id(model_name).publish_state(value_accuracy_to_string(x, 0));
|
||||||
|
|
||||||
|
- platform: modbus_controller
|
||||||
|
name: "Serial Number"
|
||||||
|
entity_category: diagnostic
|
||||||
|
disabled_by_default: True
|
||||||
|
modbus_controller_id: powersupply
|
||||||
|
address: 1
|
||||||
|
skip_updates: 10
|
||||||
|
register_type: holding
|
||||||
|
value_type: U_DWORD
|
||||||
|
accuracy_decimals: 0
|
||||||
|
|
||||||
|
- platform: modbus_controller
|
||||||
|
modbus_controller_id: powersupply
|
||||||
|
address: 3
|
||||||
|
name: "Firmware version"
|
||||||
|
entity_category: diagnostic
|
||||||
|
disabled_by_default: True
|
||||||
|
unit_of_measurement: ""
|
||||||
|
register_type: holding
|
||||||
|
value_type: U_WORD
|
||||||
|
accuracy_decimals: 2
|
||||||
|
filters:
|
||||||
|
- multiply: 0.01
|
||||||
|
|
||||||
|
- platform: modbus_controller
|
||||||
|
modbus_controller_id: powersupply
|
||||||
|
address: 10
|
||||||
|
name: "Output voltage"
|
||||||
|
device_class: voltage
|
||||||
|
state_class: measurement
|
||||||
|
unit_of_measurement: "V"
|
||||||
|
register_type: holding
|
||||||
|
value_type: U_WORD
|
||||||
|
accuracy_decimals: ${${model}_voltage_accuracy}
|
||||||
|
filters:
|
||||||
|
- multiply: ${${model}_voltage_multiplier}
|
||||||
|
|
||||||
|
- platform: modbus_controller
|
||||||
|
modbus_controller_id: powersupply
|
||||||
|
address: 11
|
||||||
|
name: "Output current"
|
||||||
|
device_class: current
|
||||||
|
state_class: measurement
|
||||||
|
unit_of_measurement: "A"
|
||||||
|
register_type: holding
|
||||||
|
value_type: U_WORD
|
||||||
|
accuracy_decimals: ${${model}_current_accuracy}
|
||||||
|
filters:
|
||||||
|
- multiply: ${${model}_current_multiplier}
|
||||||
|
|
||||||
|
- platform: modbus_controller
|
||||||
|
modbus_controller_id: powersupply
|
||||||
|
address: 12
|
||||||
|
name: "Output Power"
|
||||||
|
device_class: power
|
||||||
|
state_class: measurement
|
||||||
|
unit_of_measurement: "W"
|
||||||
|
register_type: holding
|
||||||
|
value_type: U_DWORD
|
||||||
|
accuracy_decimals: 2
|
||||||
|
filters:
|
||||||
|
- multiply: 0.01
|
||||||
|
|
||||||
|
- platform: modbus_controller
|
||||||
|
modbus_controller_id: powersupply
|
||||||
|
address: 33
|
||||||
|
name: "Battery voltage"
|
||||||
|
device_class: voltage
|
||||||
|
state_class: measurement
|
||||||
|
unit_of_measurement: "V"
|
||||||
|
register_type: holding
|
||||||
|
value_type: U_WORD
|
||||||
|
accuracy_decimals: ${${model}_voltage_accuracy}
|
||||||
|
filters:
|
||||||
|
- multiply: ${${model}_voltage_multiplier}
|
||||||
|
|
||||||
|
- platform: modbus_controller
|
||||||
|
modbus_controller_id: powersupply
|
||||||
|
address: 38
|
||||||
|
name: "Battery charge"
|
||||||
|
device_class: "energy_storage"
|
||||||
|
state_class: measurement
|
||||||
|
unit_of_measurement: "Ah"
|
||||||
|
icon: "mdi:battery-60"
|
||||||
|
register_type: holding
|
||||||
|
value_type: U_DWORD
|
||||||
|
accuracy_decimals: 3
|
||||||
|
filters:
|
||||||
|
- multiply: 0.001
|
||||||
|
|
||||||
|
- platform: modbus_controller
|
||||||
|
modbus_controller_id: powersupply
|
||||||
|
address: 40
|
||||||
|
name: "Battery energy"
|
||||||
|
device_class: "energy_storage"
|
||||||
|
state_class: measurement
|
||||||
|
unit_of_measurement: "Wh"
|
||||||
|
icon: "mdi:battery-60"
|
||||||
|
register_type: holding
|
||||||
|
value_type: U_DWORD
|
||||||
|
accuracy_decimals: 3
|
||||||
|
filters:
|
||||||
|
- multiply: 0.001
|
||||||
|
|
||||||
|
- platform: modbus_controller
|
||||||
|
modbus_controller_id: powersupply
|
||||||
|
address: 14
|
||||||
|
name: "Input voltage"
|
||||||
|
device_class: voltage
|
||||||
|
unit_of_measurement: "V"
|
||||||
|
register_type: holding
|
||||||
|
value_type: U_WORD
|
||||||
|
accuracy_decimals: ${${model}_voltage_accuracy}
|
||||||
|
filters:
|
||||||
|
- multiply: ${${model}_voltage_multiplier}
|
||||||
|
|
||||||
|
- platform: modbus_controller
|
||||||
|
name: "Temperature"
|
||||||
|
device_class: temperature
|
||||||
|
state_class: measurement
|
||||||
|
modbus_controller_id: powersupply
|
||||||
|
register_type: holding
|
||||||
|
address: 4
|
||||||
|
value_type: S_DWORD
|
||||||
|
unit_of_measurement: "°C"
|
||||||
|
|
||||||
|
- platform: modbus_controller
|
||||||
|
name: "Temperature external"
|
||||||
|
state_class: measurement
|
||||||
|
modbus_controller_id: powersupply
|
||||||
|
register_type: holding
|
||||||
|
address: 34
|
||||||
|
value_type: S_DWORD
|
||||||
|
device_class: temperature
|
||||||
|
unit_of_measurement: "°C"
|
||||||
|
|
||||||
|
- platform: wifi_signal
|
||||||
|
name: "Wi-Fi Signal"
|
||||||
|
disabled_by_default: True
|
||||||
|
update_interval: 60s
|
||||||
|
|
||||||
|
# Uptime is used internally only
|
||||||
|
- platform: uptime
|
||||||
|
id: sensor_uptime
|
||||||
|
# This sensor is an alternative for the uptime sensor, which only sends the
|
||||||
|
# startup timestamp of the device to home assistant once
|
||||||
|
- platform: template
|
||||||
|
id: sensor_uptime_timestamp
|
||||||
|
name: "Uptime"
|
||||||
|
entity_category: diagnostic
|
||||||
|
device_class: "timestamp"
|
||||||
|
accuracy_decimals: 0
|
||||||
|
update_interval: never
|
||||||
|
lambda: |-
|
||||||
|
static float timestamp = (
|
||||||
|
id(time_homeassistant).utcnow().timestamp - id(sensor_uptime).state
|
||||||
|
);
|
||||||
|
return timestamp;
|
||||||
|
|
||||||
|
text_sensor:
|
||||||
|
- platform: wifi_info
|
||||||
|
ip_address:
|
||||||
|
name: "IP Address"
|
||||||
|
disabled_by_default: True
|
||||||
|
ssid:
|
||||||
|
name: "Wi-Fi SSID"
|
||||||
|
disabled_by_default: True
|
||||||
|
bssid:
|
||||||
|
name: "Wi-Fi BSSID"
|
||||||
|
disabled_by_default: True
|
||||||
|
- platform: template
|
||||||
|
id: model_name
|
||||||
|
name: "Model Name"
|
||||||
|
entity_category: diagnostic
|
||||||
|
# Updated by model number
|
||||||
|
update_interval: never
|
||||||
|
filters:
|
||||||
|
- map:
|
||||||
|
- 60062 -> RD6006
|
||||||
|
- 60065 -> RD6006P
|
||||||
|
- 60121 -> RD6012
|
||||||
|
- 60181 -> RD6018
|
||||||
|
|
||||||
|
binary_sensor:
|
||||||
|
- platform: modbus_controller
|
||||||
|
modbus_controller_id: powersupply
|
||||||
|
name: "Keypad lock"
|
||||||
|
entity_category: diagnostic
|
||||||
|
device_class: lock
|
||||||
|
address: 15
|
||||||
|
register_type: holding
|
||||||
|
bitmask: 0x1
|
||||||
|
filters:
|
||||||
|
- invert:
|
||||||
|
- platform: modbus_controller
|
||||||
|
modbus_controller_id: powersupply
|
||||||
|
address: 32
|
||||||
|
name: "Battery mode"
|
||||||
|
device_class: connectivity
|
||||||
|
register_type: holding
|
||||||
|
bitmask: 0x1
|
||||||
|
- platform: modbus_controller
|
||||||
|
modbus_controller_id: powersupply
|
||||||
|
name: "Over Voltage Protection"
|
||||||
|
device_class: problem
|
||||||
|
address: 16
|
||||||
|
register_type: holding
|
||||||
|
bitmask: 0x1
|
||||||
|
- platform: modbus_controller
|
||||||
|
modbus_controller_id: powersupply
|
||||||
|
name: "Over Current Protection"
|
||||||
|
device_class: problem
|
||||||
|
address: 16
|
||||||
|
register_type: holding
|
||||||
|
bitmask: 0x2
|
||||||
|
- platform: modbus_controller
|
||||||
|
modbus_controller_id: powersupply
|
||||||
|
name: "Constant Voltage"
|
||||||
|
address: 17
|
||||||
|
register_type: holding
|
||||||
|
bitmask: 0x1
|
||||||
|
filters:
|
||||||
|
- invert:
|
||||||
|
- platform: modbus_controller
|
||||||
|
modbus_controller_id: powersupply
|
||||||
|
name: "Constant Current"
|
||||||
|
address: 17
|
||||||
|
register_type: holding
|
||||||
|
bitmask: 0x1
|
||||||
|
|
||||||
|
number:
|
||||||
|
- platform: modbus_controller
|
||||||
|
modbus_controller_id: powersupply
|
||||||
|
name: "Backlight"
|
||||||
|
icon: "mdi:lightbulb"
|
||||||
|
entity_category: config
|
||||||
|
address: 72
|
||||||
|
value_type: U_WORD
|
||||||
|
min_value: 0
|
||||||
|
max_value: 5
|
||||||
|
|
||||||
|
- platform: modbus_controller
|
||||||
|
modbus_controller_id: powersupply
|
||||||
|
name: "Output voltage"
|
||||||
|
device_class: voltage
|
||||||
|
unit_of_measurement: "V"
|
||||||
|
entity_category: config
|
||||||
|
address: 8
|
||||||
|
value_type: U_WORD
|
||||||
|
min_value: 0
|
||||||
|
max_value: ${${model}_voltage_maximum}
|
||||||
|
step: ${${model}_voltage_multiplier}
|
||||||
|
lambda: !lambda return x * ${${model}_voltage_multiplier};
|
||||||
|
write_lambda: !lambda return x * (1/${${model}_voltage_multiplier});
|
||||||
|
|
||||||
|
- platform: modbus_controller
|
||||||
|
modbus_controller_id: powersupply
|
||||||
|
name: "Output current"
|
||||||
|
device_class: current
|
||||||
|
unit_of_measurement: "A"
|
||||||
|
entity_category: config
|
||||||
|
address: 9
|
||||||
|
value_type: U_WORD
|
||||||
|
min_value: 0
|
||||||
|
max_value: ${${model}_current_maximum}
|
||||||
|
step: ${${model}_current_multiplier}
|
||||||
|
lambda: !lambda return x * ${${model}_current_multiplier};
|
||||||
|
write_lambda: !lambda return x * (1/${${model}_current_multiplier});
|
||||||
|
|
||||||
|
- platform: modbus_controller
|
||||||
|
modbus_controller_id: powersupply
|
||||||
|
name: "Over Voltage Protection"
|
||||||
|
device_class: voltage
|
||||||
|
unit_of_measurement: "V"
|
||||||
|
address: 82
|
||||||
|
entity_category: config
|
||||||
|
value_type: U_WORD
|
||||||
|
min_value: 0
|
||||||
|
max_value: ${${model}_voltage_maximum}
|
||||||
|
step: ${${model}_voltage_multiplier}
|
||||||
|
lambda: !lambda return x * ${${model}_voltage_multiplier};
|
||||||
|
write_lambda: !lambda return x * (1/${${model}_voltage_multiplier});
|
||||||
|
|
||||||
|
|
||||||
|
- platform: modbus_controller
|
||||||
|
modbus_controller_id: powersupply
|
||||||
|
name: "Over Current Protection"
|
||||||
|
device_class: current
|
||||||
|
unit_of_measurement: "A"
|
||||||
|
address: 83
|
||||||
|
entity_category: config
|
||||||
|
value_type: U_WORD
|
||||||
|
min_value: 0
|
||||||
|
max_value: ${${model}_current_maximum}
|
||||||
|
step: ${${model}_current_multiplier}
|
||||||
|
lambda: !lambda return x * ${${model}_current_multiplier};
|
||||||
|
write_lambda: !lambda return x * (1/${${model}_current_multiplier});
|
||||||
|
|
||||||
|
# Date components are kept internal
|
||||||
|
- platform: modbus_controller
|
||||||
|
id: date_year
|
||||||
|
modbus_controller_id: powersupply
|
||||||
|
entity_category: diagnostic
|
||||||
|
register_type: holding
|
||||||
|
address: 48
|
||||||
|
value_type: U_WORD
|
||||||
|
- platform: modbus_controller
|
||||||
|
id: date_month
|
||||||
|
modbus_controller_id: powersupply
|
||||||
|
entity_category: diagnostic
|
||||||
|
register_type: holding
|
||||||
|
address: 49
|
||||||
|
value_type: U_WORD
|
||||||
|
- platform: modbus_controller
|
||||||
|
id: date_day
|
||||||
|
modbus_controller_id: powersupply
|
||||||
|
entity_category: diagnostic
|
||||||
|
register_type: holding
|
||||||
|
address: 50
|
||||||
|
value_type: U_WORD
|
||||||
|
- platform: modbus_controller
|
||||||
|
id: date_hour
|
||||||
|
modbus_controller_id: powersupply
|
||||||
|
entity_category: diagnostic
|
||||||
|
register_type: holding
|
||||||
|
address: 51
|
||||||
|
value_type: U_WORD
|
||||||
|
- platform: modbus_controller
|
||||||
|
id: date_minute
|
||||||
|
modbus_controller_id: powersupply
|
||||||
|
entity_category: diagnostic
|
||||||
|
register_type: holding
|
||||||
|
address: 52
|
||||||
|
value_type: U_WORD
|
||||||
|
- platform: modbus_controller
|
||||||
|
id: date_second
|
||||||
|
modbus_controller_id: powersupply
|
||||||
|
entity_category: diagnostic
|
||||||
|
register_type: holding
|
||||||
|
address: 53
|
||||||
|
value_type: U_WORD
|
||||||
|
|
||||||
|
switch:
|
||||||
|
- platform: modbus_controller
|
||||||
|
modbus_controller_id: powersupply
|
||||||
|
name: "Output"
|
||||||
|
address: 18
|
||||||
|
register_type: holding
|
||||||
|
bitmask: 0x1
|
||||||
|
entity_category: config
|
||||||
|
|
||||||
|
button:
|
||||||
|
- platform: safe_mode
|
||||||
|
name: "Safe mode"
|
BIN
image/24c27e221bdb3c4e659741e304d0ee9d/256x256
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
image/2519dc3f137090683d62ff576d91a0c9/256x256
Normal file
After Width: | Height: | Size: 6.8 KiB |
BIN
image/2d5009a2c9dab46f57b9ddaf569b947e/256x256
Normal file
After Width: | Height: | Size: 5.3 KiB |
BIN
image/3ad2f364a964574f42a8f3206d104fdb/256x256
Normal file
After Width: | Height: | Size: 5.5 KiB |
BIN
image/5fedb57f4aec74efb42840acd1a05380/256x256
Normal file
After Width: | Height: | Size: 8.0 KiB |
BIN
image/681fb9bf3fd83a14a1ec95b48169723c/256x256
Normal file
After Width: | Height: | Size: 6.4 KiB |
BIN
image/681fb9bf3fd83a14a1ec95b48169723c/512x512
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
image/681fb9bf3fd83a14a1ec95b48169723c/original
Normal file
After Width: | Height: | Size: 685 KiB |
BIN
image/68915e3f6106faf4706cb9531f406398/256x256
Normal file
After Width: | Height: | Size: 6.5 KiB |
BIN
image/a117f2afda53dcd987db921889f23bb8/256x256
Normal file
After Width: | Height: | Size: 112 KiB |
BIN
image/cd75aa150fe6b7181701c1246d6dfc71/256x256
Normal file
After Width: | Height: | Size: 6.9 KiB |
@@ -1,6 +0,0 @@
|
|||||||
bdm:
|
|
||||||
name: BDM
|
|
||||||
mac: BLE_34:14:B5:A0:99:BD
|
|
||||||
icon:
|
|
||||||
picture:
|
|
||||||
track: true
|
|
162
mqtt.yaml
@@ -7,7 +7,7 @@
|
|||||||
value_template: '{{ float(value) * 99 + 1 }}'
|
value_template: '{{ float(value) * 99 + 1 }}'
|
||||||
state_class: measurement
|
state_class: measurement
|
||||||
entity_category: diagnostic
|
entity_category: diagnostic
|
||||||
state_topic: rtl_433/sdr/devices/Auriol-AHFL/1/244/battery_ok
|
state_topic: rtl_433/sdr/devices/Auriol-AHFL/1/216/battery_ok
|
||||||
unique_id: Auriol-AHFL-1-106-B
|
unique_id: Auriol-AHFL-1-106-B
|
||||||
device:
|
device:
|
||||||
identifiers: Auriol-AHFL-1-106
|
identifiers: Auriol-AHFL-1-106
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
unit_of_measurement: °C
|
unit_of_measurement: °C
|
||||||
value_template: '{{ value|float }}'
|
value_template: '{{ value|float }}'
|
||||||
state_class: measurement
|
state_class: measurement
|
||||||
state_topic: rtl_433/sdr/devices/Auriol-AHFL/1/244/temperature_C
|
state_topic: rtl_433/sdr/devices/Auriol-AHFL/1/216/temperature_C
|
||||||
unique_id: Auriol-AHFL-1-106-T
|
unique_id: Auriol-AHFL-1-106-T
|
||||||
device:
|
device:
|
||||||
identifiers: Auriol-AHFL-1-106
|
identifiers: Auriol-AHFL-1-106
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
unit_of_measurement: '%'
|
unit_of_measurement: '%'
|
||||||
value_template: '{{ value|float }}'
|
value_template: '{{ value|float }}'
|
||||||
state_class: measurement
|
state_class: measurement
|
||||||
state_topic: rtl_433/sdr/devices/Auriol-AHFL/1/244/humidity
|
state_topic: rtl_433/sdr/devices/Auriol-AHFL/1/216/humidity
|
||||||
unique_id: Auriol-AHFL-1-106-H
|
unique_id: Auriol-AHFL-1-106-H
|
||||||
device:
|
device:
|
||||||
identifiers: Auriol-AHFL-1-106
|
identifiers: Auriol-AHFL-1-106
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
value_template: '{{ float(value) * 99 + 1 }}'
|
value_template: '{{ float(value) * 99 + 1 }}'
|
||||||
state_class: measurement
|
state_class: measurement
|
||||||
entity_category: diagnostic
|
entity_category: diagnostic
|
||||||
state_topic: rtl_433/sdr/devices/Vauno-EN8822C/1/244/battery_ok
|
state_topic: rtl_433/sdr/devices/Vauno-EN8822C/1/216/battery_ok
|
||||||
unique_id: Vauno-EN8822C-1-244-B
|
unique_id: Vauno-EN8822C-1-244-B
|
||||||
device:
|
device:
|
||||||
identifiers: Vauno-EN8822C-1-244
|
identifiers: Vauno-EN8822C-1-244
|
||||||
@@ -81,3 +81,157 @@
|
|||||||
model: Vauno-EN8822C
|
model: Vauno-EN8822C
|
||||||
manufacturer: rtl_433
|
manufacturer: rtl_433
|
||||||
|
|
||||||
|
- device_class: humidity
|
||||||
|
unit_of_measurement: '%'
|
||||||
|
value_template: '{{ value|float }}'
|
||||||
|
state_class: measurement
|
||||||
|
state_topic: rtl_433/sdr/devices/Nexus-TH/1/238/humidity
|
||||||
|
unique_id: Nexus-TH-1-238-H
|
||||||
|
device:
|
||||||
|
identifiers:
|
||||||
|
- Nexus-TH-1-238
|
||||||
|
model: Nexus-TH
|
||||||
|
manufacturer: rtl_433
|
||||||
|
name: Nexus-TH-1-238
|
||||||
|
name: Humidity
|
||||||
|
|
||||||
|
- device_class: humidity
|
||||||
|
unit_of_measurement: '%'
|
||||||
|
value_template: '{{ value|float }}'
|
||||||
|
state_class: measurement
|
||||||
|
state_topic: rtl_433/sdr/devices/Nexus-TH/1/238/humidity
|
||||||
|
unique_id: Nexus-TH-1-238-H
|
||||||
|
device:
|
||||||
|
identifiers:
|
||||||
|
- Nexus-TH-1-238
|
||||||
|
model: Nexus-TH
|
||||||
|
manufacturer: rtl_433
|
||||||
|
name: Nexus-TH-1-238
|
||||||
|
name: Humidity
|
||||||
|
|
||||||
|
# Cotech-367959
|
||||||
|
|
||||||
|
- device_class: battery
|
||||||
|
unit_of_measurement: '%'
|
||||||
|
value_template: '{{ ((float(value) * 99)|round(0)) + 1 }}'
|
||||||
|
state_class: measurement
|
||||||
|
entity_category: diagnostic
|
||||||
|
state_topic: rtl_433/sdr/devices/Cotech-367959/130/battery_ok
|
||||||
|
unique_id: Cotech-367959-130-B
|
||||||
|
device:
|
||||||
|
identifiers:
|
||||||
|
- Cotech-367959-130
|
||||||
|
model: Cotech-367959
|
||||||
|
manufacturer: rtl_433
|
||||||
|
name: Cotech-367959-130
|
||||||
|
name: Battery
|
||||||
|
|
||||||
|
- device_class: temperature
|
||||||
|
unit_of_measurement: °F
|
||||||
|
value_template: '{{ value|float|round(1) }}'
|
||||||
|
state_class: measurement
|
||||||
|
state_topic: rtl_433/sdr/devices/Cotech-367959/130/temperature_F
|
||||||
|
unique_id: Cotech-367959-130-F
|
||||||
|
device:
|
||||||
|
identifiers:
|
||||||
|
- Cotech-367959-130
|
||||||
|
model: Cotech-367959
|
||||||
|
manufacturer: rtl_433
|
||||||
|
name: Cotech-367959-130
|
||||||
|
name: Temperature
|
||||||
|
|
||||||
|
- device_class: humidity
|
||||||
|
unit_of_measurement: '%'
|
||||||
|
value_template: '{{ value|float }}'
|
||||||
|
state_class: measurement
|
||||||
|
state_topic: rtl_433/sdr/devices/Cotech-367959/130/humidity
|
||||||
|
unique_id: Cotech-367959-130-H
|
||||||
|
device:
|
||||||
|
identifiers:
|
||||||
|
- Cotech-367959-130
|
||||||
|
model: Cotech-367959
|
||||||
|
manufacturer: rtl_433
|
||||||
|
name: Cotech-367959-130
|
||||||
|
name: Humidity
|
||||||
|
|
||||||
|
- device_class: precipitation
|
||||||
|
unit_of_measurement: mm
|
||||||
|
value_template: '{{ value|float|round(2) }}'
|
||||||
|
state_class: total_increasing
|
||||||
|
state_topic: rtl_433/sdr/devices/Cotech-367959/130/rain_mm
|
||||||
|
unique_id: Cotech-367959-130-RT
|
||||||
|
device:
|
||||||
|
identifiers:
|
||||||
|
- Cotech-367959-130
|
||||||
|
model: Cotech-367959
|
||||||
|
manufacturer: rtl_433
|
||||||
|
name: Cotech-367959-130
|
||||||
|
name: Rain Total
|
||||||
|
|
||||||
|
- unit_of_measurement: °
|
||||||
|
value_template: '{{ value|float }}'
|
||||||
|
state_class: measurement
|
||||||
|
state_topic: rtl_433/sdr/devices/Cotech-367959/130/wind_dir_deg
|
||||||
|
unique_id: Cotech-367959-130-WD
|
||||||
|
device:
|
||||||
|
identifiers:
|
||||||
|
- Cotech-367959-130
|
||||||
|
model: Cotech-367959
|
||||||
|
manufacturer: rtl_433
|
||||||
|
name: Cotech-367959-130
|
||||||
|
name: Wind Direction
|
||||||
|
|
||||||
|
- device_class: wind_speed
|
||||||
|
unit_of_measurement: km/h
|
||||||
|
value_template: '{{ (float(value|float) * 3.6) | round(2) }}'
|
||||||
|
state_class: measurement
|
||||||
|
state_topic: rtl_433/sdr/devices/Cotech-367959/130/wind_avg_m_s
|
||||||
|
unique_id: Cotech-367959-130-WS
|
||||||
|
device:
|
||||||
|
identifiers:
|
||||||
|
- Cotech-367959-130
|
||||||
|
model: Cotech-367959
|
||||||
|
manufacturer: rtl_433
|
||||||
|
name: Cotech-367959-130
|
||||||
|
name: Wind Average
|
||||||
|
|
||||||
|
- device_class: wind_speed
|
||||||
|
unit_of_measurement: km/h
|
||||||
|
value_template: '{{ (float(value|float) * 3.6) | round(2) }}'
|
||||||
|
state_class: measurement
|
||||||
|
state_topic: rtl_433/sdr/devices/Cotech-367959/130/wind_max_m_s
|
||||||
|
unique_id: Cotech-367959-130-GS
|
||||||
|
device:
|
||||||
|
identifiers:
|
||||||
|
- Cotech-367959-130
|
||||||
|
model: Cotech-367959
|
||||||
|
manufacturer: rtl_433
|
||||||
|
name: Cotech-367959-130
|
||||||
|
name: Wind max
|
||||||
|
|
||||||
|
- device_class: illuminance
|
||||||
|
unit_of_measurement: lx
|
||||||
|
value_template: '{{ value|int }}'
|
||||||
|
state_class: measurement
|
||||||
|
state_topic: rtl_433/sdr/devices/Cotech-367959/130/light_lux
|
||||||
|
unique_id: Cotech-367959-130-lux
|
||||||
|
device:
|
||||||
|
identifiers:
|
||||||
|
- Cotech-367959-130
|
||||||
|
model: Cotech-367959
|
||||||
|
manufacturer: rtl_433
|
||||||
|
name: Cotech-367959-130
|
||||||
|
name: Outside Luminance
|
||||||
|
|
||||||
|
- unit_of_measurement: UV Index
|
||||||
|
value_template: '{{ value|float|round(1) }}'
|
||||||
|
state_class: measurement
|
||||||
|
state_topic: rtl_433/sdr/devices/Cotech-367959/130/uv
|
||||||
|
unique_id: Cotech-367959-130-uv
|
||||||
|
device:
|
||||||
|
identifiers:
|
||||||
|
- Cotech-367959-130
|
||||||
|
model: Cotech-367959
|
||||||
|
manufacturer: rtl_433
|
||||||
|
name: Cotech-367959-130
|
||||||
|
name: UV Index
|
||||||
|
8
mqtt_statestream.yaml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
base_topic: homeassistant
|
||||||
|
publish_attributes: true
|
||||||
|
publish_timestamps: true
|
||||||
|
# include:
|
||||||
|
# entities:
|
||||||
|
# - sensor.Netzleistung
|
||||||
|
|
||||||
|
|
8
notify.yaml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
#All Mobile Phones
|
||||||
|
- platform: group
|
||||||
|
name: "Alle mobilen Geräte"
|
||||||
|
services:
|
||||||
|
- service: mobile_app_le2123
|
||||||
|
- service: mobile_app_apollo
|
||||||
|
- service: mobile_app_xt2125_4
|
||||||
|
|
@@ -29,6 +29,7 @@
|
|||||||
- sensor.*_wi_fi_signal
|
- sensor.*_wi_fi_signal
|
||||||
- sensor.*_wifi_strenght
|
- sensor.*_wifi_strenght
|
||||||
- sensor.*_uptime
|
- sensor.*_uptime
|
||||||
|
- sensor.sun*
|
||||||
entities:
|
entities:
|
||||||
- sun.sun # Don't record sun data
|
- sun.sun # Don't record sun data
|
||||||
- sensor.fritzbox_device_uptime
|
- sensor.fritzbox_device_uptime
|
||||||
|
@@ -61,7 +61,8 @@
|
|||||||
{% set kaffeemaschine = states('sensor.kaffeemaschine_leistung_2') | float %}
|
{% set kaffeemaschine = states('sensor.kaffeemaschine_leistung_2') | float %}
|
||||||
{% set waeschetrockner = states('sensor.waschetrockner_leistung') | float %}
|
{% set waeschetrockner = states('sensor.waschetrockner_leistung') | float %}
|
||||||
{% set waschmaschine = states('sensor.waschmaschine_leistung') | float %}
|
{% set waschmaschine = states('sensor.waschmaschine_leistung') | float %}
|
||||||
{{ (total + solar - raumduft - keller - musik - bett - heimkino_sz - deko - schreibtisch - serverraum - heimkino_wz - spieleschrank - kuehlschrank - kaffeemaschine - waeschetrockner - waschmaschine) | round(1) }}
|
{% 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) }}
|
||||||
device_class: power
|
device_class: power
|
||||||
state_class: measurement
|
state_class: measurement
|
||||||
attributes:
|
attributes:
|
||||||
|