Compare commits

...

42 Commits

Author SHA1 Message Date
a947806618 2 last minor fixes (typo and stuff) 2025-09-04 20:45:39 +02:00
439ead1047 Added back notification group, which got deleted. 2025-09-04 20:24:54 +02:00
20c86831f1 Minor tidy up 2025-09-04 20:21:51 +02:00
61b018270b Updated, modified and streamlined a whole bunch of stuff. 2025-09-04 14:18:29 +02:00
911477d381 Sleep as Android, Gdoor, fixes and update. 2025-09-03 23:57:54 +02:00
1ee475ee4a Added HomeAssitant Voice. 2025-09-01 21:36:09 +02:00
cae28341fe Added webserver image/stream. Added replacement for Außensensor (with cam). 2025-08-29 08:03:15 +02:00
17cfa429ea Added Mute logic (that does not fix newly introduced crashes). 2025-08-29 08:02:05 +02:00
791d7d504f Fixed comment. 2025-08-29 08:01:22 +02:00
631cdae751 Updated HA. 2025-08-29 08:00:51 +02:00
06321730e0 Changed Mobile IDs (Lineage OS) 2025-08-29 08:00:38 +02:00
5ac8ceacfe Remainders 2025-07-26 12:18:35 +02:00
4981cbfa2e Export HA entities to mqtt 2025-07-26 12:16:43 +02:00
e5ddf02c34 Updated HA, updated esphome devices. 2025-05-23 23:32:05 +02:00
ebf11021e2 Generated image 2025-05-12 11:55:24 +02:00
72ce055810 Updated hochwasserportal integration 2025-05-12 11:21:49 +02:00
8461f43bca Updated HA. 2025-05-12 11:17:29 +02:00
ca599eab7a Updated ics_calendar to restore compatibility with HA 2025-05-08 10:02:22 +02:00
fef90d5a78 Updated HA. 2025-05-08 09:30:16 +02:00
1f151a804e Migrated Raspiaudio Muse Luxe to esp-idf-based voice sattelite/speaker 2025-05-08 09:30:01 +02:00
31da165db1 Disabled BLE tracking. Degrades performance. 2025-05-07 09:41:50 +02:00
dd390001ee Updated HA. 2025-05-07 09:39:55 +02:00
7993a9174a Fixed changed rtl_433 ids. 2025-03-18 12:17:49 +01:00
fe40743fd1 Updated HA. 2025-03-18 12:17:25 +01:00
01bad587b8 Updated Sketch for voice assistant. 2025-03-08 16:57:37 +01:00
4888393a12 Updated on_click actions to dictionaries. 2025-03-08 14:53:19 +01:00
05f8cc1af8 New, generated image sizes. 2025-03-08 14:23:45 +01:00
5c98baa24b Inverted image colors. Changed default behavior in recent update. 2025-03-08 14:21:28 +01:00
490b00301b Updated HA. 2025-03-08 14:08:41 +01:00
c3e813551b Added / Renamed ab psu. 2025-03-08 14:08:24 +01:00
474805328b Enabled automation 2025-01-30 13:30:06 +01:00
fd18934e97 Added BLE Proxy for DC Load. 2025-01-30 13:29:04 +01:00
5e1d197e7a Updated HA. 2025-01-30 13:23:14 +01:00
ff59c494a7 Added invert binary sensor template. 2025-01-06 09:13:57 +01:00
c4d9c253f4 Updated HA. 2025-01-06 09:13:08 +01:00
99007cda76 Updated Voice Assistant sattelites 2025-01-06 09:12:47 +01:00
373675be4f Added additional, heated temp/humid sensor. 2025-01-06 09:12:11 +01:00
90ed6df217 XMas automations 2025-01-06 09:11:14 +01:00
e1984a199d Removed old pycache files. 2025-01-06 09:08:40 +01:00
3fcca0c3e1 Added unique_id to hygrostat. 2024-10-15 15:34:12 +02:00
5ddbe9962f Updated HA. 2024-10-15 15:33:10 +02:00
f05405bc75 Added unique_ids to derivative sensors. 2024-10-15 15:32:59 +02:00
65 changed files with 3444 additions and 660 deletions

View File

@@ -1 +1 @@
2024.10.1 2025.9.0

View File

@@ -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: ''
@@ -68,79 +68,143 @@
- id: '1623673821789' - id: '1623673821789'
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.
trigger: triggers:
- platform: state - trigger: state
entity_id: input_text.sleep_as_android entity_id:
to: sleep_tracking_started - event.sleep_as_android_schlaf_tracking
condition: [] attribute: event_type
action: to: started
- service: light.turn_off conditions: []
target: actions:
- target:
area_id: area_id:
- wohnzimmer - wohnzimmer
- kuche - kuche
- schlafzimmer - schlafzimmer
- kinderzimmer - kinderzimmer
data: {} data: {}
action: light.turn_off
- type: turn_off - type: turn_off
device_id: 6d1be741876624a70ab5b01b54c6fd6f device_id: 6d1be741876624a70ab5b01b54c6fd6f
entity_id: switch.kuche_musik entity_id: switch.kuche_musik
domain: switch domain: switch
- service: notify.mobile_app_le2123 - data:
data:
message: Gute Nacht! message: Gute Nacht!
- service: media_player.play_media 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: 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:
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.
- type: turn_off media_content_type: provider
device_id: c4ead7f6227e2ee4c43c4b0df829cd84 metadata:
entity_id: 7f7284b11f2bf50ae2f0ebeeb35411c0 title: Gute Nacht, schlaf gut.
domain: switch thumbnail: https://brands.home-assistant.io/_/tts/logo.png
- service: media_player.turn_off media_class: app
target: children_media_class:
navigateIds:
- {}
- media_content_type: app
media_content_id: media-source://tts
- media_content_type: provider
media_content_id: media-source://tts/tts.piper?message=Gute+Nacht%2C+schlaf+gut.
action: media_player.play_media
enabled: true
- target:
area_id: area_id:
- wohnzimmer - wohnzimmer
data: {} data: {}
action: media_player.turn_off
- action: switch.turn_off
metadata: {}
data: {}
target:
device_id:
- 1f3c4b5de4aea99bac83688ceb22293b
- 48dafb7f4a8ed6ccbb046758cb660c23
- 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)
@@ -167,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
@@ -239,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
@@ -249,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
@@ -301,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
@@ -344,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
@@ -451,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}}
@@ -479,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'')}}
@@ -510,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
@@ -529,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}}
@@ -546,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))
@@ -557,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}}
@@ -598,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'')}}
@@ -665,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 #}
@@ -682,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'
@@ -724,48 +846,51 @@
- 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)
description: '' description: ''
trigger: triggers:
- platform: sun - event: sunset
event: sunset
offset: '-1:00' offset: '-1:00'
condition: trigger: sun
conditions:
- condition: state - condition: state
entity_id: person.marcus_scholz entity_id: person.marcus_scholz
state: home state: home
action: actions:
- data: {} - data: {}
action: script.moodlight_xmas action: script.moodlight_xmas
- type: turn_on - action: switch.turn_on
device_id: c4ead7f6227e2ee4c43c4b0df829cd84 metadata: {}
entity_id: 7f7284b11f2bf50ae2f0ebeeb35411c0 data: {}
domain: switch target:
device_id:
- 1f3c4b5de4aea99bac83688ceb22293b
- 48dafb7f4a8ed6ccbb046758cb660c23
mode: single mode: single
- id: '1700483035319' - id: '1700483035319'
alias: Licht bei Sonnenaufgang ausschalten (XMas) alias: Licht bei Sonnenaufgang ausschalten (XMas)
description: '' description: ''
trigger: triggers:
- platform: sun - event: sunrise
event: sunrise
offset: '+1:00' offset: '+1:00'
condition: [] trigger: sun
action: conditions: []
actions:
- target: - target:
area_id: area_id:
- schlafzimmer - schlafzimmer
@@ -782,11 +907,13 @@
data: data:
transition: 2 transition: 2
action: light.turn_off action: light.turn_off
- type: turn_off - action: switch.turn_off
device_id: c4ead7f6227e2ee4c43c4b0df829cd84 metadata: {}
entity_id: 7f7284b11f2bf50ae2f0ebeeb35411c0 data: {}
domain: switch target:
enabled: true device_id:
- 1f3c4b5de4aea99bac83688ceb22293b
- 48dafb7f4a8ed6ccbb046758cb660c23
mode: single mode: single
- id: '1701774106609' - id: '1701774106609'
alias: IKEA STYRBAR Wohnzimmer alias: IKEA STYRBAR Wohnzimmer
@@ -798,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'
@@ -858,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
@@ -1122,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

View File

@@ -0,0 +1,27 @@
blueprint:
name: Invert a binary sensor
description: Creates a binary_sensor which holds the inverted value of a reference binary_sensor
domain: template
source_url: https://github.com/home-assistant/core/blob/dev/homeassistant/components/template/blueprints/inverted_binary_sensor.yaml
input:
reference_entity:
name: Binary sensor to be inverted
description: The binary_sensor which needs to have its value inverted
selector:
entity:
domain: binary_sensor
variables:
reference_entity: !input reference_entity
binary_sensor:
state: >
{% if states(reference_entity) == 'on' %}
off
{% elif states(reference_entity) == 'off' %}
on
{% else %}
{{ states(reference_entity) }}
{% endif %}
# delay_on: not_used in this example
# delay_off: not_used in this example
# auto_off: not_used in this example
availability: "{{ states(reference_entity) not in ('unknown', 'unavailable') }}"

View File

@@ -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) }}"

View File

@@ -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,10 +50,11 @@ 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
unique_id: '3728344225387'
humidifier: fan.badezimmer_ventilator humidifier: fan.badezimmer_ventilator
target_sensor: sensor.bathroom_badezimmer_luftfeuchtigkeit target_sensor: sensor.bathroom_badezimmer_luftfeuchtigkeit
min_humidity: 30 min_humidity: 30
@@ -73,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

View File

@@ -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,
} }
), ),

View File

@@ -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"
} }

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View 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,
)

View File

@@ -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"

View File

@@ -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
""" """

View 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

View File

@@ -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
""" """

View File

@@ -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"
} }

View 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.

View File

@@ -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

View File

@@ -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,

View 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": {
}
}
}

View 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": {
}
}
}

View 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": {
}
}
}

View 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": {
}
}
}

View 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": {
}
}
}

View 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": {
}
}
}

View File

@@ -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."""

Binary file not shown.

View 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"

View File

@@ -42,16 +42,34 @@ ota:
platform: esphome platform: esphome
password: !secret ota password: !secret ota
# DHT22 sensor # Initialize I²C
i2c:
- id: bus_a
sda: 6
scl: 7
scan: true
# Temp/humidity sensors
sensor: sensor:
- platform: dht - platform: dht
model: dht22 model: dht22
pin: 4 pin: 4
temperature:
name: "Badezimmer Temperatur DHT"
humidity:
name: "Badezimmer Luftfeuchtigkeit DHT"
update_interval: 60s
- platform: sht3xd
temperature: temperature:
name: "Badezimmer Temperatur" name: "Badezimmer Temperatur"
filters:
- offset: -4.4
humidity: humidity:
name: "Badezimmer Luftfeuchtigkeit" name: "Badezimmer Luftfeuchtigkeit"
address: 0x44
heater_enabled: True
update_interval: 60s update_interval: 60s
# WiFi signal strength # WiFi signal strength
- platform: wifi_signal - platform: wifi_signal
name: "WiFi Signalstärke" name: "WiFi Signalstärke"

View File

@@ -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

View 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
View 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

View File

@@ -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

View File

@@ -1,20 +1,20 @@
# 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 min_version: 2025.2.0
version: "1.0"
min_version: 2023.5.0
esp32: esp32:
board: m5stack-atom board: m5stack-atom
framework: framework:
type: arduino type: esp-idf
# Enable Home Assistant API # Enable Home Assistant API
api: api:
@@ -22,10 +22,13 @@ api:
key: !secret apikey key: !secret apikey
ota: ota:
platform: esphome - platform: esphome
password: !secret ota id: ota_esphome
password: !secret ota
wifi: wifi:
on_connect:
- delay: 5s # Gives time for improv results to be transmitted
ssid: Voltage-legacy ssid: Voltage-legacy
password: !secret voltage_legacy_psk password: !secret voltage_legacy_psk
use_address: ${name}.home use_address: ${name}.home
@@ -38,16 +41,17 @@ wifi:
logger: logger:
dashboard_import:
package_import_url: github://esphome/media-players/m5stack-atom-echo.yaml@main
captive_portal: captive_portal:
improv_serial: button:
- platform: factory_reset
id: factory_reset_btn
name: Factory reset
i2s_audio: i2s_audio:
i2s_lrclk_pin: GPIO33 - id: i2s_audio_bus
i2s_bclk_pin: GPIO19 i2s_lrclk_pin: GPIO33
i2s_bclk_pin: GPIO19
microphone: microphone:
- platform: i2s_audio - platform: i2s_audio
@@ -56,88 +60,301 @@ microphone:
adc_type: external adc_type: external
pdm: true pdm: true
speaker:
- platform: i2s_audio
id: echo_speaker
i2s_dout_pin: GPIO22
dac_type: external
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
microphone: echo_microphone microphone: echo_microphone
on_start: media_player: echo_media_player
noise_suppression_level: 2
auto_gain: 31dBFS
volume_multiplier: 2.0
on_listening:
- light.turn_on: - light.turn_on:
id: led id: led
blue: 100% blue: 100%
red: 0% red: 0%
green: 0% green: 0%
effect: none effect: "Slow Pulse"
on_stt_vad_end:
- light.turn_on:
id: led
blue: 100%
red: 0%
green: 0%
effect: "Fast Pulse"
on_tts_start: on_tts_start:
- light.turn_on: - light.turn_on:
id: led id: led
blue: 0% blue: 100%
red: 0% red: 0%
green: 100% green: 0%
brightness: 100%
effect: none effect: none
on_tts_end:
- media_player.play_media: !lambda return x;
- light.turn_on:
id: led
blue: 0%
red: 0%
green: 100%
effect: pulse
on_end: on_end:
- delay: 1s - delay: 100ms
- wait_until: - script.execute: start_wake_word
not:
media_player.is_playing: media_out
- light.turn_off: led
on_error: on_error:
- light.turn_on: - light.turn_on:
id: led id: led
blue: 0%
red: 100% red: 100%
green: 0% green: 0%
blue: 0%
brightness: 100%
effect: none effect: none
- delay: 1s - delay: 2s
- script.execute: reset_led
on_client_connected:
- delay: 2s # Give the api server time to settle
- script.execute: start_wake_word
on_client_disconnected:
- voice_assistant.stop:
- micro_wake_word.stop:
on_timer_finished:
- voice_assistant.stop:
- micro_wake_word.stop:
- wait_until:
not:
microphone.is_capturing:
- switch.turn_on: timer_ringing
- light.turn_on:
id: led
red: 0%
green: 100%
blue: 0%
brightness: 100%
effect: "Fast Pulse"
- wait_until:
- switch.is_off: timer_ringing
- light.turn_off: led - light.turn_off: led
- switch.turn_off: timer_ringing
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
inverted: true inverted: true
name: Button name: Button
disabled_by_default: true
entity_category: diagnostic
id: echo_button id: echo_button
on_multi_click: on_multi_click:
- timing: - timing:
- ON FOR AT MOST 350ms - ON for at least 50ms
- OFF FOR AT LEAST 10ms - OFF for at least 50ms
then: then:
- media_player.toggle: media_out - if:
condition:
switch.is_on: timer_ringing
then:
- switch.turn_off: timer_ringing
else:
- script.execute: start_wake_word
- timing: - timing:
- ON FOR AT LEAST 350ms - ON for at least 10s
then: then:
- voice_assistant.start: - button.press: factory_reset_btn
- timing:
- ON FOR AT LEAST 350ms
- OFF FOR AT LEAST 10ms
then:
- voice_assistant.stop:
media_player:
- platform: i2s_audio
id: media_out
name: None
dac_type: external
i2s_dout_pin: GPIO22
mode: mono
light: light:
- platform: esp32_rmt_led_strip - platform: esp32_rmt_led_strip
id: led id: led
name: None name: None
disabled_by_default: true
entity_category: config
pin: GPIO27 pin: GPIO27
default_transition_length: 0s default_transition_length: 0s
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"
transition_length: 250ms transition_length: 250ms
update_interval: 250ms update_interval: 250ms
min_brightness: 50%
max_brightness: 100%
- pulse:
name: "Fast Pulse"
transition_length: 100ms
update_interval: 100ms
min_brightness: 50%
max_brightness: 100%
script:
- id: reset_led
then:
- if:
condition:
- lambda: return id(wake_word_engine_location).state == "On device";
- switch.is_on: use_listen_light
then:
- light.turn_on:
id: led
red: 100%
green: 89%
blue: 71%
brightness: 60%
effect: none
else:
- if:
condition:
- lambda: return id(wake_word_engine_location).state != "On device";
- switch.is_on: use_listen_light
then:
- light.turn_on:
id: led
red: 0%
green: 100%
blue: 100%
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:
condition:
lambda: return id(wake_word_engine_location).state == "On device";
then:
- voice_assistant.stop
- micro_wake_word.stop:
- delay: 1s
- script.execute: reset_led
- script.wait: 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
name: Use listen light
id: use_listen_light
optimistic: true
restore_mode: RESTORE_DEFAULT_ON
entity_category: config
on_turn_on:
- script.execute: reset_led
on_turn_off:
- script.execute: reset_led
- platform: template
id: timer_ringing
optimistic: true
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:
# 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
- switch.turn_off: timer_ringing
select:
- platform: template
entity_category: config
name: Wake word engine location
id: wake_word_engine_location
optimistic: true
restore_value: true
options:
- In Home Assistant
- On device
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
micro_wake_word:
on_wake_word_detected:
- voice_assistant.start:
wake_word: !lambda return wake_word;
vad:
models:
- model: ${micro_wake_word_model}

View File

@@ -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:
@@ -8,8 +16,17 @@ api:
key: !secret apikey key: !secret apikey
ota: ota:
platform: esphome - platform: esphome
password: !secret ota id: ota_esphome
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
@@ -25,123 +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: 2023.5.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
captive_portal:
improv_serial:
##########
# Hardware Configuration
es8388:
id: my_es8388
psram:
mode: quad
speed: 80MHz
#######
# Buses Configuration
i2c: i2c:
sda: GPIO18 sda: GPIO18
scl: GPIO23 scl: GPIO23
dashboard_import: #####################
package_import_url: github://esphome/media-players/raspiaudio-muse-luxe.yaml@main # Internal Components
output:
captive_portal: - platform: gpio
id: dac_mute
improv_serial: pin:
external_components:
- source: github://pr#3552 # DAC support https://github.com/esphome/esphome/pull/3552
components: [es8388]
refresh: 0s
es8388:
i2s_audio:
- i2s_lrclk_pin: GPIO25
i2s_bclk_pin: GPIO5
media_player:
- platform: i2s_audio
name: None
id: luxe_out
dac_type: external
i2s_dout_pin: GPIO26
mode: stereo
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
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
@@ -153,7 +222,13 @@ binary_sensor:
pullup: true pullup: true
name: Volume Up name: 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
@@ -163,41 +238,250 @@ binary_sensor:
pullup: true pullup: true
name: Volume Down name: 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
on_multi_click: on_click:
- timing: - if:
- ON FOR AT MOST 350ms condition:
- OFF FOR AT LEAST 10ms - lambda: 'return(id(mute));'
then: then:
- media_player.toggle: luxe_out - script.execute: mute_off
- timing: - lambda: id(mute) = false;
- ON FOR AT LEAST 350ms else:
then: - script.execute: mute_on
- voice_assistant.start: - lambda: id(mute) = true;
- timing: on_double_click:
- ON FOR AT LEAST 350ms - if:
- OFF FOR AT LEAST 10ms condition:
then: - lambda: 'return(id(phase) == 2);'
- voice_assistant.stop: then:
- 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
i2s_audio:
i2s_lrclk_pin: GPIO25
i2s_bclk_pin: GPIO5
i2s_mclk_pin: GPIO0
microphone:
- platform: i2s_audio
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;

View File

@@ -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

View 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"

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 685 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

@@ -1,6 +0,0 @@
bdm:
name: BDM
mac: BLE_34:14:B5:A0:99:BD
icon:
picture:
track: true

162
mqtt.yaml
View File

@@ -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
View File

@@ -0,0 +1,8 @@
base_topic: homeassistant
publish_attributes: true
publish_timestamps: true
# include:
# entities:
# - sensor.Netzleistung

8
notify.yaml Normal file
View 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

View File

@@ -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

View File

@@ -77,30 +77,30 @@ moodlight_orange_plasma:
moodlight_xmas: moodlight_xmas:
alias: Moodlight XMas alias: Moodlight XMas
sequence: sequence:
- service: light.turn_on - data:
data:
brightness_pct: 20 brightness_pct: 20
transition: 2 transition: 2
effect: Plasma effect: Glitter
target: target:
device_id: device_id:
- 6dcbd87b459412144bddc42af3ae8b83 - 6dcbd87b459412144bddc42af3ae8b83
- 4edd9b9df7d1f6f2fe7dcc2e5c0eb968 - 4edd9b9df7d1f6f2fe7dcc2e5c0eb968
- c64e7c3dcda7f1c23e456959f2c60f39 - c64e7c3dcda7f1c23e456959f2c60f39
- service: select.select_option action: light.turn_on
target: - target:
entity_id: select.wohnzimmer_hinten_color_palette, select.wohnzimmer_vorne_color_palette, entity_id: select.wohnzimmer_hinten_color_palette, select.wohnzimmer_vorne_color_palette,
select.kuche_color_palette select.kuche_color_palette
data: data:
option: Orangery option: Orangery
action: select.select_option
- condition: state - condition: state
state: 'on' state: 'on'
entity_id: media_player.lg_webos_smart_tv entity_id: media_player.lg_webos_smart_tv
- service: light.turn_off - target:
target:
device_id: 6dcbd87b459412144bddc42af3ae8b83 device_id: 6dcbd87b459412144bddc42af3ae8b83
data: data:
transition: 2 transition: 2
action: light.turn_off
mode: single mode: single
icon: mdi:lightbulb-on icon: mdi:lightbulb-on
wled_wohnzimmer_nachster_effekt: wled_wohnzimmer_nachster_effekt:

View File

@@ -31,13 +31,11 @@
unit_of_measurement: "Octets" unit_of_measurement: "Octets"
- platform: derivative - platform: derivative
unique_id: '5853787105973'
source: sensor.snmp_wan_in source: sensor.snmp_wan_in
unit_time: s unit_time: s
unit: B unit: B
name: wan_in_derivative name: wan_in_derivative
- platform: derivative - platform: derivative
unique_id: '2245248666397'
source: sensor.snmp_wan_out source: sensor.snmp_wan_out
unit_time: s unit_time: s
unit: B unit: B

View File

@@ -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: