Compare commits

...

120 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
5467789c71 Removed WLED effect template. Caused errors, when light was turned off. 2024-10-11 15:31:24 +02:00
7f9a366171 Made templates for WLED effect and palette working on its own. 2024-10-10 14:57:16 +02:00
bf3b4e5e81 Added unique_ids 2024-10-08 12:59:18 +02:00
083cdbb857 Added room pictures. 2024-10-08 11:52:41 +02:00
32e9c3af4d Added landesübergreifendes Hochwasserportal 2024-10-05 17:30:45 +02:00
a0d7950829 Updated HA. 2024-10-05 17:29:50 +02:00
8cd5ae5283 Notify about washing mashine / dryer finish. 2024-10-03 17:20:22 +02:00
574be34e36 Added additional power plugs to other/untracked power. 2024-10-03 12:43:06 +02:00
0dfe1950c9 Adopted bmp280 platform setting. 2024-10-03 12:42:22 +02:00
9def77c3f3 Updated HA. 2024-10-03 12:41:58 +02:00
fb027e0c45 Updated HA. 2024-09-15 00:33:33 +02:00
f744021925 Remove InfluxDB 2024-09-15 00:32:40 +02:00
daaec3cc60 Syntax adjustments. 2024-09-15 00:32:26 +02:00
683ffe7767 Updated HA. MPD config migrated to UI. 2024-07-05 21:30:05 +02:00
730d1cf338 Added unique_ids to utility meters. 2024-07-01 14:06:06 +02:00
27408f02dd Remove deprecated mqtt sensors. 2024-07-01 14:05:36 +02:00
624ee50d14 Disable M's out-of-home 2024-07-01 14:04:54 +02:00
48d29a6585 Updated ota platform config. 2024-07-01 14:02:31 +02:00
04eff2e382 Updated HA 2024-07-01 14:01:14 +02:00
8f63ede1f0 DWD logo, HA Update, WLED effect 2024-03-21 09:25:39 +01:00
d32328f1bb Added zigbee blueprints. 2024-03-01 15:48:55 +01:00
c15266fc20 Added IKEA zigbee buttons, changed hourly voice taunt. 2024-03-01 15:48:28 +01:00
9ec14848d5 Updated HA. 2024-03-01 15:47:44 +01:00
87868f079c Escaped sensor value. 2024-02-16 00:10:11 +01:00
c0755f5add Add NINA logo. 2024-02-16 00:09:52 +01:00
acb1c63483 Updated HA. 2024-02-16 00:08:26 +01:00
565b1b1b81 Added humidifier set humid automation. Removed states.state occurances to the less error-prone alternative. 2024-01-21 00:15:44 +01:00
392e8508d3 Removed and ignored pycache. 2024-01-20 00:12:05 +01:00
6985991d3a 1st working esp32-cam implementation. 2024-01-20 00:04:45 +01:00
0162c1074f Updated HA. 2024-01-20 00:03:53 +01:00
8a5ba47dfa Changes to serverroom sensor. 2024-01-20 00:03:43 +01:00
db6a6e9e30 Let's Encrypt automation. 2024-01-17 11:54:42 +01:00
8cad164008 IKEA blueprint, replaced bathroom sensor with ESPHome. 2024-01-17 11:50:02 +01:00
d8cb73ebf0 Added IKEA Somrig for sleeping room. 2024-01-17 11:46:54 +01:00
0ac87ad81c Updated HA. 2024-01-17 11:45:48 +01:00
395bf7bb9c Cleaned up duplicate pin usage. 2023-12-25 16:45:19 +01:00
c0ebd9188d Entity name changes (and typo) because of update. 2023-12-25 00:51:42 +01:00
f36ea1b591 Added ics_calender custom component. 2023-12-14 22:15:21 +01:00
ee90402c8b WIP junk for lights management using zigbee remote. 2023-12-14 18:41:43 +01:00
479a1cb8e1 Updated HA. 2023-12-14 14:36:54 +01:00
2cf58f3f34 Added IKEA Zigbee button blueprint/automation, changed automations for xmas. 2023-12-14 14:36:09 +01:00
0dff2d90ff Fixed Raspiaudio Muse luxe speaker due to ESPHome breaking change. 2023-12-14 14:34:30 +01:00
92f95f7bae Updated phone call notification. 2023-11-14 11:34:11 +01:00
d5be7fea6b Adopted 2. Awtrix Light (Desk). 2023-11-14 11:14:27 +01:00
8a446a727b Updated HA. 2023-11-14 11:13:05 +01:00
2d3c9cf674 Add RD6018 Lab PSU via ESPhome WiFi. 2023-11-06 23:22:06 +01:00
ce9ad46e5b Update HA. 2023-11-06 23:21:29 +01:00
6482453a1d Make pixelclock power-on independant of alarm going off. 2023-11-04 00:58:43 +01:00
5f1f08b79e Fat fingers. 2023-11-02 21:53:16 +01:00
48338c9233 Adopted Awtrix Pixelclock to automations + clock effects. 2023-11-02 21:47:33 +01:00
671c9d6b77 Made green blinking less annoying. 2023-11-02 21:46:20 +01:00
94f390ddf7 Updated HA. 2023-11-02 21:35:06 +01:00
429dce72fe Fixed temperature offset with filter. Native offset not reliable. 2023-11-02 21:34:12 +01:00
36a482de26 Updated HA. 2023-10-17 13:42:53 +02:00
9e5a74a999 Cleaned up stale entities and fixed working ones. 2023-10-17 13:42:15 +02:00
9ba761dcd3 Merged bathroom sensors into one device. 2023-08-19 20:18:04 +02:00
b414af5686 Updated HA. 2023-08-19 20:17:44 +02:00
eef9f31a94 Adopted new MQTT naming conventions. 2023-08-04 14:47:48 +02:00
a6b8e2d9e2 Updated HA 2023-08-04 14:47:21 +02:00
663f5d5daf Changed default moodlight animation. 2023-08-04 14:47:06 +02:00
fb4e6a9f41 Enabled snmp traffic recording. 2023-07-01 19:37:26 +02:00
cfcbdeea9d Added Solar enery power meters. 2023-07-01 19:37:00 +02:00
6fdacdc6e9 Switched SNMP traffic sensors to 64bit counters and into modern confg format. 2023-07-01 19:36:31 +02:00
8b43bf239b Oops. Rounded the wrong sensors. 2023-06-25 23:41:02 +02:00
1ade598c97 Round power meters to 1 digit after the dot. 2023-06-25 23:21:11 +02:00
84bea6cc2b Flush to DB less often. Fix comment. 2023-06-25 23:04:30 +02:00
4d2c5b0cba Completely new Energy monitoring, with cumulative import, export and consumption. 2023-06-25 22:41:46 +02:00
0191bc13b7 Added energy returned and new moodlight preset. 2023-06-24 22:08:39 +02:00
6e48e18384 Fixed 'Leckstrom' template sensor to not go wild because of solar power. 2023-06-24 11:37:05 +02:00
55e1a02544 Updated HA. 2023-06-24 11:36:32 +02:00
a433286324 Fixed substration. 2023-06-09 21:57:23 +02:00
0d00747fe4 New automation that taunts to open the windows when temperatur is falling. Plus presence condition. 2023-06-09 18:27:01 +02:00
d17935a803 Added command_line sensors. 2023-06-09 12:15:21 +02:00
929d6b1830 Adopted deprecated YAML config change for command_line based entities. 2023-06-09 12:14:24 +02:00
a57178b5e1 Removed oboleted config items. DWD/MQTT 2023-06-09 11:55:19 +02:00
883225a67e Ignore Zigbee DB files. 2023-06-09 11:41:17 +02:00
39a432eece Updated HA to 2023.6.1 2023-06-09 11:41:00 +02:00
c080d2ead3 Fixed condition for temp warning. 2023-06-09 11:39:03 +02:00
97 changed files with 11148 additions and 1458 deletions

View File

@@ -1 +1 @@
2023.5.4
2025.9.0

3
.gitignore vendored
View File

@@ -1,6 +1,7 @@
automations_webhooks.yaml
spotify.yaml
calendars.yaml
ics_calendars.yaml
esphome/common/secrets.yaml
esphome/secrets.yaml
secrets.yaml
@@ -9,3 +10,5 @@ tts/
media/
home-assistant.log*
.storage/
zigbee.db*
*__pycache__*

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,80 @@
blueprint:
name: Awtrix random effect
description: This blueprint allows you to select the effects, which should be randomly displayed on your Awtrix light
domain: automation
author: N1c093
input:
awtrix_light:
name: Awtrix Display
description: Select the target Awtrix display.
selector:
device:
model: "AWTRIX Light"
effect_list:
name: Effects
description: 'Select the effects which should randomly be displayed. See: https://blueforcer.github.io/awtrix-light/#/effects'
selector:
select:
options:
- BrickBreaker
- Fireworks
- Radar
- Snake
- TheaterChase
- SwirlOut
- LookingEyes
- Pacifica
- PlasmaCloud
- Checkerboard
- PingPong
- Ripple
- TwinklingStars
- ColorWaves
- SwirlIn
- Matrix
- Plasma
- MovingLine
mode: list
multiple: true
duration:
name: Effect duration
description: Select how long each effect should be displayed.
default: "10"
selector:
number:
min: 1
max: 999
mode: box
unit_of_measurement: seconds
change_interval:
name: Effect change interval
description: 'Select how often (in minutes) the effect should change. Input must start with "/" Example: "/5"'
default: "/5"
selector:
text:
suffix: minutes
mode: queued
variables:
device_id: !input awtrix_light
awtrix_light: "{{ iif( device_attr(device_id, 'name_by_user') != none, device_attr(device_id, 'name_by_user'), device_attr(device_id, 'name') ) }}"
effect_list: !input effect_list
effect_random: "{{effect_list|random}}"
duration: !input duration
trigger:
- platform: time_pattern
minutes: !input change_interval
action:
- service: mqtt.publish
data:
qos: 0
retain: false
topic: "{{awtrix_light}}/custom/effect"
payload: |-
{
"effect": "{{ effect_random }}",
"duration": "{{ duration }}"
}

View File

@@ -0,0 +1,152 @@
---
blueprint:
name: AWTRIX Solar Energy Monitor
description: >
This blueprint will show the current solar energy received.
It uses a icons 54156 (solar-green), 50557 (solar-white-dyn), 50546 (solar-static) that you need to install.
domain: automation
input:
awtrix:
name: AWTRIX Device
description: Select the Awtrix light device
selector:
device:
integration: mqtt
manufacturer: Blueforcer
model: AWTRIX Light
multiple: true
power_source:
name: Power Sensor
description: A sensor providing the current power received from your solar system.
selector:
entity:
domain:
- sensor
multiple: false
threshold_high:
name: Threshold for high solar production (W)
description: The threshold above which the energy production of your solar system should be visualized as high. Input in Watts (W).
selector:
number:
min: 0
max: 100000
unit_of_measurement: Watt
mode: slider
default: 400
threshold_low:
name: Threshold for low solar production (W)
description: The threshold below which the energy production of your solar system should be visualized as low. Input in Watts (W).
selector:
number:
min: 0
max: 100000
unit_of_measurement: Watt
mode: slider
default: 100
skip_if_zero_watts:
name: Hide solar production if at 0 Watts
description: 'This will not show the solar energy production on your awtrix if the production is below 0 Watts.'
selector:
boolean:
default: false
skip_during_night_hours:
name: Hide solar production during night time
description: 'This will not show the solar energy production on your awtrix during night hours (as specified below).'
selector:
boolean:
default: false
night_starts_after_time:
name: Night Time Start
description: Set the start of the night time.
default: 00:00:00
selector:
time: {}
night_ends_after_time:
name: Night Time End
description: Set the end of the night time.
default: 00:00:00
selector:
time: {}
mode: single
variables:
device_ids: !input awtrix
devices_topics: >-
{%- macro get_device_topic(device_id) %}
{{ states((device_entities(device_id) | select('search','device_topic') | list)[0]) }}
{%- endmacro %}
{%- set ns = namespace(devices=[]) %}
{%- for device_id in device_ids %}
{%- set device=get_device_topic(device_id)|replace(' ','') %}
{% set ns.devices = ns.devices + [ device ~ '/custom/solar_power'] %}
{%- endfor %}
{{ ns.devices }}
power_sensor: !input power_source
power_level: >-
{{ states[power_sensor].state | int(0) | abs }}
threshold_low: !input threshold_low
threshold_high: !input threshold_high
power_level_icon: >-
{%- if power_level > threshold_high %}{{54156}}{%- endif %}
{%- if (power_level <= threshold_high) and (power_level > threshold_low) %}{{50557}}{%- endif %}
{%- if power_level <= threshold_low %}{{50546}}{%- endif %}
power_level_color: >-
{%- if power_level > threshold_high %}{{"#04FE04"}}{%- endif %}
{%- if (power_level <= threshold_high) and (power_level > threshold_low) %}{{"#FCFEFC"}}{%- endif %}
{%- if power_level <= threshold_low %}{{"#FF4E1A"}}{%- endif %}
power_level_text: >-
{%- if power_level > 1000 %}{{ ((power_level | float(default=0)) / 1000) | round(1)}} kW{%- else %}{{power_level | round(0)}} W{%- endif %}
skip_if_zero_watts: !input skip_if_zero_watts
skip_during_night_hours: !input skip_during_night_hours
payload: >-
{"icon":"{{ power_level_icon }}", "text": "{{ power_level_text }}", "color": "{{ power_level_color }}"}
night_start: !input night_starts_after_time
night_end: !input night_ends_after_time
trigger:
- platform: time_pattern
minutes: "/1"
condition:
action:
- choose:
- alias: "Skipping"
conditions:
- condition: template
value_template: >
{% set now_time = now().strftime("%H:%M") %}
{% set night_start = night_start %}
{% set night_end = night_end %}
{{ (skip_during_night_hours and ((now_time < night_end) or (now_time > night_start))) or (skip_if_zero_watts and (power_level == 0)) }}
sequence:
# It is night time, skipping sending solar power data to Awtrix Light.
- repeat:
for_each: "{{ devices_topics }}"
sequence:
- service: mqtt.publish
data:
qos: 0
retain: false
topic: "{{ repeat.item }}"
payload: '{}'
- alias: "Not skipping"
conditions:
- condition: template
value_template: >
{% set now_time = now().strftime("%H:%M") %}
{% set night_start = night_start %}
{% set night_end = night_end %}
{{ not((skip_during_night_hours and ((now_time < night_end) or (now_time > night_start))) or (skip_if_zero_watts and (power_level == 0))) }}
sequence:
- repeat:
for_each: "{{ devices_topics }}"
sequence:
- service: mqtt.publish
data:
qos: 0
retain: false
topic: "{{ repeat.item }}"
payload: >
{{ payload }}

View File

@@ -0,0 +1,141 @@
blueprint:
name: AWTRIX Calendar Notifier
description: 'This blueprint will print notification when calendar event happens.
[Google calendar integration](https://www.home-assistant.io/integrations/google)
was used for initial testing. Other calendars might work too.
Any icons can be used to describe calendar events. Upload them to AWTRIX ICONS
folder and make sure your event title is mapped to them in icon map setting of
your automation. I''ve used trash bins of various colours to remind me about need
to take appropriate bin to the street :).
Message is shown every 5 minutes in interval from configurable amount of hours
before event start time till event start.
Inspired by awesome work of Jeeftor '
domain: automation
input:
awtrix:
name: AWTRIX Device
description: Select the Awtrix light
selector:
device:
integration: mqtt
manufacturer: Blueforcer
model: AWTRIX Light
multiple: true
app_name:
name: Awtrix Application name
description: This is the app name listed in the MQTT topic - it should be unique
selector:
text:
multiline: false
default: calendar_notifier
calendar:
name: Calendar with schedule
description: A calendar with schedule
selector:
entity:
multiple: false
message_attr_name:
name: Name of calendar event message attribute
description: This is the name of calendar event message attribute
selector:
text:
multiline: false
default: message
start_time_attr_name:
name: Name of calendar event start time attribute
description: This is the name of calendar event start time attribute
selector:
text:
multiline: false
default: start_time
hours_before:
name: Number of Hours before Event
description: Number of hours to start notify before actual event
selector:
number:
max: 96.0
min: 0.0
unit_of_measurement: hours
mode: box
step: 1.0
default: 15
icon_map:
name: An event message-to-icon name map
description: An event message-to-icon name map in JSON format
selector:
text:
multiline: true
default: '{"light green bin":"trash_light_green","green bin":"trash_green","blue
bin":"trash_blue"}'
duration:
name: Duration (in seconds)
description: Sets how long the app or notification should be displayed.
default: '10'
lifetime:
name: Lifetime of the app (in seconds)
description: Removes the custom app when there is no update after the given
time in seconds. Keep this value higher than 59 seconds to get the AWTRIX
app automatically deleted when disabling the automation.
default: '70'
push_icon:
name: Icon Mode
description: "Please select the pushIcon setting for the icon\n\n - `0` Icon
doesn't move\n\n - `1` Icon moves with text and will not appear again\n\n
\ - `2` Icon moves with text but appears again when the text starts to scroll
again\n"
selector:
select:
options:
- label: Icon doesn't move (default)
value: '0'
- label: Icon moves with text and will not appear again
value: '1'
- label: Icon moves with text but appears again when the text starts to
scroll again
value: '2'
custom_value: false
multiple: false
sort: false
source_url: https://raw.githubusercontent.com/Brunas/HomeAssistant/master/blueprints/automation/awtrix_calendar_notifier.yaml
mode: single
variables:
device_ids: !input awtrix
app_name: !input app_name
devices_topics: "{%- macro get_device_topic(device_id) %} {{- states((device_entities(device_id)
| select('search','device_topic') | list)[0]) }} {%- endmacro %}\n{%- set ns =
namespace(devices=[]) %} {%- for device_id in device_ids %}\n {%- set device=get_device_topic(device_id)|replace('
','') %}\n {% set ns.devices = ns.devices + [ device ~ '/custom/' ~ app_name]
%}\n{%- endfor %} {{ ns.devices | reject('match','unavailable') | list}}"
calendar: !input calendar
message_attr_name: !input message_attr_name
start_time_attr_name: !input start_time_attr_name
hours_before: !input hours_before
icon_map: !input icon_map
icon: '{%- set icon_map_json = icon_map|from_json %} {{icon_map_json[state_attr(calendar,message_attr_name)]}}'
duration: !input duration
lifetime: !input lifetime
push_icon: !input push_icon
payload_internal: "{\"icon\":\"{{icon}}\",\n \"text\":\"{{state_attr(calendar,start_time_attr_name)}}
{{state_attr(calendar,message_attr_name)}}\",\n \"pushIcon\":\"{{push_icon}}\",\n
\"repeat\":1,\"textCase\":2,\"textOffset\":33,\"duration\":{{duration}},\"lifetime\":{{lifetime}}}"
payload: '{{ iif(now() >= state_attr(calendar,start_time_attr_name)| as_datetime
| default(now(), true)|as_local - timedelta(hours = hours_before), payload_internal,
"{}") }}'
trigger:
- platform: time_pattern
minutes: /5
condition: []
action:
- repeat:
for_each: '{{ devices_topics }}'
sequence:
- service: mqtt.publish
data:
qos: 0
retain: false
topic: '{{ repeat.item }}'
payload: '{{payload}}'

View File

@@ -0,0 +1,426 @@
blueprint:
name: Controller - IKEA E1743 TRÅDFRI On/Off Switch & Dimmer
description: "# Controller - IKEA E1743 TRÅDFRI On/Off Switch & Dimmer\n\nController
automation for executing any kind of action triggered by the provided IKEA E1743
TRÅDFRI On/Off Switch & Dimmer. Allows to optionally loop an action on a button
long press.\nSupports deCONZ, ZHA, Zigbee2MQTT.\n\nAutomations created with this
blueprint can be connected with one or more [Hooks](https://epmatt.github.io/awesome-ha-blueprints/docs/blueprints/hooks)
supported by this controller.\nHooks allow to easily create controller-based automations
for interacting with media players, lights, covers and more.\nSee the list of
[Hooks available for this controller](https://epmatt.github.io/awesome-ha-blueprints/docs/blueprints/controllers/ikea_e1743#available-hooks)
for additional details.\n\n\U0001F4D5 Full documentation regarding this blueprint
is available [here](https://epmatt.github.io/awesome-ha-blueprints/docs/blueprints/controllers/ikea_e1743).\n\n\U0001F680
This blueprint is part of the **[Awesome HA Blueprints](https://epmatt.github.io/awesome-ha-blueprints)
project**.\n\n Version 2022.08.08\n"
source_url: https://github.com/EPMatt/awesome-ha-blueprints/blob/main/blueprints/controllers/ikea_e1743/ikea_e1743.yaml
domain: automation
input:
integration:
name: (Required) Integration
description: Integration used for connecting the remote with Home Assistant.
Select one of the available values.
selector:
select:
options:
- deCONZ
- ZHA
- Zigbee2MQTT
custom_value: false
multiple: false
sort: false
controller_device:
name: (deCONZ, ZHA) Controller Device
description: The controller device to use for the automation. Choose a value
only if the remote is integrated with deCONZ, ZHA.
default: ''
selector:
device: {}
controller_entity:
name: (Zigbee2MQTT) Controller Entity
description: The action sensor of the controller to use for the automation.
Choose a value only if the remote is integrated with Zigbee2MQTT.
default: ''
selector:
entity:
domain:
- sensor
multiple: false
helper_last_controller_event:
name: (Required) Helper - Last Controller Event
description: Input Text used to store the last event fired by the controller.
You will need to manually create a text input entity for this, please read
the blueprint Additional Notes for more info.
default: ''
selector:
entity:
domain:
- input_text
multiple: false
action_button_up_short:
name: (Optional) Up button short press
description: Action to run on short up button press.
default: []
selector:
action: {}
action_button_up_long:
name: (Optional) Up button long press
description: Action to run on long up button press.
default: []
selector:
action: {}
action_button_up_release:
name: (Optional) Up button release
description: Action to run on up button release after long press.
default: []
selector:
action: {}
action_button_up_double:
name: (Optional) Up button double press
description: Action to run on double up button press.
default: []
selector:
action: {}
action_button_down_short:
name: (Optional) Down button short press
description: Action to run on short down button press.
default: []
selector:
action: {}
action_button_down_long:
name: (Optional) Down button long press
description: Action to run on long down button press.
default: []
selector:
action: {}
action_button_down_release:
name: (Optional) Down button release
description: Action to run on down button release after long press.
default: []
selector:
action: {}
action_button_down_double:
name: (Optional) Down button double press
description: Action to run on double down button press.
default: []
selector:
action: {}
button_up_long_loop:
name: (Optional) Up button long press - loop until release
description: Loop the up button action until the button is released.
default: false
selector:
boolean: {}
button_up_long_max_loop_repeats:
name: (Optional) Up button long press - Maximum loop repeats
description: Maximum number of repeats for the custom action, when looping is
enabled. Use it as a safety limit to prevent an endless loop in case the corresponding
stop event is not received.
default: 500
selector:
number:
min: 1.0
max: 5000.0
mode: slider
step: 1.0
button_down_long_loop:
name: (Optional) Down button long press - loop until release
description: Loop the down button action until the button is released.
default: false
selector:
boolean: {}
button_down_long_max_loop_repeats:
name: (Optional) Down button long press - Maximum loop repeats
description: Maximum number of repeats for the custom action, when looping is
enabled. Use it as a safety limit to prevent an endless loop in case the corresponding
stop event is not received.
default: 500
selector:
number:
min: 1.0
max: 5000.0
mode: slider
step: 1.0
button_up_double_press:
name: (Optional) Expose up button double press event
description: Choose whether or not to expose the virtual double press event
for the up button. Turn this on if you are providing an action for the up
button double press event.
default: false
selector:
boolean: {}
button_down_double_press:
name: (Optional) Expose down button double press event
description: Choose whether or not to expose the virtual double press event
for the down button. Turn this on if you are providing an action for the down
button double press event.
default: false
selector:
boolean: {}
helper_double_press_delay:
name: (Optional) Helper - Double Press delay
description: Max delay between the first and the second button press for the
double press event. Provide a value only if you are using a double press action.
Increase this value if you notice that the double press action is not triggered
properly.
default: 500
selector:
number:
min: 100.0
max: 5000.0
unit_of_measurement: milliseconds
mode: box
step: 10.0
helper_debounce_delay:
name: (Optional) Helper - Debounce delay
description: Delay used for debouncing RAW controller events, by default set
to 0. A value of 0 disables the debouncing feature. Increase this value if
you notice custom actions or linked Hooks running multiple times when interacting
with the device. When the controller needs to be debounced, usually a value
of 100 is enough to remove all duplicate events.
default: 0
selector:
number:
min: 0.0
max: 1000.0
unit_of_measurement: milliseconds
mode: box
step: 10.0
variables:
integration: !input integration
button_up_long_loop: !input button_up_long_loop
button_up_long_max_loop_repeats: !input button_up_long_max_loop_repeats
button_up_double_press: !input button_up_double_press
button_down_long_loop: !input button_down_long_loop
button_down_long_max_loop_repeats: !input button_down_long_max_loop_repeats
button_down_double_press: !input button_down_double_press
helper_last_controller_event: !input helper_last_controller_event
helper_double_press_delay: !input helper_double_press_delay
helper_debounce_delay: !input helper_debounce_delay
integration_id: '{{ integration | lower }}'
adjusted_double_press_delay: '{{ [helper_double_press_delay - helper_debounce_delay,
100] | max }}'
actions_mapping:
deconz:
button_up_short:
- '1002'
button_up_long:
- '1001'
button_up_release:
- '1003'
button_down_short:
- '2002'
button_down_long:
- '2001'
button_down_release:
- '2003'
zha:
button_up_short:
- 'on'
button_up_long:
- move_with_on_off_0_83
button_up_release:
- stop
button_down_short:
- 'off'
button_down_long:
- move_1_83
button_down_release:
- stop
zigbee2mqtt:
button_up_short:
- 'on'
button_up_long:
- brightness_move_up
button_up_release:
- brightness_stop
button_down_short:
- 'off'
button_down_long:
- brightness_move_down
button_down_release:
- brightness_stop
button_up_short: '{{ actions_mapping[integration_id]["button_up_short"] }}'
button_up_long: '{{ actions_mapping[integration_id]["button_up_long"] }}'
button_up_release: '{{ actions_mapping[integration_id]["button_up_release"] }}'
button_down_short: '{{ actions_mapping[integration_id]["button_down_short"] }}'
button_down_long: '{{ actions_mapping[integration_id]["button_down_long"] }}'
button_down_release: '{{ actions_mapping[integration_id]["button_down_release"]
}}'
integrations_with_prev_event_storage:
- zha
- zigbee2mqtt
controller_entity: !input controller_entity
controller_device: !input controller_device
controller_id: '{% if integration_id=="zigbee2mqtt" %}{{controller_entity}}{% else
%}{{controller_device}}{% endif %}'
mode: restart
max_exceeded: silent
trigger:
- platform: event
event_type: state_changed
event_data:
entity_id: !input controller_entity
- platform: event
event_type:
- deconz_event
- zha_event
event_data:
device_id: !input controller_device
condition:
- condition: and
conditions:
- '{%- set trigger_action -%} {%- if integration_id == "zigbee2mqtt" -%} {{ trigger.event.data.new_state.state
}} {%- elif integration_id == "deconz" -%} {{ trigger.event.data.event }} {%-
elif integration_id == "zha" -%} {{ trigger.event.data.command }}{{"_" if trigger.event.data.args|length
> 0}}{{ trigger.event.data.args|join("_") }} {%- endif -%} {%- endset -%} {{ trigger_action
not in ["","None"] }}'
- '{{ integration_id != "zigbee2mqtt" or trigger.event.data.new_state.state != trigger.event.data.old_state.state
}}'
action:
- delay:
milliseconds: !input helper_debounce_delay
- variables:
trigger_action: '{%- if integration_id == "zigbee2mqtt" -%} {{ trigger.event.data.new_state.state
}} {%- elif integration_id == "deconz" -%} {{ trigger.event.data.event }} {%-
elif integration_id == "zha" -%} {{ trigger.event.data.command }}{{"_" if trigger.event.data.args|length
> 0}}{{ trigger.event.data.args|join("_") }} {%- endif -%}'
trigger_delta: '{{ (as_timestamp(now()) - ((states(helper_last_controller_event)
| from_json).t if helper_last_controller_event is not none and (states(helper_last_controller_event)
| regex_match("^\{((\"a\": \".*\"|\"t\": \d+\.\d+)(, )?){2}\}$")) else as_timestamp("1970-01-01
00:00:00"))) * 1000 }}'
last_controller_event: '{{ (states(helper_last_controller_event) | from_json).a
if helper_last_controller_event is not none and (states(helper_last_controller_event)
| regex_match("^\{((\"a\": \".*\"|\"t\": \d+\.\d+)(, )?){2}\}$")) else "" }}'
- service: input_text.set_value
data:
entity_id: !input helper_last_controller_event
value: '{{ {"a":trigger_action,"t":as_timestamp(now())} | to_json }}'
- choose:
- conditions: '{{ trigger_action | string in button_up_short }}'
sequence:
- choose:
- conditions: '{{ button_up_double_press }}'
sequence:
- choose:
- conditions: '{{ trigger_action | string in states(helper_last_controller_event)
and trigger_delta | int <= helper_double_press_delay | int }}'
sequence:
- service: input_text.set_value
data:
entity_id: !input helper_last_controller_event
value: '{{ {"a":"double_press","t":as_timestamp(now())} | to_json
}}'
- event: ahb_controller_event
event_data:
controller: '{{ controller_id }}'
action: button_up_double
- choose:
- conditions: []
sequence: !input action_button_up_double
default:
- delay:
milliseconds: '{{ adjusted_double_press_delay }}'
- event: ahb_controller_event
event_data:
controller: '{{ controller_id }}'
action: button_up_short
- choose:
- conditions: []
sequence: !input action_button_up_short
default:
- event: ahb_controller_event
event_data:
controller: '{{ controller_id }}'
action: button_up_short
- choose:
- conditions: []
sequence: !input action_button_up_short
- conditions: '{{ trigger_action | string in button_up_long }}'
sequence:
- event: ahb_controller_event
event_data:
controller: '{{ controller_id }}'
action: button_up_long
- choose:
- conditions: '{{ button_up_long_loop }}'
sequence:
- repeat:
while: '{{ repeat.index < button_up_long_max_loop_repeats | int }}'
sequence: !input action_button_up_long
default: !input action_button_up_long
- conditions:
- '{{ trigger_action | string in button_up_release }}'
- '{{ not integration_id in integrations_with_prev_event_storage or last_controller_event
| string in button_up_long }}'
sequence:
- event: ahb_controller_event
event_data:
controller: '{{ controller_id }}'
action: button_up_release
- choose:
- conditions: []
sequence: !input action_button_up_release
- conditions: '{{ trigger_action | string in button_down_short }}'
sequence:
- choose:
- conditions: '{{ button_down_double_press }}'
sequence:
- choose:
- conditions: '{{ trigger_action | string in states(helper_last_controller_event)
and trigger_delta | int <= helper_double_press_delay | int }}'
sequence:
- service: input_text.set_value
data:
entity_id: !input helper_last_controller_event
value: '{{ {"a":"double_press","t":as_timestamp(now())} | to_json
}}'
- event: ahb_controller_event
event_data:
controller: '{{ controller_id }}'
action: button_down_double
- choose:
- conditions: []
sequence: !input action_button_down_double
default:
- delay:
milliseconds: '{{ adjusted_double_press_delay }}'
- event: ahb_controller_event
event_data:
controller: '{{ controller_id }}'
action: button_down_short
- choose:
- conditions: []
sequence: !input action_button_down_short
default:
- event: ahb_controller_event
event_data:
controller: '{{ controller_id }}'
action: button_down_short
- choose:
- conditions: []
sequence: !input action_button_down_short
- conditions: '{{ trigger_action | string in button_down_long }}'
sequence:
- event: ahb_controller_event
event_data:
controller: '{{ controller_id }}'
action: button_down_long
- choose:
- conditions: '{{ button_down_long_loop }}'
sequence:
- repeat:
while: '{{ repeat.index < button_down_long_max_loop_repeats | int }}'
sequence: !input action_button_down_long
default: !input action_button_down_long
- conditions:
- '{{ trigger_action | string in button_down_release }}'
- '{{ not integration_id in integrations_with_prev_event_storage or last_controller_event
| string in button_down_long }}'
sequence:
- event: ahb_controller_event
event_data:
controller: '{{ controller_id }}'
action: button_down_release
- choose:
- conditions: []
sequence: !input action_button_down_release

View File

@@ -0,0 +1,704 @@
blueprint:
name: Controller - IKEA E2001/E2002 STYRBAR Remote control
description: "# Controller - IKEA E2001/E2002 STYRBAR Remote control\n\nController
automation for executing any kind of action triggered by the provided IKEA E2001/E2002
STYRBAR Remote control. Allows to optionally loop an action on a button long press.\nSupports
deCONZ, ZHA, Zigbee2MQTT.\n\nAutomations created with this blueprint can be connected
with one or more [Hooks](https://epmatt.github.io/awesome-ha-blueprints/docs/blueprints/hooks)
supported by this controller.\nHooks allow to easily create controller-based automations
for interacting with media players, lights, covers and more.\nSee the list of
[Hooks available for this controller](https://epmatt.github.io/awesome-ha-blueprints/docs/blueprints/controllers/ikea_e2001_e2002#available-hooks)
for additional details.\n\n\U0001F4D5 Full documentation regarding this blueprint
is available [here](https://epmatt.github.io/awesome-ha-blueprints/docs/blueprints/controllers/ikea_e2001_e2002).\n\n\U0001F680
This blueprint is part of the **[Awesome HA Blueprints](https://epmatt.github.io/awesome-ha-blueprints)
project**.\n\n Version 2022.08.08\n"
source_url: https://github.com/EPMatt/awesome-ha-blueprints/blob/main/blueprints/controllers/ikea_e2001_e2002/ikea_e2001_e2002.yaml
domain: automation
input:
integration:
name: (Required) Integration
description: Integration used for connecting the remote with Home Assistant.
Select one of the available values.
selector:
select:
options:
- deCONZ
- ZHA
- Zigbee2MQTT
sort: false
multiple: false
custom_value: false
controller_device:
name: (deCONZ, ZHA) Controller Device
description: The controller device to use for the automation. Choose a value
only if the remote is integrated with deCONZ, ZHA.
default: ''
selector:
device: {}
controller_entity:
name: (Zigbee2MQTT) Controller Entity
description: The action sensor of the controller to use for the automation.
Choose a value only if the remote is integrated with Zigbee2MQTT.
default: ''
selector:
entity:
domain:
- sensor
multiple: false
helper_last_controller_event:
name: (Required) Helper - Last Controller Event
description: Input Text used to store the last event fired by the controller.
You will need to manually create a text input entity for this, please read
the blueprint Additional Notes for more info.
default: ''
selector:
entity:
domain:
- input_text
multiple: false
action_button_left_short:
name: (Optional) Left button short press
description: Action to run on short left button press.
default: []
selector:
action: {}
action_button_left_long:
name: (Optional) Left button long press
description: Action to run on long left button press.
default: []
selector:
action: {}
action_button_left_release:
name: (Optional) Left button release
description: Action to run on left button release after long press.
default: []
selector:
action: {}
action_button_left_double:
name: (Optional) Left button double press
description: Action to run on double left button press.
default: []
selector:
action: {}
action_button_right_short:
name: (Optional) Right button short press
description: Action to run on short right button press.
default: []
selector:
action: {}
action_button_right_long:
name: (Optional) Right button long press
description: Action to run on long right button press.
default: []
selector:
action: {}
action_button_right_release:
name: (Optional) Right button release
description: Action to run on right button release after long press.
default: []
selector:
action: {}
action_button_right_double:
name: (Optional) Right button double press
description: Action to run on double right button press.
default: []
selector:
action: {}
action_button_up_short:
name: (Optional) Up button short press
description: Action to run on short up button press.
default: []
selector:
action: {}
action_button_up_long:
name: (Optional) Up button long press
description: Action to run on long up button press.
default: []
selector:
action: {}
action_button_up_release:
name: (Optional) Up button release
description: Action to run on up button release after long press.
default: []
selector:
action: {}
action_button_up_double:
name: (Optional) Up button double press
description: Action to run on double up button press.
default: []
selector:
action: {}
action_button_down_short:
name: (Optional) Down button short press
description: Action to run on short down button press.
default: []
selector:
action: {}
action_button_down_long:
name: (Optional) Down button long press
description: Action to run on long down button press.
default: []
selector:
action: {}
action_button_down_release:
name: (Optional) Down button release
description: Action to run on down button release after long press.
default: []
selector:
action: {}
action_button_down_double:
name: (Optional) Down button double press
description: Action to run on double down button press.
default: []
selector:
action: {}
button_left_long_loop:
name: (Optional) Left button long press - loop until release
description: Loop the left button action until the button is released.
default: false
selector:
boolean: {}
button_left_long_max_loop_repeats:
name: (Optional) Left button long press - Maximum loop repeats
description: Maximum number of repeats for the custom action, when looping is
enabled. Use it as a safety limit to prevent an endless loop in case the corresponding
stop event is not received.
default: 500
selector:
number:
min: 1.0
max: 5000.0
mode: slider
step: 1.0
button_right_long_loop:
name: (Optional) Right button long press - loop until release
description: Loop the right button action until the button is released.
default: false
selector:
boolean: {}
button_right_long_max_loop_repeats:
name: (Optional) Right button long press - Maximum loop repeats
description: Maximum number of repeats for the custom action, when looping is
enabled. Use it as a safety limit to prevent an endless loop in case the corresponding
stop event is not received.
default: 500
selector:
number:
min: 1.0
max: 5000.0
mode: slider
step: 1.0
button_up_long_loop:
name: (Optional) Up button long press - loop until release
description: Loop the up button action until the button is released.
default: false
selector:
boolean: {}
button_up_long_max_loop_repeats:
name: (Optional) Up button long press - Maximum loop repeats
description: Maximum number of repeats for the custom action, when looping is
enabled. Use it as a safety limit to prevent an endless loop in case the corresponding
stop event is not received.
default: 500
selector:
number:
min: 1.0
max: 5000.0
mode: slider
step: 1.0
button_down_long_loop:
name: (Optional) Down button long press - loop until release
description: Loop the down button action until the button is released.
default: false
selector:
boolean: {}
button_down_long_max_loop_repeats:
name: (Optional) Down button long press - Maximum loop repeats
description: Maximum number of repeats for the custom action, when looping is
enabled. Use it as a safety limit to prevent an endless loop in case the corresponding
stop event is not received.
default: 500
selector:
number:
min: 1.0
max: 5000.0
mode: slider
step: 1.0
button_left_double_press:
name: (Optional) Expose left button double press event
description: Choose whether or not to expose the virtual double press event
for the left button. Turn this on if you are providing an action for the left
button double press event.
default: false
selector:
boolean: {}
button_right_double_press:
name: (Optional) Expose right button double press event
description: Choose whether or not to expose the virtual double press event
for the right button. Turn this on if you are providing an action for the
right button double press event.
default: false
selector:
boolean: {}
button_up_double_press:
name: (Optional) Expose up button double press event
description: Choose whether or not to expose the virtual double press event
for the up button. Turn this on if you are providing an action for the up
button double press event.
default: false
selector:
boolean: {}
button_down_double_press:
name: (Optional) Expose down button double press event
description: Choose whether or not to expose the virtual double press event
for the down button. Turn this on if you are providing an action for the down
button double press event.
default: false
selector:
boolean: {}
helper_double_press_delay:
name: (Optional) Helper - Double Press delay
description: Max delay between the first and the second button press for the
double press event. Provide a value only if you are using a double press action.
Increase this value if you notice that the double press action is not triggered
properly.
default: 500
selector:
number:
min: 100.0
max: 5000.0
unit_of_measurement: milliseconds
mode: box
step: 10.0
helper_debounce_delay:
name: (Optional) Helper - Debounce delay
description: Delay used for debouncing RAW controller events, by default set
to 0. A value of 0 disables the debouncing feature. Increase this value if
you notice custom actions or linked Hooks running multiple times when interacting
with the device. When the controller needs to be debounced, usually a value
of 100 is enough to remove all duplicate events.
default: 0
selector:
number:
min: 0.0
max: 1000.0
unit_of_measurement: milliseconds
mode: box
step: 10.0
variables:
integration: !input integration
button_left_long_loop: !input button_left_long_loop
button_left_long_max_loop_repeats: !input button_left_long_max_loop_repeats
button_left_double_press: !input button_left_double_press
button_right_long_loop: !input button_right_long_loop
button_right_long_max_loop_repeats: !input button_right_long_max_loop_repeats
button_right_double_press: !input button_right_double_press
button_up_long_loop: !input button_up_long_loop
button_up_long_max_loop_repeats: !input button_up_long_max_loop_repeats
button_up_double_press: !input button_up_double_press
button_down_long_loop: !input button_down_long_loop
button_down_long_max_loop_repeats: !input button_down_long_max_loop_repeats
button_down_double_press: !input button_down_double_press
helper_last_controller_event: !input helper_last_controller_event
helper_double_press_delay: !input helper_double_press_delay
helper_debounce_delay: !input helper_debounce_delay
integration_id: '{{ integration | lower }}'
adjusted_double_press_delay: '{{ [helper_double_press_delay - helper_debounce_delay,
100] | max }}'
actions_mapping:
deconz:
button_left_short:
- '3002'
button_left_long:
- '3001'
button_left_release:
- '3003'
button_right_short:
- '4002'
button_right_long:
- '4001'
button_right_release:
- '4003'
button_up_short:
- '1002'
button_up_long:
- '1001'
button_up_release:
- '1003'
button_down_short:
- '2002'
button_down_long:
- '2001'
button_down_release:
- '2003'
zha:
button_left_short:
- press_257_13_0
button_left_long:
- hold_3329_0
button_left_release:
- release_1365
button_right_short:
- press_256_13_0
button_right_long:
- hold_3328_0
button_right_release:
- release_-27903
button_up_short:
- 'on'
button_up_long:
- move_with_on_off_0_83
button_up_release:
- stop
button_down_short:
- 'off'
button_down_long:
- move_1_83
button_down_release:
- stop
zigbee2mqtt:
button_left_short:
- arrow_left_click
button_left_long:
- arrow_left_hold
button_left_release:
- arrow_left_release
button_right_short:
- arrow_right_click
button_right_long:
- arrow_right_hold
button_right_release:
- arrow_right_release
button_up_short:
- 'on'
button_up_long:
- brightness_move_up
button_up_release:
- brightness_stop
button_down_short:
- 'off'
button_down_long:
- brightness_move_down
button_down_release:
- brightness_stop
button_left_short: '{{ actions_mapping[integration_id]["button_left_short"] }}'
button_left_long: '{{ actions_mapping[integration_id]["button_left_long"] }}'
button_left_release: '{{ actions_mapping[integration_id]["button_left_release"]
}}'
button_right_short: '{{ actions_mapping[integration_id]["button_right_short"] }}'
button_right_long: '{{ actions_mapping[integration_id]["button_right_long"] }}'
button_right_release: '{{ actions_mapping[integration_id]["button_right_release"]
}}'
button_up_short: '{{ actions_mapping[integration_id]["button_up_short"] }}'
button_up_long: '{{ actions_mapping[integration_id]["button_up_long"] }}'
button_up_release: '{{ actions_mapping[integration_id]["button_up_release"] }}'
button_down_short: '{{ actions_mapping[integration_id]["button_down_short"] }}'
button_down_long: '{{ actions_mapping[integration_id]["button_down_long"] }}'
button_down_release: '{{ actions_mapping[integration_id]["button_down_release"]
}}'
integrations_with_prev_event_storage:
- zha
- zigbee2mqtt
controller_entity: !input controller_entity
controller_device: !input controller_device
controller_id: '{% if integration_id=="zigbee2mqtt" %}{{controller_entity}}{% else
%}{{controller_device}}{% endif %}'
mode: restart
max_exceeded: silent
trigger:
- platform: event
event_type: state_changed
event_data:
entity_id: !input controller_entity
- platform: event
event_type:
- deconz_event
- zha_event
event_data:
device_id: !input controller_device
condition:
- condition: and
conditions:
- '{%- set trigger_action -%} {%- if integration_id == "zigbee2mqtt" -%} {{ trigger.event.data.new_state.state
}} {%- elif integration_id == "deconz" -%} {{ trigger.event.data.event }} {%-
elif integration_id == "zha" -%} {{ trigger.event.data.command }}{{"_" if trigger.event.data.args|length
> 0}}{{ trigger.event.data.args|join("_") }} {%- endif -%} {%- endset -%} {{ trigger_action
not in ["","None"] }}'
- '{{ integration_id != "zigbee2mqtt" or trigger.event.data.new_state.state != trigger.event.data.old_state.state
}}'
action:
- delay:
milliseconds: !input helper_debounce_delay
- variables:
trigger_action: '{%- if integration_id == "zigbee2mqtt" -%} {{ trigger.event.data.new_state.state
}} {%- elif integration_id == "deconz" -%} {{ trigger.event.data.event }} {%-
elif integration_id == "zha" -%} {{ trigger.event.data.command }}{{"_" if trigger.event.data.args|length
> 0}}{{ trigger.event.data.args|join("_") }} {%- endif -%}'
trigger_delta: '{{ (as_timestamp(now()) - ((states(helper_last_controller_event)
| from_json).t if helper_last_controller_event is not none and (states(helper_last_controller_event)
| regex_match("^\{((\"a\": \".*\"|\"t\": \d+\.\d+)(, )?){2}\}$")) else as_timestamp("1970-01-01
00:00:00"))) * 1000 }}'
last_controller_event: '{{ (states(helper_last_controller_event) | from_json).a
if helper_last_controller_event is not none and (states(helper_last_controller_event)
| regex_match("^\{((\"a\": \".*\"|\"t\": \d+\.\d+)(, )?){2}\}$")) else "" }}'
- service: input_text.set_value
data:
entity_id: !input helper_last_controller_event
value: '{{ {"a":trigger_action,"t":as_timestamp(now())} | to_json }}'
- choose:
- conditions: '{{ trigger_action | string in button_left_short }}'
sequence:
- choose:
- conditions: '{{ button_left_double_press }}'
sequence:
- choose:
- conditions: '{{ trigger_action | string in states(helper_last_controller_event)
and trigger_delta | int <= helper_double_press_delay | int }}'
sequence:
- service: input_text.set_value
data:
entity_id: !input helper_last_controller_event
value: '{{ {"a":"double_press","t":as_timestamp(now())} | to_json
}}'
- event: ahb_controller_event
event_data:
controller: '{{ controller_id }}'
action: button_left_double
- choose:
- conditions: []
sequence: !input action_button_left_double
default:
- delay:
milliseconds: '{{ adjusted_double_press_delay }}'
- event: ahb_controller_event
event_data:
controller: '{{ controller_id }}'
action: button_left_short
- choose:
- conditions: []
sequence: !input action_button_left_short
default:
- event: ahb_controller_event
event_data:
controller: '{{ controller_id }}'
action: button_left_short
- choose:
- conditions: []
sequence: !input action_button_left_short
- conditions: '{{ trigger_action | string in button_left_long }}'
sequence:
- event: ahb_controller_event
event_data:
controller: '{{ controller_id }}'
action: button_left_long
- choose:
- conditions: '{{ button_left_long_loop }}'
sequence:
- repeat:
while: '{{ repeat.index < button_left_long_max_loop_repeats | int }}'
sequence: !input action_button_left_long
default: !input action_button_left_long
- conditions:
- '{{ trigger_action | string in button_left_release }}'
- '{{ not integration_id in integrations_with_prev_event_storage or last_controller_event
| string in button_left_long }}'
sequence:
- event: ahb_controller_event
event_data:
controller: '{{ controller_id }}'
action: button_left_release
- choose:
- conditions: []
sequence: !input action_button_left_release
- conditions: '{{ trigger_action | string in button_right_short }}'
sequence:
- choose:
- conditions: '{{ button_right_double_press }}'
sequence:
- choose:
- conditions: '{{ trigger_action | string in states(helper_last_controller_event)
and trigger_delta | int <= helper_double_press_delay | int }}'
sequence:
- service: input_text.set_value
data:
entity_id: !input helper_last_controller_event
value: '{{ {"a":"double_press","t":as_timestamp(now())} | to_json
}}'
- event: ahb_controller_event
event_data:
controller: '{{ controller_id }}'
action: button_right_double
- choose:
- conditions: []
sequence: !input action_button_right_double
default:
- delay:
milliseconds: '{{ adjusted_double_press_delay }}'
- event: ahb_controller_event
event_data:
controller: '{{ controller_id }}'
action: button_right_short
- choose:
- conditions: []
sequence: !input action_button_right_short
default:
- event: ahb_controller_event
event_data:
controller: '{{ controller_id }}'
action: button_right_short
- choose:
- conditions: []
sequence: !input action_button_right_short
- conditions: '{{ trigger_action | string in button_right_long }}'
sequence:
- event: ahb_controller_event
event_data:
controller: '{{ controller_id }}'
action: button_right_long
- choose:
- conditions: '{{ button_right_long_loop }}'
sequence:
- repeat:
while: '{{ repeat.index < button_right_long_max_loop_repeats | int }}'
sequence: !input action_button_right_long
default: !input action_button_right_long
- conditions:
- '{{ trigger_action | string in button_right_release }}'
- '{{ not integration_id in integrations_with_prev_event_storage or last_controller_event
| string in button_right_long }}'
sequence:
- event: ahb_controller_event
event_data:
controller: '{{ controller_id }}'
action: button_right_release
- choose:
- conditions: []
sequence: !input action_button_right_release
- conditions: '{{ trigger_action | string in button_up_short }}'
sequence:
- choose:
- conditions: '{{ button_up_double_press }}'
sequence:
- choose:
- conditions: '{{ trigger_action | string in states(helper_last_controller_event)
and trigger_delta | int <= helper_double_press_delay | int }}'
sequence:
- service: input_text.set_value
data:
entity_id: !input helper_last_controller_event
value: '{{ {"a":"double_press","t":as_timestamp(now())} | to_json
}}'
- event: ahb_controller_event
event_data:
controller: '{{ controller_id }}'
action: button_up_double
- choose:
- conditions: []
sequence: !input action_button_up_double
default:
- delay:
milliseconds: '{{ adjusted_double_press_delay }}'
- event: ahb_controller_event
event_data:
controller: '{{ controller_id }}'
action: button_up_short
- choose:
- conditions: []
sequence: !input action_button_up_short
default:
- event: ahb_controller_event
event_data:
controller: '{{ controller_id }}'
action: button_up_short
- choose:
- conditions: []
sequence: !input action_button_up_short
- conditions: '{{ trigger_action | string in button_up_long }}'
sequence:
- event: ahb_controller_event
event_data:
controller: '{{ controller_id }}'
action: button_up_long
- choose:
- conditions: '{{ button_up_long_loop }}'
sequence:
- repeat:
while: '{{ repeat.index < button_up_long_max_loop_repeats | int }}'
sequence: !input action_button_up_long
default: !input action_button_up_long
- conditions:
- '{{ trigger_action | string in button_up_release }}'
- '{{ not integration_id in integrations_with_prev_event_storage or last_controller_event
| string in button_up_long }}'
sequence:
- event: ahb_controller_event
event_data:
controller: '{{ controller_id }}'
action: button_up_release
- choose:
- conditions: []
sequence: !input action_button_up_release
- conditions: '{{ trigger_action | string in button_down_short }}'
sequence:
- choose:
- conditions: '{{ button_down_double_press }}'
sequence:
- choose:
- conditions: '{{ trigger_action | string in states(helper_last_controller_event)
and trigger_delta | int <= helper_double_press_delay | int }}'
sequence:
- service: input_text.set_value
data:
entity_id: !input helper_last_controller_event
value: '{{ {"a":"double_press","t":as_timestamp(now())} | to_json
}}'
- event: ahb_controller_event
event_data:
controller: '{{ controller_id }}'
action: button_down_double
- choose:
- conditions: []
sequence: !input action_button_down_double
default:
- delay:
milliseconds: '{{ adjusted_double_press_delay }}'
- event: ahb_controller_event
event_data:
controller: '{{ controller_id }}'
action: button_down_short
- choose:
- conditions: []
sequence: !input action_button_down_short
default:
- event: ahb_controller_event
event_data:
controller: '{{ controller_id }}'
action: button_down_short
- choose:
- conditions: []
sequence: !input action_button_down_short
- conditions: '{{ trigger_action | string in button_down_long }}'
sequence:
- event: ahb_controller_event
event_data:
controller: '{{ controller_id }}'
action: button_down_long
- choose:
- conditions: '{{ button_down_long_loop }}'
sequence:
- repeat:
while: '{{ repeat.index < button_down_long_max_loop_repeats | int }}'
sequence: !input action_button_down_long
default: !input action_button_down_long
- conditions:
- '{{ trigger_action | string in button_down_release }}'
- '{{ not integration_id in integrations_with_prev_event_storage or last_controller_event
| string in button_down_long }}'
sequence:
- event: ahb_controller_event
event_data:
controller: '{{ controller_id }}'
action: button_down_release
- choose:
- conditions: []
sequence: !input action_button_down_release

View File

@@ -0,0 +1,315 @@
blueprint:
name: AWTRIX Night Clock
description: "## AWTRIX Night Clock\nThis blueprint provides a night clock mode
for AWTRIX Light. It allows you to personalize various aspects of the clockface
to suit your preferences.\n### Screenshot\n\n ![](https://raw.githubusercontent.com/RDG88/Homeassistant_Blueprints/main/thumbnails/SCR-nightclock.png)\n
\ \n### Features\n- This blueprint features a night clock mode, which displays
a customized color, and you have the possibility to disable app transitions, automatic
brightness, and the color of the night clock.\n### Prerequisites\nAWTRIX v0.72\n"
domain: automation
input:
awtrix:
name: AWTRIX Light
description: Select the Awtrix light
selector:
device:
filter:
- integration: mqtt
manufacturer: Blueforcer
model: AWTRIX Light
multiple: true
sleep_time:
name: Night mode time
description: At what time does the clock need to activate night mode?
selector:
time: {}
default: '23:00:00'
wake_up_time:
name: Day mode time
description: At what time does the clock need activate day mode?
selector:
time: {}
default: 08:00:00
sleep_settings_atrans:
name: Enable automatic app transition in night mode.
description: 'This setting allows you to enable or disable automatic transition
of apps in night mode.
'
selector:
boolean: {}
default: false
sleep_settings_bri:
name: Night mode brightness setting
description: 'What is the brightness for night mode?
'
selector:
number:
min: 0.0
max: 255.0
mode: slider
step: 1.0
default: '1'
sleep_settings_abri:
name: Enable automatic brightness when the night mode is active.
description: 'This setting allows you to enable or disable automatic brightening
in night mode.
'
selector:
boolean: {}
default: false
sleep_settings_color:
name: Color setting for the night mode clock
description: This setting allows you to change the color of the night mode clock.
selector:
color_rgb: {}
default:
- 255
- 0
- 0
sleep_weekday_bar:
name: Enable the weekday bar for the night mode clock
description: 'This setting allows you to enable or disable the weekday bar for
the night mode clock.
'
selector:
boolean: {}
default: false
sleep_time_format:
name: Select the time format for the night mode clock
description: 'Select the time format for the night mode clock
'
selector:
select:
options:
- label: '13:30:45'
value: '%H:%M:%S'
- label: '1:30:45'
value: '%l:%M:%S'
- label: '13:30'
value: '%H:%M'
- label: 13:30 with blinking colon
value: '%H %M'
- label: '1:30'
value: '%l:%M'
- label: 1:30 with blinking colon
value: '%l %M'
- label: 1:30 PM
value: '%l:%M %p'
- label: 1:30 PM with blinking colon
value: '%l %M %p'
sort: false
custom_value: false
multiple: false
default: '%H:%M'
wakeup_settings_atrans:
name: Enable automatic app transitions in day mode?
description: 'This setting allows you to enable or disable the automatic transition
of apps in day mode.
'
selector:
boolean: {}
default: true
wakeup_settings_bri:
name: Day mode brightness setting (1-255)
description: 'This setting allows you to adjust the brightness for day mode,
pick a value between 1 and 255.
'
selector:
number:
min: 1.0
max: 255.0
mode: slider
step: 1.0
default: '127'
wakeup_settings_abri:
name: Enable automatic brightness in day mode
description: 'This setting allows you to enable or disable automatic brightening
in day mode.
'
selector:
boolean: {}
default: false
wakeup_settings_color:
name: Color setting for the day mode clock
description: This setting allows you to change the color of the day mode clock.
selector:
color_rgb: {}
default:
- 255
- 255
- 255
wakeup_settings_calendar_color:
name: Color setting for the day mode calendar
description: This setting allows you to change the color of the day mode calendar.
selector:
color_rgb: {}
default:
- 255
- 0
- 0
wakeup_settings_calendar_text_color:
name: Color setting for the day mode calendar text
description: This settings allows you to change the color of the day mode text
in the calendar.
selector:
color_rgb: {}
default:
- 0
- 0
- 0
wakeup_weekday_bar:
name: Enable the weekday bar for the day mode clock
description: 'This setting allows you to enable or disable the weekday bar for
the day mode clock.
'
selector:
boolean: {}
default: false
wakeup_calendar_style:
name: Select the time/calendar style for the day mode clock
description: "Select the time/calendar style\n\n ![](https://raw.githubusercontent.com/RDG88/Homeassistant_Blueprints/main/thumbnails/tmode-0.svg)
`Style 0` \n\n ---\n\n ![](https://raw.githubusercontent.com/RDG88/Homeassistant_Blueprints/main/thumbnails/tmode-1.svg)
`Style 1` \n\n ---\n\n ![](https://raw.githubusercontent.com/RDG88/Homeassistant_Blueprints/main/thumbnails/tmode-2.svg)
`Style 2` \n\n ---\n\n ![](https://raw.githubusercontent.com/RDG88/Homeassistant_Blueprints/main/thumbnails/tmode-3.svg)
`Style 3` \n\n ---\n\n ![](https://raw.githubusercontent.com/RDG88/Homeassistant_Blueprints/main/thumbnails/tmode-4.svg)
`Style 4`\n"
selector:
select:
options:
- label: Style 0
value: '0'
- label: Style 1
value: '1'
- label: Style 2
value: '2'
- label: Style 3
value: '3'
- label: Style 4
value: '4'
sort: false
custom_value: false
multiple: false
default: '1'
wakeup_time_format:
name: Select the time format for the day mode clock
description: 'Select the time format for the day mode clock
'
selector:
select:
options:
- label: '13:30:45'
value: '%H:%M:%S'
- label: '1:30:45'
value: '%l:%M:%S'
- label: '13:30'
value: '%H:%M'
- label: 13:30 with blinking colon
value: '%H %M'
- label: '1:30'
value: '%l:%M'
- label: 1:30 with blinking colon
value: '%l %M'
- label: 1:30 PM
value: '%l:%M %p'
- label: 1:30 PM with blinking colon
value: '%l %M %p'
sort: false
custom_value: false
multiple: false
default: '%H:%M'
source_url: https://raw.githubusercontent.com/RDG88/Homeassistant_Blueprints/main/nightclock_awtrix.yaml
variables:
device_ids: !input awtrix
wake_up_time: !input wake_up_time
sleep_time: !input sleep_time
wakeup_weekday_bar: !input wakeup_weekday_bar
sleep_weekday_bar: !input sleep_weekday_bar
wakeup_settings_atrans: !input wakeup_settings_atrans
wakeup_settings_color: !input wakeup_settings_color
sleep_settings_atrans: !input sleep_settings_atrans
sleep_settings_color: !input sleep_settings_color
sleep_settings_abri: !input sleep_settings_abri
sleep_settings_bri: !input sleep_settings_bri
sleep_time_format: !input sleep_time_format
wakeup_settings_bri: !input wakeup_settings_bri
wakeup_settings_abri: !input wakeup_settings_abri
wakeup_calendar_style: !input wakeup_calendar_style
wakeup_settings_calendar_color: !input wakeup_settings_calendar_color
wakeup_settings_calendar_text_color: !input wakeup_settings_calendar_text_color
wakeup_time_format: !input wakeup_time_format
awtrix_devices: "{%- set ns = namespace(awtrix = []) -%} {%- for device_id in device_ids
-%}\n {%- set device_name = iif(device_attr(device_id, 'name_by_user') != none,
device_attr(device_id, 'name_by_user'), device_attr(device_id, 'name')) -%}\n
\ {%- set entity = expand(device_entities(device_id)) | select('search', 'device_topic')
| map(attribute='entity_id') | first -%}\n {%- set topic = states(entity) -%}\n
\ {% set ns.awtrix = ns.awtrix + [{\"device\": device_name, \"entity\": entity,
\"topic\": topic}] -%}\n{%- endfor -%} {{ ns.awtrix }}"
payload_sleep_switch: "{\n \"name\": \"time\"\n}"
payload_sleep_settings: "{ \"ATRANS\": {{ sleep_settings_atrans | lower }}, \n \"BRI\":
{{ sleep_settings_bri }}, \n \"ABRI\": {{ sleep_settings_abri | lower }},\n \"TMODE\":
0,\n \"TFORMAT\": \"{{ sleep_time_format }}\",\n \"WD\": {{ sleep_weekday_bar
| lower }},\n \"TIME_COL\": {{ sleep_settings_color }}\n}"
payload_wakeup_settings: "{ \"ATRANS\": {{ wakeup_settings_atrans | lower }}, \n
\ \"BRI\": {{ wakeup_settings_bri }}, \n \"ABRI\": {{ wakeup_settings_abri |
lower }},\n \"CCOL\": {{ wakeup_settings_calendar_color }},\n \"CTCOL\": {{
wakeup_settings_calendar_text_color }},\n \"TMODE\": {{ wakeup_calendar_style
}},\n \"TFORMAT\": \"{{ wakeup_time_format }}\",\n \"WD\": {{ wakeup_weekday_bar
| lower }},\n \"TIME_COL\": {{ wakeup_settings_color }}\n}"
trigger:
- platform: time
at: !input wake_up_time
id: wakeup_timer
- platform: time
at: !input sleep_time
id: sleep_timer
condition: []
action:
- repeat:
for_each: '{{ awtrix_devices }}'
sequence:
- choose:
- conditions:
- condition: template
value_template: '{{ states(repeat.item.entity) not in [''unavailable'',
''unknown''] }}
'
sequence:
- if:
- condition: trigger
id:
- wakeup_timer
then:
- service: mqtt.publish
data:
qos: 0
retain: false
topic: '{{ repeat.item.topic ~ ''/settings''}}'
payload: '{{ payload_wakeup_settings }}'
- if:
- condition: trigger
id:
- sleep_timer
then:
- service: mqtt.publish
data:
qos: 0
retain: false
topic: '{{ repeat.item.topic ~ ''/settings''}}'
payload: '{{ payload_sleep_settings }}'
- service: mqtt.publish
data:
qos: 0
retain: false
topic: '{{ repeat.item.topic ~ ''/switch''}}'
payload: '{{ payload_sleep_switch }}'

View File

@@ -0,0 +1,110 @@
blueprint:
name: ZHA - IKEA Somrig remote dot-buttons control
description: Fully customisable dot-buttons, with options for single, double and
long press for each one.
domain: automation
input:
remote:
name: Remote
description: IKEA Somrig remote to use
selector:
device:
integration: zha
manufacturer: IKEA of Sweden
model: SOMRIG shortcut button
multiple: false
single_dot_single_press:
name: Single dot (Single press)
description: Action to run on single dot press
default: []
selector:
action: {}
single_dot_double_press:
name: Single dot (Double press)
description: Action to run on single dot double press
default: []
selector:
action: {}
single_dot_long_press:
name: Single dot (Long press)
description: Action to run on single dot long press
default: []
selector:
action: {}
single_dot_long_release:
name: Single dot (Release after long press)
description: Action to run on releasing after a long press on the single dot
default: []
selector:
action: {}
double_dot_single_press:
name: Double dot (Single press)
description: Action to run on double dot press
default: []
selector:
action: {}
double_dot_double_press:
name: Double dot (Double press)
description: Action to run on double dot double press
default: []
selector:
action: {}
double_dot_long_press:
name: Double dot (Long press)
description: Action to run on double dot long press
default: []
selector:
action: {}
double_dot_long_release:
name: Double dot (Release after long press)
description: Action to run on releasing after a long press on the double dot
default: []
selector:
action: {}
source_url: https://community.home-assistant.io/t/ikea-somrig-remote-e2213-zha/668671
mode: single
max_exceeded: silent
trigger:
- platform: event
event_type: zha_event
event_data:
device_id: !input remote
action:
- variables:
command: '{{ trigger.event.data.command }}'
cluster_id: '{{ trigger.event.data.cluster_id }}'
endpoint_id: '{{ trigger.event.data.endpoint_id }}'
args: '{{ trigger.event.data.args }}'
- choose:
- conditions:
- '{{ command == ''short_release'' }}'
- '{{ endpoint_id == 1 }}'
sequence: !input single_dot_single_press
- conditions:
- '{{ command == ''multi_press_complete'' }}'
- '{{ endpoint_id == 1 }}'
sequence: !input single_dot_double_press
- conditions:
- '{{ command == ''long_press'' }}'
- '{{ endpoint_id == 1 }}'
sequence: !input single_dot_long_press
- conditions:
- '{{ command == ''long_release'' }}'
- '{{ endpoint_id == 1 }}'
sequence: !input single_dot_long_release
- conditions:
- '{{ command == ''short_release'' }}'
- '{{ endpoint_id == 2 }}'
sequence: !input double_dot_single_press
- conditions:
- '{{ command == ''multi_press_complete'' }}'
- '{{ endpoint_id == 2 }}'
sequence: !input double_dot_double_press
- conditions:
- '{{ command == ''long_press'' }}'
- '{{ endpoint_id == 2 }}'
sequence: !input double_dot_long_press
- conditions:
- '{{ command == ''long_release'' }}'
- '{{ endpoint_id == 2 }}'
sequence: !input double_dot_long_release

View File

@@ -0,0 +1,677 @@
blueprint:
name: "AWTRIX Weather ⛈️ + Forecast + \U0001F315"
description: "\nThis is somewhat of a mega-weather blueprint with moon phase support.
However for it work correctly you will need a variety of different things setup.
It was initially designed to use in partnership with a personal weather station
however it seems to work fine with OpenWeather as well or any other provider that
offers an hourly forecast.\n\n\n![](https://raw.githubusercontent.com/jeeftor/HomeAssistant/master/docs/weather.gif)\n\n![](https://raw.githubusercontent.com/jeeftor/HomeAssistant/master/docs/sunset.gif)\nThis
blueprint will publish to two separate topics. `jeef_weather` for the weather
report and `jeef_weather_sun` if its near sunrise/set\n## ⚠️ REQUIREMENTS ⚠️\nFor
this blueprint to work you MUST have a few things pre-setup. \n### Moon Integration
\U0001F315\n .------.\n ( I MOON ) ..\n `------' .' /\n O
\ / ;\n o i OO\n C `-. Make sure you've\n |
\ <-' enabled\n ( ,--. the MOON Sensor\n V
\ \\_)\n \\ :\n `._\\. \n\n\nThe moon integration
is required. You can add it via the [moon](https://www.home-assistant.io/integrations/moon/)
page or just by [clicking here](https://my.home-assistant.io/redirect/config_flow_start?domain=moon)\n###
Moon Rise/Set Sensor \U0001F315 ⏲️\n\n M\n (X)\n // \\\\
\ Lets use a GeoLocation to find\n // \\\\ out the Moon Rise / Set\n
\ // \\\\ TIMES\n // \\\\\n / \\\n\nAs Home
Assistant doesn't _currently_ provide moon rise/set times you will need to get
this from some api. You can use the [ipgeolocation](https://app.ipgeolocation.io)
API.\nTo do so you will need to create an account and extract your `API_KEY`.
Additionally you need your `LAT` and `LON`.\nThen you can add a [REST](https://www.home-assistant.io/integrations/sensor.rest/)
sensor to your `configuraiton.yaml` file like the one here:\n\n resource: https://api.ipgeolocation.io/astronomy?lat=<LAT>&long=<LON>&apiKey=<API_KEY>\n
\ name: ip_geo_location\n scan_interval: 300\n value_template: \"OK\"\n
\ json_attributes:\n - moonrise\n - moonset\n - moon_altitude\n\n###
Icons\nYou can call my custom script which will prompt you for an Awtrix device
and then upload the required icons:\n \n (If you have windows I don't know if
this will work)\n\n bash -c \"$(curl -fsSL https://raw.githubusercontent.com/jeeftor/HomeAssistant/master/icons/upload_icon.sh)\"\n\n_This
blueprint ~will~ may be updated as new features_\n![](https://www.gravatar.com/avatar/3b9968835eb719e5d78a04ba7a2bafbd?s=64)
https://raw.githubusercontent.com/jeeftor/HomeAssistant/master/blueprints/automation/awtrix_weatherflow.yaml\n"
domain: automation
input:
awtrix:
name: AWTRIX Device
description: Select the Awtrix light
selector:
device:
integration: mqtt
manufacturer: Blueforcer
model: AWTRIX Light
multiple: true
forecast_var:
name: Hourly Forecast
description: "Select a sensor that provides an Hourly forecast (not a daily
one)\nThis integration has been tested with:\n\n - HACS [Weatherflow](https://github.com/briis/hass-weatherflow)
integration \n \n - HomeAssistant [Openweather](https://www.home-assistant.io/integrations/openweathermap/)\n"
selector:
entity:
filter:
- domain:
- weather
multiple: false
hours_to_show:
name: Forecast Hours to Show
description: 'How many hours of forecast do you wish to show along the bottom
of the display
'
selector:
number:
max: 24.0
min: 0.0
unit_of_measurement: hours
mode: box
step: 1.0
default: 12
forecast_temp_field:
name: Temperature Attributes
description: "Once you've selected your hourly forecast you will need to identify
which attributes in the forecast provides a temperature value. \n\n - If
you are using [Weatherflow](https://github.com/briis/hass-weatherflow) you
may be able to select from either `feels_like` or `temperature`\n\n - In
[Openweather](https://www.home-assistant.io/integrations/openweathermap/)
you only have access to `temperature`\n"
selector:
text: {}
default: feels_like
temp_digits:
name: Temp Digits
description: 'By default we will round the temp to the nearest whole-number.
If you want percisions you can change this to 1 or 2 in order to see more
decimalm places.
'
selector:
number:
min: 0.0
max: 2.0
step: 1.0
mode: box
unit_of_measurement: Decimal places
default: 0
temp_suffix:
name: Temperature suffix
description: "How do you want to display the temperature\nIf you live in a country
with the following flags:\n\U0001F1FA\U0001F1F8\U0001F1F5\U0001F1F7\U0001F1F5\U0001F1FC\U0001F1E7\U0001F1FF\U0001F1F0\U0001F1FE\U0001F1EB\U0001F1F2\U0001F1F2\U0001F1ED\U0001F1FB\U0001F1EE\U0001F1EC\U0001F1FA\nYou
probbaly use Farenheit.\nEverybody else in the \U0001F5FA seems to rock the
Metric System"
selector:
select:
options:
- label: None
value: ''
- label: °
value: °
- label: °F
value: °F
- label: °C
value: °C
- label: F
value: F
- label: C
value: C
sort: false
custom_value: false
multiple: false
default: °
current_temp_var:
name: The current outside temperature
description: "Select a sensor either from a PWS or a forecast that provides
the current outside temperature you wish to display:\n\n - `sensor.openweathermap_feels_like_temperature`\n"
selector:
entity:
domain:
- sensor
multiple: false
default: sensor.weatherflow_air_temperature
color_matrix_json:
name: Color Matrix
description: "The `Color Matrix` will control colors map to temperature ranges
on the display. The format of this map is **JSON** \nHere you can enter a
temperature to color mapping. \n> Please note the format is *JSON*,\n \n\nSome
possible mappings are:\n#### USA: Farenheit 0-100 (Based on NOAA scale from
0-100)\n\n\n {\"0\": \"#FEC4FF\",\"10\": \"#D977DF\",\"20\": \"#9545BC\",\"30\":
\"#4B379C\",\"40\": \"#31B8DB\",\"50\": \"#31DB8B\",\"60\": \"#6ED228\",\"70\":
\"#FFFF28\",\"80\": \"#F87E27\",\"90\": \"#CF3927\",\"100\": \"#A12527\"}\n\n\n####
EURO: -12°c to -38°c based on USA NOAA Colors \n\n {\"-12\": \"#D977DF\",\"-6\":
\"#9545BC\",\"-1\": \"#4B379C\",\"0\": \"#FEC4FF\",\"4\": \"#31B8DB\",\"10\":
\"#31DB8B\",\"15\": \"#6ED228\",\"21\": \"#FFFF28\",\"27\": \"#F87E27\",\"32\":
\"#CF3927\",\"38\": \"#A12527\"}\n"
selector:
text:
multiline: true
default: "{\n \"0\": \"#FEC4FF\",\n \"10\": \"#D977DF\",\n \"20\": \"#9545BC\",\n
\ \"30\": \"#4B379C\",\n \"40\": \"#31B8DB\",\n \"50\": \"#31DB8B\",\n \"60\":
\"#6ED228\",\n \"70\": \"#FFFF28\",\n \"80\": \"#F87E27\",\n \"90\": \"#CF3927\",\n
\ \"100\": \"#A12527\"\n}\n"
moon:
name: Moon Phase Sensor
description: "\U0001F311\U0001F312\U0001F313\U0001F314\U0001F316\U0001F317\U0001F318\nTo
setup a moon sensor see here: https://www.home-assistant.io/integrations/moon/\nor
just [clicking here](https://my.home-assistant.io/redirect/config_flow_start?domain=moon)\n"
selector:
entity:
multiple: false
filter:
- integration: moon
moon_rise_set:
name: Moon Riese/Set Sensor
description: "As Home Assistant doesn't provide moon rise/set times you will
need to get this from some api. In my personal setup I use [ipgeolocation](https://app.ipgeolocation.io)
as my api.\nYou can create a custom REST sensor as follows:\n``` sensor: -
platform: rest\n resource: https://api.ipgeolocation.io/astronomy?lat=<LAT>&long=-<LON>&apiKey=<API_KEY>\n
\ name: ip_geo_location\n scan_interval: 300\n value_template: \"OK\"\n
\ json_attributes:\n - moonrise\n - moonset\n - moon_altitude\n```\n"
selector:
entity:
multiple: false
filter:
- integration: rest
when_show_moon:
name: When should the moon be displayed
description: "Some people are really into the moon \U0001F43A and they are
called Wearwolves or maybe Astronomers. \n\nPlease select how and when you
want the moon displayed\n\nBy selecting `Always show moon` the moon will always
be drawn to the right of the display. Otherwise the moon will only be drawn
if its risen depending on the option selected.\n### NOTE:\n\n At Brightness
values less than 29 the greys of the moon will render green on the clock.\n"
selector:
select:
options:
- label: Never show moon
value: never
- label: Always show moon
value: always
- label: Only show moon if its risen
value: risen
- label: Only show moon if risen + night
value: night
sort: false
custom_value: false
multiple: false
default: night
use_moon_clear_night:
name: Swap Clear Night for Moon
description: '
The default case is for the moon to be drawn to the right-side of the clock,
however, you have the option if this is selected to repalce the `clear_night`
icon with the moon icon. This will only swap icons if the moon is currently
being displayed.
- ![](https://developer.lametric.com/content/apps/icon_thumbs/2314_icon_thumb.png?v=1)
- `full_moon`
- ![](https://developer.lametric.com/content/apps/icon_thumbs/2315_icon_thumb.png?v=1)
- `waning_gibbous`
- ![](https://developer.lametric.com/content/apps/icon_thumbs/2316_icon_thumb.png?v=1)
- `last_quarter`
- ![](https://developer.lametric.com/content/apps/icon_thumbs/2317_icon_thumb.png?v=1)
- `waning_crescent`
- ![](https://developer.lametric.com/content/apps/icon_thumbs/2318_icon_thumb.png?v=1)
- `new_moon`
- ![](https://developer.lametric.com/content/apps/icon_thumbs/2319_icon_thumb.png?v=1)
- `waxing_crescent`
- ![](https://developer.lametric.com/content/apps/icon_thumbs/2320_icon_thumb.png?v=1)
- `first_quarter`
- ![](https://developer.lametric.com/content/apps/icon_thumbs/2321_icon_thumb.png?v=1)
- `waxing_gibbous`
If you wish to use a different icon please enter its text in the box to the
right'
selector:
boolean: {}
default: true
use_moon_sunny_night:
name: Swap Sunny + Night for the Moon
description: Some weather integrations may not correctly implement the `clear-night`
weather state. In that case you can use this option to automatically swap
out the moon for if you have night + sunny
selector:
boolean: {}
default: true
show_sun_rise_set:
name: ☀️ Show Sunrise/Sunset
description: "Prior to both sunrise and sunset times offer a message about pending
sun transitional state.\n\n :\n `. ; .'\n
\ `. .-'''-. .'\n ;' __ _;'\n / '_ _`\\
\ TURN ME ON!\n | _( a ( a |\n ''''| (_) > |``````\n
\ \\ \\ / /\n `. `--'.'\n .' `-,,,-' `.\n
\ .' : `.\n :\n\n\n_You can change the icons
for sun rise/set way down below._\n"
selector:
boolean: {}
default: true
sun_event_minute_threshold:
name: "Sun Time Prior \U0001F570"
description: "This value controls when to show sunrise/set notifications. \n\nIf
the sunrise will occur in `50` minutes and this value is set to `60` it will
show, however if this value is only `30` it won't show."
selector:
number:
min: 5.0
max: 1440.0
unit_of_measurement: min
step: 1.0
mode: slider
default: 30
sun_time_type:
name: Sun Time Type
description: "When showing a notification about sun rise/set it can offer 3
different time formats:\n\n - Relative Time: `12 min`\n - Actual Time:
\ `8:31 pm` or `22:31`\n"
selector:
select:
options:
- Relative
- Actual
sort: false
custom_value: false
multiple: false
default: Actual
sun_time_format:
name: Actual Time Format
description: "If you are using actual time you can enter a STRFTIME format string
here for the time. Some options would be:\n\n - `%H%M` which would render
`0529`\n \n - `%-I%M%p` which woudl render `529AM`\n - `%-I%:M%p` which
woudl render `5:29AM`\n\n\n\n For details see https://strftime.org/\n"
selector:
text:
type: text
multiline: false
default: '%-I%M%p'
message_duration_forecast:
name: Forecast Duration ⏱️
description: How long should the forecast message remain on the screen(in seconds). *If
you select `0` it will use the Global App Time*
selector:
number:
min: 0.0
max: 300.0
unit_of_measurement: sec
step: 1.0
mode: slider
default: 30
message_duration_riseset:
name: Sun Rise/Set Duration ⏱️
description: How long should the sunrise sunset message remain on the screen(in
seconds). *If you select `0` it will use the Global App Time*
selector:
number:
min: 0.0
max: 300.0
unit_of_measurement: sec
step: 1.0
mode: slider
default: 30
icon_clear_night:
name: Icon for clear-night
description: "\nThe default clear_night icon is: \n\n ![](https://developer.lametric.com/content/apps/icon_thumbs/53383_icon_thumb.gif?v=2)
- `53383`\n"
selector:
text: {}
default: w-clear-night
icon_cloudy:
name: Icon for cloudy
description: 'This is the icon ID which maps to the weather state: `cloudy`
![](https://developer.lametric.com/content/apps/icon_thumbs/53384_icon_thumb.gif?v=1)
'
selector:
text: {}
default: w-cloudy
icon_exceptional:
name: Icon for exceptional
description: 'This is the icon ID which maps to the weather state: `exceptional`
![](https://developer.lametric.com/content/apps/icon_thumbs/36637_icon_thumb.gif?v=1)
'
selector:
text: {}
default: w-exceptional
icon_fog:
name: Icon for fog
description: 'This is the icon ID which maps to the weather state: `fog`
![](https://developer.lametric.com/content/apps/icon_thumbs/17055_icon_thumb.gif?v=1)
'
selector:
text: {}
default: w-fog
icon_hail:
name: Icon for hail
description: 'This is the icon ID which maps to the weather state: `hail` (IF
YOU HAVE A BETTER ONE PLEASE LET ME KNOW)
![](https://developer.lametric.com/content/apps/icon_thumbs/53385_icon_thumb.gif?v=1)
'
selector:
text: {}
default: w-hail
icon_lightning:
name: Icon for lightning
description: 'This is the icon ID which maps to the weather state: `lightning`
![](https://developer.lametric.com/content/apps/icon_thumbs/29839_icon_thumb.gif?v=1)
'
selector:
text: {}
default: w-lightning
icon_lightning_rainy:
name: Icon for lightning-rainy
description: 'This is the icon ID which maps to the weather state: `lightning-rainy`
![](https://developer.lametric.com/content/apps/icon_thumbs/49299_icon_thumb.gif?v=4)
'
selector:
text: {}
default: w-lightning-rainy
icon_partlycloudy:
name: Icon for partlycloudy
description: "This is the icon ID which maps to the weather state: `partlycloudy`\n
\n![](https://developer.lametric.com/content/apps/icon_thumbs/2286_icon_thumb.gif?v=1)\n"
selector:
text: {}
default: w-partlycloudy
icon_pouring:
name: Icon for pouring
description: 'This is the default icon which maps to the weather state: `pouring`
![](https://developer.lametric.com/content/apps/icon_thumbs/49300_icon_thumb.gif?v=1)
'
selector:
text: {}
default: w-pouring
icon_rainy:
name: Icon for rainy
description: 'This is the default icon which maps to the weather state: `rainy`
![](https://developer.lametric.com/content/apps/icon_thumbs/2720_icon_thumb.gif?v=1)
'
selector:
text: {}
default: w-rainy
icon_snowy:
name: Icon for snowy
description: 'This is the icon ID which maps to the weather state: `snowy`
![](https://developer.lametric.com/content/apps/icon_thumbs/2289_icon_thumb.gif?v=1)
'
selector:
text: {}
default: w-snowy
icon_snowy_rainy:
name: Icon for snowy-rainy
description: 'This is the icon ID which maps to the weather state: `snowy-rainy`
![](https://developer.lametric.com/content/apps/icon_thumbs/49301_icon_thumb.gif?v=2)
'
selector:
text: {}
default: w-snowy-rainy
icon_sunny:
name: Icon for sunny
description: 'This is the icon ID which maps to the weather state: `sunny`
![](https://developer.lametric.com/content/apps/icon_thumbs/53386_icon_thumb.gif?v=1)
'
selector:
text: {}
default: w-sunny
icon_windy:
name: Icon for windy
description: 'This is the icon ID which maps to the weather state: `windy`
![](https://developer.lametric.com/content/apps/icon_thumbs/3363_icon_thumb.gif?v=1)
'
selector:
text: {}
default: w-windy
icon_windy_variant:
name: Icon for windy-variant
description: 'This is the icon ID which maps to the weather state: `windy-variant`
![](https://developer.lametric.com/content/apps/icon_thumbs/3363_icon_thumb.gif?v=1)
'
selector:
text: {}
default: w-windy-variant
icon_sunrise:
name: Icon for sunrise
description: 'This is the icon ID which maps to the `sunrise`
![](https://developer.lametric.com/content/apps/icon_thumbs/53418_icon_thumb.gif?v=1)
'
selector:
text: {}
default: w-sunrise
icon_sunset:
name: Icon for sunset
description: 'This is the icon ID which maps to the `sunset`
![](https://developer.lametric.com/content/apps/icon_thumbs/53417_icon_thumb.gif?v=1)
'
selector:
text: {}
default: w-sunset
source_url: https://raw.githubusercontent.com/jeeftor/HomeAssistant/master/blueprints/automation/awtrix_weatherflow.yaml
mode: restart
variables:
device_ids: !input awtrix
app_topic: jeef_weather
devices_topics: "{%- macro get_device_topic(device_id) %} {{ states((device_entities(device_id)
| select('search','device_topic') | list)[0]) }} {%- endmacro %}\n{%- set ns =
namespace(devices=[]) %} {%- for device_id in device_ids %}\n {%- set device=get_device_topic(device_id)|replace('
','') %}\n {% set ns.devices = ns.devices + [ device ~ '/custom/' ~ app_topic]
%}\n{%- endfor %} {{ ns.devices | reject('match','unavailable') | list}}"
forecast_var: !input forecast_var
forecast: '{{state_attr(forecast_var,''forecast'')}}'
weather: '{{states(forecast_var)}}'
hours_to_show: !input hours_to_show
moon: !input moon
moon_phase: '{{states(moon)}}'
moon_times: !input moon_rise_set
moon_rise: '{{state_attr(moon_times,''moonrise'')}}'
moon_set: '{{state_attr(moon_times,''moonset'')}}'
moon_alt: '{{state_attr(moon_times,''moon_altitude'')}}'
moon_risen: '{{moon_alt > 0}}'
when_show_moon: !input when_show_moon
show_moon: '{%- if when_show_moon == ''always'' %} True {%- elif when_show_moon
== ''never'' %} False {%- elif when_show_moon == ''risen'' %} {{moon_risen}} {%-
else %} {{state_attr(''sun.sun'', ''elevation'') < 0 and moon_risen}} {%- endif
%}'
message_duration: !input message_duration_forecast
message_duration_riseset: !input message_duration_riseset
current_temp_var: !input current_temp_var
temp_digits: !input temp_digits
temp_suffix: !input temp_suffix
current_temp: '{{states(current_temp_var)}}'
temp_text: "{%- macro round_and_set_temp(temp_var, temp_suffix, digits=0) -%} {%-
if has_value(temp_var) -%}\n {{ states(temp_var) | round(digits) ~ temp_suffix}}
\n{%- else -%} ?? {%- endif -%} {%- endmacro -%} {{ round_and_set_temp(current_temp_var,
temp_suffix, temp_digits)}}"
forecast_temp_field: !input forecast_temp_field
text_available_width: '{%- if show_moon %}16{%- else %}24{%- endif %}
'
text_len: "{%- macro get_text_len(string) %} {%- set length = namespace(value=0)
%} {%- for char in string %}\n {%- if char.isdigit() %}\n {%- set length.value
= length.value + 3 %}\n {%- elif char == '°' %}\n {%- set length.value = length.value
+ 2 %}\n {%- elif char == '.' %}\n {%- set length.value = length.value + 1
%}\n {%- elif char in ['-','C','F'] %}\n {%- set length.value = length.value
+ 3 %}\n {%- else %}\n {%- set length.value = length.value + 1 %}\n {%- endif
%}\n {%- if not loop.last %}\n {%- set length.value = length.value + 1 %}{%-
endif -%}\n{%- endfor -%} {{ length.value }} {%- endmacro %}\n{{get_text_len(temp_text)}}"
text_x: '{{8 + ((text_available_width - text_len)/2)}}'
sun_event_minute_threshold: !input sun_event_minute_threshold
sun_time_type: !input sun_time_type
sun_time_format: !input sun_time_format
icon_sunrise: !input icon_sunrise
icon_sunset: !input icon_sunset
show_sun_rise_set: !input show_sun_rise_set
sun_next_event: '{%- set rise = state_attr(''sun.sun'',''next_rising'') %} {%- set
set = state_attr(''sun.sun'',''next_setting'') %} {%- set ts_rise = rise |as_timestamp
%} {%- set ts_set = set |as_timestamp %} {{ iif(ts_set < ts_rise,''sunset'',''sunrise'')
}}'
sun_min_until_next_event: '{%- set rise = state_attr(''sun.sun'',''next_rising'')
%} {%- set set = state_attr(''sun.sun'',''next_setting'') %} {%- set ts_rise =
rise |as_timestamp %} {%- set ts_set = set |as_timestamp %} {{ iif(sun_next_event
== ''sunrise'',(ts_rise - utcnow()|as_timestamp) / 60,(ts_set - utcnow()|as_timestamp)
/ 60) | round(0) }}'
sun_next_str: "{%- set rise = state_attr('sun.sun','next_rising') %} {%- set set
= state_attr('sun.sun','next_setting') %} {%- set ts_rise = rise |as_timestamp
%} {%- set ts_set = set |as_timestamp %} {%- if sun_time_type == 'Actual' %}\n
\ {{ iif(sun_next_event == 'sunrise',(ts_rise | as_datetime | as_local).strftime(sun_time_format),
\ (ts_set | as_datetime | as_local).strftime(sun_time_format)) }}\n{%- else %}
{#- relative time #}\n {% set hours = sun_min_until_next_event // 60 %}\n {%
set remaining_minutes = sun_min_until_next_event % 60 %}\n\n {% if hours == 0
%}\n {{ remaining_minutes }} min\n {% else %}\n [\n {\"t\":\"{{hours}}\",
\"c\":\"#ffffff\"},\n {\"t\":\"h\", \"c\":\"#9c9d97\"},\n {\"t\":\"{{remaining_minutes}}\",
\"c\":\"#ffffff\"},\n {\"t\":\"m\", \"c\":\"#9c9d97\"}\n ]\n {% endif
%}\n \n{%- endif %}"
sun_event_icon: '{{ iif(sun_next_event == ''sunrise'', icon_sunrise, icon_sunset)
}}'
sun_event_payload: '{"icon":"{{sun_event_icon}}", "text":"{{sun_next_str}}", "duration":
{{message_duration_riseset}}}'
sun_payload: '{%- if show_sun_rise_set %} {{ iif(sun_event_minute_threshold >= sun_min_until_next_event,
sun_event_payload, "{}") }} {%- else %} {} {%- endif %}'
icon_clear_night: !input icon_clear_night
use_moon_clear_night: !input use_moon_clear_night
use_moon_sunny_night: !input use_moon_sunny_night
icon_cloudy: !input icon_cloudy
icon_exceptional: !input icon_exceptional
icon_fog: !input icon_fog
icon_hail: !input icon_hail
icon_lightning: !input icon_lightning
icon_lightning_rainy: !input icon_lightning_rainy
icon_partlycloudy: !input icon_partlycloudy
icon_pouring: !input icon_pouring
icon_rainy: !input icon_rainy
icon_snowy: !input icon_snowy
icon_snowy_rainy: !input icon_snowy_rainy
icon_sunny: !input icon_sunny
icon_windy: !input icon_windy
icon_windy_variant: !input icon_windy_variant
clear_night_dict: "{{ dict({\n 'full_moon': '2314',\n 'waning_gibbous': '2315',\n
\ 'last_quarter': '2316',\n 'waning_crescent': '2317',\n 'new_moon': '2318',\n
\ 'waxing_crescent': '2319',\n 'first_quarter': '2320',\n 'waxing_gibbous':
'2321'}) }}"
color_matrix_json: !input color_matrix_json
color_dict: "{% set b = color_matrix_json | from_json %} {%- set ns = namespace(tuples=[])
%} {%- for k,v in b | items -%}\n {%- set key = k|float -%}\n {%- set ns.tuples
= ns.tuples + [(key,v)] %} \n{% endfor %} {{ dict.from_keys(ns.tuples) }}"
icon_dict: '{{ dict({''clear-night'': icon_clear_night, ''cloudy'': icon_cloudy,
''exceptional'': icon_exceptional, ''fog'': icon_fog, ''hail'': icon_hail, ''lightning'':
icon_lightning, ''lightning-rainy'': icon_lightning_rainy, ''partlycloudy'': icon_partlycloudy,
''pouring'': icon_pouring, ''rainy'': icon_rainy, ''snowy'': icon_snowy, ''snowy-rainy'':
icon_snowy_rainy, ''sunny'': icon_sunny, ''windy'': icon_windy, ''windy-variant'':
icon_windy_variant})}}'
icon: "{%- if ((weather == 'clear_night') and use_moon_clear_night) %}\n {{clear_night_dict[moon_phase]}}\n{%-
elif (sun_next_event == 'sunrise') and use_moon_sunny_night and (weather == 'sunny')
-%}\n \n{%- else %}\n {{ icon_dict[weather] }}\n{%- endif %}\n"
moon_data: "{%- macro draw_moon(phase,x=22,y=0) %}\n {%- if phase == 'first_quarter'
\ %}\n {\"db\":[{{x}},{{y}},8,8,[0,0,3355443,3355443,14079702,14079702,0,0,0,3355443,3355443,3355443,15790320,14079702,14079702,0,3355443,3355443,3355443,3355443,13355979,13355979,14079702,14079702,3355443,3355443,1644825,3355443,13355979,15790320,15790320,14079702,3355443,3355443,1644825,3355443,15790320,15790320,15790320,14079702,3355443,3355443,3355443,3355443,15790320,13355979,14079702,14079702,0,3355443,3355443,3355443,15790320,14079702,14079702,0,0,0,3355443,3355443,14079702,14079702,0,0]]}\n
\ {%- endif %}\n {%- if phase == 'full_moon' %}\n {\"db\":[{{x}},{{y}},8,8,[0,0,14079702,14079702,14079702,14079702,0,0,0,14079702,14079702,15790320,15790320,14079702,14079702,0,14079702,14079702,15790320,15790320,11974326,11974326,14079702,14079702,14079702,15790320,11974326,15790320,11974326,15790320,15790320,14079702,14079702,15790320,11974326,15790320,15790320,15790320,15790320,14079702,14079702,14079702,15790320,15790320,15790320,11974326,14079702,14079702,0,14079702,14079702,11974326,15790320,14079702,14079702,0,0,0,14079702,14079702,14079702,14079702,0,0]]}\n
\ {%- endif %}\n {%- if phase == 'last_quarter' %}\n {\"db\":[{{x}},{{y}},8,8,[0,0,14079702,14079702,3487029,3487029,0,0,0,14079702,14079702,15790320,3487029,3487029,3487029,0,14079702,14079702,15790320,15790320,1907997,1907997,3487029,3487029,14079702,15790320,13553358,15790320,1907997,3487029,3487029,3487029,14079702,15790320,13553358,15790320,3487029,3487029,3487029,3487029,14079702,14079702,15790320,15790320,3487029,1907997,3487029,3487029,0,14079702,14079702,13553358,3487029,3487029,3487029,0,0,0,14079702,14079702,3487029,3487029,0,0]]}\n
\ {%- endif %}\n {%- if phase == 'new_moon' %}\n {\"db\":[{{x}},{{y}},8,8,[0,0,2763306,2763306,2763306,2763306,0,0,0,2763306,2763306,2763306,2763306,2763306,2763306,0,2763306,2763306,2763306,2763306,1842204,1842204,2763306,2763306,2763306,2763306,1842204,2763306,1842204,2763306,2763306,2763306,2763306,2763306,1842204,2763306,2763306,2763306,2763306,2763306,2763306,2763306,2763306,2763306,2763306,1842204,2763306,2763306,0,2763306,2763306,1842204,2763306,2763306,2763306,0,0,0,2763306,2763306,2763306,2763306,0,0]]}\n
\ {%- endif %}\n {%- if phase == 'waning_crescent' %}\n {\"db\":[{{x}},{{y}},8,8,[0,0,14079702,14079702,2763306,2763306,0,0,0,14079702,14079702,2763306,2763306,2763306,2763306,0,14079702,14079702,2763306,2763306,1842204,1842204,2763306,2763306,14079702,15790320,1842204,2763306,1842204,2763306,2763306,2763306,14079702,15790320,1842204,2763306,2763306,2763306,2763306,2763306,14079702,14079702,2763306,2763306,2763306,1842204,2763306,2763306,0,14079702,14079702,1842204,2763306,2763306,2763306,0,0,0,14079702,14079702,2763306,2763306,0,0]]}\n
\ {%- endif %}\n {%- if phase == 'waning_gibbous' %}\n {\"db\":[{{x}},{{y}},8,8,[0,0,14079702,14079702,3552822,3552822,0,0,0,14079702,14079702,15790320,15790320,3552822,3552822,0,14079702,14079702,15790320,15790320,13421772,13421772,3552822,3552822,14079702,15790320,13421772,15790320,13421772,15790320,3552822,3552822,14079702,15790320,13421772,15790320,15790320,15790320,3552822,3552822,14079702,14079702,15790320,15790320,15790320,13421772,3552822,3552822,0,14079702,14079702,13421772,15790320,3552822,3552822,0,0,0,14079702,14079702,3552822,3552822,0,0]]}\n
\ {%- endif %}\n {%- if phase == 'waxing_crescent' %}\n {\"db\":[{{x}},{{y}},8,8,[0,0,3355443,3355443,14079702,14079702,0,0,0,3355443,3355443,3355443,3355443,14079702,14079702,0,3355443,3355443,3355443,3355443,1644825,1644825,14079702,14079702,3355443,3355443,1644825,3355443,1644825,3355443,15790320,14079702,3355443,3355443,1644825,3355443,3355443,3355443,15790320,14079702,3355443,3355443,3355443,3355443,3355443,1644825,14079702,14079702,0,3355443,3355443,3355443,3355443,14079702,14079702,0,0,0,3355443,3355443,14079702,14079702,0,0]]}\n
\ {%- endif %}\n {%- if phase == 'waxing_gibbous' %}\n {\"db\":[{{x}},{{y}},8,8,[0,0,3355443,3355443,14079702,14079702,0,0,0,3355443,3355443,14079702,15790320,14079702,14079702,0,3355443,3355443,15790320,15790320,12763842,12763842,14079702,14079702,3355443,3355443,12763842,15790320,12763842,15790320,15790320,14079702,3355443,3355443,12763842,15790320,15790320,15790320,15790320,14079702,3355443,3355443,15790320,15790320,15790320,12763842,14079702,14079702,0,3355443,3355443,12763842,15790320,14079702,14079702,0,0,0,3355443,3355443,14079702,14079702,0,0]]}\n
\ {%- endif %}\n{%- endmacro %}\n{%- if weather == 'clear-night' and use_moon_clear_night
-%} {{draw_moon(moon_phase,0,0)}} {%- elif(sun_next_event == 'sunrise') and use_moon_sunny_night
and (weather == 'sunny') -%} {{draw_moon(moon_phase,0,0)}} {%- else -%} {{draw_moon(moon_phase,23,0)}}
{%- endif -%}\n"
payload: "{%- macro interpolate(dictionary, x) -%}\n \n {%- set sorted_keys =
dictionary|dictsort -%}\n {%- set above = sorted_keys|selectattr('0', 'gt', x)|map(attribute='0')|list|first
-%}\n {%- set below = sorted_keys|selectattr('0', 'lt', x)|map(attribute='0')|list|last
-%}\n\n {#- Key matches x exactly -#}\n {%- if above is defined and dictionary[above]
== x -%}\n {%- set value = dictionary[above] -%}\n {{ value }}\n {%- elif
below is defined and dictionary[below] == x -%}\n {%- set value = dictionary[below]
-%}\n {{ value }}\n {#- Interpolation between two values -#}\n {%- elif below
is defined and above is defined -%}\n {%- set lower_value = dictionary[below]
-%}\n {%- set upper_value = dictionary[above] -%}\n {%- set lower_rgb =
lower_value[1:] -%}\n {%- set upper_rgb = upper_value[1:] -%}\n\n {%- set
lower_r = lower_rgb[0:2]|int(base=16) -%}\n {%- set lower_g = lower_rgb[2:4]|int(base=16)
-%}\n {%- set lower_b = lower_rgb[4:6]|int(base=16) -%}\n\n {%- set upper_r
= upper_rgb[0:2]|int(base=16) -%}\n {%- set upper_g = upper_rgb[2:4]|int(base=16)
-%}\n {%- set upper_b = upper_rgb[4:6]|int(base=16) -%}\n\n {%- set interpolation_factor
= (x - below) / (above - below) -%}\n {%- set interpolated_r = ((1 - interpolation_factor)
* lower_r + interpolation_factor * upper_r)|int -%}\n {%- set interpolated_g
= ((1 - interpolation_factor) * lower_g + interpolation_factor * upper_g)|int
-%}\n {%- set interpolated_b = ((1 - interpolation_factor) * lower_b + interpolation_factor
* upper_b)|int -%}\n\n {%- set interpolated_hex = '#' ~ '%02X' % interpolated_r
~ '%02X' % interpolated_g ~ '%02X' % interpolated_b -%}\n {{ interpolated_hex
}}\n {#- Only below key available -#}\n {%- elif below is defined -%}\n {%-
set value = dictionary[below] -%}\n {{ value }}\n {#- Only above key available
-#}\n {%- elif above is defined -%}\n {%- set value = dictionary[above] -%}\n
\ {{ value }}\n {#- No matching keys available -#}\n {%- else -%}\n No
matching key found.\n {%- endif -%}\n{%- endmacro -%}\n{#- Define macro to get
length of the forecast} {%- macro str_len(str) %} {%- if '.' in str %} {%- set
char_count = (str | length) -1 %}{{char_count * 3 + 1 + char_count}} {%- else
%} {%- set char_count = (str | length) %}{{char_count * 3 + (char_count - 1)}}
{%- endif %} {%- endmacro %}\n{#- Define a macro to draw out the forecast lines#}
{%- macro draw_forecast_lines(x,hours,height) %}\n {%- for hour in range(hours)
%}\n {%- if height == 0 %}\n {\"dp\": [{{x+hour}},7,\"{{interpolate(color_dict,
forecast[hour][forecast_temp_field]) }}\"]}\n {%- else %}\n {\"dl\": [{{x+hour}},7,{{x+hour}},{{7
- height}},\"{{interpolate(color_dict, forecast[hour][forecast_temp_field]) }}\"]}\n
\ {%- endif %}\n {%- if hour+1 != hours %},{%endif%}\n {%- endfor %}\n{%-
endmacro %}\n\n{# Define the color mapping dictionary #} { \"draw\": [\n {%-
if hours_to_show > 0 %}\n {{draw_forecast_lines(8,hours_to_show,0)}}\n {%- endif
%}\n {%- if current_temp != 'unavailable' -%}\n ,{\"dt\":[{{text_x}},1,\"{{temp_text}}\",\"{{interpolate(color_dict,
current_temp | float)}}\"]}\n {%- else -%}\n {\"dt\":\"err\"}\n {%- endif -%}\n
\n {% if show_moon %}\n ,{{moon_data}}\n {% endif %}\n], \"icon\": \"{{icon}}\",
\"duration\": {{message_duration}}, \"pushIcon\": 2, \"lifetime\": 120, \"lifetimeMode\":1,
\"weather\": \"{{weather}}\" }\n"
trigger:
- platform: time_pattern
seconds: /5
- platform: state
entity_id: !input forecast_var
id: Changes
enabled: true
condition: []
action:
- repeat:
for_each: '{{ devices_topics }}'
sequence:
- service: mqtt.publish
data:
qos: 0
retain: false
topic: '{{ repeat.item }}'
payload: '{{payload}}
'
- service: mqtt.publish
data:
qos: 0
retain: false
topic: '{{ repeat.item ~ ''_sun''}} '
payload: '{{sun_payload}}
'

View File

@@ -0,0 +1,179 @@
blueprint:
source_url: https://github.com/niro1987/homeassistant-config/blob/main/blueprints/automation/niro1987/zha_ikea_tradfri_styrbar_color.yaml
name: ZHA - IKEA TRADFRI - STYRBAR - Color Lights
description: This automation simulates the use of the IKEA TRADFRI STYRBAR remote
control connected through ZHA.
domain: automation
input:
remote:
name: IKEA TRADFRI remote control
description: Select the remote control you wish to use.
selector:
device:
filter:
- integration: zha
manufacturer: IKEA of Sweden
model: Remote Control N2
multiple: false
light:
name: Light
description: Select the light entity you wish to control.
selector:
entity:
filter:
- domain:
- light
multiple: false
speed:
name: Speed
description: The speed in which to update the light when the button is held.
selector:
number:
min: 100.0
max: 1000.0
step: 100.0
unit_of_measurement: milliseconds
mode: slider
default: 500
mode: restart
max_exceeded: silent
variables:
var_light: !input light
var_speed: !input speed
trigger:
- platform: event
event_type: zha_event
event_data:
device_id: !input remote
action:
- choose:
- conditions:
- condition: template
value_template: '{{ trigger.event.data.command == "on" }}'
- condition: state
entity_id: !input light
state: 'off'
sequence:
- service: light.turn_on
target:
entity_id: !input light
data:
brightness: 254
hs_color:
- 38.222
- 52.941
transition: '{{ (var_speed / 1000)|float }}'
- conditions:
- condition: template
value_template: '{{ trigger.event.data.command == "move_with_on_off" }}'
sequence:
- repeat:
while: []
sequence:
- service: light.turn_on
target:
entity_id: !input light
data:
brightness_step_pct: 10
transition: '{{ (var_speed / 1000)|float }}'
- delay:
milliseconds: !input speed
- conditions:
- condition: template
value_template: '{{ trigger.event.data.command == "off" }}'
sequence:
- service: light.turn_off
target:
entity_id: !input light
data:
transition: '{{ (var_speed / 1000)|float }}'
- conditions:
- condition: template
value_template: '{{ trigger.event.data.command == "move" }}'
sequence:
- repeat:
while: []
sequence:
- service: light.turn_on
target:
entity_id: !input light
data:
brightness_step_pct: -10
transition: '{{ (var_speed / 1000)|float }}'
- delay:
milliseconds: !input speed
- conditions:
- condition: template
value_template: '{{ trigger.event.data.command == "press" }}'
- condition: template
value_template: '{{ trigger.event.data.args == [257,13,0] }}'
sequence:
- service: light.turn_on
target:
entity_id: !input light
data:
hs_color:
- '{{ state_attr(var_light, "hs_color")[0] }}'
- "{% if state_attr(var_light, \"hs_color\")[1] - 20 < 0 %}\n {{ state_attr(var_light,
\"hs_color\")[1] - 20 + 100 }}\n{% else %}\n {{ state_attr(var_light, \"hs_color\")[1]
- 20 }}\n{% endif %}"
transition: '{{ (var_speed / 1000)|float }}'
- conditions:
- condition: template
value_template: '{{ trigger.event.data.command == "hold" }}'
- condition: template
value_template: '{{ trigger.event.data.args == [3329,0] }}'
sequence:
- repeat:
while: []
sequence:
- service: light.turn_on
target:
entity_id: !input light
data:
hs_color:
- '{{ state_attr(var_light, "hs_color")[0] }}'
- "{% if state_attr(var_light, \"hs_color\")[1] - 10 < 0 %}\n {{ state_attr(var_light,
\"hs_color\")[1] - 10 + 100 }}\n{% else %}\n {{ state_attr(var_light,
\"hs_color\")[1] - 10 }}\n{% endif %}"
transition: '{{ (var_speed / 1000)|float }}'
- delay:
milliseconds: !input speed
- conditions:
- condition: template
value_template: '{{ trigger.event.data.command == "press" }}'
- condition: template
value_template: '{{ trigger.event.data.args == [256,13,0] }}'
sequence:
- service: light.turn_on
target:
entity_id: !input light
data:
hs_color:
- "{% if state_attr(var_light, \"hs_color\")[0] + 18 > 360 %}\n {{ state_attr(var_light,
\"hs_color\")[0] + 18 - 360 }}\n{% else %}\n {{ state_attr(var_light, \"hs_color\")[0]
+ 18 }}\n{% endif %}"
- '{{ state_attr(var_light, "hs_color")[1] }}'
transition: '{{ (var_speed / 1000)|float }}'
- conditions:
- condition: template
value_template: '{{ trigger.event.data.command == "hold" }}'
- condition: template
value_template: '{{ trigger.event.data.args == [3328,0] }}'
sequence:
- repeat:
while: []
sequence:
- service: light.turn_on
target:
entity_id: !input light
data:
hs_color:
- "{% if state_attr(var_light, \"hs_color\")[0] + 18 > 360 %}\n {{ state_attr(var_light,
\"hs_color\")[0] + 18 - 360 }}\n{% else %}\n {{ state_attr(var_light,
\"hs_color\")[0] + 18 }}\n{% endif %}"
- '{{ state_attr(var_light, "hs_color")[1] }}'
transition: '{{ (var_speed / 1000)|float }}'
- delay:
milliseconds: !input speed
default: []

View File

@@ -0,0 +1,170 @@
blueprint:
name: Awtrix current playing song
description: Shows the title and artist of your current playing song on Awtrix.
domain: automation
author: N1c093
input:
awtrix_light:
name: Awtrix Display
description: Select the target Awtrix display.
selector:
device:
model: "AWTRIX Light"
media_player:
name: Media Player Entity
description: Select your Media Player.
selector:
entity:
filter:
- domain: media_player
multiple: false
icon_in:
name: Icon
description: Enter the Icon Name or ID of the icon.
selector:
text:
default: ""
push_icon:
name: Push Icon
description: Icon behavior
selector:
select:
options:
- label: "Icon doesn't move"
value: "0"
- label: "Icon moves with text and will not appear again"
value: "1"
- label: "Icon moves with text but appears again when the text starts"
value: "2"
mode: dropdown
default: "2"
repeat_text:
name: Repeat
description: Select how how often the text should be repeated.
default: "2"
selector:
text:
text_case:
name: Text Case
description: Select how you would like your text to display.
selector:
select:
options:
- label: "Use global setting"
value: "0"
- label: "Force Uppercase"
value: "1"
- label: "Show as the media player reports it"
value: "2"
mode: dropdown
default: "0"
display_type:
name: Custom App/Notification
description: Select if you want the information as a single notification or an custom app.
selector:
select:
options:
- label: "Notification"
value: "0"
- label: "Custom App"
value: "1"
mode: dropdown
default: "1"
background_color:
name: Background Color
description: Select the Background color
selector:
color_rgb:
default: [0, 0, 0]
text_color:
name: Text Color
description: Select the Text color.
selector:
color_rgb:
default: [255, 255, 255]
show_rainbow:
name: Rainbow Colors
description: Should the notification be shown in Rainbow colors?
selector:
boolean:
default: false
mode: queued
trigger:
- platform: state
entity_id: !input media_player
attribute: media_title
from:
- platform: state
entity_id: !input media_player
from:
variables:
device_id: !input awtrix_light
awtrix_light: "{{ iif( device_attr(device_id, 'name_by_user') != none, device_attr(device_id, 'name_by_user'), device_attr(device_id, 'name') ) }}"
repeat_text: !input repeat_text
show_rainbow: !input show_rainbow
push_icon: !input push_icon
icon_in: !input icon_in
background_color: !input background_color
text_color: !input text_color
text_case: !input text_case
media_player: !input media_player
display_type: !input display_type
all_text: "{{state_attr(media_player, 'media_title')}} - {{state_attr(media_player, 'media_artist')}}"
action:
if:
- condition: state
entity_id: !input media_player
state: playing
then:
if: "{{ display_type == '1' }}"
then:
- service: mqtt.publish
data:
qos: 0
retain: false
topic: "{{awtrix_light}}/custom/mediaplayer"
payload: |-
{
"text": "{{ all_text }}",
"icon": "{{ icon_in }}",
"background": {{ background_color }},
"color": {{ text_color }},
"textCase": {{ text_case }},
"pushIcon": {{ push_icon }},
"rainbow": {{ iif(show_rainbow, "true", "false") }},
"repeat": {{ repeat_text }}
}
else:
- service: mqtt.publish
data:
qos: 0
retain: false
topic: "{{awtrix_light}}/notify"
payload: |-
{
"text": "{{ all_text }}",
"icon": "{{ icon_in }}",
"background": {{ background_color }},
"color": {{ text_color }},
"textCase": {{ text_case }},
"pushIcon": {{ push_icon }},
"rainbow": {{ iif(show_rainbow, "true", "false") }},
"repeat": {{ repeat_text }}
}
else:
- if: "{{ display_type == '1' }}"
then:
- service: mqtt.publish
data:
qos: 0
retain: false
topic: "{{awtrix_light}}/custom/mediaplayer"
payload: |-
{}

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

@@ -11,15 +11,13 @@ http:
tts:
- platform: picotts_remote
language: "de-DE"
- platform: google_translate
language: "de"
# Include modules
group: !include groups.yaml
automation: !include automations.yaml
automation webhooks: !include automations_webhooks.yaml
script: !include scripts.yaml
scene: !include scenes.yaml
notify: !include notify.yaml
# Enable additional integrations
# Enable 'wake_on_lan' intrgration
@@ -34,28 +32,11 @@ sensor: !include sensors.yaml
utility_meter: !include utility_meters.yaml
# MQTT sensors
mqtt: !include mqtt.yaml
mqtt: !include mqtt.yaml
mqtt_statestream: !include mqtt_statestream.yaml
# Template sensors
template:
- sensor:
- name: "Leckstrom"
unit_of_measurement: "W"
icon: mdi:flash
state: >
{% set total = states('sensor.netzleistung') | float %}
{% set raumduft = states('sensor.flur_raumduft_power') | float %}
{% set keller = states('sensor.keller_power') | float %}
{% set anrichte = states('sensor.kuche_anrichte_power') | float %}
{% set musik = states('sensor.kuche_musik_power') | float %}
{% set bett = states('sensor.schlafzimmer_bett_power') | float %}
{% set heimkino_sz = states('sensor.schlafzimmer_heimkino_power') | float %}
{% set deko = states('sensor.schreibtisch_deko_power') | float %}
{% set schreibtisch = states('sensor.schreibtisch_power') | float %}
{% set serverraum = states('sensor.serverraum_power') | float %}
{% set heimkino_wz = states('sensor.wohnzimmer_heimkino_power') | float %}
{% set spieleschrank = states('sensor.wohnzimmer_spieleschrank_power') | float %}
{{ (total - raumduft - keller - musik - bett - heimkino_sz - deko - schreibtisch - serverraum - heimkino_wz - spieleschrank) | round(1) }}
template: !include template.yaml
# calendar integration
calendar: !include calendars.yaml
@@ -63,34 +44,19 @@ calendar: !include calendars.yaml
# DB-recorder configuration
recorder: !include recorder.yaml
# Home Assistant InfluxDB integration
influxdb:
host: a0d7b954-influxdb
port: 8086
database: homeassistant
username: homeassistant
password: !secret influx_pass
max_retries: 3
default_measurement: state
# Enable Bluetooth
bluetooth:
# Bluetooth Low Energy tracker
device_tracker:
- platform: bluetooth_le_tracker
track_new_devices: true
# Configure MPD addon as media player
media_player:
platform: mpd
host: 192.168.122.48
scan_interval: 1
track_new_devices: false
generic_hygrostat:
- name: Badezimmer
unique_id: '3728344225387'
humidifier: fan.badezimmer_ventilator
target_sensor: sensor.badezimmer_luftfeuchtigkeit
target_sensor: sensor.bathroom_badezimmer_luftfeuchtigkeit
min_humidity: 30
max_humidity: 70
target_humidity: 50
@@ -105,4 +71,3 @@ generic_hygrostat:
away_humidity: 60
away_fixed: true
sensor_stale_duration: 00:15:00

View File

@@ -0,0 +1,76 @@
"""The Länderübergreifendes Hochwasser Portal integration."""
from __future__ import annotations
from lhpapi import HochwasserPortalAPI, LHPError
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from .const import (
CONF_ADD_UNAVAILABLE,
CONF_PEGEL_IDENTIFIER,
DOMAIN,
LOGGER,
PLATFORMS,
)
from .coordinator import HochwasserPortalCoordinator
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a config entry."""
pegel_identifier: str = entry.data[CONF_PEGEL_IDENTIFIER]
# Initialize the API and coordinator.
try:
api = await hass.async_add_executor_job(HochwasserPortalAPI, pegel_identifier)
coordinator = HochwasserPortalCoordinator(hass, api)
except LHPError as err:
LOGGER.exception("Setup of %s failed: %s", pegel_identifier, err)
return False
# No need to refresh via the following line because api runs
# update during init automatically
# await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Migrate old entry."""
LOGGER.debug(
"Migrating %s from version %s.%s",
config_entry.title,
config_entry.version,
config_entry.minor_version,
)
if config_entry.version > 1:
# This means the user has downgraded from a future version
return False
if config_entry.version == 1:
new = {**config_entry.data}
if config_entry.minor_version < 2:
new[CONF_ADD_UNAVAILABLE] = True # Behaviour as in 1.1
config_entry.minor_version = 2
hass.config_entries.async_update_entry(config_entry, data=new)
LOGGER.debug(
"Migration of %s to version %s.%s successful",
config_entry.title,
config_entry.version,
config_entry.minor_version,
)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok

View File

@@ -0,0 +1,85 @@
"""Config flow for the hochwasserportal integration."""
from __future__ import annotations
from typing import Any
from lhpapi import HochwasserPortalAPI, LHPError, get_all_stations
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow
from homeassistant.data_entry_flow import FlowResult
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
class HochwasserPortalConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle the config flow for the hochwasserportal integration."""
VERSION = 1
MINOR_VERSION = 2
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step."""
errors: dict = {}
if user_input is not None:
pegel_identifier = user_input[CONF_PEGEL_IDENTIFIER]
# Validate pegel identifier using the API
try:
api = await self.hass.async_add_executor_job(
HochwasserPortalAPI, pegel_identifier
)
LOGGER.debug(
"%s (%s): Successfully added!",
api.ident,
api.name,
)
except LHPError as err:
LOGGER.exception("Setup of %s failed: %s", pegel_identifier, err)
errors["base"] = "invalid_identifier"
if not errors:
# Set the unique ID for this config entry.
await self.async_set_unique_id(f"{DOMAIN}_{pegel_identifier.lower()}")
self._abort_if_unique_id_configured()
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(
step_id="user",
errors=errors,
data_schema=vol.Schema(
{
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,
}
),
)

View File

@@ -0,0 +1,46 @@
"""Constants for the Länderübergreifendes Hochwasser Portal integration."""
from __future__ import annotations
from datetime import timedelta
import logging
from typing import Final
from homeassistant.const import Platform
LOGGER = logging.getLogger(__package__)
DOMAIN: Final = "hochwasserportal"
CONF_PEGEL_IDENTIFIER: Final = "pegel_identifier"
CONF_ADD_UNAVAILABLE: Final = "add_unavailable"
ATTR_DATA_PROVIDERS: Final[dict[str, str]] = {
"BB": "LfU Brandenburg",
"BE": "SenMVKU Berlin",
"BW": "LUBW Baden-Württemberg",
"BY": "LfU Bayern",
"HB": "SUKW Bremen",
"HE": "HLNUG",
"HH": "LSBG Hamburg",
"MV": "LUNG Mecklenburg-Vorpommern",
"NI": "NLWKN",
"NW": "LANUV Nordrhein-Westfalen",
"RP": "Luf Rheinland-Pfalz",
"SH": "Luf Schleswig-Holstein",
"SL": "LUA Saarland",
"SN": "LfULG Sachsen",
"ST": "Land Sachsen-Anhalt",
"TH": "TLUBN",
}
ATTR_LAST_UPDATE: Final = "last_update"
ATTR_URL: Final = "url"
ATTR_HINT: Final = "hint"
LEVEL_SENSOR: Final = "level"
STAGE_SENSOR: Final = "stage"
FLOW_SENSOR: Final = "flow"
DEFAULT_SCAN_INTERVAL: Final = timedelta(minutes=15)
PLATFORMS: Final[list[Platform]] = [Platform.SENSOR]

View File

@@ -0,0 +1,32 @@
"""Data coordinator for the hochwasserportal integration."""
from __future__ import annotations
from lhpapi import HochwasserPortalAPI, LHPError
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, LOGGER
class HochwasserPortalCoordinator(DataUpdateCoordinator[None]):
"""Custom coordinator for the hochwasserportal integration."""
def __init__(self, hass: HomeAssistant, api: HochwasserPortalAPI) -> None:
"""Initialize the hochwasserportal coordinator."""
super().__init__(
hass, LOGGER, name=DOMAIN, update_interval=DEFAULT_SCAN_INTERVAL
)
self.api = api
LOGGER.debug("%s", repr(self.api))
async def _async_update_data(self) -> None:
"""Get the latest data from the hochwasserportal API."""
try:
await self.hass.async_add_executor_job(self.api.update)
LOGGER.debug("%s", repr(self.api))
except LHPError as err:
LOGGER.exception("Update of %s failed: %s", self.api.ident, err)
return False

View File

@@ -0,0 +1,14 @@
{
"domain": "hochwasserportal",
"name": "Länderübergreifendes Hochwasser Portal",
"codeowners": ["@stephan192"],
"config_flow": true,
"dependencies": [],
"documentation": "https://github.com/stephan192/hochwasserportal",
"integration_type": "device",
"iot_class": "cloud_polling",
"issue_tracker": "https://github.com/stephan192/hochwasserportal/issues",
"loggers": ["hochwasserportal"],
"requirements": ["lhpapi==1.0.5"],
"version": "1.0.3"
}

View File

@@ -0,0 +1,148 @@
"""Platform for sensor integration."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from lhpapi import HochwasserPortalAPI
from homeassistant.components.sensor import (
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import UnitOfLength
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import (
ATTR_DATA_PROVIDERS,
ATTR_HINT,
ATTR_LAST_UPDATE,
ATTR_URL,
CONF_ADD_UNAVAILABLE,
DOMAIN,
FLOW_SENSOR,
LEVEL_SENSOR,
LOGGER,
STAGE_SENSOR,
)
from .coordinator import HochwasserPortalCoordinator
@dataclass(frozen=True, kw_only=True)
class HochwasserPortalSensorEntityDescription(SensorEntityDescription):
"""Describes HochwasserPortal sensor entity."""
value_fn: Callable[[HochwasserPortalAPI], int | float | None]
available_fn: Callable[[HochwasserPortalAPI], bool]
SENSOR_TYPES: tuple[HochwasserPortalSensorEntityDescription, ...] = (
HochwasserPortalSensorEntityDescription(
key=LEVEL_SENSOR,
translation_key=LEVEL_SENSOR,
icon="mdi:waves",
native_unit_of_measurement=UnitOfLength.CENTIMETERS,
device_class=None,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda api: api.level,
available_fn=lambda api: api.level is not None,
),
HochwasserPortalSensorEntityDescription(
key=STAGE_SENSOR,
translation_key=STAGE_SENSOR,
icon="mdi:waves-arrow-up",
native_unit_of_measurement=None,
device_class=None,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda api: api.stage,
available_fn=lambda api: api.stage is not None,
),
HochwasserPortalSensorEntityDescription(
key=FLOW_SENSOR,
translation_key=FLOW_SENSOR,
icon="mdi:waves-arrow-right",
native_unit_of_measurement="m³/s",
device_class=None,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda api: api.flow,
available_fn=lambda api: api.flow is not None,
),
)
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up entities from config entry."""
coordinator: HochwasserPortalCoordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities(
[
HochwasserPortalSensor(coordinator, entry, description)
for description in SENSOR_TYPES
if description.available_fn(coordinator.api)
or entry.data.get(CONF_ADD_UNAVAILABLE, False)
]
)
class HochwasserPortalSensor(
CoordinatorEntity[HochwasserPortalCoordinator], SensorEntity
):
"""Sensor representation."""
entity_description: HochwasserPortalSensorEntityDescription
_attr_has_entity_name = True
def __init__(
self,
coordinator: HochwasserPortalCoordinator,
entry: ConfigEntry,
description: HochwasserPortalSensorEntityDescription,
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator)
self.api = coordinator.api
self.entity_description = description
self._attr_unique_id = f"{entry.unique_id}_{description.key}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, entry.entry_id)},
name=f"{entry.title}",
configuration_url=self.api.url,
manufacturer=f"{ATTR_DATA_PROVIDERS[self.api.ident[:2]]}",
model=f"{self.api.ident}",
)
self._attr_attribution = (
f"Data provided by {ATTR_DATA_PROVIDERS[self.api.ident[:2]]}"
)
LOGGER.debug("Setting up sensor: %s", self._attr_unique_id)
@property
def native_value(self):
"""Return the state of the sensor."""
return self.entity_description.value_fn(self.api)
@property
def extra_state_attributes(self):
"""Return the state attributes of the sensor."""
data = {}
if self.api.last_update is not None:
data[ATTR_LAST_UPDATE] = self.api.last_update
if self.api.url is not None:
data[ATTR_URL] = self.api.url
if self.api.hint is not None:
data[ATTR_HINT] = self.api.hint
if bool(data):
return data
return None
@property
def available(self) -> bool:
"""Could the device be accessed during the last update call."""
return self.entity_description.available_fn(self.api)

View File

@@ -0,0 +1,33 @@
{
"config": {
"step": {
"user": {
"description": "To identify the pegel, the pegel ID is required.",
"data": {
"pegel_identifier": "Pegel ID",
"add_unavailable": "Add unavailable entities anyway"
}
}
},
"error": {
"invalid_identifier": "The specified pegel identifier is invalid."
},
"abort": {
"already_configured": "Pegel is already configured.",
"invalid_identifier": "[%key:component::hochwasserportal::config::error::invalid_identifier%]"
}
},
"entity": {
"sensor": {
"level": {
"name": "Level"
},
"stage": {
"name": "Stage"
},
"flow": {
"name": "Flow"
}
}
}
}

View File

@@ -0,0 +1,33 @@
{
"config": {
"step": {
"user": {
"description": "Um den Pegel zu identifizieren, ist die Pegel-ID erforderlich.",
"data": {
"pegel_identifier": "Pegel ID",
"add_unavailable": "Nicht verfügbare Entitäten trotzdem hinzufügen"
}
}
},
"error": {
"invalid_identifier": "Der angegebene Pegel ist ungültig."
},
"abort": {
"already_configured": "Pegel bereits konfiguriert.",
"invalid_identifier": "[%key:component::hochwasserportal::config::error::invalid_identifier%]"
}
},
"entity": {
"sensor": {
"level": {
"name": "Pegelstand"
},
"stage": {
"name": "Warnstufe"
},
"flow": {
"name": "Abfluss"
}
}
}
}

View File

@@ -0,0 +1,33 @@
{
"config": {
"step": {
"user": {
"description": "To identify the pegel, the pegel ID is required.",
"data": {
"pegel_identifier": "Pegel ID",
"add_unavailable": "Add unavailable entities anyway"
}
}
},
"error": {
"invalid_identifier": "The specified pegel identifier is invalid."
},
"abort": {
"already_configured": "Pegel is already configured.",
"invalid_identifier": "[%key:component::hochwasserportal::config::error::invalid_identifier%]"
}
},
"entity": {
"sensor": {
"level": {
"name": "Level"
},
"stage": {
"name": "Stage"
},
"flow": {
"name": "Flow"
}
}
}
}

View File

@@ -0,0 +1,260 @@
"""ics Calendar for Home Assistant."""
import logging
import homeassistant.helpers.config_validation as cv
import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import (
CONF_EXCLUDE,
CONF_INCLUDE,
CONF_NAME,
CONF_PASSWORD,
CONF_PREFIX,
CONF_URL,
CONF_USERNAME,
Platform,
)
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 .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__)
PLATFORMS: list[Platform] = [Platform.CALENDAR]
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
# pylint: disable=no-value-for-parameter
vol.Optional(CONF_CALENDARS, default=[]): vol.All(
cv.ensure_list,
vol.Schema(
[
vol.Schema(
{
vol.Required(CONF_URL): vol.Url(),
vol.Required(CONF_NAME): cv.string,
vol.Optional(
CONF_INCLUDE_ALL_DAY, default=False
): cv.boolean,
vol.Optional(
CONF_USERNAME, default=""
): cv.string,
vol.Optional(
CONF_PASSWORD, default=""
): cv.string,
vol.Optional(
CONF_PARSER, default="rie"
): cv.string,
vol.Optional(
CONF_PREFIX, default=""
): cv.string,
vol.Optional(
CONF_DAYS, default=1
): cv.positive_int,
vol.Optional(
CONF_DOWNLOAD_INTERVAL, default=15
): cv.positive_int,
vol.Optional(
CONF_USER_AGENT, default=""
): cv.string,
vol.Optional(
CONF_EXCLUDE, default=""
): cv.string,
vol.Optional(
CONF_INCLUDE, default=""
): cv.string,
vol.Optional(
CONF_OFFSET_HOURS, default=0
): int,
vol.Optional(
CONF_ACCEPT_HEADER, default=""
): cv.string,
vol.Optional(
CONF_CONNECTION_TIMEOUT, default=300
): cv.positive_float,
vol.Optional(
CONF_SUMMARY_DEFAULT,
default=CONF_SUMMARY_DEFAULT_DEFAULT,
): cv.string,
}
)
]
),
)
}
)
},
extra=vol.ALLOW_EXTRA,
)
STORAGE_KEY = DOMAIN
STORAGE_VERSION_MAJOR = 1
STORAGE_VERSION_MINOR = 0
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up calendars."""
_LOGGER.debug("Setting up ics_calendar component")
hass.data.setdefault(DOMAIN, {})
if DOMAIN in config and config[DOMAIN]:
_LOGGER.debug("discovery.load_platform called")
discovery.load_platform(
hass=hass,
component=PLATFORMS[0],
platform=DOMAIN,
discovered=config[DOMAIN],
hass_config=config,
)
async_create_issue(
hass,
DOMAIN,
"deprecated_yaml_configuration",
is_fixable=False,
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
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

@@ -0,0 +1,381 @@
"""Support for ICS Calendar."""
import logging
from datetime import datetime, timedelta
from typing import Any, Optional
# import homeassistant.helpers.config_validation as cv
# import voluptuous as vol
from homeassistant.components.calendar import (
ENTITY_ID_FORMAT,
CalendarEntity,
CalendarEvent,
extract_offset,
is_offset_reached,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_EXCLUDE,
CONF_INCLUDE,
CONF_NAME,
CONF_PASSWORD,
CONF_PREFIX,
CONF_URL,
CONF_USERNAME,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import generate_entity_id
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.util.dt import now as hanow
from .calendardata import CalendarData
from .const import (
CONF_ACCEPT_HEADER,
CONF_CALENDARS,
CONF_CONNECTION_TIMEOUT,
CONF_DAYS,
CONF_DOWNLOAD_INTERVAL,
CONF_INCLUDE_ALL_DAY,
CONF_OFFSET_HOURS,
CONF_PARSER,
CONF_SET_TIMEOUT,
CONF_SUMMARY_DEFAULT,
CONF_USER_AGENT,
DOMAIN,
)
from .filter import Filter
from .getparser import GetParser
from .parserevent import ParserEvent
_LOGGER = logging.getLogger(__name__)
OFFSET = "!!"
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(
hass: HomeAssistant,
config: ConfigType,
add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
):
"""Set up ics_calendar platform.
:param hass: Home Assistant object
:type hass: HomeAssistant
:param config: Config information for the platform
:type config: ConfigType
:param add_entities: Callback to add entities to HA
:type add_entities: AddEntitiesCallback
:param discovery_info: Config information for the platform
:type discovery_info: DiscoveryInfoType | None, optional
"""
_LOGGER.debug("Setting up ics calendars")
if discovery_info is not None:
_LOGGER.debug(
"setup_platform: ignoring discovery_info, already imported!"
)
# calendars: list = discovery_info.get(CONF_CALENDARS)
calendars = []
else:
_LOGGER.debug("setup_platform: discovery_info is None")
calendars: list = config.get(CONF_CALENDARS)
calendar_devices = []
for calendar in calendars:
device_data = {
CONF_NAME: calendar.get(CONF_NAME),
CONF_URL: calendar.get(CONF_URL),
CONF_INCLUDE_ALL_DAY: calendar.get(CONF_INCLUDE_ALL_DAY),
CONF_USERNAME: calendar.get(CONF_USERNAME),
CONF_PASSWORD: calendar.get(CONF_PASSWORD),
CONF_PARSER: calendar.get(CONF_PARSER),
CONF_PREFIX: calendar.get(CONF_PREFIX),
CONF_DAYS: calendar.get(CONF_DAYS),
CONF_DOWNLOAD_INTERVAL: calendar.get(CONF_DOWNLOAD_INTERVAL),
CONF_USER_AGENT: calendar.get(CONF_USER_AGENT),
CONF_EXCLUDE: calendar.get(CONF_EXCLUDE),
CONF_INCLUDE: calendar.get(CONF_INCLUDE),
CONF_OFFSET_HOURS: calendar.get(CONF_OFFSET_HOURS),
CONF_ACCEPT_HEADER: calendar.get(CONF_ACCEPT_HEADER),
CONF_CONNECTION_TIMEOUT: calendar.get(CONF_CONNECTION_TIMEOUT),
}
device_id = f"{device_data[CONF_NAME]}"
entity_id = generate_entity_id(ENTITY_ID_FORMAT, device_id, hass=hass)
calendar_devices.append(
ICSCalendarEntity(hass, entity_id, device_data)
)
add_entities(calendar_devices)
class ICSCalendarEntity(CalendarEntity):
"""A CalendarEntity for an ICS Calendar."""
def __init__(
self,
hass: HomeAssistant,
entity_id: str,
device_data,
unique_id: str = None,
):
"""Construct ICSCalendarEntity.
:param entity_id: Entity id for the calendar
:type entity_id: str
:param device_data: dict describing the calendar
:type device_data: dict
"""
_LOGGER.debug(
"Initializing calendar: %s with URL: %s, uniqueid: %s",
device_data[CONF_NAME],
device_data[CONF_URL],
unique_id,
)
self.data = ICSCalendarData(hass, device_data)
self.entity_id = entity_id
self._attr_unique_id = f"ICSCalendar.{unique_id}"
self._event = None
self._attr_name = device_data[CONF_NAME]
self._last_call = None
@property
def event(self) -> Optional[CalendarEvent]:
"""Return the current or next upcoming event or None.
:return: The current event as a dict
:rtype: dict
"""
return self._event
@property
def should_poll(self) -> bool:
"""Indicate if the calendar should be polled.
If the last call to update or get_api_events was not within the minimum
update time, then async_schedule_update_ha_state(True) is also called.
:return: True
:rtype: boolean
"""
this_call = hanow()
if (
self._last_call is None
or (this_call - self._last_call) > MIN_TIME_BETWEEN_UPDATES
):
self._last_call = this_call
self.async_schedule_update_ha_state(True)
return True
async def async_get_events(
self, hass: HomeAssistant, start_date: datetime, end_date: datetime
) -> list[CalendarEvent]:
"""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
:type start_date: datetime
:param end_date: The last starting date to consider
:type end_date: datetime
"""
_LOGGER.debug(
"%s: async_get_events called; calling internal.", self.name
)
return await self.data.async_get_events(start_date, end_date)
async def async_update(self):
"""Get the current or next event."""
await self.data.async_update()
self._event: CalendarEvent | None = self.data.event
self._attr_extra_state_attributes = {
"offset_reached": (
is_offset_reached(
self._event.start_datetime_local, self.data.offset
)
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 to use the calendar ICS client object to get next event."""
def __init__(self, hass: HomeAssistant, device_data):
"""Set up how we are going to connect to the URL.
:param device_data Information about the calendar
"""
self.name = device_data[CONF_NAME]
self._days = device_data[CONF_DAYS]
self._offset_hours = device_data[CONF_OFFSET_HOURS]
self.include_all_day = device_data[CONF_INCLUDE_ALL_DAY]
self._summary_prefix: str = device_data[CONF_PREFIX]
self._summary_default: str = device_data[CONF_SUMMARY_DEFAULT]
self.parser = GetParser.get_parser(device_data[CONF_PARSER])
self.parser.set_filter(
Filter(device_data[CONF_EXCLUDE], device_data[CONF_INCLUDE])
)
self.offset = None
self.event = None
self._hass = hass
self._calendar_data = CalendarData(
get_async_client(hass),
_LOGGER,
{
"name": self.name,
"url": device_data[CONF_URL],
"min_update_time": timedelta(
minutes=device_data[CONF_DOWNLOAD_INTERVAL]
),
},
)
self._calendar_data.set_headers(
device_data[CONF_USERNAME],
device_data[CONF_PASSWORD],
device_data[CONF_USER_AGENT],
device_data[CONF_ACCEPT_HEADER],
)
if device_data.get(CONF_SET_TIMEOUT):
self._calendar_data.set_timeout(
device_data[CONF_CONNECTION_TIMEOUT]
)
async def async_get_events(
self, start_date: datetime, end_date: datetime
) -> list[CalendarEvent]:
"""Get all events in a specific time frame.
:param start_date: The first starting date to consider
:type start_date: datetime
:param end_date: The last starting date to consider
:type end_date: datetime
"""
event_list: list[ParserEvent] = []
if await self._calendar_data.download_calendar():
_LOGGER.debug("%s: Setting calendar content", self.name)
self.parser.set_content(self._calendar_data.get())
try:
event_list = self.parser.get_event_list(
start=start_date,
end=end_date,
include_all_day=self.include_all_day,
offset_hours=self._offset_hours,
)
except: # pylint: disable=W0702
_LOGGER.error(
"async_get_events: %s: Failed to parse ICS!",
self.name,
exc_info=True,
)
event_list: list[ParserEvent] = []
for event in event_list:
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
async def async_update(self):
"""Get the current or next event."""
_LOGGER.debug("%s: Update was called", self.name)
parser_event: ParserEvent | None = None
if await self._calendar_data.download_calendar():
_LOGGER.debug("%s: Setting calendar content", self.name)
self.parser.set_content(self._calendar_data.get())
try:
parser_event: ParserEvent | None = self.parser.get_current_event(
include_all_day=self.include_all_day,
now=hanow(),
days=self._days,
offset_hours=self._offset_hours,
)
except: # pylint: disable=W0702
_LOGGER.error(
"update: %s: Failed to parse ICS!", self.name, exc_info=True
)
if parser_event is not None:
_LOGGER.debug(
"%s: got event: %s; start: %s; end: %s; all_day: %s",
self.name,
parser_event.summary,
parser_event.start,
parser_event.end,
parser_event.all_day,
)
(summary, offset) = extract_offset(parser_event.summary, OFFSET)
parser_event.summary = self._summary_prefix + summary
if not parser_event.summary:
parser_event.summary = self._summary_default
self.offset = offset
# Invoke validation here, since it was skipped when creating the
# ParserEvent
parser_event.validate()
self.event: CalendarEvent = parser_event
return True
_LOGGER.debug("%s: No event found!", self.name)
return False

View File

@@ -0,0 +1,224 @@
"""Provide CalendarData class."""
import re
from logging import Logger
from math import floor
import httpx
import httpx_auth
from homeassistant.util.dt import now as hanow
# from urllib.error import ContentTooShortError, HTTPError, URLError
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.
The CalendarData class is used to download and cache calendar data from a
given URL. Use the get method to retrieve the data after constructing your
instance.
"""
def __init__(
self,
async_client: httpx.AsyncClient,
logger: Logger,
conf: dict,
):
"""Construct CalendarData object.
:param async_client: An httpx.AsyncClient object for requests
:type httpx.AsyncClient
:param logger: The logger for reporting problems
:type logger: Logger
:param conf: Configuration options
:type conf: dict
"""
self._auth = None
self._calendar_data = None
self._headers = []
self._last_download = None
self._min_update_time = conf["min_update_time"]
self.logger = logger
self.name = conf["name"]
self.url = conf["url"]
self.connection_timeout = None
self._httpx = async_client
async def download_calendar(self) -> bool:
"""Download the calendar data.
This only downloads data if self.min_update_time has passed since the
last download.
returns: True if data was downloaded, otherwise False.
rtype: bool
"""
self.logger.debug("%s: download_calendar start", self.name)
if (
self._calendar_data is None
or self._last_download is None
or (hanow() - self._last_download) > self._min_update_time
):
self._calendar_data = None
next_url: str = self._make_url()
self.logger.debug(
"%s: Downloading calendar data from: %s",
self.name,
next_url,
)
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
self.logger.debug("%s: download_calendar skipped download", self.name)
return False
def get(self) -> str:
"""Get the calendar data that was downloaded.
:return: The downloaded calendar data.
:rtype: str
"""
return self._calendar_data
def set_headers(
self,
user_name: str,
password: str,
user_agent: str,
accept_header: str,
):
"""Set a user agent, accept header, and/or user name and password.
The user name and password will be set into an auth object that
supports both Basic Auth and Digest Auth for httpx.
If the user_agent parameter is not "", a User-agent header will be
added to the urlopener.
:param user_name: The user name
:type user_name: str
:param password: The password
:type password: str
:param user_agent: The User Agent string to use or ""
:type user_agent: str
:param accept_header: The accept header string to use or ""
:type accept_header: str
"""
if user_name != "" and password != "":
self._auth = httpx_auth.Basic(
user_name, password
) + DigestWithMultiAuth(user_name, password)
if user_agent != "":
self._headers.append(("User-agent", user_agent))
if accept_header != "":
self._headers.append(("Accept", accept_header))
def set_timeout(self, connection_timeout: float):
"""Set the connection timeout.
:param connection_timeout: The timeout value in seconds.
:type connection_timeout: float
"""
self.connection_timeout = connection_timeout
def _decode_data(self, data):
return data.replace("\0", "")
async def _download_data(self, url): # noqa: C901
"""Download the calendar data."""
self.logger.debug("%s: _download_data start", self.name)
try:
response = await self._httpx.get(
url,
auth=self._auth,
headers=self._headers,
follow_redirects=True,
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(
"%s: Failed to open url(%s): %s",
self.name,
self.url,
http_status_error.response.status_code,
)
except httpx.TimeoutException:
self.logger.error(
"%s: Timeout opening url: %s", self.name, self.url
)
except httpx.DecodingError:
self.logger.error(
"%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
self.logger.error(
"%s: Failed to open url!", self.name, exc_info=True
)
def _make_url(self):
"""Replace templates in url and encode."""
now = hanow()
year: int = now.year
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

@@ -0,0 +1,24 @@
"""Constants for ics_calendar platform."""
VERSION = "5.1.3"
DOMAIN = "ics_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"
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

@@ -0,0 +1,125 @@
"""Provide Filter class."""
import re
from ast import literal_eval
from typing import List, Optional, Pattern
from .parserevent import ParserEvent
class Filter:
"""Filter class.
The Filter class is used to filter events according to the exclude and
include rules.
"""
def __init__(self, exclude: str, include: str):
"""Construct Filter class.
:param exclude: The exclude rules
:type exclude: str
:param include: The include rules
:type include: str
"""
self._exclude = Filter.set_rules(exclude)
self._include = Filter.set_rules(include)
@staticmethod
def set_rules(rules: str) -> List[Pattern]:
"""Set the given rules into an array which is returned.
:param rules: The rules to set
:type rules: str
:return: An array of regular expressions
:rtype: List[Pattern]
"""
arr = []
if rules != "":
for rule in literal_eval(rules):
if rule.startswith("/"):
re_flags = re.NOFLAG
[expr, flags] = rule[1:].split("/")
for flag in flags:
match flag:
case "i":
re_flags |= re.IGNORECASE
case "m":
re_flags |= re.MULTILINE
case "s":
re_flags |= re.DOTALL
arr.append(re.compile(expr, re_flags))
else:
arr.append(re.compile(rule, re.IGNORECASE))
return arr
def _is_match(
self, summary: str, description: Optional[str], regexes: List[Pattern]
) -> bool:
"""Indicate if the event matches the given list of regular expressions.
:param summary: The event summary to examine
:type summary: str
:param description: The event description summary to examine
:type description: Optional[str]
:param regexes: The regular expressions to match against
:type regexes: List[]
:return: True if the event matches the exclude filter
:rtype: bool
"""
for regex in regexes:
if regex.search(summary) or (
description and regex.search(description)
):
return True
return False
def _is_excluded(self, summary: str, description: Optional[str]) -> bool:
"""Indicate if the event should be excluded.
:param summary: The event summary to examine
:type summary: str
:param description: The event description summary to examine
:type description: Optional[str]
:return: True if the event matches the exclude filter
:rtype: bool
"""
return self._is_match(summary, description, self._exclude)
def _is_included(self, summary: str, description: Optional[str]) -> bool:
"""Indicate if the event should be included.
:param summary: The event summary to examine
:type summary: str
:param description: The event description summary to examine
:type description: Optional[str]
:return: True if the event matches the include filter
:rtype: bool
"""
return self._is_match(summary, description, self._include)
def filter(self, summary: str, description: Optional[str]) -> bool:
"""Check if the event should be included or not.
:param summary: The event summary to examine
:type summary: str
:param description: The event description summary to examine
:type description: Optional[str]
:return: true if the event should be included, otherwise false
:rtype: bool
"""
add_event = not self._is_excluded(summary, description)
if not add_event:
add_event = self._is_included(summary, description)
return add_event
def filter_event(self, event: ParserEvent) -> bool:
"""Check if the event should be included or not.
:param event: The event to examine
:type event: ParserEvent
:return: true if the event should be included, otherwise false
:rtype: bool
"""
return self.filter(event.summary, event.description)

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

@@ -0,0 +1,72 @@
"""Provide ICalendarParser class."""
from datetime import datetime
from typing import Optional
from .filter import Filter
from .parserevent import ParserEvent
class ICalendarParser:
"""Provide interface for various parser classes."""
def set_content(self, content: str):
"""Parse content into a calendar object.
This must be called at least once before get_event_list or
get_current_event.
:param content is the calendar data
:type content str
"""
def set_filter(self, filt: Filter):
"""Set a Filter object to filter events.
:param filt: The Filter object
:type exclude: Filter
"""
def get_event_list(
self,
start: datetime,
end: datetime,
include_all_day: bool,
offset_hours: int = 0,
) -> list[ParserEvent]:
"""Get a list of events.
Gets the events from start to end, including or excluding all day
events.
:param start the earliest start time of events to return
:type start datetime
:param end the latest start time of events to return
:type end datetime
:param include_all_day if true, all day events will be included.
:type include_all_day boolean
:param offset_hours the number of hours to offset the event
:type offset_hours int
:returns a list of events, or an empty list
:rtype list[ParserEvent]
"""
def get_current_event(
self,
include_all_day: bool,
now: datetime,
days: int,
offset_hours: int = 0,
) -> Optional[ParserEvent]:
"""Get the current or next event.
Gets the current event, or the next upcoming event with in the
specified number of days, if there is no current event.
:param include_all_day if true, all day events will be included.
:type include_all_day boolean
:param now the current date and time
:type now datetime
:param days the number of days to check for an upcoming event
:type days int
:param offset_hours the number of hours to offset the event
:type offset_hours int
:returns a ParserEvent or None
"""

View File

@@ -0,0 +1,13 @@
{
"domain": "ics_calendar",
"name": "ics Calendar",
"codeowners": ["@franc6"],
"config_flow": true,
"dependencies": [],
"documentation": "https://github.com/franc6/ics_calendar",
"integration_type": "service",
"iot_class": "cloud_polling",
"issue_tracker": "https://github.com/franc6/ics_calendar/issues",
"requirements": ["icalendar~=6.1","python-dateutil>=2.9.0.post0","pytz>=2024.1","recurring_ical_events~=3.5,>=3.5.2","ics==0.7.2","arrow","httpx_auth>=0.22.0,<=0.23.1"],
"version": "5.1.3"
}

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

@@ -0,0 +1 @@
"""Provide parsers."""

View File

@@ -0,0 +1,191 @@
"""Support for ics parser."""
import re
from datetime import date, datetime, timedelta
from typing import Optional, Union
from arrow import Arrow, get as arrowget
from ics import Calendar
from ..filter import Filter
from ..icalendarparser import ICalendarParser
from ..parserevent import ParserEvent
from ..utility import compare_event_dates
class ParserICS(ICalendarParser):
"""Class to provide parser using ics module."""
def __init__(self):
"""Construct ParserICS."""
self._re_method = re.compile("^METHOD:.*$", flags=re.MULTILINE)
self._calendar = None
self._filter = Filter("", "")
def set_content(self, content: str):
"""Parse content into a calendar object.
This must be called at least once before get_event_list or
get_current_event.
:param content is the calendar data
:type content str
"""
self._calendar = Calendar(re.sub(self._re_method, "", content))
def set_filter(self, filt: Filter):
"""Set a Filter object to filter events.
:param filt: The Filter object
:type exclude: Filter
"""
self._filter = filt
def get_event_list(
self, start, end, include_all_day: bool, offset_hours: int = 0
) -> list[ParserEvent]:
"""Get a list of events.
Gets the events from start to end, including or excluding all day
events.
:param start the earliest start time of events to return
:type datetime
:param end the latest start time of events to return
:type datetime
:param include_all_day if true, all day events will be included.
:type boolean
:param offset_hours the number of hours to offset the event
:type offset_hours int
:returns a list of events, or an empty list
:rtype list[ParserEvent]
"""
event_list: list[ParserEvent] = []
if self._calendar is not None:
# ics 0.8 takes datetime not Arrow objects
# ar_start = start
# ar_end = end
ar_start = arrowget(start - timedelta(hours=offset_hours))
ar_end = arrowget(end - timedelta(hours=offset_hours))
for event in self._calendar.timeline.included(ar_start, ar_end):
if event.all_day and not include_all_day:
continue
summary: str = ""
# ics 0.8 uses 'summary' reliably, older versions use 'name'
# if hasattr(event, "summary"):
# summary = event.summary
# elif hasattr(event, "name"):
summary = event.name
calendar_event: ParserEvent = ParserEvent(
summary=summary,
start=ParserICS.get_date(
event.begin, event.all_day, offset_hours
),
end=ParserICS.get_date(
event.end, event.all_day, offset_hours
),
location=event.location,
description=event.description,
)
if self._filter.filter_event(calendar_event):
event_list.append(calendar_event)
return event_list
def get_current_event( # noqa: $701
self,
include_all_day: bool,
now: datetime,
days: int,
offset_hours: int = 0,
) -> Optional[ParserEvent]:
"""Get the current or next event.
Gets the current event, or the next upcoming event with in the
specified number of days, if there is no current event.
:param include_all_day if true, all day events will be included.
:type boolean
:param now the current date and time
:type datetime
:param days the number of days to check for an upcoming event
:type int
:param offset_hours the number of hours to offset the event
:type int
:returns a ParserEvent or None
"""
if self._calendar is None:
return None
temp_event = None
now = now - timedelta(offset_hours)
end = now + timedelta(days=days)
for event in self._calendar.timeline.included(
arrowget(now), arrowget(end)
):
if event.all_day and not include_all_day:
continue
if not self._filter.filter(event.name, event.description):
continue
if temp_event is None or compare_event_dates(
now,
temp_event.end,
temp_event.begin,
temp_event.all_day,
event.end,
event.begin,
event.all_day,
):
temp_event = event
if temp_event is None:
return None
# if hasattr(event, "summary"):
# summary = temp_event.summary
# elif hasattr(event, "name"):
summary = temp_event.name
return ParserEvent(
summary=summary,
start=ParserICS.get_date(
temp_event.begin, temp_event.all_day, offset_hours
),
end=ParserICS.get_date(
temp_event.end, temp_event.all_day, offset_hours
),
location=temp_event.location,
description=temp_event.description,
)
@staticmethod
def get_date(
arw: Arrow, is_all_day: bool, offset_hours: int
) -> Union[datetime, date]:
"""Get datetime.
:param arw The arrow object representing the date.
:type Arrow
:param is_all_day If true, the returned datetime will have the time
component set to 0.
:type: bool
:param offset_hours the number of hours to offset the event
:type int
:returns The datetime.
:rtype datetime
"""
# if isinstance(arw, Arrow):
if is_all_day:
return arw.date()
# else:
# if arw.tzinfo is None or arw.tzinfo.utcoffset(arw) is None
# or is_all_day:
# arw = arw.astimezone()
# if is_all_day:
# return arw.date()
#
arw = arw.shift(hours=offset_hours)
return_value = arw.datetime
if return_value.tzinfo is None:
return_value = return_value.astimezone()
return return_value

View File

@@ -0,0 +1,199 @@
"""Support for recurring_ical_events parser."""
from datetime import date, datetime, timedelta
from typing import Optional, Union
import recurring_ical_events as rie
from icalendar import Calendar
from ..filter import Filter
from ..icalendarparser import ICalendarParser
from ..parserevent import ParserEvent
from ..utility import compare_event_dates
class ParserRIE(ICalendarParser):
"""Provide parser using recurring_ical_events."""
def __init__(self):
"""Construct ParserRIE."""
self._calendar = None
self.oneday = timedelta(days=1)
self.oneday2 = timedelta(hours=23, minutes=59, seconds=59)
self._filter = Filter("", "")
def set_content(self, content: str):
"""Parse content into a calendar object.
This must be called at least once before get_event_list or
get_current_event.
:param content is the calendar data
:type content str
"""
self._calendar = Calendar.from_ical(content)
def set_filter(self, filt: Filter):
"""Set a Filter object to filter events.
:param filt: The Filter object
:type exclude: Filter
"""
self._filter = filt
def get_event_list(
self,
start: datetime,
end: datetime,
include_all_day: bool,
offset_hours: int = 0,
) -> list[ParserEvent]:
"""Get a list of events.
Gets the events from start to end, including or excluding all day
events.
:param start the earliest start time of events to return
:type datetime
:param end the latest start time of events to return
:type datetime
:param include_all_day if true, all day events will be included.
:type boolean
:param offset_hours the number of hours to offset the event
:type offset_hours int
:returns a list of events, or an empty list
:rtype list[ParserEvent]
"""
event_list: list[ParserEvent] = []
if self._calendar is not None:
for event in rie.of(self._calendar, skip_bad_series=True).between(
start - timedelta(hours=offset_hours),
end - timedelta(hours=offset_hours),
):
start, end, all_day = self.is_all_day(event, offset_hours)
if all_day and not include_all_day:
continue
calendar_event: ParserEvent = ParserEvent(
summary=event.get("SUMMARY"),
start=start,
end=end,
location=event.get("LOCATION"),
description=event.get("DESCRIPTION"),
)
if self._filter.filter_event(calendar_event):
event_list.append(calendar_event)
return event_list
def get_current_event( # noqa: R701
self,
include_all_day: bool,
now: datetime,
days: int,
offset_hours: int = 0,
) -> Optional[ParserEvent]:
"""Get the current or next event.
Gets the current event, or the next upcoming event with in the
specified number of days, if there is no current event.
:param include_all_day if true, all day events will be included.
:type boolean
:param now the current date and time
:type datetime
:param days the number of days to check for an upcoming event
:type int
:param offset_hours the number of hours to offset the event
:type offset_hours int
:returns a ParserEvent or None
"""
if self._calendar is None:
return None
temp_event = None
temp_start: date | datetime = None
temp_end: date | datetime = None
temp_all_day: bool = None
end: datetime = now + timedelta(days=days)
for event in rie.of(self._calendar, skip_bad_series=True).between(
now - timedelta(hours=offset_hours),
end - timedelta(hours=offset_hours),
):
start, end, all_day = self.is_all_day(event, offset_hours)
if all_day and not include_all_day:
continue
if not self._filter.filter(
event.get("SUMMARY"), event.get("DESCRIPTION")
):
continue
if temp_start is None or compare_event_dates(
now, temp_end, temp_start, temp_all_day, end, start, all_day
):
temp_event = event
temp_start = start
temp_end = end
temp_all_day = all_day
if temp_event is None:
return None
return ParserEvent(
summary=temp_event.get("SUMMARY"),
start=temp_start,
end=temp_end,
location=temp_event.get("LOCATION"),
description=temp_event.get("DESCRIPTION"),
)
@staticmethod
def get_date(date_time) -> Union[datetime, date]:
"""Get datetime with timezone information.
If a date object is passed, it will first have a time component added,
set to 0.
:param date_time The date or datetime object
:type date_time datetime or date
:type: bool
:returns The datetime.
:rtype datetime
"""
# Must use type here, since a datetime is also a date!
if isinstance(date_time, date) and not isinstance(date_time, datetime):
date_time = datetime.combine(date_time, datetime.min.time())
return date_time.astimezone()
def is_all_day(self, event, offset_hours: int):
"""Determine if the event is an all day event.
Return all day status and start and end times for the event.
:param event The event to examine
:param offset_hours the number of hours to offset the event
:type offset_hours int
"""
start: datetime | date = ParserRIE.get_date(event.get("DTSTART").dt)
end: datetime | date = ParserRIE.get_date(event.get("DTEND").dt)
all_day = False
diff = event.get("DURATION")
if diff is not None:
diff = diff.dt
else:
diff = end - start
if (start == end or diff in {self.oneday, self.oneday2}) and all(
x == 0 for x in [start.hour, start.minute, start.second]
):
# if all_day, start and end must be date, not datetime!
start = start.date()
end = end.date()
all_day = True
else:
start = start + timedelta(hours=offset_hours)
end = end + timedelta(hours=offset_hours)
if start.tzinfo is None:
start = start.astimezone()
if end.tzinfo is None:
end = end.astimezone()
return start, end, all_day

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

@@ -0,0 +1,38 @@
"""Utility methods."""
from datetime import date, datetime
def make_datetime(val):
"""Ensure val is a datetime, not a date."""
if isinstance(val, date) and not isinstance(val, datetime):
return datetime.combine(val, datetime.min.time()).astimezone()
return val
def compare_event_dates( # pylint: disable=R0913,R0917
now, end2, start2, all_day2, end, start, all_day
) -> bool:
"""Determine if end2 and start2 are newer than end and start."""
# Make sure we only compare datetime values, not dates with datetimes.
# Set each date object to a datetime at midnight.
end = make_datetime(end)
end2 = make_datetime(end2)
start = make_datetime(start)
start2 = make_datetime(start2)
if all_day2 == all_day:
if end2 == end:
return start2 > start
return end2 > end and start2 >= start
if now.tzinfo is None:
now = now.astimezone()
event2_current = start2 <= now <= end2
event_current = start <= now <= end
if event_current and event2_current:
return all_day
return start2 >= start or end2 >= end

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

@@ -39,6 +39,7 @@ api:
key: !secret apikey
ota:
platform: esphome
password: !secret ota
# DHT22 sensor

View File

@@ -0,0 +1,76 @@
substitutions:
name: "bathroom"
friendly_name: "Bad"
esphome:
name: ${name}
friendly_name: ${friendly_name}
name_add_mac_suffix: false
project:
name: sensor.outdoor
version: "0.8"
min_version: 2022.1.0
esp32:
board: esp32-c3-devkitm-1
framework:
type: arduino
wifi:
ssid: "Voltage-legacy"
password: !secret voltage_legacy_psk
use_address: bathroom.home
power_save_mode: high
fast_connect: on
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "Bad 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: 6
scl: 7
scan: true
# Temp/humidity sensors
sensor:
- platform: dht
model: dht22
pin: 4
temperature:
name: "Badezimmer Temperatur DHT"
humidity:
name: "Badezimmer Luftfeuchtigkeit DHT"
update_interval: 60s
- platform: sht3xd
temperature:
name: "Badezimmer Temperatur"
filters:
- offset: -4.4
humidity:
name: "Badezimmer Luftfeuchtigkeit"
address: 0x44
heater_enabled: True
update_interval: 60s
# WiFi signal strength
- platform: wifi_signal
name: "WiFi Signalstärke"
update_interval: 60s

View File

@@ -46,6 +46,7 @@ api:
key: !secret apikey
ota:
platform: esphome
password: !secret ota
# Power key of li-ion charger
@@ -118,7 +119,7 @@ sensor:
update_interval: 60s
- platform: homeassistant
entity_id: sensor.wohnzimmer_feinstaub_pm_2_5um
entity_id: sensor.livingroom_wohnzimmer_feinstaub_pm_2_5_m
id: pm25_livingroom
- platform: homeassistant
@@ -134,7 +135,7 @@ sensor:
id: co2_livingroom
- platform: homeassistant
entity_id: sensor.feinstaub_pm_2_5um
entity_id: sensor.sleepingroom_feinstaub_pm_2_5_m
id: pm25_sleepingroom
- platform: homeassistant
@@ -150,19 +151,19 @@ sensor:
id: co2_sleepingroom
- platform: homeassistant
entity_id: sensor.temperatur_aussen
entity_id: sensor.aussentemperatur
id: temp_outdoor
- platform: homeassistant
entity_id: sensor.auriol_ahfl_1_106_h
entity_id: sensor.aussenluftfeuchtigkeit
id: humid_outdoor
- platform: homeassistant
entity_id: sensor.luftdruck
entity_id: sensor.serverroom_luftdruck
id: airpressure
- platform: homeassistant
entity_id: sensor.serveraum_temperatur
entity_id: sensor.serverroom_serveraum_temperatur
id: temp_serverroom
- platform: homeassistant
@@ -196,12 +197,14 @@ sensor:
text_sensor:
- platform: homeassistant
name: "Sun Rising ESP"
#entity_id: sensor.sun_next_rising
entity_id: sensor.sun_rising_template
id: sun_rising
internal: true
- platform: homeassistant
name: "Sun Setting ESP"
#entity_id: sensor.sun_next_setting
entity_id: sensor.sun_setting_template
id: sun_setting
internal: true
@@ -386,6 +389,7 @@ image:
id: c1024_logo
type: binary
resize: 77x40
invert_alpha: True
spi:
clk_pin: 23
@@ -437,7 +441,7 @@ display:
it.print(375, 130, id(mdi_small), TextAlign::BASELINE_CENTER, "󰖎"); // water percent icon
it.print(95, 80, id(mdi_small), TextAlign::BASELINE_LEFT, "󰐼"); // radioactive icon
if(outdoor_radiation > 0 && outdoor_radiation < 100) {
if(outdoor_radiation >= 0) {
it.printf(200, 80, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, "%2.2f", outdoor_radiation);
it.print(205, 80, id(sensor_unit), TextAlign::BASELINE_LEFT, "µS/h");
}
@@ -460,7 +464,7 @@ display:
it.print(357, 130, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, " - %");
}
if(air_pressure < 1200 && air_pressure >=800) {
if(air_pressure < 1200 && air_pressure >=700) {
it.printf(220, 130, id(big_sensor_font), TextAlign::BASELINE_RIGHT, "%4.1f", air_pressure);
it.print(225, 130, id(sensor_unit), TextAlign::BASELINE_LEFT, "hPa");
}

View File

@@ -39,6 +39,7 @@ api:
key: !secret apikey
ota:
platform: esphome
password: !secret ota
spi:

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

@@ -0,0 +1,92 @@
substitutions:
name: kamera-wohnzimmer
friendly_name: Kamera Wohnzimmer
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: esp32dev
framework:
type: arduino
wifi:
ssid: "Voltage-legacy"
password: !secret voltage_legacy_psk
use_address: cam-livingroom.home
power_save_mode: high
fast_connect: on
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "Cam Wohnzimmer 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
# Camera
esp32_camera:
name: ${friendly_name}
external_clock:
pin: GPIO0
frequency: 20MHz
i2c_pins:
sda: GPIO26
scl: GPIO27
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
sensor:
- platform: wifi_signal
name: "WiFi Signalstärke"
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

@@ -79,6 +79,7 @@ api:
# Enable over-the-air updates
ota:
platform: esphome
password: !secret ota
# Initialize I²C
@@ -199,11 +200,12 @@ sensor:
temperature:
name: "Wohnzimmer Temperatur"
accuracy_decimals: 2
filters:
- offset: -2.5
humidity:
name: "Wohnzimmer Luftfeuchtigkeit"
accuracy_decimals: 1
# SCD30 temp sensor is a bit off
temperature_offset: 0 °C
ambient_pressure_compensation: 1
automatic_self_calibration: True
address: 0x61

View File

@@ -1,29 +1,34 @@
# Source: https://github.com/esphome/wake-word-voice-assistants/blob/main/m5stack-atom-echo/m5stack-atom-echo.yaml
substitutions:
name: "m5stack-atom-echo"
friendly_name: "M5Stack Atom Echo"
name: m5stack-atom-echo
friendly_name: M5Stack Atom Echo
micro_wake_word_model: okay_nabu # alexa, hey_jarvis, hey_mycroft are also supported
esphome:
name: ${name}
friendly_name: ${friendly_name}
name_add_mac_suffix: False
project:
name: m5stack.atom-echo
version: "1.0"
min_version: 2023.5.0
friendly_name: ${friendly_name}
min_version: 2025.2.0
esp32:
board: m5stack-atom
framework:
type: arduino
type: esp-idf
# Enable Home Assistant API
api:
encryption:
key: !secret apikey
ota:
password: !secret ota
- platform: esphome
id: ota_esphome
password: !secret ota
wifi:
on_connect:
- delay: 5s # Gives time for improv results to be transmitted
ssid: Voltage-legacy
password: !secret voltage_legacy_psk
use_address: ${name}.home
@@ -36,16 +41,17 @@ wifi:
logger:
dashboard_import:
package_import_url: github://esphome/media-players/m5stack-atom-echo.yaml@main
captive_portal:
improv_serial:
button:
- platform: factory_reset
id: factory_reset_btn
name: Factory reset
i2s_audio:
i2s_lrclk_pin: GPIO33
i2s_bclk_pin: GPIO19
- id: i2s_audio_bus
i2s_lrclk_pin: GPIO33
i2s_bclk_pin: GPIO19
microphone:
- platform: i2s_audio
@@ -54,95 +60,301 @@ microphone:
adc_type: external
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:
id: va
microphone: echo_microphone
speaker: atom_echo_speaker
on_start:
media_player: echo_media_player
noise_suppression_level: 2
auto_gain: 31dBFS
volume_multiplier: 2.0
on_listening:
- light.turn_on:
id: led
blue: 100%
red: 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:
- light.turn_on:
id: led
blue: 0%
blue: 100%
red: 0%
green: 100%
green: 0%
brightness: 100%
effect: none
on_tts_end:
- light.turn_on:
id: led
blue: 0%
red: 0%
green: 100%
effect: pulse
on_end:
- delay: 1s
- wait_until:
not:
speaker.is_playing: atom_echo_speaker
- light.turn_off: led
- delay: 100ms
- script.execute: start_wake_word
on_error:
- light.turn_on:
id: led
blue: 0%
red: 100%
green: 0%
blue: 0%
brightness: 100%
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
- switch.turn_off: timer_ringing
binary_sensor:
# button does the following:
# short click - stop a timer
# if no timer then restart either microwakeword or voice assistant continuous
- platform: gpio
pin:
number: GPIO39
inverted: true
name: Button
disabled_by_default: true
entity_category: diagnostic
id: echo_button
on_multi_click:
- timing:
- ON FOR AT MOST 350ms
- OFF FOR AT LEAST 10ms
- ON for at least 50ms
- OFF for at least 50ms
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:
- ON FOR AT LEAST 350ms
- ON for at least 10s
then:
- voice_assistant.start:
- 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
speaker:
- platform: i2s_audio
id: atom_echo_speaker
dac_type: external
i2s_dout_pin: GPIO22
mode: mono
- button.press: factory_reset_btn
light:
- platform: esp32_rmt_led_strip
id: led
name: None
disabled_by_default: true
entity_category: config
pin: GPIO27
default_transition_length: 0s
chipset: SK6812
num_leds: 1
rgb_order: grb
rmt_channel: 0
effects:
- pulse:
name: "Slow Pulse"
transition_length: 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,13 +1,32 @@
# Source: https://github.com/RASPIAUDIO/esphomeLuxe/blob/main/luxe_microWW.yaml
substitutions:
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
api:
encryption:
key: !secret apikey
ota:
password: !secret ota
- platform: esphome
id: ota_esphome
password: !secret ota
external_components:
- source: github://RASPIAUDIO/esphomeLuxe@main
# - source:
# type: local
# path: components
components: [es8388]
refresh: 0s
wifi:
ssid: Voltage-legacy
@@ -23,130 +42,175 @@ wifi:
esphome:
name: ${name}
friendly_name: ${friendly_name}
min_version: 2025.2.0
name_add_mac_suffix: false
project:
name: raspiaudio.muse-luxe
version: "1.0"
min_version: 2023.5.0
platformio_options:
board_build.flash_mode: dio
board_build.arduino.memory_type: qio_opi
on_boot:
priority: -100.0
then:
- media_player.volume_set:
id: luxe_out
volume: 50%
- lambda: id(phase) = 0;
- script.execute: update_led
esp32:
board: esp-wrover-kit
board: esp-wrover-kit
flash_size: 4MB
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:
level: DEBUG
captive_portal:
improv_serial:
##########
# Hardware Configuration
es8388:
id: my_es8388
psram:
mode: quad
speed: 80MHz
#######
# Buses Configuration
i2c:
sda: GPIO18
scl: GPIO23
dashboard_import:
package_import_url: github://esphome/media-players/raspiaudio-muse-luxe.yaml@main
captive_portal:
improv_serial:
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:
#####################
# Internal Components
output:
- platform: gpio
id: dac_mute
pin:
number: GPIO21
inverted: true
mode:
output: true
speaker:
- platform: i2s_audio
id: luxe_out_speaker
dac_type: external
i2s_dout_pin: GPIO26
mode: stereo
globals:
- id: Vol
type: float
initial_value: '0.6'
- id: phase
type: int
initial_value: '0'
- id: mute
type: bool
initial_value: 'false'
- id: muteH
type: bool
initial_value: 'false'
microphone:
- platform: i2s_audio
id: luxe_microphone
i2s_din_pin: GPIO35
adc_type: external
pdm: false
interval:
- interval: 0.1sec
then:
- if:
condition:
- speaker.is_stopped:
then:
- if:
condition:
- not:
- lambda: 'return(id(muteH));'
then:
- output.turn_on: dac_mute
- lambda: id(muteH) = true;
- logger.log: "====> hardware mute"
voice_assistant:
microphone: luxe_microphone
speaker: luxe_out_speaker
on_start:
- light.turn_on:
id: top_led
blue: 100%
red: 0%
green: 0%
effect: none
on_tts_start:
- light.turn_on:
id: top_led
blue: 60%
red: 20%
green: 20%
effect: none
on_tts_end:
- light.turn_on:
id: top_led
blue: 60%
red: 20%
green: 20%
effect: pulse
on_end:
- delay: 1s
- wait_until:
not:
speaker.is_playing: luxe_out_speaker
- 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
- if:
condition:
- speaker.is_playing:
then:
- if:
condition:
- lambda: 'return(id(muteH));'
then:
- output.turn_off: dac_mute
- lambda: id(muteH) = false;
- logger.log: "====> hardware unmute"
sensor:
- platform: adc
pin: GPIO33
name: Battery
icon: "mdi:battery-outline"
name: Battery voltage
device_class: voltage
unit_of_measurement: "V"
accuracy_decimals: 2
state_class: measurement
entity_category: diagnostic
unit_of_measurement: V
update_interval: 15s
accuracy_decimals: 3
attenuation: 11db
raw: true
attenuation: auto
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:
alpha: 0.2
send_every: 2
- 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:
- platform: gpio
@@ -158,7 +222,13 @@ binary_sensor:
pullup: true
name: Volume Up
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
pin:
number: GPIO32
@@ -168,41 +238,250 @@ binary_sensor:
pullup: true
name: Volume Down
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
pin:
number: GPIO12
inverted: true
mode:
input: true
pullup: true
name: Action
on_multi_click:
- timing:
- ON FOR AT MOST 350ms
- OFF FOR AT LEAST 10ms
then:
- media_player.toggle: luxe_out
- timing:
- ON FOR AT LEAST 350ms
then:
- voice_assistant.start:
- timing:
- ON FOR AT LEAST 350ms
- OFF FOR AT LEAST 10ms
then:
- voice_assistant.stop:
pullup: true
name: Mute
on_click:
- if:
condition:
- lambda: 'return(id(mute));'
then:
- script.execute: mute_off
- lambda: id(mute) = false;
else:
- script.execute: mute_on
- lambda: id(mute) = true;
on_double_click:
- if:
condition:
- lambda: 'return(id(phase) == 2);'
then:
- media_player.stop:
light:
- platform: fastled_clockless
- platform: esp32_rmt_led_strip
name: None
id: top_led
pin: GPIO22
chipset: SK6812
chipset: WS2812
num_leds: 1
rgb_order: grb
# rmt_channel: 0
default_transition_length: 0s
gamma_correct: 2.8
effects:
- pulse:
name: pulse
transition_length: 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

@@ -0,0 +1,506 @@
substitutions:
name: "riden-labornetzteil-18a"
friendly_name: "Riden Labornetzteil 18A"
# Change this model to fit your particular one.
# You can find it in Home Assistant as the device diagnostic "Model Name".
model: "RD6018"
device_name: "rd6018-controller"
device_friendly_name: "Riden RD6018"
device_description: "Monitor and control a RD6018 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-18a.home
power_save_mode: high
fast_connect: on
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "RD6018 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"

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"

View File

@@ -1,8 +1,20 @@
esphome:
name: serverroom
substitutions:
name: "serverroom"
friendly_name: "Serverraum Luft"
platform: ESP32
board: nodemcu-32s
esphome:
name: ${name}
friendly_name: ${friendly_name}
name_add_mac_suffix: false
project:
name: sensor.outdoor
version: "0.8"
min_version: 2022.1.0
esp32:
board: esp32-c3-devkitm-1
framework:
type: arduino
wifi:
ssid: "Voltage-legacy"
@@ -23,23 +35,23 @@ logger:
# Enable Home Assistant API
api:
password: !secret api
encryption:
key: !secret apikey
ota:
platform: esphome
password: !secret ota
# Initialize I²C
i2c:
- id: bus_a
sda: 32
scl: 25
sda: 4
scl: 3
scan: true
# DHT22 sensor
# BMP280 sensor
sensor:
- platform: bmp280
- platform: bmp280_i2c
i2c_id: bus_a
temperature:
name: "Serveraum Temperatur"

View File

@@ -78,6 +78,7 @@ api:
# Enable over-the-air updates
ota:
platform: esphome
password: !secret ota
# Initialize I²C
@@ -198,11 +199,12 @@ sensor:
temperature:
name: "Schlafzimmer Temperatur"
accuracy_decimals: 2
filters:
- offset: -2.5
humidity:
name: "Schlafzimmer Luftfeuchtigkeit"
accuracy_decimals: 1
# SCD30 temp sensor is a bit off
temperature_offset: 0 °C
ambient_pressure_compensation: 1
automatic_self_calibration: True
address: 0x61

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

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: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 623 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: 373 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

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

268
mqtt.yaml
View File

@@ -1,44 +1,13 @@
sensor:
# MQTT bathroom sensors
- name: "Badezimmer Temperatur"
unique_id: bathroom.temperature
state_topic: "home/bathroom/climate/sensor/temperature"
device_class: 'temperature'
state_class: 'measurement'
unit_of_measurement: '°C'
last_reset_topic: "home/bathroom/climate/sensor/temperature"
last_reset_value_template: '1970-01-01T00:00:00+00:00'
device:
identifiers: MagicMirror-DHT22-T
name: MagicMirror-DHT22-T
model: DHT22
manufacturer: mqtt-io
force_update: true
- name: "Badezimmer Luftfeuchtigkeit"
unique_id: bathroom.humidity
state_topic: "home/bathroom/climate/sensor/humidity"
device_class: 'humidity'
state_class: 'measurement'
unit_of_measurement: '%'
last_reset_topic: "home/bathroom/climate/sensor/humidity"
last_reset_value_template: '1970-01-01T00:00:00+00:00'
device:
identifiers: MagicMirror-DHT22-H
name: MagicMirror-DHT22-H
model: DHT22
manufacturer: mqtt-io
force_update: true
# MQTT/RTL_433 (outdoor) sensors
# Reliable sensor with good placement (good values) and good reception
- device_class: battery
name: Auriol-AHFL-1-106-B
name: Batterie
unit_of_measurement: '%'
value_template: '{{ float(value) * 99 + 1 }}'
state_class: measurement
entity_category: diagnostic
state_topic: rtl_433/sdr/devices/Auriol-AHFL/1/106/battery_ok
state_topic: rtl_433/sdr/devices/Auriol-AHFL/1/216/battery_ok
unique_id: Auriol-AHFL-1-106-B
device:
identifiers: Auriol-AHFL-1-106
@@ -47,11 +16,11 @@
manufacturer: rtl_433
- device_class: temperature
name: Auriol-AHFL-1-106-T
name: Temperatur
unit_of_measurement: °C
value_template: '{{ value|float }}'
state_class: measurement
state_topic: rtl_433/sdr/devices/Auriol-AHFL/1/106/temperature_C
state_topic: rtl_433/sdr/devices/Auriol-AHFL/1/216/temperature_C
unique_id: Auriol-AHFL-1-106-T
device:
identifiers: Auriol-AHFL-1-106
@@ -60,11 +29,11 @@
manufacturer: rtl_433
- device_class: humidity
name: Auriol-AHFL-1-106-H
name: Luftfeuchtigkeit
unit_of_measurement: '%'
value_template: '{{ value|float }}'
state_class: measurement
state_topic: rtl_433/sdr/devices/Auriol-AHFL/1/106/humidity
state_topic: rtl_433/sdr/devices/Auriol-AHFL/1/216/humidity
unique_id: Auriol-AHFL-1-106-H
device:
identifiers: Auriol-AHFL-1-106
@@ -72,98 +41,197 @@
model: Auriol-AHFL
manufacturer: rtl_433
# Only temperature
- device_class: temperature
name: AmbientWeather-TX8300-1-29-T
unit_of_measurement: °C
value_template: '{{ value|float }}'
state_class: measurement
state_topic: rtl_433/sdr/devices/AmbientWeather-TX8300/1/29/temperature_C
unique_id: AmbientWeather-TX8300-1-29-T
device:
identifiers: AmbientWeather-TX8300-1-29
name: AmbientWeather-TX8300-1-29
model: AmbientWeather-TX8300
manufacturer: rtl_433
# fair T/H/Bat
- device_class: battery
name: inFactory-TH-1-129-B
name: Batterie
unit_of_measurement: '%'
value_template: '{{ float(value) * 99 + 1 }}'
state_class: measurement
entity_category: diagnostic
state_topic: rtl_433/sdr/devices/inFactory-TH/1/129/battery_ok
unique_id: inFactory-TH-1-129-B
state_topic: rtl_433/sdr/devices/Vauno-EN8822C/1/216/battery_ok
unique_id: Vauno-EN8822C-1-244-B
device:
identifiers: inFactory-TH-1-129
name: inFactory-TH-1-129
model: inFactory-TH
identifiers: Vauno-EN8822C-1-244
name: Vauno-EN8822C-1-244
model: Vauno-EN8822C
manufacturer: rtl_433
- device_class: temperature
name: inFactory-TH-1-129-F
name: Temperatur
unit_of_measurement: °C
value_template: '{{ value|float }}'
state_class: measurement
state_topic: rtl_433/sdr/devices/Vauno-EN8822C/1/244/temperature_C
unique_id: Vauno-EN8822C-1-244-T
device:
identifiers: Vauno-EN8822C-1-244
name: Vauno-EN8822C-1-244
model: Vauno-EN8822C
manufacturer: rtl_433
- device_class: humidity
name: Luftfeuchtigkeit
unit_of_measurement: '%'
value_template: '{{ value|float }}'
state_class: measurement
state_topic: rtl_433/sdr/devices/Vauno-EN8822C/1/244/humidity
unique_id: Vauno-EN8822C-1-244-H
device:
identifiers: Vauno-EN8822C-1-244
name: Vauno-EN8822C-1-244
model: Vauno-EN8822C
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 }}'
value_template: '{{ value|float|round(1) }}'
state_class: measurement
state_topic: rtl_433/sdr/devices/inFactory-TH/1/129/temperature_F
unique_id: inFactory-TH-1-129-F
state_topic: rtl_433/sdr/devices/Cotech-367959/130/temperature_F
unique_id: Cotech-367959-130-F
device:
identifiers: inFactory-TH-1-129
name: inFactory-TH-1-129
model: inFactory-TH
identifiers:
- Cotech-367959-130
model: Cotech-367959
manufacturer: rtl_433
name: Cotech-367959-130
name: Temperature
- device_class: humidity
name: inFactory-TH-1-129-H
unit_of_measurement: '%'
value_template: '{{ value|float }}'
state_class: measurement
state_topic: rtl_433/sdr/devices/inFactory-TH/1/129/humidity
unique_id: inFactory-TH-1-129-H
state_topic: rtl_433/sdr/devices/Cotech-367959/130/humidity
unique_id: Cotech-367959-130-H
device:
identifiers: inFactory-TH-1-129
name: inFactory-TH-1-129
model: inFactory-TH
identifiers:
- Cotech-367959-130
model: Cotech-367959
manufacturer: rtl_433
name: Cotech-367959-130
name: Humidity
# weak, good T/H/Bat
- device_class: battery
name: Nexus-TH-1-224-B
unit_of_measurement: '%'
value_template: '{{ float(value) * 99 + 1 }}'
state_class: measurement
entity_category: diagnostic
state_topic: rtl_433/sdr/devices/Nexus-TH/1/224/battery_ok
unique_id: Nexus-TH-1-224-B
- 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: Nexus-TH-1-224
name: Nexus-TH-1-224
model: Nexus-TH
identifiers:
- Cotech-367959-130
model: Cotech-367959
manufacturer: rtl_433
name: Cotech-367959-130
name: Rain Total
- device_class: temperature
name: Nexus-TH-1-224-T
unit_of_measurement: °C
- unit_of_measurement: °
value_template: '{{ value|float }}'
state_class: measurement
state_topic: rtl_433/sdr/devices/Nexus-TH/1/224/temperature_C
unique_id: Nexus-TH-1-224-T
state_topic: rtl_433/sdr/devices/Cotech-367959/130/wind_dir_deg
unique_id: Cotech-367959-130-WD
device:
identifiers: Nexus-TH-1-224
name: Nexus-TH-1-224
model: Nexus-TH
identifiers:
- Cotech-367959-130
model: Cotech-367959
manufacturer: rtl_433
name: Cotech-367959-130
name: Wind Direction
- device_class: humidity
name: Nexus-TH-1-224-H
unit_of_measurement: '%'
value_template: '{{ value|float }}'
- 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/Nexus-TH/1/224/humidity
unique_id: Nexus-TH-1-224-H
state_topic: rtl_433/sdr/devices/Cotech-367959/130/wind_avg_m_s
unique_id: Cotech-367959-130-WS
device:
identifiers: Nexus-TH-1-224
name: Nexus-TH-1-224
model: Nexus-TH
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

@@ -1,10 +1,16 @@
db_url: !secret ha_recorder_dburl
# Commit to db only every X seconds
commit_interval: 10
# Delete events and states older than 2 weeks
commit_interval: 60
# Delete events and states older than 1 week
auto_purge: true
purge_keep_days: 7
# include:
# entity_globs:
# - sensor.rd6018_controller_*
# entities:
# - sensor.line_power_channel_a_voltage
# - sensor.line_power_channel_b_voltage
# - sensor.line_power_channel_c_voltage
# domains:
# - sensor
# - switch
@@ -16,20 +22,20 @@
entity_globs:
- weather.zuhause_*
- sensor.*_power_factor
- sensor.*_current
- sensor.*_voltage
- sensor.dwd*
- binary_sensor.*firmware_update
- binary_sensor.*_overpowering
- binary_sensor.*_overheating
- sensor.*_wi_fi_signal
- sensor.*_energy_returned
- sensor.*_wifi_strenght
- sensor.*_uptime
- sensor.sun*
entities:
- sun.sun # Don't record sun data
- sensor.fritzbox_device_uptime
- sensor.snmp_wan_in
- sensor.snmp_wan_out
- sensor.time
- sensor.awtrix_kitchen_current_app
- sensor.awtrix_desk_current_app
# event_types:
# - call_service # Don't record service calls

File diff suppressed because it is too large Load Diff

View File

@@ -45,3 +45,72 @@ moodlight_neutral:
transition: 2
mode: single
icon: mdi:lightbulb-on
moodlight_orange_plasma:
alias: Moodlight Orange Plasma
sequence:
- service: light.turn_on
data:
brightness_pct: 20
transition: 2
effect: Lake
target:
device_id:
- 6dcbd87b459412144bddc42af3ae8b83
- 4edd9b9df7d1f6f2fe7dcc2e5c0eb968
- c64e7c3dcda7f1c23e456959f2c60f39
- service: select.select_option
target:
entity_id: select.wohnzimmer_hinten_color_palette, select.wohnzimmer_vorne_color_palette,
select.kuche_color_palette
data:
option: Orangery
- condition: state
state: 'on'
entity_id: media_player.lg_webos_smart_tv
- service: light.turn_off
target:
device_id: 6dcbd87b459412144bddc42af3ae8b83
data:
transition: 2
mode: single
icon: mdi:lightbulb-on
moodlight_xmas:
alias: Moodlight XMas
sequence:
- data:
brightness_pct: 20
transition: 2
effect: Glitter
target:
device_id:
- 6dcbd87b459412144bddc42af3ae8b83
- 4edd9b9df7d1f6f2fe7dcc2e5c0eb968
- c64e7c3dcda7f1c23e456959f2c60f39
action: light.turn_on
- target:
entity_id: select.wohnzimmer_hinten_color_palette, select.wohnzimmer_vorne_color_palette,
select.kuche_color_palette
data:
option: Orangery
action: select.select_option
- condition: state
state: 'on'
entity_id: media_player.lg_webos_smart_tv
- target:
device_id: 6dcbd87b459412144bddc42af3ae8b83
data:
transition: 2
action: light.turn_off
mode: single
icon: mdi:lightbulb-on
wled_wohnzimmer_nachster_effekt:
alias: 'WLED: Wohnzimmer nächster Effekt'
sequence:
- service: light.turn_on
target:
entity_id: light.wohnzimmer_hinten, light.wohnzimmer_vorne
data:
effect: '{{ state_attr(''light.wohnzimmer_hinten'', ''effect_list'') | random
}}'
mode: single
icon: mdi:firework

View File

@@ -3,16 +3,6 @@
display_options:
- 'time'
- 'date'
# Raspberry Pi CPU temp
- platform: command_line
name: "CPU Temp"
command: "cat /sys/class/thermal/thermal_zone0/temp"
unit_of_measurement: "°C"
value_template: "{{ value | multiply(0.001) | round(1) }}"
# DWD weather warnings
- platform: dwd_weather_warnings
name: "DWD Unwetterwarnungen"
region_name: "Münster-Süd"
# Database size sensor
- platform: sql
db_url: !secret ha_recorder_dburl
@@ -25,14 +15,20 @@
# SNMP (Juniper) router traffic sensor
- platform: snmp
name: snmp_wan_in
unique_id: '3303381540758'
host: !secret router_ip
community: !secret router_community
baseoid: .1.3.6.1.2.1.2.2.1.10.511
baseoid: .1.3.6.1.2.1.31.1.1.1.6.511
version: 2c
unit_of_measurement: "Octets"
- platform: snmp
name: snmp_wan_out
unique_id: '1573258703922'
host: !secret router_ip
community: !secret router_community
baseoid: .1.3.6.1.2.1.2.2.1.16.511
baseoid: .1.3.6.1.2.1.31.1.1.1.10.511
version: 2c
unit_of_measurement: "Octets"
- platform: derivative
source: sensor.snmp_wan_in
@@ -45,24 +41,15 @@
unit: B
name: wan_out_derivative
- platform: template
sensors:
internet_speed_in:
friendly_name: 'Internet Speed IN'
value_template: "{{ (( states('sensor.wan_in_derivative') | float * 8 / 1000000 ) | round(2)) }}"
unit_of_measurement: 'Mbps'
internet_speed_out:
friendly_name: 'Internet Speed OUT'
value_template: "{{ (( states('sensor.wan_out_derivative') | float * 8 / 1000000 ) | round(2)) }}"
unit_of_measurement: 'Mbps'
- platform: statistics
name: 'WAN Traffic In'
unique_id: '9081721471264'
state_characteristic: mean
entity_id: sensor.internet_speed_in
sampling_size: 10
- platform: statistics
name: 'WAN Traffic Out'
unique_id: '8688955223027'
state_characteristic: mean
entity_id: sensor.internet_speed_out
sampling_size: 10
@@ -71,11 +58,39 @@
sensors:
sun_rising_template:
friendly_name: "Sun Rising Template"
unique_id: '0680294616247'
value_template: "{{ as_timestamp(states.sun.sun.attributes.next_rising) | timestamp_custom ('%H:%M') }}"
- platform: template
sensors:
sun_setting_template:
friendly_name: "Sun Setting Template"
unique_id: '8298170865533'
value_template: "{{ as_timestamp(states.sun.sun.attributes.next_setting) | timestamp_custom ('%H:%M') }}"
# Sensor for Riemann sum of energy import (W -> Wh)
- platform: integration
source: sensor.power_import
name: energy_import_sum
unique_id: '6355740355352'
unit_prefix: k
round: 2
method: left
# Sensor for Riemann sum of energy export (W -> Wh)
- platform: integration
source: sensor.power_export
name: energy_export_sum
unique_id: '6978829126367'
unit_prefix: k
round: 2
method: left
# Sensor for Riemann sum of energy consumption (W -> Wh)
- platform: integration
source: sensor.power_consumption
name: energy_consumption_sum
unique_id: '8749045190416'
unit_prefix: k
round: 2
method: left

135
template.yaml Normal file
View File

@@ -0,0 +1,135 @@
# - select:
# - name: "Wohnzimmer Effekt"
# unique_id: '6641823075755'
# state: "{{ state_attr('light.wohnzimmer_hinten', 'effect') }}"
# icon: mdi:firework
# options: >
# {{ state_attr('light.wohnzimmer_hinten', 'effect_list') }}
# select_option:
# - service: light.turn_on
# target:
# entity_id: light.wohnzimmer_hinten, light.wohnzimmer_vorne
# data:
# effect: "{{ option }}"
- select:
- name: "Wohnzimmer Palette"
unique_id: '3107042775387'
state: "{{ states('select.wohnzimmer_hinten_color_palette') }}"
icon: mdi:palette
options: >
{{ state_attr('select.wohnzimmer_hinten_color_palette', 'options') }}
select_option:
- service: select.select_option
target:
entity_id: select.wohnzimmer_hinten_color_palette, select.wohnzimmer_vorne_color_palette
data:
option: '{{ option }}'
- select:
- name: "Available Media Players"
unique_id: '6284128947660'
state: "{{ states('input_text.selected_media_player') }}"
options: >
{{ states.media_player
| rejectattr('state', 'in', ['off', 'idle', 'unavailable', 'unknown'])
| map(attribute ='entity_id') | list }}
select_option:
- service: input_text.set_value
target:
entity_id: input_text.selected_media_player
data:
value: "{{ option }}"
- sensor:
- name: "power_other"
unique_id: '5579422933393'
unit_of_measurement: "W"
icon: mdi:flash
state: >
{% set total = states('sensor.netzleistung') | float %}
{% set solar = states('sensor.balkonkraftwerk_power') | float %}
{% set raumduft = states('sensor.flur_raumduft_power') | float %}
{% set keller = states('sensor.keller_power') | float %}
{% set anrichte = states('sensor.kuche_anrichte_power') | float %}
{% set musik = states('sensor.kuche_musik_power') | float %}
{% set bett = states('sensor.schlafzimmer_bett_power') | float %}
{% set heimkino_sz = states('sensor.schlafzimmer_heimkino_power') | float %}
{% set deko = states('sensor.schreibtisch_deko_power') | float %}
{% set schreibtisch = states('sensor.schreibtisch_power') | float %}
{% set serverraum = states('sensor.serverraum_power') | float %}
{% set heimkino_wz = states('sensor.wohnzimmer_heimkino_power') | float %}
{% set spieleschrank = states('sensor.wohnzimmer_spieleschrank_power') | float %}
{% set kuehlschrank = states('sensor.tz3000_ww6drja5_ts011f_leistung') | float %}
{% set kaffeemaschine = states('sensor.kaffeemaschine_leistung_2') | float %}
{% set waeschetrockner = states('sensor.waschetrockner_leistung') | float %}
{% set waschmaschine = states('sensor.waschmaschine_leistung') | float %}
{% set arcade = states('sensor.arcade_automat_leistung') | float %}
{{ (total + solar - raumduft - keller - musik - bett - heimkino_sz - deko - schreibtisch - serverraum - heimkino_wz - spieleschrank - kuehlschrank - kaffeemaschine - waeschetrockner - waschmaschine - arcade) | round(1) }}
device_class: power
state_class: measurement
attributes:
last_reset: '1970-01-01T00:00:00+00:00'
# Shelly 3EM cumulative sensors (incl. PV)
- sensor:
# Template sensor for values of power import (active_power > 0)
- name: power_import
unique_id: '2385816278013'
unit_of_measurement: 'W'
state: >
{% if (states('sensor.line_power_channel_a_power')|float + states('sensor.line_power_channel_b_power')|float + states('sensor.line_power_channel_c_power')|float) > 0 %}
{{ (states('sensor.line_power_channel_a_power')|float + states('sensor.line_power_channel_b_power')|float + states('sensor.line_power_channel_c_power')|float)|round(1) }}
{% else %}
{{ 0 }}
{% endif %}
device_class: power
state_class: measurement
icon: mdi:transmission-tower-export
attributes:
last_reset: '1970-01-01T00:00:00+00:00'
# Template sensor for values of power export (active_power < 0)
- name: power_export
unique_id: '9143524256421'
unit_of_measurement: 'W'
state: >
{% if (states('sensor.line_power_channel_a_power')|float + states('sensor.line_power_channel_b_power')|float + states('sensor.line_power_channel_c_power')|float) < 0 %}
{{ ((states('sensor.line_power_channel_a_power')|float + states('sensor.line_power_channel_b_power')|float + states('sensor.line_power_channel_c_power')|float) * -1 ) | round(1) }}
{% else %}
{{ 0 }}
{% endif %}
device_class: power
state_class: measurement
icon: mdi:transmission-tower-import
attributes:
last_reset: '1970-01-01T00:00:00+00:00'
# Template sensor for values of power consumption
- name: power_consumption
unique_id: '3502047649408'
unit_of_measurement: 'W'
state: >
{% if (states('sensor.power_export')|float(0)) > 0 and (states('sensor.balkonkraftwerk_power')|float(0) - states('sensor.power_export')|float(0)) < 0 %}
{% elif (states('sensor.power_export')|float(0)) > 0 and (states('sensor.balkonkraftwerk_power')|float(0) - states('sensor.power_export')|float(0)) > 0 %}
{{ ((states('sensor.balkonkraftwerk_power')|float(0)) - states('sensor.power_export')|float(0)) | round(1) }}
{% else %}
{{ (states('sensor.power_import')|float(0) + states('sensor.balkonkraftwerk_power')|float(0)) | round(1) }}
{% endif %}
device_class: power
state_class: measurement
icon: mdi:home-lightning-bolt
attributes:
last_reset: '1970-01-01T00:00:00+00:00'
# Internet Speed template sensor
- name: internet_speed_in
unique_id: '9519483670666'
state: >
{{ (( states('sensor.wan_in_derivative') | float * 8 / 1000000 ) | round(2)) }}
unit_of_measurement: 'Mbps'
attributes:
last_reset: '1970-01-01T00:00:00+00:00'
- name: internet_speed_out
state: >
{{ (( states('sensor.wan_out_derivative') | float * 8 / 1000000 ) | round(2)) }}
unit_of_measurement: 'Mbps'
attributes:
last_reset: '1970-01-01T00:00:00+00:00'

View File

@@ -1,24 +1,75 @@
# Internet traffic
internet_usage_in_monthly:
source: sensor.snmp_wan_in
name: Monthly internet traffic in
unique_id: monthly_internet_traffic_in
cycle: monthly
internet_usage_out_monthly:
source: sensor.snmp_wan_out
name: Monthly internet traffic out
unique_id: monthly_internet_traffic_out
cycle: monthly
internet_usage_in_daily:
source: sensor.snmp_wan_in
name: Daily internet traffic in
unique_id: daily_internet_traffic_in
cycle: daily
internet_usage_out_daily:
source: sensor.snmp_wan_out
name: Daily internet traffic out
unique_id: daily_internet_traffic_out
cycle: daily
internet_usage_in_hourly:
source: sensor.snmp_wan_in
name: Hourly internet traffic in
unique_id: hourly_internet_traffic_in
cycle: hourly
internet_usage_out_hourly:
source: sensor.snmp_wan_out
name: Hourly internet traffic out
unique_id: hourly_internet_traffic_out
cycle: hourly
# Energy
energy_import_daily:
source: sensor.energy_import_sum
name: Energy Import Daily
unique_id: energy_import_daily
cycle: daily
energy_import_monthly:
source: sensor.energy_import_sum
name: Energy Import Monthly
unique_id: energy_import_monthly
cycle: monthly
energy_export_daily:
source: sensor.energy_export_sum
name: Energy Export Daily
unique_id: energy_export_daily
cycle: daily
energy_export_monthly:
source: sensor.energy_export_sum
name: Energy Export Monthly
unique_id: energy_export_monthly
cycle: monthly
energy_consumption_daily:
source: sensor.energy_consumption_sum
name: Energy Consumption Daily
unique_id: energy_consumption_daily
cycle: daily
energy_consumption_monthly:
source: sensor.energy_consumption_sum
name: Energy Consumption Monthly
unique_id: energy_consumption_monthly
cycle: monthly
# Energy (Solar)
energy_solar_daily:
source: sensor.balkonkraftwerk_energy
name: Energy Solar Daily
unique_id: energy_solar_daily
cycle: daily
energy_solar_monthly:
source: sensor.balkonkraftwerk_energy
name: Energy Solar Monthly
unique_id: energy_solar_monthly
cycle: monthly

18
www/DWD.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

13
www/Nina_app.svg Normal file
View File

@@ -0,0 +1,13 @@
<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg" width="300" height="220">
<path fill="#E72" d="m149,58a52,52 0 1,0 2,0z"/>
<path fill="#237" d="m112,135h76l-38-66z"/>
<path fill="none" stroke="#EEE" stroke-width="12"
d="m63,8a72,117 0 0,0 0,204m174,0a72,117 0 0,0 0-204M82
30a70,99 0 0,0 0,160m136,0a72,101 0 0,0 0-160M101
54a68,78 0 0,0 0,112m98,0a68,78 0 0,0 0-112"/>
<path fill="none" stroke="#E72" stroke-width="12"
d="m63,22a96,112 0 0,0 0,176m174,0a96,112 0 0,0 0-176M80
40a80,93 0 0,0 0,140m140,0a80,93 0 0,0 0-140M100,60a62
66 0 0,0 0,100m100,0a62,66 0 0,0 0-100"/>
</svg>

After

Width:  |  Height:  |  Size: 594 B