Compare commits
228 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4e46f60b65 | |||
| 9e7684e294 | |||
| 818b06f3d7 | |||
| 99bb2594e2 | |||
| 9104cd8fdc | |||
| 96b88624d9 | |||
| c1ba3012d4 | |||
| e77d9ee8d2 | |||
| bf49633fac | |||
| 99579a30f7 | |||
| 250bfa94bd | |||
| c342efc08f | |||
| 13e58355da | |||
| ee814c4afe | |||
| 6149a9e994 | |||
| 3b7c30634e | |||
| 38da2cebdd | |||
| 3da69c19ab | |||
| 35e7f81224 | |||
| e4a4aacc63 | |||
| 0d12317089 | |||
| 746dd833c8 | |||
| 3eb482e40b | |||
| e9a6064b4b | |||
| 38616556cb | |||
| bef1cfb1d0 | |||
| 0a8364d7ed | |||
| 3eb9eb3a2f | |||
| 7407955465 | |||
| 2ff8b5cafe | |||
| 7e08c4a8b9 | |||
| 855f013095 | |||
| a68b527c48 | |||
| cecec14be1 | |||
| 4e5df1ad0c | |||
| 2c16ba1679 | |||
| c21f2a5c7b | |||
| 2a1902ccf1 | |||
| 31848bf26c | |||
| d7337fd634 | |||
| 5b287303f1 | |||
| 816cc35861 | |||
| 058c186d63 | |||
| 22e5f9d767 | |||
| f9b32d06a4 | |||
| 3f890ae02f | |||
| f0cb74cc27 | |||
| 6038075e9e | |||
| 2dbdfd213c | |||
| e2eb886eef | |||
| f4f19eb847 | |||
| b3e307dcba | |||
| 1b3b4870e7 | |||
| 69c2166414 | |||
| 2fcf71d8ce | |||
| 676829a988 | |||
| 08201f1bc1 | |||
| 23cbd520a6 | |||
| 6f4e2b9319 | |||
| e5104fc005 | |||
| 91f3f69bd9 | |||
| 4bd62f539c | |||
| a3bd9ff213 | |||
| 24fc4bf9f4 | |||
| 0bfc2eff34 | |||
| 8f18a043af | |||
| 5b0a992686 | |||
| ef233296e9 | |||
| c0af2b734b | |||
| a94386babe | |||
| f23166b765 | |||
| 8438785241 | |||
| b0b6093619 | |||
| c463a5e28f | |||
| a3e06a85d6 | |||
| 6795c31d43 | |||
| e504cdf90c | |||
| c9807a7bf0 | |||
| fdbbbb5300 | |||
| fda6e6abff | |||
| ee01b56fb6 | |||
| db1edfaf24 | |||
| e07c66563c | |||
| 2ff40a0da7 | |||
| 24b4d07228 | |||
| 8b6f3012f4 | |||
| 792fc87166 | |||
| 67b2c2edde | |||
| db9b8a73d3 | |||
| 093be6f2d2 | |||
| 3d5693ceaa | |||
| 247c96df5d | |||
| 13cdc7ee67 | |||
| 24eecc7c97 | |||
| aea16e59c4 | |||
| 48bfa10bf7 | |||
| 80560ef9aa | |||
| ab46bff258 | |||
| fe081bf145 | |||
| 85abd64ea0 | |||
| b7581d3bad | |||
| 93b2d4f401 | |||
| ca0ef18df3 | |||
| cd6625b08e | |||
| 4810652d99 | |||
| 741b283218 | |||
| 8800f7b847 | |||
| a947806618 | |||
| 439ead1047 | |||
| 20c86831f1 | |||
| 61b018270b | |||
| 911477d381 | |||
| 1ee475ee4a | |||
| cae28341fe | |||
| 17cfa429ea | |||
| 791d7d504f | |||
| 631cdae751 | |||
| 06321730e0 | |||
| 5ac8ceacfe | |||
| 4981cbfa2e | |||
| e5ddf02c34 | |||
| ebf11021e2 | |||
| 72ce055810 | |||
| 8461f43bca | |||
| ca599eab7a | |||
| fef90d5a78 | |||
| 1f151a804e | |||
| 31da165db1 | |||
| dd390001ee | |||
| 7993a9174a | |||
| fe40743fd1 | |||
| 01bad587b8 | |||
| 4888393a12 | |||
| 05f8cc1af8 | |||
| 5c98baa24b | |||
| 490b00301b | |||
| c3e813551b | |||
| 474805328b | |||
| fd18934e97 | |||
| 5e1d197e7a | |||
| ff59c494a7 | |||
| c4d9c253f4 | |||
| 99007cda76 | |||
| 373675be4f | |||
| 90ed6df217 | |||
| e1984a199d | |||
| 3fcca0c3e1 | |||
| 5ddbe9962f | |||
| f05405bc75 | |||
| 5467789c71 | |||
| 7f9a366171 | |||
| bf3b4e5e81 | |||
| 083cdbb857 | |||
| 32e9c3af4d | |||
| a0d7950829 | |||
| 8cd5ae5283 | |||
| 574be34e36 | |||
| 0dfe1950c9 | |||
| 9def77c3f3 | |||
| fb027e0c45 | |||
| f744021925 | |||
| daaec3cc60 | |||
| 683ffe7767 | |||
| 730d1cf338 | |||
| 27408f02dd | |||
| 624ee50d14 | |||
| 48d29a6585 | |||
| 04eff2e382 | |||
| 8f63ede1f0 | |||
| d32328f1bb | |||
| c15266fc20 | |||
| 9ec14848d5 | |||
| 87868f079c | |||
| c0755f5add | |||
| acb1c63483 | |||
| 565b1b1b81 | |||
| 392e8508d3 | |||
| 6985991d3a | |||
| 0162c1074f | |||
| 8a5ba47dfa | |||
| db6a6e9e30 | |||
| 8cad164008 | |||
| d8cb73ebf0 | |||
| 0ac87ad81c | |||
| 395bf7bb9c | |||
| c0ebd9188d | |||
| f36ea1b591 | |||
| ee90402c8b | |||
| 479a1cb8e1 | |||
| 2cf58f3f34 | |||
| 0dff2d90ff | |||
| 92f95f7bae | |||
| d5be7fea6b | |||
| 8a446a727b | |||
| 2d3c9cf674 | |||
| ce9ad46e5b | |||
| 6482453a1d | |||
| 5f1f08b79e | |||
| 48338c9233 | |||
| 671c9d6b77 | |||
| 94f390ddf7 | |||
| 429dce72fe | |||
| 36a482de26 | |||
| 9e5a74a999 | |||
| 9ba761dcd3 | |||
| b414af5686 | |||
| eef9f31a94 | |||
| a6b8e2d9e2 | |||
| 663f5d5daf | |||
| fb4e6a9f41 | |||
| cfcbdeea9d | |||
| 6fdacdc6e9 | |||
| 8b43bf239b | |||
| 1ade598c97 | |||
| 84bea6cc2b | |||
| 4d2c5b0cba | |||
| 0191bc13b7 | |||
| 6e48e18384 | |||
| 55e1a02544 | |||
| a433286324 | |||
| 0d00747fe4 | |||
| d17935a803 | |||
| 929d6b1830 | |||
| a57178b5e1 | |||
| 883225a67e | |||
| 39a432eece | |||
| c080d2ead3 | |||
| abef860f2e |
@@ -1 +1 @@
|
|||||||
2023.5.4
|
2026.7.2
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
automations_webhooks.yaml
|
automations_webhooks.yaml
|
||||||
spotify.yaml
|
spotify.yaml
|
||||||
calendars.yaml
|
calendars.yaml
|
||||||
|
ics_calendars.yaml
|
||||||
esphome/common/secrets.yaml
|
esphome/common/secrets.yaml
|
||||||
esphome/secrets.yaml
|
esphome/secrets.yaml
|
||||||
secrets.yaml
|
secrets.yaml
|
||||||
@@ -9,3 +10,7 @@ tts/
|
|||||||
media/
|
media/
|
||||||
home-assistant.log*
|
home-assistant.log*
|
||||||
.storage/
|
.storage/
|
||||||
|
zigbee.db*
|
||||||
|
*__pycache__*
|
||||||
|
.ha_run.lock
|
||||||
|
.cache
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "themes/blackened"]
|
||||||
|
path = themes/blackened
|
||||||
|
url = https://github.com/home-assistant-community-themes/blackened.git
|
||||||
@@ -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 }}"
|
||||||
|
}
|
||||||
@@ -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 }}
|
||||||
@@ -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}}'
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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 \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 
|
||||||
|
`Style 0` \n\n ---\n\n 
|
||||||
|
`Style 1` \n\n ---\n\n 
|
||||||
|
`Style 2` \n\n ---\n\n 
|
||||||
|
`Style 3` \n\n ---\n\n 
|
||||||
|
`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 }}'
|
||||||
@@ -0,0 +1,302 @@
|
|||||||
|
mode: restart
|
||||||
|
blueprint:
|
||||||
|
name: IKEA BILRESA Dual Button
|
||||||
|
author: Skaronator
|
||||||
|
description: "# IKEA BILRESA Dual Button Remote Automation\n\n## Current Version
|
||||||
|
2.0.1\n\n## Features\n\nSimple automation blueprint for IKEA BILRESA dual-button
|
||||||
|
remote.\n\n### Supported Actions for Each Button\n\n* Single Press\n* Double Press\n*
|
||||||
|
Long Press (on press and release)\n* While Holding (with configurable interval)\n\n###
|
||||||
|
Configuration Options\n\n* Configurable interval for \"while holding\" actions.\n*
|
||||||
|
Safety limit on maximum iterations for \"while holding\" actions to prevent infinite
|
||||||
|
loops.\n\n## Changelog\n\n### 2.0.1\n - Removed Matter integration filter for
|
||||||
|
button event selector. This will allow users to use the Ikea BILRESA remote over
|
||||||
|
3rd party Matter integration.\n### 2.0.0\n - Added support for selecting multiple
|
||||||
|
entities for each button. Allowing to have multiple remotes triggering the same
|
||||||
|
actions.\n - See [these upgrade notes](https://community.home-assistant.io/t/ikea-bilresa-dual-button-remote/965815/28?u=skaronator)
|
||||||
|
for migrating existing automations using this blueprint.\n### 1.2.2\n - Improved
|
||||||
|
descriptions and documentation.\n### 1.2.1\n - Improved descriptions for blueprint
|
||||||
|
inputs.\n### 1.2.0\n - Added max iterations setting for \"while holding\" actions
|
||||||
|
to prevent infinite loops.\n### 1.1.0\n - Added \"while holding\" actions for
|
||||||
|
both buttons with configurable interval.\n### 1.0.0\n - Initial release with
|
||||||
|
support for all button actions.\n\n## Support and Feedback\n\n* GitHub: [Skaronator/home-assistant-blueprints](https://github.com/Skaronator/home-assistant-blueprints)\n*
|
||||||
|
Blueprint Exchange: [IKEA BILRESA Dual Button](https://community.home-assistant.io/t/ikea-bilresa-dual-button-remote/965815)\n\nThank
|
||||||
|
you for using this blueprint! Your feedback and contributions are welcome. \U0001F49A\n"
|
||||||
|
domain: automation
|
||||||
|
source_url: https://raw.githubusercontent.com/Skaronator/home-assistant-blueprints/refs/heads/main/automation/ikea-bilresa-dual-button.yaml
|
||||||
|
homeassistant:
|
||||||
|
min_version: 2025.12.0
|
||||||
|
input:
|
||||||
|
setup:
|
||||||
|
name: ⚙️ Button Setup
|
||||||
|
description: 'Select the event entities for all 2 buttons on the IKEA BILRESA
|
||||||
|
dual-button remote.
|
||||||
|
|
||||||
|
'
|
||||||
|
collapsed: true
|
||||||
|
input:
|
||||||
|
button1_event:
|
||||||
|
name: Entity
|
||||||
|
description: 'Select the event entity for Button 1
|
||||||
|
|
||||||
|
'
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
multiple: true
|
||||||
|
filter:
|
||||||
|
- domain:
|
||||||
|
- event
|
||||||
|
device_class:
|
||||||
|
- button
|
||||||
|
reorder: false
|
||||||
|
button2_event:
|
||||||
|
name: Entity
|
||||||
|
description: 'Select the event entity for Button 2
|
||||||
|
|
||||||
|
'
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
multiple: true
|
||||||
|
filter:
|
||||||
|
- domain:
|
||||||
|
- event
|
||||||
|
device_class:
|
||||||
|
- button
|
||||||
|
reorder: false
|
||||||
|
button1:
|
||||||
|
name: '1️⃣ Button 1 Actions
|
||||||
|
|
||||||
|
'
|
||||||
|
description: 'Define actions when pressing Button 1.
|
||||||
|
|
||||||
|
'
|
||||||
|
collapsed: true
|
||||||
|
input:
|
||||||
|
button1_single:
|
||||||
|
name: Single Press
|
||||||
|
description: Action for Button 1 single press.
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
button1_double:
|
||||||
|
name: Double Press
|
||||||
|
description: Action for Button 1 double press.
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
button1_long_press:
|
||||||
|
name: Long Press (on press)
|
||||||
|
description: Action for Button 1 long press completion.
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
button1_long_release:
|
||||||
|
name: Long Press (on release)
|
||||||
|
description: Action for Button 1 long press completion.
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
button1_holding_interval:
|
||||||
|
name: Holding Interval (seconds)
|
||||||
|
description: Interval in seconds for repeating "while holding" actions.
|
||||||
|
default: 0.2
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 0.05
|
||||||
|
max: 2.0
|
||||||
|
step: 0.05
|
||||||
|
mode: slider
|
||||||
|
button1_holding_max_iterations:
|
||||||
|
name: Holding Max Iterations
|
||||||
|
description: Maximum number of iterations for "while holding" actions. This
|
||||||
|
is a safety limit to prevent infinite loops.
|
||||||
|
default: 100
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 1.0
|
||||||
|
max: 1000.0
|
||||||
|
step: 1.0
|
||||||
|
mode: slider
|
||||||
|
button1_while_holding:
|
||||||
|
name: While Holding
|
||||||
|
description: Action for Button 1 while holding. This action will be repeated
|
||||||
|
at the defined interval while the button is held down.
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
button2:
|
||||||
|
name: '2️⃣ Button 2 Actions
|
||||||
|
|
||||||
|
'
|
||||||
|
description: 'Define actions when pressing Button 2.
|
||||||
|
|
||||||
|
'
|
||||||
|
collapsed: true
|
||||||
|
input:
|
||||||
|
button2_single:
|
||||||
|
name: Single Press
|
||||||
|
description: Action for Button 2 single press.
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
button2_double:
|
||||||
|
name: Double Press
|
||||||
|
description: Action for Button 2 double press.
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
button2_long_press:
|
||||||
|
name: Long Press (on press)
|
||||||
|
description: Action for Button 2 long press completion.
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
button2_long_release:
|
||||||
|
name: Long Press (on release)
|
||||||
|
description: Action for Button 2 long press completion.
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
button2_holding_interval:
|
||||||
|
name: Holding Interval (seconds)
|
||||||
|
description: Interval in seconds for repeating "while holding" actions.
|
||||||
|
default: 0.2
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 0.05
|
||||||
|
max: 2.0
|
||||||
|
step: 0.05
|
||||||
|
mode: slider
|
||||||
|
button2_holding_max_iterations:
|
||||||
|
name: Holding Max Iterations
|
||||||
|
description: Maximum number of iterations for "while holding" actions. This
|
||||||
|
is a safety limit to prevent infinite loops.
|
||||||
|
default: 100
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 1.0
|
||||||
|
max: 1000.0
|
||||||
|
step: 1.0
|
||||||
|
mode: slider
|
||||||
|
button2_while_holding:
|
||||||
|
name: While Holding
|
||||||
|
description: Action for Button 2 while holding. This action will be repeated
|
||||||
|
at the defined interval while the button is held down.
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
trigger:
|
||||||
|
- platform: state
|
||||||
|
id: button1
|
||||||
|
entity_id: !input button1_event
|
||||||
|
- platform: state
|
||||||
|
id: button2
|
||||||
|
entity_id: !input button2_event
|
||||||
|
condition:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ trigger.from_state.state not in [''unknown'', ''unavailable'']
|
||||||
|
}}
|
||||||
|
|
||||||
|
'
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ trigger.to_state.state not in [''unknown'', ''unavailable'']
|
||||||
|
}}
|
||||||
|
|
||||||
|
'
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ trigger.from_state.state != trigger.to_state.state }}
|
||||||
|
|
||||||
|
'
|
||||||
|
action:
|
||||||
|
- variables:
|
||||||
|
trigger_id: '{{ trigger.id }}'
|
||||||
|
press_type: '{{ trigger.to_state.attributes.event_type }}'
|
||||||
|
button1_max_iterations: !input button1_holding_max_iterations
|
||||||
|
button2_max_iterations: !input button2_holding_max_iterations
|
||||||
|
- parallel:
|
||||||
|
- repeat:
|
||||||
|
while:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ press_type == ''long_press'' and trigger_id == ''button1''
|
||||||
|
and states(trigger.entity_id) == trigger.to_state.state }}
|
||||||
|
|
||||||
|
'
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ repeat.index|default(0) <= button1_max_iterations }}
|
||||||
|
|
||||||
|
'
|
||||||
|
sequence:
|
||||||
|
- sequence: !input button1_while_holding
|
||||||
|
- delay:
|
||||||
|
seconds: !input button1_holding_interval
|
||||||
|
- repeat:
|
||||||
|
while:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ press_type == ''long_press'' and trigger_id == ''button2''
|
||||||
|
and states(trigger.entity_id) == trigger.to_state.state }}
|
||||||
|
|
||||||
|
'
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ repeat.index|default(0) <= button2_max_iterations }}
|
||||||
|
|
||||||
|
'
|
||||||
|
sequence:
|
||||||
|
- sequence: !input button2_while_holding
|
||||||
|
- delay:
|
||||||
|
seconds: !input button2_holding_interval
|
||||||
|
- choose:
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ trigger_id == ''button1'' and press_type == ''multi_press_1''
|
||||||
|
}}
|
||||||
|
|
||||||
|
'
|
||||||
|
sequence: !input button1_single
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ trigger_id == ''button1'' and press_type == ''multi_press_2''
|
||||||
|
}}
|
||||||
|
|
||||||
|
'
|
||||||
|
sequence: !input button1_double
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ trigger_id == ''button1'' and press_type == ''long_press''
|
||||||
|
}}
|
||||||
|
|
||||||
|
'
|
||||||
|
sequence: !input button1_long_press
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ trigger_id == ''button1'' and press_type == ''long_release''
|
||||||
|
}}
|
||||||
|
|
||||||
|
'
|
||||||
|
sequence: !input button1_long_release
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ trigger_id == ''button2'' and press_type == ''multi_press_1''
|
||||||
|
}}
|
||||||
|
|
||||||
|
'
|
||||||
|
sequence: !input button2_single
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ trigger_id == ''button2'' and press_type == ''multi_press_2''
|
||||||
|
}}
|
||||||
|
|
||||||
|
'
|
||||||
|
sequence: !input button2_double
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ trigger_id == ''button2'' and press_type == ''long_press''
|
||||||
|
}}
|
||||||
|
|
||||||
|
'
|
||||||
|
sequence: !input button2_long_press
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ trigger_id == ''button2'' and press_type == ''long_release''
|
||||||
|
}}
|
||||||
|
|
||||||
|
'
|
||||||
|
sequence: !input button2_long_release
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
blueprint:
|
||||||
|
name: IKEA MYGGSPRAY E2494 Motion Sensor (Matter) v2.1
|
||||||
|
description: 'Full-featured automation for the IKEA MYGGSPRAY E2494 Matter motion
|
||||||
|
sensor. Supports independent actions for motion, illuminance (high/low), and battery
|
||||||
|
alerts. '
|
||||||
|
domain: automation
|
||||||
|
source_url: https://github.com/aledziko/HA-blueprints/blob/main/IKEA/Matter/ikea-myggspray-e2494/ikea-myggspray-e2494-matter-motion-sensor.yaml
|
||||||
|
author: aledziko
|
||||||
|
input:
|
||||||
|
motion_entity:
|
||||||
|
name: MYGGSPRAY Motion Sensor
|
||||||
|
description: Select the MYGGSPRAY motion sensor (binary_sensor).
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
filter:
|
||||||
|
- device_class:
|
||||||
|
- occupancy
|
||||||
|
domain:
|
||||||
|
- binary_sensor
|
||||||
|
- device_class:
|
||||||
|
- motion
|
||||||
|
domain:
|
||||||
|
- binary_sensor
|
||||||
|
reorder: false
|
||||||
|
multiple: false
|
||||||
|
use_time_window:
|
||||||
|
name: Enable Active Hours?
|
||||||
|
description: Restrict automation to a specific time window.
|
||||||
|
default: false
|
||||||
|
selector:
|
||||||
|
boolean: {}
|
||||||
|
active_start_time:
|
||||||
|
name: Active Start Time
|
||||||
|
description: Automation starts working at this time (if enabled).
|
||||||
|
default: 08:00:00
|
||||||
|
selector:
|
||||||
|
time: {}
|
||||||
|
active_end_time:
|
||||||
|
name: Active End Time
|
||||||
|
description: Automation stops working at this time (if enabled).
|
||||||
|
default: '22:00:00'
|
||||||
|
selector:
|
||||||
|
time: {}
|
||||||
|
no_motion_wait:
|
||||||
|
name: Wait time (Motion)
|
||||||
|
description: Time to wait after last motion is detected before running the "off"
|
||||||
|
action.
|
||||||
|
default: 120
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 0.0
|
||||||
|
max: 3600.0
|
||||||
|
unit_of_measurement: seconds
|
||||||
|
step: 1.0
|
||||||
|
mode: slider
|
||||||
|
motion_on_action:
|
||||||
|
name: 'Action: Motion Detected'
|
||||||
|
description: Run when motion is detected (and dark enough).
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
motion_off_action:
|
||||||
|
name: 'Action: Motion Stopped'
|
||||||
|
description: Run after the sensor stops detecting motion and wait time elapsed.
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
illuminance_entity:
|
||||||
|
name: (Optional) Illuminance Sensor
|
||||||
|
description: Select the light sensor (sensor.illuminance).
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
filter:
|
||||||
|
- domain:
|
||||||
|
- sensor
|
||||||
|
device_class:
|
||||||
|
- illuminance
|
||||||
|
reorder: false
|
||||||
|
multiple: false
|
||||||
|
motion_cutoff:
|
||||||
|
name: (Optional) Motion Light Cutoff
|
||||||
|
description: Only trigger "Motion Detected" action if LUX is BELOW this value
|
||||||
|
(0 to disable).
|
||||||
|
default: 0
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 0.0
|
||||||
|
max: 3000.0
|
||||||
|
unit_of_measurement: lux
|
||||||
|
step: 1.0
|
||||||
|
mode: slider
|
||||||
|
illuminance_high_threshold:
|
||||||
|
name: (Optional) Light High Threshold
|
||||||
|
description: Trigger "High Action" when LUX goes ABOVE this value (e.g. 500
|
||||||
|
lx).
|
||||||
|
default: 500
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 0.0
|
||||||
|
max: 3000.0
|
||||||
|
unit_of_measurement: lux
|
||||||
|
step: 1.0
|
||||||
|
mode: slider
|
||||||
|
high_action:
|
||||||
|
name: 'Action: Light High'
|
||||||
|
description: Run when light exceeds threshold (e.g. close curtains).
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
illuminance_low_threshold:
|
||||||
|
name: (Optional) Light Low Threshold
|
||||||
|
description: Trigger "Low Action" when LUX goes BELOW this value (e.g. 5 lx).
|
||||||
|
default: 5
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 0.0
|
||||||
|
max: 3000.0
|
||||||
|
unit_of_measurement: lux
|
||||||
|
step: 1.0
|
||||||
|
mode: slider
|
||||||
|
low_action:
|
||||||
|
name: 'Action: Light Low'
|
||||||
|
description: Run when light drops below threshold (e.g. open curtains).
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
battery_entity:
|
||||||
|
name: (Optional) Battery Sensor
|
||||||
|
description: Select the battery level sensor (sensor.battery).
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
filter:
|
||||||
|
- domain:
|
||||||
|
- sensor
|
||||||
|
device_class:
|
||||||
|
- battery
|
||||||
|
reorder: false
|
||||||
|
multiple: false
|
||||||
|
battery_low_threshold:
|
||||||
|
name: (Optional) Battery Low Level
|
||||||
|
description: Threshold for battery alert (%).
|
||||||
|
default: 30
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 0.0
|
||||||
|
max: 100.0
|
||||||
|
unit_of_measurement: '%'
|
||||||
|
step: 1.0
|
||||||
|
mode: slider
|
||||||
|
battery_low_action:
|
||||||
|
name: 'Action: Battery Low'
|
||||||
|
description: Run when battery drops below threshold.
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
mode: parallel
|
||||||
|
max_exceeded: silent
|
||||||
|
trigger_variables:
|
||||||
|
motion_entity_input: !input motion_entity
|
||||||
|
illuminance_entity_input: !input illuminance_entity
|
||||||
|
motion_cutoff_input: !input motion_cutoff
|
||||||
|
high_threshold_input: !input illuminance_high_threshold
|
||||||
|
low_threshold_input: !input illuminance_low_threshold
|
||||||
|
battery_entity_input: !input battery_entity
|
||||||
|
battery_threshold_input: !input battery_low_threshold
|
||||||
|
use_time_window_input: !input use_time_window
|
||||||
|
triggers:
|
||||||
|
- trigger: state
|
||||||
|
entity_id: !input motion_entity
|
||||||
|
from: 'off'
|
||||||
|
to: 'on'
|
||||||
|
id: motion_on
|
||||||
|
- trigger: state
|
||||||
|
entity_id: !input motion_entity
|
||||||
|
from: 'on'
|
||||||
|
to: 'off'
|
||||||
|
for:
|
||||||
|
seconds: !input no_motion_wait
|
||||||
|
id: motion_off
|
||||||
|
- trigger: template
|
||||||
|
value_template: "{{ illuminance_entity_input != none and \n states(illuminance_entity_input)
|
||||||
|
| is_number and \n states(illuminance_entity_input) | float(0) > high_threshold_input
|
||||||
|
| float(100000) }}\n"
|
||||||
|
id: light_high
|
||||||
|
- trigger: template
|
||||||
|
value_template: "{{ illuminance_entity_input != none and \n states(illuminance_entity_input)
|
||||||
|
| is_number and \n states(illuminance_entity_input) | float(100000) < low_threshold_input
|
||||||
|
| float(-1) }}\n"
|
||||||
|
id: light_low
|
||||||
|
- trigger: template
|
||||||
|
value_template: "{{ battery_entity_input != none and \n states(battery_entity_input)
|
||||||
|
| is_number and \n states(battery_entity_input) | float(100) < battery_threshold_input
|
||||||
|
| float(-1) }}\n"
|
||||||
|
id: battery_low
|
||||||
|
condition:
|
||||||
|
- condition: or
|
||||||
|
conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ not use_time_window_input }}'
|
||||||
|
- condition: time
|
||||||
|
after: !input active_start_time
|
||||||
|
before: !input active_end_time
|
||||||
|
actions:
|
||||||
|
- choose:
|
||||||
|
- conditions:
|
||||||
|
- condition: trigger
|
||||||
|
id: motion_on
|
||||||
|
sequence:
|
||||||
|
- delay: 1.5
|
||||||
|
- condition: template
|
||||||
|
value_template: "{% set cutoff = motion_cutoff_input | float(0) %} {% if cutoff
|
||||||
|
== 0 %}\n true\n{% else %}\n {% set lux = states(illuminance_entity_input)
|
||||||
|
| float(-1) %}\n {{ lux == -1 or lux <= cutoff }}\n{% endif %}\n"
|
||||||
|
- sequence: !input motion_on_action
|
||||||
|
- conditions:
|
||||||
|
- condition: trigger
|
||||||
|
id: motion_off
|
||||||
|
sequence: !input motion_off_action
|
||||||
|
- conditions:
|
||||||
|
- condition: trigger
|
||||||
|
id: light_high
|
||||||
|
sequence: !input high_action
|
||||||
|
- conditions:
|
||||||
|
- condition: trigger
|
||||||
|
id: light_low
|
||||||
|
sequence: !input low_action
|
||||||
|
- conditions:
|
||||||
|
- condition: trigger
|
||||||
|
id: battery_low
|
||||||
|
sequence: !input battery_low_action
|
||||||
@@ -0,0 +1,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
|
||||||
@@ -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\n\n\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://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.
|
||||||
|
|
||||||
|
- 
|
||||||
|
- `full_moon`
|
||||||
|
|
||||||
|
- 
|
||||||
|
- `waning_gibbous`
|
||||||
|
|
||||||
|
- 
|
||||||
|
- `last_quarter`
|
||||||
|
|
||||||
|
- 
|
||||||
|
- `waning_crescent`
|
||||||
|
|
||||||
|
- 
|
||||||
|
- `new_moon`
|
||||||
|
|
||||||
|
- 
|
||||||
|
- `waxing_crescent`
|
||||||
|
|
||||||
|
- 
|
||||||
|
- `first_quarter`
|
||||||
|
|
||||||
|
- 
|
||||||
|
- `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 
|
||||||
|
- `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`
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
'
|
||||||
|
selector:
|
||||||
|
text: {}
|
||||||
|
default: w-cloudy
|
||||||
|
icon_exceptional:
|
||||||
|
name: Icon for exceptional
|
||||||
|
description: 'This is the icon ID which maps to the weather state: `exceptional`
|
||||||
|
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
'
|
||||||
|
selector:
|
||||||
|
text: {}
|
||||||
|
default: w-exceptional
|
||||||
|
icon_fog:
|
||||||
|
name: Icon for fog
|
||||||
|
description: 'This is the icon ID which maps to the weather state: `fog`
|
||||||
|
|
||||||
|

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

|
||||||
|
|
||||||
|
'
|
||||||
|
selector:
|
||||||
|
text: {}
|
||||||
|
default: w-hail
|
||||||
|
icon_lightning:
|
||||||
|
name: Icon for lightning
|
||||||
|
description: 'This is the icon ID which maps to the weather state: `lightning`
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
'
|
||||||
|
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`
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
'
|
||||||
|
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\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`
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
'
|
||||||
|
selector:
|
||||||
|
text: {}
|
||||||
|
default: w-pouring
|
||||||
|
icon_rainy:
|
||||||
|
name: Icon for rainy
|
||||||
|
description: 'This is the default icon which maps to the weather state: `rainy`
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
'
|
||||||
|
selector:
|
||||||
|
text: {}
|
||||||
|
default: w-rainy
|
||||||
|
icon_snowy:
|
||||||
|
name: Icon for snowy
|
||||||
|
description: 'This is the icon ID which maps to the weather state: `snowy`
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
'
|
||||||
|
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`
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
'
|
||||||
|
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`
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
'
|
||||||
|
selector:
|
||||||
|
text: {}
|
||||||
|
default: w-sunny
|
||||||
|
icon_windy:
|
||||||
|
name: Icon for windy
|
||||||
|
description: 'This is the icon ID which maps to the weather state: `windy`
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
'
|
||||||
|
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`
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
'
|
||||||
|
selector:
|
||||||
|
text: {}
|
||||||
|
default: w-windy-variant
|
||||||
|
icon_sunrise:
|
||||||
|
name: Icon for sunrise
|
||||||
|
description: 'This is the icon ID which maps to the `sunrise`
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
'
|
||||||
|
selector:
|
||||||
|
text: {}
|
||||||
|
default: w-sunrise
|
||||||
|
icon_sunset:
|
||||||
|
name: Icon for sunset
|
||||||
|
description: 'This is the icon ID which maps to the `sunset`
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
'
|
||||||
|
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}}
|
||||||
|
|
||||||
|
'
|
||||||
@@ -0,0 +1,506 @@
|
|||||||
|
blueprint:
|
||||||
|
name: Ikea_bilresa_scroll_wheel
|
||||||
|
description: 'Blueprint for Ikea Bilresa (button and scroll wheel for 3 different
|
||||||
|
channels)
|
||||||
|
|
||||||
|
Version: 2025-01-01
|
||||||
|
|
||||||
|
'
|
||||||
|
domain: automation
|
||||||
|
input:
|
||||||
|
remote:
|
||||||
|
name: Remote
|
||||||
|
description: Ikea Bilresa scroll wheel remote control
|
||||||
|
selector:
|
||||||
|
device:
|
||||||
|
multiple: false
|
||||||
|
channel1_section:
|
||||||
|
name: Channel 1
|
||||||
|
collapsed: true
|
||||||
|
input:
|
||||||
|
click_action_ch1:
|
||||||
|
name: 'Channel 1: <click> action'
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
double_click_action_ch1:
|
||||||
|
name: 'Channel 1: <double-click> action'
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
long_click_action_ch1:
|
||||||
|
name: 'Channel 1: <long-click> action'
|
||||||
|
description: ''
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
on_hold_action_ch1:
|
||||||
|
name: 'Channel 1: <on-hold> action'
|
||||||
|
description: repeat action until button is released, see also on_hold_delay
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
scroll_wheel_target_ch1:
|
||||||
|
name: 'Channel 1: Scroll wheel target entity'
|
||||||
|
description: Light or media player to be controlled by scroll wheel for
|
||||||
|
channel 1
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
multiple: true
|
||||||
|
filter:
|
||||||
|
- domain:
|
||||||
|
- light
|
||||||
|
- media_player
|
||||||
|
reorder: false
|
||||||
|
default: []
|
||||||
|
scroll_wheel_mode_ch1:
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
options:
|
||||||
|
- 'lights: dim'
|
||||||
|
- 'lights: color temp and color hue'
|
||||||
|
- 'lights: color temp only'
|
||||||
|
- 'lights: color hue only'
|
||||||
|
- 'media: volume control'
|
||||||
|
custom_value: false
|
||||||
|
sort: false
|
||||||
|
multiple: false
|
||||||
|
default: dim_only
|
||||||
|
name: 'Channel 1: Scroll wheel mode'
|
||||||
|
description: Select attribute of target entity controlled by scroll wheel
|
||||||
|
channel2_section:
|
||||||
|
name: Channel 2
|
||||||
|
collapsed: true
|
||||||
|
input:
|
||||||
|
click_action_ch2:
|
||||||
|
name: 'Channel 2: <click> action'
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
double_click_action_ch2:
|
||||||
|
name: 'Channel 2: <double-click> action'
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
long_click_action_ch2:
|
||||||
|
name: 'Channel 2: <long-click> action'
|
||||||
|
description: ''
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
on_hold_action_ch2:
|
||||||
|
name: 'Channel 2: <on-hold> action'
|
||||||
|
description: repeat action until button is released, see also on_hold_delay
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
scroll_wheel_target_ch2:
|
||||||
|
name: 'Channel 2: Scroll wheel target entity'
|
||||||
|
description: Light or media player to be controlled by scroll wheel for
|
||||||
|
channel 2
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
multiple: true
|
||||||
|
filter:
|
||||||
|
- domain:
|
||||||
|
- light
|
||||||
|
- media_player
|
||||||
|
reorder: false
|
||||||
|
default: []
|
||||||
|
scroll_wheel_mode_ch2:
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
options:
|
||||||
|
- 'lights: dim'
|
||||||
|
- 'lights: color temp and color hue'
|
||||||
|
- 'lights: color temp only'
|
||||||
|
- 'lights: color hue only'
|
||||||
|
- 'media: volume control'
|
||||||
|
custom_value: false
|
||||||
|
sort: false
|
||||||
|
multiple: false
|
||||||
|
default: dim_only
|
||||||
|
name: 'Channel 2: Scroll wheel mode'
|
||||||
|
description: Select attribute of target entity controlled by scroll wheel
|
||||||
|
channel3_section:
|
||||||
|
name: Channel 3
|
||||||
|
collapsed: true
|
||||||
|
input:
|
||||||
|
click_action_ch3:
|
||||||
|
name: 'Channel 3: <click> action'
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
double_click_action_ch3:
|
||||||
|
name: 'Channel 3: <double-click> action'
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
long_click_action_ch3:
|
||||||
|
name: 'Channel 3: <long-click> action'
|
||||||
|
description: ''
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
on_hold_action_ch3:
|
||||||
|
name: 'Channel 3: <on-hold> action'
|
||||||
|
description: repeat action until button is released, see also on_hold_delay
|
||||||
|
default: []
|
||||||
|
selector:
|
||||||
|
action: {}
|
||||||
|
scroll_wheel_target_ch3:
|
||||||
|
name: 'Channel 3: Scroll wheel target entity'
|
||||||
|
description: Light or media player to be controlled by scroll wheel for
|
||||||
|
channel 3
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
multiple: true
|
||||||
|
filter:
|
||||||
|
- domain:
|
||||||
|
- light
|
||||||
|
- media_player
|
||||||
|
reorder: false
|
||||||
|
default: []
|
||||||
|
scroll_wheel_mode_ch3:
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
options:
|
||||||
|
- 'lights: dim'
|
||||||
|
- 'lights: color temp and color hue'
|
||||||
|
- 'lights: color temp only'
|
||||||
|
- 'lights: color hue only'
|
||||||
|
- 'media: volume control'
|
||||||
|
custom_value: false
|
||||||
|
sort: false
|
||||||
|
multiple: false
|
||||||
|
default: dim_only
|
||||||
|
name: 'Channel 3: Scroll wheel mode'
|
||||||
|
description: Select attribute of target entity controlled by scroll wheel
|
||||||
|
global_section:
|
||||||
|
name: Global settings
|
||||||
|
collapsed: true
|
||||||
|
input:
|
||||||
|
dim_step_pct:
|
||||||
|
name: Dimmer step percentage
|
||||||
|
description: change in brightness in %
|
||||||
|
default: 5
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
mode: box
|
||||||
|
step: 1.0
|
||||||
|
dim_min_pct:
|
||||||
|
name: Minimum brightness
|
||||||
|
description: minimum brightness in %
|
||||||
|
default: 5
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
mode: box
|
||||||
|
step: 1.0
|
||||||
|
min_color_temp:
|
||||||
|
name: Minimum color temperature
|
||||||
|
description: minimum color temperature in K (for color_mode = color_temp)
|
||||||
|
default: 2200
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
mode: box
|
||||||
|
step: 1.0
|
||||||
|
max_color_temp:
|
||||||
|
name: Maximum color temperature
|
||||||
|
description: maximum color temperature in K (for color_mode = color_temp)
|
||||||
|
default: 4000
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
mode: box
|
||||||
|
step: 1.0
|
||||||
|
color_temp_step:
|
||||||
|
name: Color temperature step
|
||||||
|
description: change in color temperature in K (for color_mode = color_temp)
|
||||||
|
default: 100
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
mode: box
|
||||||
|
step: 1.0
|
||||||
|
color_hue_step:
|
||||||
|
name: Color hue step
|
||||||
|
description: change in color hue in degree (0..360) (for color_mode = color_hue)
|
||||||
|
default: 4
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
mode: box
|
||||||
|
step: 1.0
|
||||||
|
color_saturation:
|
||||||
|
name: Color saturation
|
||||||
|
description: color saturation in % (for color_mode = color_hue)
|
||||||
|
default: 100
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
mode: box
|
||||||
|
step: 1.0
|
||||||
|
on_hold_delay:
|
||||||
|
name: On hold delay
|
||||||
|
description: delay between separate action calls while on hold button
|
||||||
|
default: 0.1
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
mode: box
|
||||||
|
step: 0.1
|
||||||
|
volume_step_pct:
|
||||||
|
name: Media player volume step percentage
|
||||||
|
description: change in volume in %
|
||||||
|
default: 2
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
mode: box
|
||||||
|
step: 1.0
|
||||||
|
volume_max_pct:
|
||||||
|
name: Media player max volume percentage
|
||||||
|
description: max volume in %
|
||||||
|
default: 50
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
mode: box
|
||||||
|
step: 1.0
|
||||||
|
source_url: https://gist.github.com/jhol-byte/b2731a4d2476f530d76b9ff409f7f3a4
|
||||||
|
alias: Ikea_bilresa_scroll_wheel
|
||||||
|
trigger:
|
||||||
|
- event_type: state_changed
|
||||||
|
event_data:
|
||||||
|
entity_id: '{{ var_button_ch1 }}'
|
||||||
|
trigger: event
|
||||||
|
id: button_ch1
|
||||||
|
- event_type: state_changed
|
||||||
|
event_data:
|
||||||
|
entity_id: '{{ var_scroll_left_ch1 }}'
|
||||||
|
trigger: event
|
||||||
|
id: scroll_left_ch1
|
||||||
|
- event_type: state_changed
|
||||||
|
event_data:
|
||||||
|
entity_id: '{{ var_scroll_right_ch1 }}'
|
||||||
|
trigger: event
|
||||||
|
id: scroll_right_ch1
|
||||||
|
- event_type: state_changed
|
||||||
|
event_data:
|
||||||
|
entity_id: '{{ var_button_ch2 }}'
|
||||||
|
trigger: event
|
||||||
|
id: button_ch2
|
||||||
|
- event_type: state_changed
|
||||||
|
event_data:
|
||||||
|
entity_id: '{{ var_scroll_left_ch2 }}'
|
||||||
|
trigger: event
|
||||||
|
id: scroll_left_ch2
|
||||||
|
- event_type: state_changed
|
||||||
|
event_data:
|
||||||
|
entity_id: '{{ var_scroll_right_ch2 }}'
|
||||||
|
trigger: event
|
||||||
|
id: scroll_right_ch2
|
||||||
|
- event_type: state_changed
|
||||||
|
event_data:
|
||||||
|
entity_id: '{{ var_button_ch3 }}'
|
||||||
|
trigger: event
|
||||||
|
id: button_ch3
|
||||||
|
- event_type: state_changed
|
||||||
|
event_data:
|
||||||
|
entity_id: '{{ var_scroll_left_ch3 }}'
|
||||||
|
trigger: event
|
||||||
|
id: scroll_left_ch3
|
||||||
|
- event_type: state_changed
|
||||||
|
event_data:
|
||||||
|
entity_id: '{{ var_scroll_right_ch3 }}'
|
||||||
|
trigger: event
|
||||||
|
id: scroll_right_ch3
|
||||||
|
condition:
|
||||||
|
- condition: template
|
||||||
|
value_template: "{{ trigger.event.data.old_state.state != 'unavailable' \n and
|
||||||
|
trigger.event.data.new_state.state != 'unavailable'\n}}"
|
||||||
|
action:
|
||||||
|
- variables:
|
||||||
|
channel: "{% if trigger.id in ['button_ch1', 'scroll_left_ch1', 'scroll_right_ch1']
|
||||||
|
%}\n ch1\n{% elif trigger.id in ['button_ch2', 'scroll_left_ch2', 'scroll_right_ch2']
|
||||||
|
%}\n ch2\n{% elif trigger.id in ['button_ch3', 'scroll_left_ch3', 'scroll_right_ch3']
|
||||||
|
%}\n ch3\n{% endif %}"
|
||||||
|
- choose:
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ trigger.id == (''button_'' ~ channel) and trigger.event.data.new_state.attributes.event_type
|
||||||
|
== ''multi_press_1'' }}
|
||||||
|
|
||||||
|
'
|
||||||
|
sequence:
|
||||||
|
- if: '{{ channel == ''ch1''}}'
|
||||||
|
then: !input click_action_ch1
|
||||||
|
- if: '{{ channel == ''ch2''}}'
|
||||||
|
then: !input click_action_ch2
|
||||||
|
- if: '{{ channel == ''ch3''}}'
|
||||||
|
then: !input click_action_ch3
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ trigger.id == (''button_'' ~ channel) and trigger.event.data.new_state.attributes.event_type
|
||||||
|
== ''multi_press_2'' }}
|
||||||
|
|
||||||
|
'
|
||||||
|
sequence:
|
||||||
|
- if: '{{ channel == ''ch1''}}'
|
||||||
|
then: !input double_click_action_ch1
|
||||||
|
- if: '{{ channel == ''ch2''}}'
|
||||||
|
then: !input double_click_action_ch2
|
||||||
|
- if: '{{ channel == ''ch3''}}'
|
||||||
|
then: !input double_click_action_ch3
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ trigger.id == (''button_'' ~ channel) and trigger.event.data.new_state.attributes.event_type
|
||||||
|
== ''long_press'' }}
|
||||||
|
|
||||||
|
'
|
||||||
|
sequence:
|
||||||
|
- if: '{{ channel == ''ch1''}}'
|
||||||
|
then:
|
||||||
|
sequence:
|
||||||
|
- sequence: !input long_click_action_ch1
|
||||||
|
- repeat:
|
||||||
|
while: '{{ repeat.index < 100 and state_attr(var_button_ch1, ''event_type'')
|
||||||
|
== ''long_press'' }}'
|
||||||
|
sequence:
|
||||||
|
- sequence: !input on_hold_action_ch1
|
||||||
|
- delay: '{{ var_on_hold_delay }}'
|
||||||
|
- if: '{{ channel == ''ch2''}}'
|
||||||
|
then:
|
||||||
|
sequence:
|
||||||
|
- sequence: !input long_click_action_ch2
|
||||||
|
- repeat:
|
||||||
|
while: '{{ repeat.index < 100 and state_attr(var_button_ch2, ''event_type'')
|
||||||
|
== ''long_press'' }}'
|
||||||
|
sequence:
|
||||||
|
- sequence: !input on_hold_action_ch2
|
||||||
|
- delay: '{{ var_on_hold_delay }}'
|
||||||
|
- if: '{{ channel == ''ch3''}}'
|
||||||
|
then:
|
||||||
|
sequence:
|
||||||
|
- sequence: !input long_click_action_ch3
|
||||||
|
- repeat:
|
||||||
|
while: '{{ repeat.index < 100 and state_attr(var_button_ch3, ''event_type'')
|
||||||
|
== ''long_press'' }}'
|
||||||
|
sequence:
|
||||||
|
- sequence: !input on_hold_action_ch3
|
||||||
|
- delay: '{{ var_on_hold_delay }}'
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ trigger.id == (''scroll_left_'' ~ channel) or trigger.id
|
||||||
|
== (''scroll_right_'' ~ channel) }}
|
||||||
|
|
||||||
|
'
|
||||||
|
sequence:
|
||||||
|
- repeat:
|
||||||
|
for_each: '{{ vars[''targets_entities_'' ~ channel] }}'
|
||||||
|
sequence:
|
||||||
|
- choose:
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ vars[''scroll_wheel_mode_'' ~ channel] == ''lights:
|
||||||
|
dim''}}'
|
||||||
|
sequence:
|
||||||
|
- if:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ is_state(repeat.item, ''on'') }}'
|
||||||
|
then:
|
||||||
|
- action: light.turn_on
|
||||||
|
target:
|
||||||
|
entity_id: '{{ repeat.item }}'
|
||||||
|
data:
|
||||||
|
brightness_step_pct: "{{ iif(trigger.id == 'scroll_right_' ~ channel,
|
||||||
|
trigger.event.data.new_state.attributes.totalNumberOfPressesCounted
|
||||||
|
* var_dim_step_pct, -1*([trigger.event.data.new_state.attributes.totalNumberOfPressesCounted
|
||||||
|
* var_dim_step_pct, state_attr(repeat.item, 'brightness')*100/254\n
|
||||||
|
- var_min_pct]|min)) \n}}"
|
||||||
|
transition: 0.1
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ vars[''scroll_wheel_mode_'' ~ channel] == ''lights:
|
||||||
|
color temp only'' or (vars[''scroll_wheel_mode_'' ~ channel] == ''lights:
|
||||||
|
color temp and color hue'' and state_attr(repeat.item, ''color_temp_kelvin'')
|
||||||
|
is not none ) }}'
|
||||||
|
sequence:
|
||||||
|
- action: script.light_color_temp_helper
|
||||||
|
data:
|
||||||
|
mode: '{{ iif(trigger.id == ''scroll_left_'' ~ channel, ''down'',
|
||||||
|
''up'') }}'
|
||||||
|
min_color_temp: '{{ var_min_color_temp }}'
|
||||||
|
max_color_temp: '{{ var_max_color_temp }}'
|
||||||
|
color_temp_step: '{{ var_color_temp_step * trigger.event.data.new_state.attributes.totalNumberOfPressesCounted
|
||||||
|
}}'
|
||||||
|
target_light: '{{ repeat.item }}'
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ vars[''scroll_wheel_mode_'' ~ channel] == ''lights:
|
||||||
|
color hue only'' or (vars[''scroll_wheel_mode_'' ~ channel] == ''lights:
|
||||||
|
color temp and color hue'' and state_attr(repeat.item, ''color_temp_kelvin'')
|
||||||
|
is none ) }}'
|
||||||
|
sequence:
|
||||||
|
- action: script.light_color_hs_helper
|
||||||
|
data:
|
||||||
|
target_light: '{{ repeat.item }}'
|
||||||
|
color_hue_step: '{{ var_color_hue_step * trigger.event.data.new_state.attributes.totalNumberOfPressesCounted
|
||||||
|
}}'
|
||||||
|
color_saturation: '{{ var_color_saturation }}'
|
||||||
|
mode: '{{ iif(trigger.id == ''scroll_left_'' ~ channel, ''down'',
|
||||||
|
''up'') }}'
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: '{{ vars[''scroll_wheel_mode_'' ~ channel] == ''media:
|
||||||
|
volume control'' and is_state(repeat.item, ''playing'')}}'
|
||||||
|
sequence:
|
||||||
|
- action: media_player.volume_set
|
||||||
|
target:
|
||||||
|
entity_id: '{{ repeat.item }}'
|
||||||
|
data:
|
||||||
|
volume_level: "{{ [[state_attr(repeat.item, 'volume_level')*100 +
|
||||||
|
\n iif(trigger.id == 'scroll_right_' ~ channel, +1, -1)* trigger.event.data.new_state.attributes.totalNumberOfPressesCounted
|
||||||
|
* var_volume_step_pct, var_volume_max_pct] | min, 0] | max / 100\n}}"
|
||||||
|
mode: single
|
||||||
|
max_exceeded: silent
|
||||||
|
variables:
|
||||||
|
vars:
|
||||||
|
targets_entities_ch1: !input scroll_wheel_target_ch1
|
||||||
|
targets_entities_ch2: !input scroll_wheel_target_ch2
|
||||||
|
targets_entities_ch3: !input scroll_wheel_target_ch3
|
||||||
|
scroll_wheel_mode_ch1: !input scroll_wheel_mode_ch1
|
||||||
|
scroll_wheel_mode_ch2: !input scroll_wheel_mode_ch2
|
||||||
|
scroll_wheel_mode_ch3: !input scroll_wheel_mode_ch3
|
||||||
|
var_min_pct: !input dim_min_pct
|
||||||
|
var_dim_step_pct: !input dim_step_pct
|
||||||
|
var_color_hue_step: !input color_hue_step
|
||||||
|
var_color_saturation: !input color_saturation
|
||||||
|
var_min_color_temp: !input min_color_temp
|
||||||
|
var_max_color_temp: !input max_color_temp
|
||||||
|
var_color_temp_step: !input color_temp_step
|
||||||
|
var_on_hold_delay: !input on_hold_delay
|
||||||
|
var_volume_step_pct: !input volume_step_pct
|
||||||
|
var_volume_max_pct: !input volume_max_pct
|
||||||
|
trigger_variables:
|
||||||
|
var_remote: !input remote
|
||||||
|
var_button_ch1: "{% for item in device_entities(var_remote) %} {% set match = item
|
||||||
|
| regex_findall(find='^event\\..+[a-zA-Z]_3.*$') %} {% if (match | length) > 0
|
||||||
|
%}\n {{ match[0] }}\n{% endif %} {% endfor %}"
|
||||||
|
var_scroll_left_ch1: "{% for item in device_entities(var_remote) %} {% set match
|
||||||
|
= item | regex_findall(find='^event\\..+[a-zA-Z]_2.*$') %} {% if (match | length)
|
||||||
|
> 0 %}\n {{ match[0] }}\n{% endif %} {% endfor %}"
|
||||||
|
var_scroll_right_ch1: "{% for item in device_entities(var_remote) %} {% set match
|
||||||
|
= item | regex_findall(find='^event\\..+[a-zA-Z]_1.*$') %} {% if (match | length)
|
||||||
|
> 0 %}\n {{ match[0] }}\n{% endif %} {% endfor %}"
|
||||||
|
var_button_ch2: "{% for item in device_entities(var_remote) %} {% set match = item
|
||||||
|
| regex_findall(find='^event\\..+[a-zA-Z]_6.*$') %} {% if (match | length) > 0
|
||||||
|
%}\n {{ match[0] }}\n{% endif %} {% endfor %}"
|
||||||
|
var_scroll_left_ch2: "{% for item in device_entities(var_remote) %} {% set match
|
||||||
|
= item | regex_findall(find='^event\\..+[a-zA-Z]_5.*$') %} {% if (match | length)
|
||||||
|
> 0 %}\n {{ match[0] }}\n{% endif %} {% endfor %}"
|
||||||
|
var_scroll_right_ch2: "{% for item in device_entities(var_remote) %} {% set match
|
||||||
|
= item | regex_findall(find='^event\\..+[a-zA-Z]_4.*$') %} {% if (match | length)
|
||||||
|
> 0 %}\n {{ match[0] }}\n{% endif %} {% endfor %}"
|
||||||
|
var_button_ch3: "{% for item in device_entities(var_remote) %} {% set match = item
|
||||||
|
| regex_findall(find='^event\\..+[a-zA-Z]_9.*$') %} {% if (match | length) > 0
|
||||||
|
%}\n {{ match[0] }}\n{% endif %} {% endfor %}"
|
||||||
|
var_scroll_left_ch3: "{% for item in device_entities(var_remote) %} {% set match
|
||||||
|
= item | regex_findall(find='^event\\..+[a-zA-Z]_8.*$') %} {% if (match | length)
|
||||||
|
> 0 %}\n {{ match[0] }}\n{% endif %} {% endfor %}"
|
||||||
|
var_scroll_right_ch3: "{% for item in device_entities(var_remote) %} {% set match
|
||||||
|
= item | regex_findall(find='^event\\..+[a-zA-Z]_7.*$') %} {% if (match | length)
|
||||||
|
> 0 %}\n {{ match[0] }}\n{% endif %} {% endfor %}"
|
||||||
@@ -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: []
|
||||||
@@ -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: |-
|
||||||
|
{}
|
||||||
@@ -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') }}"
|
||||||
@@ -11,15 +11,13 @@ http:
|
|||||||
tts:
|
tts:
|
||||||
- platform: picotts_remote
|
- platform: picotts_remote
|
||||||
language: "de-DE"
|
language: "de-DE"
|
||||||
- platform: google_translate
|
|
||||||
language: "de"
|
|
||||||
|
|
||||||
# Include modules
|
# Include modules
|
||||||
group: !include groups.yaml
|
group: !include groups.yaml
|
||||||
automation: !include automations.yaml
|
automation: !include automations.yaml
|
||||||
automation webhooks: !include automations_webhooks.yaml
|
|
||||||
script: !include scripts.yaml
|
script: !include scripts.yaml
|
||||||
scene: !include scenes.yaml
|
scene: !include scenes.yaml
|
||||||
|
notify: !include notify.yaml
|
||||||
|
|
||||||
# Enable additional integrations
|
# Enable additional integrations
|
||||||
# Enable 'wake_on_lan' intrgration
|
# Enable 'wake_on_lan' intrgration
|
||||||
@@ -35,27 +33,24 @@ utility_meter: !include utility_meters.yaml
|
|||||||
|
|
||||||
# MQTT sensors
|
# MQTT sensors
|
||||||
mqtt: !include mqtt.yaml
|
mqtt: !include mqtt.yaml
|
||||||
|
mqtt_statestream: !include mqtt_statestream.yaml
|
||||||
|
|
||||||
|
# Calendar data is normalised in a sandboxed script before ESPHome receives it.
|
||||||
|
python_script:
|
||||||
|
|
||||||
|
# Home Assistant-managed e-paper quote library
|
||||||
|
input_select: !include input_select.yaml
|
||||||
|
|
||||||
# Template sensors
|
# Template sensors
|
||||||
template:
|
template: !include template.yaml
|
||||||
- sensor:
|
|
||||||
- name: "Leckstrom"
|
# Database size sensor
|
||||||
unit_of_measurement: "W"
|
sql:
|
||||||
icon: mdi:flash
|
- name: "Home Assistant Datenbankgröße"
|
||||||
state: >
|
db_url: !secret ha_recorder_dburl
|
||||||
{% set total = states('sensor.netzleistung') | float %}
|
query: 'SELECT table_schema "database", Round(Sum(data_length + index_length) / 1048576, 2) "value" FROM information_schema.tables WHERE table_schema="homeassistant" GROUP BY table_schema;'
|
||||||
{% set raumduft = states('sensor.flur_raumduft_power') | float %}
|
column: 'value'
|
||||||
{% set keller = states('sensor.keller_power') | float %}
|
unit_of_measurement: MB
|
||||||
{% 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) }}
|
|
||||||
|
|
||||||
# calendar integration
|
# calendar integration
|
||||||
calendar: !include calendars.yaml
|
calendar: !include calendars.yaml
|
||||||
@@ -63,34 +58,19 @@ calendar: !include calendars.yaml
|
|||||||
# DB-recorder configuration
|
# DB-recorder configuration
|
||||||
recorder: !include recorder.yaml
|
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
|
# Enable Bluetooth
|
||||||
bluetooth:
|
bluetooth:
|
||||||
|
|
||||||
# Bluetooth Low Energy tracker
|
# Bluetooth Low Energy tracker
|
||||||
device_tracker:
|
device_tracker:
|
||||||
- platform: bluetooth_le_tracker
|
- platform: bluetooth_le_tracker
|
||||||
track_new_devices: true
|
track_new_devices: false
|
||||||
|
|
||||||
# Configure MPD addon as media player
|
|
||||||
media_player:
|
|
||||||
platform: mpd
|
|
||||||
host: 192.168.122.48
|
|
||||||
scan_interval: 1
|
|
||||||
|
|
||||||
generic_hygrostat:
|
generic_hygrostat:
|
||||||
- name: Badezimmer
|
- name: Badezimmer
|
||||||
|
unique_id: '3728344225387'
|
||||||
humidifier: fan.badezimmer_ventilator
|
humidifier: fan.badezimmer_ventilator
|
||||||
target_sensor: sensor.badezimmer_luftfeuchtigkeit
|
target_sensor: sensor.bathroom_badezimmer_luftfeuchtigkeit
|
||||||
min_humidity: 30
|
min_humidity: 30
|
||||||
max_humidity: 70
|
max_humidity: 70
|
||||||
target_humidity: 50
|
target_humidity: 50
|
||||||
@@ -106,3 +86,10 @@ generic_hygrostat:
|
|||||||
away_fixed: true
|
away_fixed: true
|
||||||
sensor_stale_duration: 00:15:00
|
sensor_stale_duration: 00:15:00
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
themes: !include_dir_merge_named themes
|
||||||
|
|
||||||
|
freedns:
|
||||||
|
url: !secret freedns6_url
|
||||||
|
# - name: IPv4
|
||||||
|
# url: !secret freedns4_url
|
||||||
|
|||||||
@@ -0,0 +1,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
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
@@ -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]
|
||||||
@@ -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
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Home Assistant Setup**
|
||||||
|
Please indicate your version of HA and how it is installed.
|
||||||
|
|
||||||
|
Version:
|
||||||
|
|
||||||
|
Installation Type (put an X between the square brackets for your HA):
|
||||||
|
[] Home Assistant OS
|
||||||
|
[] Home Assistant Supervised
|
||||||
|
[] Home Assistant Container
|
||||||
|
[] Home Assistant Core
|
||||||
|
|
||||||
|
Hardware platform:
|
||||||
|
[] ARM
|
||||||
|
[] x86-64
|
||||||
|
|
||||||
|
Are you running in a container environment like Docker or Kubernetes?
|
||||||
|
[] Yes
|
||||||
|
[] No
|
||||||
|
|
||||||
|
If running in a container, how is your image built?
|
||||||
|
[] Official HA container image
|
||||||
|
[] Official HA container image with customizations
|
||||||
|
[] Custom built container image
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of the bug
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# To get started with Dependabot version updates, you'll need to specify which
|
||||||
|
# package ecosystems to update and where the package manifests are located.
|
||||||
|
# Please see the documentation for all configuration options:
|
||||||
|
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "pip" # See documentation for possible values
|
||||||
|
directory: "/" # Location of package manifests
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
day: "monday"
|
||||||
|
reviewers:
|
||||||
|
- "franc6"
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
Fixes #
|
||||||
|
|
||||||
|
Description of change:
|
||||||
|
|
||||||
|
## Formatting, testing, and code coverage
|
||||||
|
Please note your pull request won't be accepted if you haven't properly formatted your source code, and ensured the unit tests are appropriate. Please note if you are not running on Windows, you can either run the scripts via a bash installation (like git-bash).
|
||||||
|
|
||||||
|
- [] formatstyle.sh reports no errors
|
||||||
|
- [] All unit tests pass (test.sh)
|
||||||
|
- [] Code coverage has not decreased (test.sh)
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
name: Validate with hassfest and run HACS action
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 0 * * *'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
validate:
|
||||||
|
runs-on: "ubuntu-latest"
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: "actions/checkout@v4"
|
||||||
|
- name: Validate with hassfest
|
||||||
|
uses: "home-assistant/actions/hassfest@master"
|
||||||
|
- name: HACS Action
|
||||||
|
uses: "hacs/action@main"
|
||||||
|
with:
|
||||||
|
category: integration
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
name: Lint
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [releases]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install uv
|
||||||
|
uses: astral-sh/setup-uv@v3
|
||||||
|
with:
|
||||||
|
version: "0.4.20"
|
||||||
|
|
||||||
|
- name: Setup Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version-file: "pyproject.toml"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
uv sync --prerelease=allow --dev --extra tests
|
||||||
|
|
||||||
|
- name: Run isort --check
|
||||||
|
run: |
|
||||||
|
uv run --prerelease=allow isort --check custom_components/ics_calendar tests
|
||||||
|
|
||||||
|
- name: Run black --check
|
||||||
|
run: |
|
||||||
|
uv run --prerelease=allow black --check custom_components/ics_calendar tests
|
||||||
|
|
||||||
|
- name: Run flake8
|
||||||
|
run: |
|
||||||
|
uv run --prerelease=allow flake8
|
||||||
|
|
||||||
|
- name: Run pydocstyle
|
||||||
|
run: |
|
||||||
|
uv run --prerelease=allow pydocstyle -v custom_components/ics_calendar tests
|
||||||
|
|
||||||
|
- name: Run pylint
|
||||||
|
run: |
|
||||||
|
uv run --prerelease=allow pylint custom_components/ics_calendar
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
name: Run Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [releases]
|
||||||
|
pull_request:
|
||||||
|
branches: [releases]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Set timezone
|
||||||
|
run: |
|
||||||
|
sudo timedatectl set-timezone America/New_York
|
||||||
|
timedatectl
|
||||||
|
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install uv
|
||||||
|
uses: astral-sh/setup-uv@v3
|
||||||
|
with:
|
||||||
|
version: "0.4.20"
|
||||||
|
|
||||||
|
- name: Setup Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version-file: "pyproject.toml"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
uv sync --prerelease=allow --extra tests
|
||||||
|
|
||||||
|
- name: Run pytest
|
||||||
|
run: |
|
||||||
|
PYTHONDONTWRITEBYTECODE=1 uv run --prerelease=allow pytest tests/
|
||||||
|
|
||||||
|
- name: Upload coverage to Codecov
|
||||||
|
uses: codecov/codecov-action@v4
|
||||||
|
with:
|
||||||
|
fail_ci_if_error: true
|
||||||
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
|
||||||
@@ -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, PLATFORMS)
|
||||||
|
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
|
||||||
@@ -0,0 +1,382 @@
|
|||||||
|
"""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]
|
||||||
|
),
|
||||||
|
},
|
||||||
|
).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.timeout(
|
||||||
|
device_data.get(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)
|
||||||
|
await self._hass.async_add_executor_job(
|
||||||
|
lambda: self.parser.set_content(self._calendar_data.get())
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
event_list = self.parser.get_event_list(
|
||||||
|
start=start_date,
|
||||||
|
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)
|
||||||
|
await self._hass.async_add_executor_job(
|
||||||
|
lambda: self.parser.set_content(self._calendar_data.get())
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
parser_event: ParserEvent | None = self.parser.get_current_event(
|
||||||
|
include_all_day=self.include_all_day,
|
||||||
|
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
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
"""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 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))
|
||||||
|
return self
|
||||||
|
|
||||||
|
def timeout(self, connection_timeout: float | None):
|
||||||
|
"""Set the connection timeout.
|
||||||
|
|
||||||
|
:param connection_timeout: The timeout value in seconds.
|
||||||
|
:type connection_timeout: float
|
||||||
|
"""
|
||||||
|
if connection_timeout:
|
||||||
|
self.connection_timeout = connection_timeout
|
||||||
|
return self
|
||||||
|
|
||||||
|
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)
|
||||||
|
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)
|
||||||
@@ -0,0 +1,335 @@
|
|||||||
|
"""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}
|
||||||
|
)
|
||||||
|
|
||||||
|
FILTER_DOC_URL = (
|
||||||
|
"https://github.com/franc6/ics_calendar/blob/releases/README.md#filters"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def is_array_string(arr_str: str) -> bool:
|
||||||
|
"""Return true if arr_str starts with [ and ends with ]."""
|
||||||
|
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,
|
||||||
|
description_placeholders={"filterdoc": FILTER_DOC_URL},
|
||||||
|
)
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
"""Constants for ics_calendar platform."""
|
||||||
|
|
||||||
|
VERSION = "5.1.7"
|
||||||
|
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"
|
||||||
@@ -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)
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
"""Provide GetParser class."""
|
||||||
|
|
||||||
|
from .icalendarparser import ICalendarParser
|
||||||
|
|
||||||
|
|
||||||
|
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":
|
||||||
|
from .parsers.parser_rie import ( # pylint: disable=C0415
|
||||||
|
ParserRIE,
|
||||||
|
)
|
||||||
|
|
||||||
|
return ParserRIE(*args)
|
||||||
|
if parser == "ics":
|
||||||
|
from .parsers.parser_ics import ( # pylint: disable=C0415
|
||||||
|
ParserICS,
|
||||||
|
)
|
||||||
|
|
||||||
|
return ParserICS(*args)
|
||||||
|
|
||||||
|
return None
|
||||||
@@ -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
|
||||||
|
"""
|
||||||
@@ -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","tatsu>=4.2.6,<5.8.0"],
|
||||||
|
"version": "5.1.7"
|
||||||
|
}
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Provide parsers."""
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
"""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
|
||||||
|
rrule = None
|
||||||
|
for extra in event.extra:
|
||||||
|
if extra.name == "RRULE":
|
||||||
|
rrule = extra.value
|
||||||
|
calendar_event: ParserEvent = ParserEvent(
|
||||||
|
uid=event.uid,
|
||||||
|
summary=summary,
|
||||||
|
start=ParserICS.get_date(
|
||||||
|
event.begin, event.all_day, offset_hours
|
||||||
|
),
|
||||||
|
end=ParserICS.get_date(
|
||||||
|
event.end, event.all_day, offset_hours
|
||||||
|
),
|
||||||
|
location=event.location,
|
||||||
|
description=event.description,
|
||||||
|
rrule=rrule,
|
||||||
|
recurrence_id=(
|
||||||
|
ParserICS.get_date(
|
||||||
|
event.begin, event.all_day, offset_hours
|
||||||
|
)
|
||||||
|
if rrule
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if self._filter.filter_event(calendar_event):
|
||||||
|
event_list.append(calendar_event)
|
||||||
|
|
||||||
|
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
|
||||||
|
rrule = None
|
||||||
|
for extra in temp_event.extra:
|
||||||
|
if extra.name == "RRULE":
|
||||||
|
rrule = extra.value
|
||||||
|
return ParserEvent(
|
||||||
|
uid=temp_event.uid,
|
||||||
|
summary=summary,
|
||||||
|
start=ParserICS.get_date(
|
||||||
|
temp_event.begin, temp_event.all_day, offset_hours
|
||||||
|
),
|
||||||
|
end=ParserICS.get_date(
|
||||||
|
temp_event.end, temp_event.all_day, offset_hours
|
||||||
|
),
|
||||||
|
location=temp_event.location,
|
||||||
|
description=temp_event.description,
|
||||||
|
rrule=rrule,
|
||||||
|
recurrence_id=(
|
||||||
|
ParserICS.get_date(
|
||||||
|
temp_event.begin, temp_event.all_day, offset_hours
|
||||||
|
)
|
||||||
|
if rrule
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
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
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
"""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(
|
||||||
|
uid=event.get("UID"),
|
||||||
|
summary=event.get("SUMMARY"),
|
||||||
|
start=start,
|
||||||
|
end=end,
|
||||||
|
location=event.get("LOCATION"),
|
||||||
|
description=event.get("DESCRIPTION"),
|
||||||
|
# rrule=event.get("RRULE"),
|
||||||
|
recurrence_id=ParserRIE.get_date(
|
||||||
|
event.get("RECURRENCE-ID").dt
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if self._filter.filter_event(calendar_event):
|
||||||
|
event_list.append(calendar_event)
|
||||||
|
|
||||||
|
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(
|
||||||
|
uid=temp_event.get("UID"),
|
||||||
|
summary=temp_event.get("SUMMARY"),
|
||||||
|
start=temp_start,
|
||||||
|
end=temp_end,
|
||||||
|
location=temp_event.get("LOCATION"),
|
||||||
|
description=temp_event.get("DESCRIPTION"),
|
||||||
|
# rrule=temp_event.get("RRULE"),
|
||||||
|
recurrence_id=ParserRIE.get_date(
|
||||||
|
temp_event.get("RECURRENCE-ID").dt
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
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
|
||||||
@@ -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 {filterdoc} for more information.",
|
||||||
|
"include_must_be_array": "The include option must be an array of strings or regular expressions. See {filterdoc} for more information."
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {filterdoc}.",
|
||||||
|
"include_must_be_array": "Die \"einzuschließenden Ereignisse\" müssen ein Array von Zeichenfolgen oder regulären Ausdrücken sein. Weitere Informationen finden Sie unter {filterdoc}."
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {filterdoc} for more information.",
|
||||||
|
"include_must_be_array": "The include option must be an array of strings or regular expressions. See {filterdoc} for more information."
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {filterdoc} para obtener más información.",
|
||||||
|
"include_must_be_array": "La opción de inclusión debe ser un array de cadenas o expresiones regulares. Consulte {filterdoc} para obtener más información."
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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": "L'option d'exclusion doit être un tableau de chaînes ou d'expressions régulières. Voir {filterdoc} pour plus d'informations.",
|
||||||
|
"include_must_be_array": "L'option d'inclusion doit être un tableau de chaînes ou d'expressions régulières. Voir {filterdoc} pour plus d'informations."
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {filterdoc} para mais informações.",
|
||||||
|
"include_must_be_array": "A opção de inclusão deve ser um array de strings ou expressões regulares. Veja {filterdoc} para mais informações."
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
network:
|
||||||
|
enable_ipv6: true
|
||||||
|
|
||||||
|
wifi:
|
||||||
|
ssid: "Voltage-legacy"
|
||||||
|
password: !secret voltage_legacy_psk
|
||||||
|
#use_address: atorch-dc-load.home
|
||||||
|
power_save_mode: high
|
||||||
|
fast_connect: on
|
||||||
|
min_auth_mode: WPA2
|
||||||
|
|
||||||
|
# Enable fallback hotspot (captive portal) in case wifi connection fails
|
||||||
|
ap:
|
||||||
|
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"
|
||||||
@@ -1,12 +1,25 @@
|
|||||||
|
substitutions:
|
||||||
|
name: "aussensensor"
|
||||||
|
friendly_name: "Außensensor"
|
||||||
|
|
||||||
esphome:
|
esphome:
|
||||||
name: aussensensor
|
name: ${name}
|
||||||
platform: ESP32
|
friendly_name: ${friendly_name}
|
||||||
|
name_add_mac_suffix: false
|
||||||
|
project:
|
||||||
|
name: sensor.outdoor
|
||||||
|
version: "0.8"
|
||||||
|
min_version: 2022.1.0
|
||||||
|
|
||||||
|
esp32:
|
||||||
board: nodemcu-32s
|
board: nodemcu-32s
|
||||||
|
framework:
|
||||||
|
type: arduino
|
||||||
|
|
||||||
wifi:
|
wifi:
|
||||||
ssid: "Voltage-legacy"
|
ssid: "Voltage-legacy"
|
||||||
password: !secret voltage_legacy_psk
|
password: !secret voltage_legacy_psk
|
||||||
use_address: aussensensor.home
|
use_address: ${name}.home
|
||||||
power_save_mode: high
|
power_save_mode: high
|
||||||
fast_connect: on
|
fast_connect: on
|
||||||
|
|
||||||
@@ -26,6 +39,7 @@ api:
|
|||||||
key: !secret apikey
|
key: !secret apikey
|
||||||
|
|
||||||
ota:
|
ota:
|
||||||
|
platform: esphome
|
||||||
password: !secret ota
|
password: !secret ota
|
||||||
|
|
||||||
# DHT22 sensor
|
# DHT22 sensor
|
||||||
@@ -0,0 +1,293 @@
|
|||||||
|
substitutions:
|
||||||
|
name: "owon-b35t"
|
||||||
|
friendly_name: "OWON B35T Multimeter"
|
||||||
|
device_description: "M5Stack Core 1 BLE client for OWON B35T/B35T+ multimeter with local graphical display"
|
||||||
|
owon_mac_address: !secret owon_b35t_mac_address
|
||||||
|
|
||||||
|
esphome:
|
||||||
|
name: ${name}
|
||||||
|
friendly_name: ${friendly_name}
|
||||||
|
comment: ${device_description}
|
||||||
|
min_version: 2024.6.0
|
||||||
|
includes:
|
||||||
|
- owon_b35t-m5stack-core1.h
|
||||||
|
project:
|
||||||
|
name: "custom.owon-b35t-m5stack"
|
||||||
|
version: "1.0"
|
||||||
|
|
||||||
|
esp32:
|
||||||
|
board: m5stack-core-esp32
|
||||||
|
framework:
|
||||||
|
type: esp-idf
|
||||||
|
advanced:
|
||||||
|
minimum_chip_revision: "3.1"
|
||||||
|
|
||||||
|
logger:
|
||||||
|
level: INFO
|
||||||
|
|
||||||
|
api:
|
||||||
|
encryption:
|
||||||
|
key: !secret apikey
|
||||||
|
|
||||||
|
ota:
|
||||||
|
platform: esphome
|
||||||
|
password: !secret ota
|
||||||
|
|
||||||
|
wifi:
|
||||||
|
ssid: "Voltage-legacy"
|
||||||
|
password: !secret voltage_legacy_psk
|
||||||
|
#use_address: ${name}.home
|
||||||
|
power_save_mode: none
|
||||||
|
fast_connect: on
|
||||||
|
min_auth_mode: WPA2
|
||||||
|
ap:
|
||||||
|
ssid: "OWON B35T Fallback Hotspot"
|
||||||
|
password: !secret fallback_psk
|
||||||
|
|
||||||
|
# Disabled to save RAM on the M5Stack Core 1. Re-enable temporarily if Wi-Fi recovery is needed.
|
||||||
|
# captive_portal:
|
||||||
|
|
||||||
|
interval:
|
||||||
|
- interval: 10s
|
||||||
|
then:
|
||||||
|
- lambda: |-
|
||||||
|
ESP_LOGI("mem", "heap free=%u min_free=%u internal_free=%u internal_largest=%u dma_free=%u dma_largest=%u",
|
||||||
|
static_cast<unsigned>(esp_get_free_heap_size()),
|
||||||
|
static_cast<unsigned>(esp_get_minimum_free_heap_size()),
|
||||||
|
static_cast<unsigned>(heap_caps_get_free_size(MALLOC_CAP_INTERNAL)),
|
||||||
|
static_cast<unsigned>(heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL)),
|
||||||
|
static_cast<unsigned>(heap_caps_get_free_size(MALLOC_CAP_DMA)),
|
||||||
|
static_cast<unsigned>(heap_caps_get_largest_free_block(MALLOC_CAP_DMA)));
|
||||||
|
|
||||||
|
esp32_ble_tracker:
|
||||||
|
scan_parameters:
|
||||||
|
active: true
|
||||||
|
continuous: true
|
||||||
|
|
||||||
|
ble_client:
|
||||||
|
- mac_address: ${owon_mac_address}
|
||||||
|
id: owon_ble_client
|
||||||
|
on_connect:
|
||||||
|
then:
|
||||||
|
- lambda: |-
|
||||||
|
owon_meter.on_connect();
|
||||||
|
on_disconnect:
|
||||||
|
then:
|
||||||
|
- lambda: |-
|
||||||
|
owon_meter.on_disconnect();
|
||||||
|
- logger.log:
|
||||||
|
level: WARN
|
||||||
|
format: "OWON BLE meter disconnected; restarting M5Stack to reclaim heap"
|
||||||
|
- delay: 1s
|
||||||
|
- lambda: |-
|
||||||
|
esp_restart();
|
||||||
|
|
||||||
|
spi:
|
||||||
|
clk_pin: GPIO18
|
||||||
|
mosi_pin: GPIO23
|
||||||
|
miso_pin: GPIO19
|
||||||
|
|
||||||
|
output:
|
||||||
|
- platform: ledc
|
||||||
|
pin: GPIO32
|
||||||
|
id: lcd_backlight
|
||||||
|
|
||||||
|
light:
|
||||||
|
- platform: monochromatic
|
||||||
|
output: lcd_backlight
|
||||||
|
name: "${friendly_name} Backlight"
|
||||||
|
id: backlight
|
||||||
|
restore_mode: ALWAYS_ON
|
||||||
|
|
||||||
|
font:
|
||||||
|
- file: "fonts/Roboto-Regular.ttf"
|
||||||
|
id: meter_font
|
||||||
|
size: 15
|
||||||
|
glyphs:
|
||||||
|
[
|
||||||
|
" ",
|
||||||
|
"!",
|
||||||
|
"%",
|
||||||
|
"+",
|
||||||
|
"-",
|
||||||
|
".",
|
||||||
|
"/",
|
||||||
|
"0",
|
||||||
|
"1",
|
||||||
|
"2",
|
||||||
|
"3",
|
||||||
|
"4",
|
||||||
|
"5",
|
||||||
|
"6",
|
||||||
|
"7",
|
||||||
|
"8",
|
||||||
|
"9",
|
||||||
|
"<",
|
||||||
|
">",
|
||||||
|
"A",
|
||||||
|
"B",
|
||||||
|
"C",
|
||||||
|
"D",
|
||||||
|
"E",
|
||||||
|
"F",
|
||||||
|
"G",
|
||||||
|
"H",
|
||||||
|
"I",
|
||||||
|
"L",
|
||||||
|
"M",
|
||||||
|
"N",
|
||||||
|
"O",
|
||||||
|
"P",
|
||||||
|
"R",
|
||||||
|
"S",
|
||||||
|
"T",
|
||||||
|
"U",
|
||||||
|
"V",
|
||||||
|
"W",
|
||||||
|
"X",
|
||||||
|
"Y",
|
||||||
|
"Z",
|
||||||
|
"a",
|
||||||
|
"c",
|
||||||
|
"d",
|
||||||
|
"e",
|
||||||
|
"f",
|
||||||
|
"g",
|
||||||
|
"h",
|
||||||
|
"i",
|
||||||
|
"k",
|
||||||
|
"m",
|
||||||
|
"n",
|
||||||
|
"o",
|
||||||
|
"r",
|
||||||
|
"s",
|
||||||
|
"t",
|
||||||
|
"u",
|
||||||
|
"v",
|
||||||
|
"w",
|
||||||
|
"y",
|
||||||
|
"z",
|
||||||
|
"°",
|
||||||
|
"µ",
|
||||||
|
"Ω",
|
||||||
|
]
|
||||||
|
|
||||||
|
display:
|
||||||
|
- platform: ili9xxx
|
||||||
|
id: lcd
|
||||||
|
model: M5STACK
|
||||||
|
cs_pin: GPIO14
|
||||||
|
dc_pin: GPIO27
|
||||||
|
reset_pin: GPIO33
|
||||||
|
invert_colors: true
|
||||||
|
color_palette: 8BIT
|
||||||
|
rotation: 0
|
||||||
|
update_interval: 500ms
|
||||||
|
lambda: |-
|
||||||
|
owon_meter.render(it, id(meter_font));
|
||||||
|
|
||||||
|
binary_sensor:
|
||||||
|
- platform: gpio
|
||||||
|
id: button_a
|
||||||
|
pin:
|
||||||
|
number: GPIO39
|
||||||
|
inverted: true
|
||||||
|
internal: true
|
||||||
|
on_press:
|
||||||
|
then:
|
||||||
|
- lambda: |-
|
||||||
|
owon_meter.previous_button();
|
||||||
|
- platform: gpio
|
||||||
|
id: button_b
|
||||||
|
pin:
|
||||||
|
number: GPIO38
|
||||||
|
inverted: true
|
||||||
|
internal: true
|
||||||
|
on_click:
|
||||||
|
- min_length: 50ms
|
||||||
|
max_length: 1500ms
|
||||||
|
then:
|
||||||
|
- logger.log:
|
||||||
|
level: INFO
|
||||||
|
format: "OWON short press: %s"
|
||||||
|
args: ["owon_meter.selected_button_name()"]
|
||||||
|
- ble_client.ble_write:
|
||||||
|
id: owon_ble_client
|
||||||
|
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
|
||||||
|
characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb"
|
||||||
|
value: !lambda |-
|
||||||
|
std::vector<uint8_t> data = {owon_meter.selected_button, 0x01};
|
||||||
|
return data;
|
||||||
|
- min_length: 1500ms
|
||||||
|
max_length: 5000ms
|
||||||
|
then:
|
||||||
|
- logger.log:
|
||||||
|
level: INFO
|
||||||
|
format: "OWON long press: %s"
|
||||||
|
args: ["owon_meter.selected_button_name()"]
|
||||||
|
- ble_client.ble_write:
|
||||||
|
id: owon_ble_client
|
||||||
|
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
|
||||||
|
characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb"
|
||||||
|
value: !lambda |-
|
||||||
|
uint8_t press_type = (owon_meter.selected_button == 1 || owon_meter.selected_button == 5) ? 0x01 : 0x00;
|
||||||
|
std::vector<uint8_t> data = {owon_meter.selected_button, press_type};
|
||||||
|
return data;
|
||||||
|
- platform: gpio
|
||||||
|
id: button_c
|
||||||
|
pin:
|
||||||
|
number: GPIO37
|
||||||
|
inverted: true
|
||||||
|
internal: true
|
||||||
|
on_press:
|
||||||
|
then:
|
||||||
|
- lambda: |-
|
||||||
|
owon_meter.next_button();
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
name: "${friendly_name} Connected"
|
||||||
|
lambda: |-
|
||||||
|
return owon_meter.connected;
|
||||||
|
- platform: template
|
||||||
|
name: "${friendly_name} Overload"
|
||||||
|
lambda: |-
|
||||||
|
return owon_meter.overload;
|
||||||
|
- platform: template
|
||||||
|
name: "${friendly_name} Low Battery"
|
||||||
|
lambda: |-
|
||||||
|
return owon_meter.low_battery;
|
||||||
|
|
||||||
|
sensor:
|
||||||
|
- platform: ble_client
|
||||||
|
type: characteristic
|
||||||
|
ble_client_id: owon_ble_client
|
||||||
|
id: owon_notify_source
|
||||||
|
internal: true
|
||||||
|
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
|
||||||
|
characteristic_uuid: "0000fff4-0000-1000-8000-00805f9b34fb"
|
||||||
|
notify: true
|
||||||
|
update_interval: never
|
||||||
|
lambda: |-
|
||||||
|
owon_meter.handle_notify(x);
|
||||||
|
return owon_meter.value();
|
||||||
|
|
||||||
|
- platform: wifi_signal
|
||||||
|
name: "${friendly_name} WiFi Signal"
|
||||||
|
update_interval: 60s
|
||||||
|
|
||||||
|
text_sensor:
|
||||||
|
- platform: template
|
||||||
|
name: "${friendly_name} Reading"
|
||||||
|
update_interval: 2s
|
||||||
|
lambda: |-
|
||||||
|
return owon_meter.reading_text();
|
||||||
|
- platform: template
|
||||||
|
name: "${friendly_name} Unit"
|
||||||
|
update_interval: 2s
|
||||||
|
lambda: |-
|
||||||
|
return std::string(owon_meter.scale()) + owon_meter.unit();
|
||||||
|
- platform: template
|
||||||
|
name: "${friendly_name} Mode"
|
||||||
|
update_interval: 2s
|
||||||
|
lambda: |-
|
||||||
|
return owon_meter.mode_text();
|
||||||
@@ -0,0 +1,658 @@
|
|||||||
|
/*
|
||||||
|
* ESPHome helper for OWON B35T/B35T+ BLE meter on M5Stack Core 1.
|
||||||
|
* Parser is based on the standalone Arduino sketch by Reaper7
|
||||||
|
* (Beerware license, Revision 42) and Dean Cording's owonb35 notes.
|
||||||
|
*/
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstring>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include "esp_heap_caps.h"
|
||||||
|
#include "esp_system.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
#include "esphome/components/i2c/i2c.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
#include "esphome/components/display/display.h"
|
||||||
|
|
||||||
|
namespace owon_b35t {
|
||||||
|
|
||||||
|
using esphome::Color;
|
||||||
|
using esphome::display::Display;
|
||||||
|
|
||||||
|
static const char *const TAG = "owon_b35t";
|
||||||
|
static const char *const POWER_TAG = "core2_power";
|
||||||
|
static constexpr uint8_t AXP192_ADDR = 0x34;
|
||||||
|
static esphome::i2c::I2CDevice axp192;
|
||||||
|
static bool axp192_ready = false;
|
||||||
|
|
||||||
|
static bool axp_write(uint8_t reg, uint8_t value) {
|
||||||
|
if (!axp192_ready) return false;
|
||||||
|
bool ok = axp192.write_byte(reg, value);
|
||||||
|
if (!ok) ESP_LOGW(POWER_TAG, "AXP192 write reg 0x%02X failed", reg);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool axp_read(uint8_t reg, uint8_t *value) {
|
||||||
|
if (!axp192_ready) return false;
|
||||||
|
bool ok = axp192.read_byte(reg, value);
|
||||||
|
if (!ok) ESP_LOGW(POWER_TAG, "AXP192 read reg 0x%02X failed", reg);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void axp_update(uint8_t reg, uint8_t clear_mask, uint8_t set_mask) {
|
||||||
|
uint8_t value = 0;
|
||||||
|
if (!axp_read(reg, &value)) return;
|
||||||
|
value = (value & ~clear_mask) | set_mask;
|
||||||
|
axp_write(reg, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint8_t axp_dc_voltage_data(uint16_t millivolts) {
|
||||||
|
if (millivolts < 700) millivolts = 700;
|
||||||
|
if (millivolts > 3500) millivolts = 3500;
|
||||||
|
return static_cast<uint8_t>((millivolts - 700) / 25) & 0x7F;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint8_t axp_ldo_voltage_data(uint16_t millivolts) {
|
||||||
|
if (millivolts < 1800) millivolts = 1800;
|
||||||
|
if (millivolts > 3300) millivolts = 3300;
|
||||||
|
return static_cast<uint8_t>((millivolts - 1800) / 100) & 0x0F;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void core2_axp192_set_lcd_voltage(uint16_t millivolts) {
|
||||||
|
uint8_t value = 0;
|
||||||
|
axp_read(0x27, &value);
|
||||||
|
axp_write(0x27, (value & 0x80) | axp_dc_voltage_data(millivolts)); // DCDC3, LCD backlight
|
||||||
|
}
|
||||||
|
|
||||||
|
static void core2_axp192_set_backlight(float brightness) {
|
||||||
|
if (brightness <= 0.0f) {
|
||||||
|
axp_update(0x12, 0x02, 0x00); // DCDC3 off
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (brightness > 1.0f) brightness = 1.0f;
|
||||||
|
uint16_t millivolts = static_cast<uint16_t>(2400 + brightness * 900);
|
||||||
|
core2_axp192_set_lcd_voltage(millivolts);
|
||||||
|
axp_update(0x12, 0x00, 0x02); // DCDC3 on
|
||||||
|
}
|
||||||
|
|
||||||
|
static void core2_axp192_init(esphome::i2c::I2CBus *bus) {
|
||||||
|
axp192.set_i2c_bus(bus);
|
||||||
|
axp192.set_i2c_address(AXP192_ADDR);
|
||||||
|
axp192_ready = true;
|
||||||
|
ESP_LOGI(POWER_TAG, "Initializing M5Stack Core2 AXP192 LCD power");
|
||||||
|
axp_update(0x30, 0xF9, 0x02); // Disable VBUS current limit, preserve bit 2.
|
||||||
|
axp_update(0x92, 0x07, 0x00); // GPIO1 open-drain output.
|
||||||
|
axp_update(0x93, 0x07, 0x00); // GPIO2 open-drain output.
|
||||||
|
axp_write(0x35, 0xA2); // RTC battery charging.
|
||||||
|
|
||||||
|
uint8_t value = 0;
|
||||||
|
axp_read(0x26, &value);
|
||||||
|
axp_write(0x26, (value & 0x80) | axp_dc_voltage_data(3350)); // DCDC1 ESP32 VDD.
|
||||||
|
core2_axp192_set_lcd_voltage(2800);
|
||||||
|
|
||||||
|
uint8_t ldo2 = axp_ldo_voltage_data(3300);
|
||||||
|
uint8_t ldo3 = axp_ldo_voltage_data(2000);
|
||||||
|
axp_write(0x28, (ldo2 << 4) | ldo3); // LDO2 LCD logic/SD, LDO3 vibrator.
|
||||||
|
axp_update(0x12, 0x00, 0x07); // Enable DCDC1, DCDC3, LDO2.
|
||||||
|
axp_write(0x82, 0xFF); // ADCs on.
|
||||||
|
axp_update(0x95, 0x8D, 0x84); // GPIO4 setup, as M5Core2 library does.
|
||||||
|
axp_write(0x36, 0x4C); // Power key timing.
|
||||||
|
|
||||||
|
// LCD reset through AXP192 GPIO4.
|
||||||
|
axp_update(0x96, 0x02, 0x00);
|
||||||
|
delay(100);
|
||||||
|
axp_update(0x96, 0x00, 0x02);
|
||||||
|
delay(100);
|
||||||
|
|
||||||
|
core2_axp192_set_backlight(1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const uint8_t ACCU_BMP[32] = {
|
||||||
|
0b00000000, 0b00000000,
|
||||||
|
0b00000000, 0b00000000,
|
||||||
|
0b00000000, 0b00000000,
|
||||||
|
0b11111111, 0b11111110,
|
||||||
|
0b10000000, 0b00000010,
|
||||||
|
0b10000000, 0b00000011,
|
||||||
|
0b10000000, 0b00000011,
|
||||||
|
0b10000000, 0b00000011,
|
||||||
|
0b10000000, 0b00000011,
|
||||||
|
0b10000000, 0b00000011,
|
||||||
|
0b10000000, 0b00000011,
|
||||||
|
0b10000000, 0b00000010,
|
||||||
|
0b11111111, 0b11111110,
|
||||||
|
0b00000000, 0b00000000,
|
||||||
|
0b00000000, 0b00000000,
|
||||||
|
0b00000000, 0b00000000,
|
||||||
|
};
|
||||||
|
|
||||||
|
static const uint8_t BLE_BMP[32] = {
|
||||||
|
0b00000001, 0b10000000,
|
||||||
|
0b00000001, 0b11000000,
|
||||||
|
0b00010001, 0b01100000,
|
||||||
|
0b00011001, 0b00110000,
|
||||||
|
0b00001101, 0b00011000,
|
||||||
|
0b00000111, 0b00110000,
|
||||||
|
0b00000011, 0b01100000,
|
||||||
|
0b00000001, 0b11000000,
|
||||||
|
0b00000001, 0b11000000,
|
||||||
|
0b00000011, 0b01100000,
|
||||||
|
0b00000111, 0b00110000,
|
||||||
|
0b00001101, 0b00011000,
|
||||||
|
0b00011001, 0b00110000,
|
||||||
|
0b00010001, 0b01100000,
|
||||||
|
0b00000001, 0b11000000,
|
||||||
|
0b00000001, 0b10000000,
|
||||||
|
};
|
||||||
|
|
||||||
|
static const uint8_t DIODE_BMP[32] = {
|
||||||
|
0b00001000, 0b00011000,
|
||||||
|
0b00001100, 0b00011000,
|
||||||
|
0b00001110, 0b00011000,
|
||||||
|
0b00001111, 0b00011000,
|
||||||
|
0b00001111, 0b10011000,
|
||||||
|
0b00001111, 0b11011000,
|
||||||
|
0b00001111, 0b11111000,
|
||||||
|
0b11111111, 0b11111111,
|
||||||
|
0b11111111, 0b11111111,
|
||||||
|
0b00001111, 0b11111000,
|
||||||
|
0b00001111, 0b11011000,
|
||||||
|
0b00001111, 0b10011000,
|
||||||
|
0b00001111, 0b00011000,
|
||||||
|
0b00001110, 0b00011000,
|
||||||
|
0b00001100, 0b00011000,
|
||||||
|
0b00001000, 0b00011000,
|
||||||
|
};
|
||||||
|
|
||||||
|
static const uint8_t BUZZ_BMP[32] = {
|
||||||
|
0b00000000, 0b11000000,
|
||||||
|
0b00000001, 0b11000000,
|
||||||
|
0b00000011, 0b11000001,
|
||||||
|
0b00000111, 0b11000001,
|
||||||
|
0b00001111, 0b11000101,
|
||||||
|
0b11111111, 0b11000101,
|
||||||
|
0b11111111, 0b11010101,
|
||||||
|
0b11111111, 0b11010101,
|
||||||
|
0b11111111, 0b11010101,
|
||||||
|
0b11111111, 0b11010101,
|
||||||
|
0b11111111, 0b11000101,
|
||||||
|
0b00001111, 0b11000101,
|
||||||
|
0b00000111, 0b11000001,
|
||||||
|
0b00000011, 0b11000001,
|
||||||
|
0b00000001, 0b11000000,
|
||||||
|
0b00000000, 0b11000000,
|
||||||
|
};
|
||||||
|
|
||||||
|
class Meter {
|
||||||
|
public:
|
||||||
|
static constexpr uint8_t REGPLUSMINUS = 0x00;
|
||||||
|
static constexpr uint8_t FLAGPLUS = 0b00101011;
|
||||||
|
static constexpr uint8_t FLAGMINUS = 0b00101101;
|
||||||
|
static constexpr uint8_t REGDIG1 = 0x01;
|
||||||
|
static constexpr uint8_t REGDIG2 = 0x02;
|
||||||
|
static constexpr uint8_t REGDIG3 = 0x03;
|
||||||
|
static constexpr uint8_t REGDIG4 = 0x04;
|
||||||
|
static constexpr uint8_t REGPOINT = 0x06;
|
||||||
|
static constexpr uint8_t FLAGPOINT0 = 0b00110000;
|
||||||
|
static constexpr uint8_t FLAGPOINT1 = 0b00110001;
|
||||||
|
static constexpr uint8_t FLAGPOINT2 = 0b00110010;
|
||||||
|
static constexpr uint8_t FLAGPOINT3 = 0b00110100;
|
||||||
|
static constexpr uint8_t REGMODE = 0x07;
|
||||||
|
static constexpr uint8_t FLAGMODEHOLD = 0b00000010;
|
||||||
|
static constexpr uint8_t FLAGMODEREL = 0b00000100;
|
||||||
|
static constexpr uint8_t FLAGMODEAC = 0b00001000;
|
||||||
|
static constexpr uint8_t FLAGMODEDC = 0b00010000;
|
||||||
|
static constexpr uint8_t FLAGMODEAUTO = 0b00100000;
|
||||||
|
static constexpr uint8_t REGMINMAX = 0x08;
|
||||||
|
static constexpr uint8_t FLAGMIN = 0b00010000;
|
||||||
|
static constexpr uint8_t FLAGMAX = 0b00100000;
|
||||||
|
static constexpr uint8_t REGSCALE = 0x09;
|
||||||
|
static constexpr uint8_t FLAGSCALEDUTY = 0b00000010;
|
||||||
|
static constexpr uint8_t FLAGSCALEDIODE = 0b00000100;
|
||||||
|
static constexpr uint8_t FLAGSCALEBUZZ = 0b00001000;
|
||||||
|
static constexpr uint8_t FLAGSCALEMEGA = 0b00010000;
|
||||||
|
static constexpr uint8_t FLAGSCALEKILO = 0b00100000;
|
||||||
|
static constexpr uint8_t FLAGSCALEMILLI = 0b01000000;
|
||||||
|
static constexpr uint8_t FLAGSCALEMICRO = 0b10000000;
|
||||||
|
static constexpr uint8_t REGUNIT = 0x0a;
|
||||||
|
static constexpr uint8_t FLAGUNITFAHR = 0b00000001;
|
||||||
|
static constexpr uint8_t FLAGUNITGRAD = 0b00000010;
|
||||||
|
static constexpr uint8_t FLAGUNITNF = 0b00000100;
|
||||||
|
static constexpr uint8_t FLAGUNITHZ = 0b00001000;
|
||||||
|
static constexpr uint8_t FLAGUNITHFE = 0b00010000;
|
||||||
|
static constexpr uint8_t FLAGUNITOHM = 0b00100000;
|
||||||
|
static constexpr uint8_t FLAGUNITAMP = 0b01000000;
|
||||||
|
static constexpr uint8_t FLAGUNITVOLT = 0b10000000;
|
||||||
|
|
||||||
|
bool connected{false};
|
||||||
|
bool write_available{false};
|
||||||
|
bool is_plus{false};
|
||||||
|
bool low_battery{false};
|
||||||
|
bool overload{false};
|
||||||
|
bool has_reading{false};
|
||||||
|
uint8_t selected_button{1};
|
||||||
|
uint32_t last_notify_ms{0};
|
||||||
|
|
||||||
|
bool handle_notify(const std::vector<uint8_t> &data) {
|
||||||
|
if (data.size() > sizeof(this->raw_))
|
||||||
|
return false;
|
||||||
|
if (data.size() == 6 && data[1] >= 0xF0) {
|
||||||
|
memset(this->raw_, 0, sizeof(this->raw_));
|
||||||
|
memcpy(this->raw_, data.data(), data.size());
|
||||||
|
this->is_plus = true;
|
||||||
|
this->parse_plus_();
|
||||||
|
} else if (data.size() == 14 && data[12] == 0x0D && data[13] == 0x0A) {
|
||||||
|
memset(this->value_, 0, sizeof(this->value_));
|
||||||
|
memcpy(this->value_, data.data(), data.size());
|
||||||
|
this->is_plus = false;
|
||||||
|
} else {
|
||||||
|
ESP_LOGW(TAG, "Ignoring unexpected OWON frame length=%u", static_cast<unsigned>(data.size()));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->overload = memcmp(this->value_, OVERLOAD_FRAME, sizeof(OVERLOAD_FRAME)) == 0;
|
||||||
|
this->display_value = this->calc_display_value_();
|
||||||
|
this->base_value = this->calc_base_value_();
|
||||||
|
this->has_reading = true;
|
||||||
|
this->last_notify_ms = millis();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void on_connect() {
|
||||||
|
this->connected = true;
|
||||||
|
this->write_available = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void on_disconnect() {
|
||||||
|
this->connected = false;
|
||||||
|
this->write_available = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
float value() const { return this->display_value; }
|
||||||
|
float value_base() const { return this->base_value; }
|
||||||
|
bool negative() const { return (this->value_[REGPLUSMINUS] & FLAGMINUS) == FLAGMINUS; }
|
||||||
|
bool auto_range() const { return (this->value_[REGMODE] & FLAGMODEAUTO) == FLAGMODEAUTO; }
|
||||||
|
bool hold() const { return (this->value_[REGMODE] & FLAGMODEHOLD) == FLAGMODEHOLD; }
|
||||||
|
bool relative() const { return (this->value_[REGMODE] & FLAGMODEREL) == FLAGMODEREL; }
|
||||||
|
bool ac() const { return (this->value_[REGMODE] & FLAGMODEAC) == FLAGMODEAC; }
|
||||||
|
bool dc() const { return (this->value_[REGMODE] & FLAGMODEDC) == FLAGMODEDC; }
|
||||||
|
bool min_mode() const { return (this->value_[REGMINMAX] & FLAGMIN) == FLAGMIN; }
|
||||||
|
bool max_mode() const { return (this->value_[REGMINMAX] & FLAGMAX) == FLAGMAX; }
|
||||||
|
bool diode() const { return (this->value_[REGSCALE] & FLAGSCALEDIODE) == FLAGSCALEDIODE; }
|
||||||
|
bool continuity() const { return (this->value_[REGSCALE] & FLAGSCALEBUZZ) == FLAGSCALEBUZZ; }
|
||||||
|
|
||||||
|
const char *unit() const {
|
||||||
|
switch (this->value_[REGUNIT]) {
|
||||||
|
case FLAGUNITFAHR: return "°F";
|
||||||
|
case FLAGUNITGRAD: return "°C";
|
||||||
|
case FLAGUNITNF: return "nF";
|
||||||
|
case FLAGUNITHZ: return "Hz";
|
||||||
|
case FLAGUNITHFE: return "hFE";
|
||||||
|
case FLAGUNITOHM: return "Ω";
|
||||||
|
case FLAGUNITAMP: return "A";
|
||||||
|
case FLAGUNITVOLT: return "V";
|
||||||
|
default: return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *scale() const {
|
||||||
|
if ((this->value_[REGSCALE] & FLAGSCALEDUTY) == FLAGSCALEDUTY) return "%";
|
||||||
|
if ((this->value_[REGSCALE] & FLAGSCALEMEGA) == FLAGSCALEMEGA) return "M";
|
||||||
|
if ((this->value_[REGSCALE] & FLAGSCALEKILO) == FLAGSCALEKILO) return "k";
|
||||||
|
if ((this->value_[REGSCALE] & FLAGSCALEMILLI) == FLAGSCALEMILLI) return "m";
|
||||||
|
if ((this->value_[REGSCALE] & FLAGSCALEMICRO) == FLAGSCALEMICRO) return "µ";
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string mode_text() const {
|
||||||
|
std::string out;
|
||||||
|
if (this->dc()) out += "DC ";
|
||||||
|
if (this->ac()) out += "AC ";
|
||||||
|
if (this->auto_range()) out += "AUTO ";
|
||||||
|
if (this->hold()) out += "HOLD ";
|
||||||
|
if (this->relative()) out += "REL ";
|
||||||
|
if (this->min_mode()) out += "MIN ";
|
||||||
|
if (this->max_mode()) out += "MAX ";
|
||||||
|
if (this->diode()) out += "DIODE ";
|
||||||
|
if (this->continuity()) out += "CONT ";
|
||||||
|
if (!out.empty()) out.pop_back();
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string reading_text() const {
|
||||||
|
if (!this->connected) return "Disconnected";
|
||||||
|
if (!this->has_reading) return "Waiting for data";
|
||||||
|
if (this->overload) return "OL " + std::string(this->scale()) + this->unit();
|
||||||
|
char buf[48];
|
||||||
|
snprintf(buf, sizeof(buf), "%s%.4g %s%s", this->negative() ? "-" : "", std::fabs(this->display_value), this->scale(), this->unit());
|
||||||
|
return std::string(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Kind { KIND_OTHER, KIND_VOLTAGE, KIND_CURRENT, KIND_RESISTANCE, KIND_FREQUENCY, KIND_CAPACITANCE, KIND_TEMP_C, KIND_TEMP_F, KIND_DUTY };
|
||||||
|
Kind kind() const {
|
||||||
|
if ((this->value_[REGSCALE] & FLAGSCALEDUTY) == FLAGSCALEDUTY) return KIND_DUTY;
|
||||||
|
switch (this->value_[REGUNIT]) {
|
||||||
|
case FLAGUNITVOLT: return KIND_VOLTAGE;
|
||||||
|
case FLAGUNITAMP: return KIND_CURRENT;
|
||||||
|
case FLAGUNITOHM: return KIND_RESISTANCE;
|
||||||
|
case FLAGUNITHZ: return KIND_FREQUENCY;
|
||||||
|
case FLAGUNITNF: return KIND_CAPACITANCE;
|
||||||
|
case FLAGUNITGRAD: return KIND_TEMP_C;
|
||||||
|
case FLAGUNITFAHR: return KIND_TEMP_F;
|
||||||
|
default: return KIND_OTHER;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *selected_button_name() const {
|
||||||
|
static const char *const names[] = {"SELECT", "RANGE", "HLD/LIG", "REL/BT", "HZ/DUTY", "MAX/MIN"};
|
||||||
|
uint8_t index = this->selected_button;
|
||||||
|
if (index < 1) index = 1;
|
||||||
|
if (index > 6) index = 6;
|
||||||
|
return names[index - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
void previous_button() {
|
||||||
|
if (this->selected_button > 1) this->selected_button--;
|
||||||
|
}
|
||||||
|
void next_button() {
|
||||||
|
if (this->selected_button < 6) this->selected_button++;
|
||||||
|
}
|
||||||
|
|
||||||
|
void render(Display &it, esphome::display::BaseFont *font) {
|
||||||
|
const Color bg(0, 0, 0);
|
||||||
|
const Color fg(210, 210, 210);
|
||||||
|
// Chosen to map to a neutral dark gray in the RGB332 8-bit display palette.
|
||||||
|
const Color inactive(80, 80, 80);
|
||||||
|
const Color yellow(255, 220, 0);
|
||||||
|
const Color blue(0, 80, 255);
|
||||||
|
const Color cyan(0, 255, 255);
|
||||||
|
const Color magenta(255, 0, 255);
|
||||||
|
const Color red(255, 0, 0);
|
||||||
|
const Color green(0, 220, 0);
|
||||||
|
const Color orange(255, 165, 0);
|
||||||
|
|
||||||
|
it.fill(bg);
|
||||||
|
bool status_active = this->connected && this->has_reading;
|
||||||
|
this->draw_icon_(it, 12, 8, 16, 16, ACCU_BMP, status_active ? (this->low_battery ? red : green) : inactive);
|
||||||
|
this->draw_icon_(it, 46, 8, 16, 16, BLE_BMP, this->connected ? blue : inactive);
|
||||||
|
this->label_(it, font, 86, 8, "AUTO", status_active && this->auto_range() ? fg : inactive);
|
||||||
|
this->label_(it, font, 138, 8, "MAX", status_active && this->max_mode() ? red : inactive);
|
||||||
|
this->label_(it, font, 178, 8, "MIN", status_active && this->min_mode() ? green : inactive);
|
||||||
|
this->label_(it, font, 218, 8, "HOLD", status_active && this->hold() ? blue : inactive);
|
||||||
|
this->label_(it, font, 270, 8, "REL", status_active && this->relative() ? Color(128, 128, 0) : inactive);
|
||||||
|
|
||||||
|
this->label_(it, font, 8, 66, "DC", status_active && this->dc() ? cyan : inactive);
|
||||||
|
this->label_(it, font, 8, 102, "AC", status_active && this->ac() ? magenta : inactive);
|
||||||
|
|
||||||
|
if (!this->connected) {
|
||||||
|
this->draw_digits_(it, "----", false, inactive);
|
||||||
|
it.print(160, 148, font, inactive, esphome::display::TextAlign::CENTER, "scan/connect");
|
||||||
|
} else if (!this->has_reading) {
|
||||||
|
this->draw_digits_(it, "8888", false, inactive);
|
||||||
|
it.print(160, 148, font, inactive, esphome::display::TextAlign::CENTER, "waiting");
|
||||||
|
} else if (this->overload) {
|
||||||
|
this->draw_digits_(it, " OL ", false, fg);
|
||||||
|
} else {
|
||||||
|
char d[5];
|
||||||
|
d[0] = this->digit_char_(REGDIG1);
|
||||||
|
d[1] = this->digit_char_(REGDIG2);
|
||||||
|
d[2] = this->digit_char_(REGDIG3);
|
||||||
|
d[3] = this->digit_char_(REGDIG4);
|
||||||
|
d[4] = 0;
|
||||||
|
this->draw_digits_(it, d, this->negative(), fg);
|
||||||
|
this->draw_decimal_points_(it, fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status_active) {
|
||||||
|
std::string unit_line = std::string(this->scale()) + this->unit();
|
||||||
|
it.print(270, 140, font, yellow, esphome::display::TextAlign::CENTER, unit_line.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool bargraph_active = status_active && !this->overload;
|
||||||
|
this->draw_bargraph_(it, bargraph_active ? this->digits_from_buffer_() : 0, bargraph_active);
|
||||||
|
this->draw_icon_(it, 300, 148, 16, 16, DIODE_BMP, status_active && this->diode() ? magenta : inactive);
|
||||||
|
this->draw_icon_(it, 300, 174, 16, 16, BUZZ_BMP, status_active && this->continuity() ? orange : inactive);
|
||||||
|
|
||||||
|
it.filled_rectangle(34, 212, 40, 24, this->write_available ? fg : inactive);
|
||||||
|
it.filled_rectangle(108, 212, 100, 24, this->write_available ? fg : inactive);
|
||||||
|
it.filled_rectangle(242, 212, 40, 24, this->write_available ? fg : inactive);
|
||||||
|
it.print(54, 216, font, bg, esphome::display::TextAlign::TOP_CENTER, "<");
|
||||||
|
it.print(158, 216, font, bg, esphome::display::TextAlign::TOP_CENTER, this->selected_button_name());
|
||||||
|
it.print(262, 216, font, bg, esphome::display::TextAlign::TOP_CENTER, ">");
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
uint8_t raw_[14]{};
|
||||||
|
uint8_t value_[14]{};
|
||||||
|
float display_value{NAN};
|
||||||
|
float base_value{NAN};
|
||||||
|
static constexpr uint8_t OVERLOAD_FRAME[5] = {0x2B, 0x3F, 0x30, 0x3A, 0x3F};
|
||||||
|
|
||||||
|
uint16_t digits_from_buffer_() const {
|
||||||
|
uint16_t out = 0;
|
||||||
|
if (this->value_[REGDIG1] >= '0' && this->value_[REGDIG1] <= '9') out += (this->value_[REGDIG1] - '0') * 1000;
|
||||||
|
if (this->value_[REGDIG2] >= '0' && this->value_[REGDIG2] <= '9') out += (this->value_[REGDIG2] - '0') * 100;
|
||||||
|
if (this->value_[REGDIG3] >= '0' && this->value_[REGDIG3] <= '9') out += (this->value_[REGDIG3] - '0') * 10;
|
||||||
|
if (this->value_[REGDIG4] >= '0' && this->value_[REGDIG4] <= '9') out += (this->value_[REGDIG4] - '0');
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
float calc_display_value_() const {
|
||||||
|
if (this->overload) return NAN;
|
||||||
|
|
||||||
|
if (this->is_plus) {
|
||||||
|
uint16_t pair1 = static_cast<uint16_t>(this->raw_[0]) | (static_cast<uint16_t>(this->raw_[1]) << 8);
|
||||||
|
uint8_t decimal = pair1 & 0x07;
|
||||||
|
if (decimal >= 7) return NAN;
|
||||||
|
|
||||||
|
uint16_t pair3 = static_cast<uint16_t>(this->raw_[4]) | (static_cast<uint16_t>(this->raw_[5]) << 8);
|
||||||
|
bool negative = pair3 >= 0x7FFF;
|
||||||
|
uint16_t digits = negative ? (pair3 & 0x7FFF) : pair3;
|
||||||
|
float v = static_cast<float>(digits) / std::pow(10.0f, decimal);
|
||||||
|
return negative ? -v : v;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t decimal = 0;
|
||||||
|
switch (this->value_[REGPOINT] & 0x07) {
|
||||||
|
case 0b001: decimal = 1; break;
|
||||||
|
case 0b010: decimal = 2; break;
|
||||||
|
case 0b100: decimal = 3; break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
float v = static_cast<float>(this->digits_from_buffer_()) / std::pow(10.0f, decimal);
|
||||||
|
return this->negative() ? -v : v;
|
||||||
|
}
|
||||||
|
|
||||||
|
float calc_base_value_() const {
|
||||||
|
if (std::isnan(this->display_value)) return NAN;
|
||||||
|
if (this->value_[REGUNIT] == FLAGUNITNF) return this->display_value * 1e-9f;
|
||||||
|
if ((this->value_[REGSCALE] & FLAGSCALEMEGA) == FLAGSCALEMEGA) return this->display_value * 1e6f;
|
||||||
|
if ((this->value_[REGSCALE] & FLAGSCALEKILO) == FLAGSCALEKILO) return this->display_value * 1e3f;
|
||||||
|
if ((this->value_[REGSCALE] & FLAGSCALEMILLI) == FLAGSCALEMILLI) return this->display_value * 1e-3f;
|
||||||
|
if ((this->value_[REGSCALE] & FLAGSCALEMICRO) == FLAGSCALEMICRO) return this->display_value * 1e-6f;
|
||||||
|
return this->display_value;
|
||||||
|
}
|
||||||
|
|
||||||
|
void parse_plus_() {
|
||||||
|
memset(this->value_, 0, sizeof(this->value_));
|
||||||
|
this->value_[5] = 0x20;
|
||||||
|
this->value_[12] = 0x0D;
|
||||||
|
this->value_[13] = 0x0A;
|
||||||
|
|
||||||
|
uint16_t pair1 = static_cast<uint16_t>(this->raw_[0]) | (static_cast<uint16_t>(this->raw_[1]) << 8);
|
||||||
|
uint8_t function = (pair1 >> 6) & 0x0F;
|
||||||
|
uint8_t scale = (pair1 >> 3) & 0x07;
|
||||||
|
uint8_t decimal = pair1 & 0x07;
|
||||||
|
|
||||||
|
switch (decimal) {
|
||||||
|
case 0: this->value_[REGPOINT] = FLAGPOINT0; break;
|
||||||
|
case 1: this->value_[REGPOINT] = FLAGPOINT3; break;
|
||||||
|
case 2: this->value_[REGPOINT] = FLAGPOINT2; break;
|
||||||
|
case 3: this->value_[REGPOINT] = FLAGPOINT1; break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (function) {
|
||||||
|
case 0: this->value_[REGUNIT] |= FLAGUNITVOLT; this->value_[REGMODE] |= FLAGMODEDC; break;
|
||||||
|
case 1: this->value_[REGUNIT] |= FLAGUNITVOLT; this->value_[REGMODE] |= FLAGMODEAC; break;
|
||||||
|
case 2: this->value_[REGUNIT] |= FLAGUNITAMP; this->value_[REGMODE] |= FLAGMODEDC; break;
|
||||||
|
case 3: this->value_[REGUNIT] |= FLAGUNITAMP; this->value_[REGMODE] |= FLAGMODEAC; break;
|
||||||
|
case 4: this->value_[REGUNIT] |= FLAGUNITOHM; break;
|
||||||
|
case 5: this->value_[REGUNIT] |= FLAGUNITNF; break;
|
||||||
|
case 6: this->value_[REGUNIT] |= FLAGUNITHZ; break;
|
||||||
|
case 7: this->value_[REGSCALE] |= FLAGSCALEDUTY; break;
|
||||||
|
case 8: this->value_[REGUNIT] |= FLAGUNITGRAD; break;
|
||||||
|
case 9: this->value_[REGUNIT] |= FLAGUNITFAHR; break;
|
||||||
|
case 10: this->value_[REGSCALE] |= FLAGSCALEDIODE; break;
|
||||||
|
case 11: this->value_[REGSCALE] |= FLAGSCALEBUZZ; break;
|
||||||
|
case 12: this->value_[REGUNIT] |= FLAGUNITHFE; break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (scale) {
|
||||||
|
case 2: this->value_[REGSCALE] |= FLAGSCALEMICRO; break;
|
||||||
|
case 3: this->value_[REGSCALE] |= FLAGSCALEMILLI; break;
|
||||||
|
case 5: this->value_[REGSCALE] |= FLAGSCALEKILO; break;
|
||||||
|
case 6: this->value_[REGSCALE] |= FLAGSCALEMEGA; break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t pair2 = static_cast<uint16_t>(this->raw_[2]) | (static_cast<uint16_t>(this->raw_[3]) << 8);
|
||||||
|
if (pair2 & (1 << 0)) this->value_[REGMODE] |= FLAGMODEHOLD;
|
||||||
|
if (pair2 & (1 << 1)) this->value_[REGMODE] |= FLAGMODEREL;
|
||||||
|
if (pair2 & (1 << 2)) this->value_[REGMODE] |= FLAGMODEAUTO;
|
||||||
|
this->low_battery = (pair2 & (1 << 3)) != 0;
|
||||||
|
if (pair2 & (1 << 4)) this->value_[REGMINMAX] |= FLAGMIN;
|
||||||
|
if (pair2 & (1 << 5)) this->value_[REGMINMAX] |= FLAGMAX;
|
||||||
|
|
||||||
|
uint16_t pair3 = static_cast<uint16_t>(this->raw_[4]) | (static_cast<uint16_t>(this->raw_[5]) << 8);
|
||||||
|
if (decimal < 7) {
|
||||||
|
uint16_t digits = pair3;
|
||||||
|
if (pair3 < 0x7FFF) {
|
||||||
|
this->value_[REGPLUSMINUS] = FLAGPLUS;
|
||||||
|
} else {
|
||||||
|
this->value_[REGPLUSMINUS] = FLAGMINUS;
|
||||||
|
digits = pair3 & 0x7FFF;
|
||||||
|
}
|
||||||
|
this->value_[REGDIG1] = '0' + ((digits / 1000) % 10);
|
||||||
|
this->value_[REGDIG2] = '0' + ((digits / 100) % 10);
|
||||||
|
this->value_[REGDIG3] = '0' + ((digits / 10) % 10);
|
||||||
|
this->value_[REGDIG4] = '0' + (digits % 10);
|
||||||
|
} else {
|
||||||
|
memcpy(this->value_, OVERLOAD_FRAME, sizeof(OVERLOAD_FRAME));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
char digit_char_(uint8_t reg) const {
|
||||||
|
uint8_t c = this->value_[reg];
|
||||||
|
return (c >= '0' && c <= '9') ? static_cast<char>(c) : ' ';
|
||||||
|
}
|
||||||
|
|
||||||
|
void label_(Display &it, esphome::display::BaseFont *font, int x, int y, const char *text, Color color) {
|
||||||
|
it.print(x, y, font, color, esphome::display::TextAlign::TOP_LEFT, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
void draw_digits_(Display &it, const char *text, bool negative, Color color) {
|
||||||
|
if (negative) this->draw_segment_(it, 8, 88, 26, 9, true, color);
|
||||||
|
constexpr int digit_x = 40;
|
||||||
|
constexpr int digit_y = 35;
|
||||||
|
constexpr int digit_w = 50;
|
||||||
|
constexpr int digit_h = 88;
|
||||||
|
constexpr int digit_distance = 64;
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
this->draw_seven_segment_(it, digit_x + i * digit_distance, digit_y, digit_w, digit_h, text[i], color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void draw_decimal_points_(Display &it, Color color) {
|
||||||
|
uint8_t p = this->value_[REGPOINT];
|
||||||
|
if ((p & FLAGPOINT1) == FLAGPOINT1) it.filled_rectangle(95, 117, 8, 10, color);
|
||||||
|
if ((p & FLAGPOINT2) == FLAGPOINT2) it.filled_rectangle(159, 117, 8, 10, color);
|
||||||
|
if ((p & FLAGPOINT3) == FLAGPOINT3) it.filled_rectangle(223, 117, 8, 10, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
void draw_segment_(Display &it, int x, int y, int w, int h, bool horizontal, Color color) {
|
||||||
|
if (horizontal) {
|
||||||
|
int cap = h / 2;
|
||||||
|
it.filled_rectangle(x + cap, y, w - 2 * cap, h +1, color);
|
||||||
|
it.filled_triangle(x, y + cap, x + cap, y, x + cap, y + h, color);
|
||||||
|
it.filled_triangle(x + w, y + cap, x + w - cap, y, x + w - cap, y + h, color);
|
||||||
|
} else {
|
||||||
|
int cap = w / 2;
|
||||||
|
it.filled_rectangle(x, y + cap, w +1, h - 2 * cap, color);
|
||||||
|
it.filled_triangle(x + cap, y, x, y + cap, x + w, y + cap, color);
|
||||||
|
it.filled_triangle(x + cap, y + h, x, y + h - cap, x + w, y + h - cap, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void draw_icon_(Display &it, int x, int y, int w, int h, const uint8_t *data, Color color) {
|
||||||
|
for (int row = 0; row < h; row++) {
|
||||||
|
for (int col = 0; col < w; col++) {
|
||||||
|
uint8_t byte = data[row * ((w + 7) / 8) + (col / 8)];
|
||||||
|
if ((byte & (0x80 >> (col % 8))) != 0) {
|
||||||
|
it.draw_pixel_at(x + col, y + row, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void draw_seven_segment_(Display &it, int x, int y, int w, int h, char ch, Color color) {
|
||||||
|
bool a=false,b=false,c=false,d=false,e=false,f=false,g=false;
|
||||||
|
switch (ch) {
|
||||||
|
case '0': a=b=c=d=e=f=true; break;
|
||||||
|
case '1': b=c=true; break;
|
||||||
|
case '2': a=b=d=e=g=true; break;
|
||||||
|
case '3': a=b=c=d=g=true; break;
|
||||||
|
case '4': b=c=f=g=true; break;
|
||||||
|
case '5': a=c=d=f=g=true; break;
|
||||||
|
case '6': a=c=d=e=f=g=true; break;
|
||||||
|
case '7': a=b=c=true; break;
|
||||||
|
case '8': a=b=c=d=e=f=g=true; break;
|
||||||
|
case '9': a=b=c=d=f=g=true; break;
|
||||||
|
case 'O': a=b=c=d=e=f=true; break;
|
||||||
|
case 'L': d=e=f=true; break;
|
||||||
|
case '-': g=true; break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
int t = 10;
|
||||||
|
int half = h / 2;
|
||||||
|
int gap = 1;
|
||||||
|
|
||||||
|
int top_y = y;
|
||||||
|
int mid_y = y + half - t / 2;
|
||||||
|
int bot_y = y + h - t;
|
||||||
|
|
||||||
|
int upper_v_y = top_y + t + gap;
|
||||||
|
int upper_v_h = mid_y - gap - upper_v_y;
|
||||||
|
int lower_v_y = mid_y + t + gap;
|
||||||
|
int lower_v_h = bot_y - gap - lower_v_y;
|
||||||
|
|
||||||
|
if (a) this->draw_segment_(it, x + t / 2, top_y, w - t, t, true, color);
|
||||||
|
if (b && upper_v_h > 0) this->draw_segment_(it, x + w - t, upper_v_y, t, upper_v_h, false, color);
|
||||||
|
if (c && lower_v_h > 0) this->draw_segment_(it, x + w - t, lower_v_y, t, lower_v_h, false, color);
|
||||||
|
if (d) this->draw_segment_(it, x + t / 2, bot_y, w - t, t, true, color);
|
||||||
|
if (e && lower_v_h > 0) this->draw_segment_(it, x, lower_v_y, t, lower_v_h, false, color);
|
||||||
|
if (f && upper_v_h > 0) this->draw_segment_(it, x, upper_v_y, t, upper_v_h, false, color);
|
||||||
|
if (g) this->draw_segment_(it, x + t / 2, mid_y, w - t, t, true, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
void draw_bargraph_(Display &it, uint16_t digits, bool active) {
|
||||||
|
const Color fg(255, 255, 255);
|
||||||
|
const Color inactive(80, 80, 80);
|
||||||
|
uint16_t mapped = active ? static_cast<uint16_t>(digits * 240 / 6000) : 0;
|
||||||
|
if (mapped > 240) mapped = 240;
|
||||||
|
for (uint16_t i = 0; i <= 240; i += 4) {
|
||||||
|
Color col = (active && i <= mapped) ? fg : inactive;
|
||||||
|
int h = (i % 40 == 0) ? 20 : ((i % 20 == 0) ? 15 : 10);
|
||||||
|
it.vertical_line(40 + i, 185 - h, h, col);
|
||||||
|
}
|
||||||
|
it.horizontal_line(35, 185, 250, inactive);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace owon_b35t
|
||||||
|
|
||||||
|
static owon_b35t::Meter owon_meter;
|
||||||
@@ -0,0 +1,316 @@
|
|||||||
|
# Derived work based on https://github.com/reaper7/M5Stack_BLE_client_Owon_B35T by reaper7.
|
||||||
|
# AI (ChatGPT) has been used to adopt the Arduino sketch to ESPHome.
|
||||||
|
# Ported to M5Stack Core2 due to memory constraints.
|
||||||
|
|
||||||
|
substitutions:
|
||||||
|
name: "owon-b35t-m5stack-core2"
|
||||||
|
friendly_name: "OWON B35T Multimeter Core2"
|
||||||
|
device_description: "M5Stack Core2 BLE client for OWON B35T/B35T+ multimeter with local graphical display"
|
||||||
|
owon_mac_address: !secret owon_b35t_mac_address
|
||||||
|
|
||||||
|
esphome:
|
||||||
|
name: ${name}
|
||||||
|
friendly_name: ${friendly_name}
|
||||||
|
comment: ${device_description}
|
||||||
|
min_version: 2024.6.0
|
||||||
|
includes:
|
||||||
|
- owon-b35t-m5stack-core2.h
|
||||||
|
on_boot:
|
||||||
|
priority: 850
|
||||||
|
then:
|
||||||
|
- lambda: |-
|
||||||
|
owon_b35t::core2_axp192_init(id(core2_i2c));
|
||||||
|
project:
|
||||||
|
name: "custom.owon-b35t-m5stack-core2"
|
||||||
|
version: "1.0"
|
||||||
|
|
||||||
|
esp32:
|
||||||
|
board: m5stack-core2
|
||||||
|
flash_size: 16MB
|
||||||
|
framework:
|
||||||
|
type: esp-idf
|
||||||
|
advanced:
|
||||||
|
minimum_chip_revision: "3.1"
|
||||||
|
sram1_as_iram: true
|
||||||
|
|
||||||
|
psram:
|
||||||
|
mode: quad
|
||||||
|
speed: 80MHz
|
||||||
|
|
||||||
|
logger:
|
||||||
|
level: INFO
|
||||||
|
|
||||||
|
api:
|
||||||
|
encryption:
|
||||||
|
key: !secret apikey
|
||||||
|
|
||||||
|
ota:
|
||||||
|
platform: esphome
|
||||||
|
password: !secret ota
|
||||||
|
|
||||||
|
wifi:
|
||||||
|
ssid: "Voltage-legacy"
|
||||||
|
password: !secret voltage_legacy_psk
|
||||||
|
#use_address: ${name}.home
|
||||||
|
power_save_mode: none
|
||||||
|
fast_connect: on
|
||||||
|
min_auth_mode: WPA2
|
||||||
|
ap:
|
||||||
|
ssid: "OWON B35T Fallback Hotspot"
|
||||||
|
password: !secret fallback_psk
|
||||||
|
|
||||||
|
captive_portal:
|
||||||
|
|
||||||
|
interval:
|
||||||
|
- interval: 10s
|
||||||
|
then:
|
||||||
|
- lambda: |-
|
||||||
|
ESP_LOGI("mem", "heap free=%u min_free=%u internal_free=%u internal_largest=%u dma_free=%u dma_largest=%u psram_free=%u psram_largest=%u",
|
||||||
|
static_cast<unsigned>(esp_get_free_heap_size()),
|
||||||
|
static_cast<unsigned>(esp_get_minimum_free_heap_size()),
|
||||||
|
static_cast<unsigned>(heap_caps_get_free_size(MALLOC_CAP_INTERNAL)),
|
||||||
|
static_cast<unsigned>(heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL)),
|
||||||
|
static_cast<unsigned>(heap_caps_get_free_size(MALLOC_CAP_DMA)),
|
||||||
|
static_cast<unsigned>(heap_caps_get_largest_free_block(MALLOC_CAP_DMA)),
|
||||||
|
static_cast<unsigned>(heap_caps_get_free_size(MALLOC_CAP_SPIRAM)),
|
||||||
|
static_cast<unsigned>(heap_caps_get_largest_free_block(MALLOC_CAP_SPIRAM)));
|
||||||
|
|
||||||
|
esp32_ble_tracker:
|
||||||
|
scan_parameters:
|
||||||
|
active: true
|
||||||
|
continuous: true
|
||||||
|
|
||||||
|
ble_client:
|
||||||
|
- mac_address: ${owon_mac_address}
|
||||||
|
id: owon_ble_client
|
||||||
|
on_connect:
|
||||||
|
then:
|
||||||
|
- lambda: |-
|
||||||
|
owon_meter.on_connect();
|
||||||
|
on_disconnect:
|
||||||
|
then:
|
||||||
|
- lambda: |-
|
||||||
|
owon_meter.on_disconnect();
|
||||||
|
|
||||||
|
spi:
|
||||||
|
clk_pin: GPIO18
|
||||||
|
mosi_pin: GPIO23
|
||||||
|
|
||||||
|
i2c:
|
||||||
|
id: core2_i2c
|
||||||
|
sda: GPIO21
|
||||||
|
scl: GPIO22
|
||||||
|
scan: true
|
||||||
|
|
||||||
|
output:
|
||||||
|
- platform: template
|
||||||
|
type: float
|
||||||
|
id: lcd_backlight
|
||||||
|
write_action:
|
||||||
|
- lambda: |-
|
||||||
|
owon_b35t::core2_axp192_set_backlight(state);
|
||||||
|
|
||||||
|
light:
|
||||||
|
- platform: monochromatic
|
||||||
|
output: lcd_backlight
|
||||||
|
name: "${friendly_name} Backlight"
|
||||||
|
id: backlight
|
||||||
|
restore_mode: ALWAYS_ON
|
||||||
|
|
||||||
|
font:
|
||||||
|
- file: "fonts/Roboto-Regular.ttf"
|
||||||
|
id: meter_font
|
||||||
|
size: 15
|
||||||
|
glyphs:
|
||||||
|
[
|
||||||
|
" ",
|
||||||
|
"!",
|
||||||
|
"%",
|
||||||
|
"+",
|
||||||
|
"-",
|
||||||
|
".",
|
||||||
|
"/",
|
||||||
|
"0",
|
||||||
|
"1",
|
||||||
|
"2",
|
||||||
|
"3",
|
||||||
|
"4",
|
||||||
|
"5",
|
||||||
|
"6",
|
||||||
|
"7",
|
||||||
|
"8",
|
||||||
|
"9",
|
||||||
|
"<",
|
||||||
|
">",
|
||||||
|
"A",
|
||||||
|
"B",
|
||||||
|
"C",
|
||||||
|
"D",
|
||||||
|
"E",
|
||||||
|
"F",
|
||||||
|
"G",
|
||||||
|
"H",
|
||||||
|
"I",
|
||||||
|
"L",
|
||||||
|
"M",
|
||||||
|
"N",
|
||||||
|
"O",
|
||||||
|
"P",
|
||||||
|
"R",
|
||||||
|
"S",
|
||||||
|
"T",
|
||||||
|
"U",
|
||||||
|
"V",
|
||||||
|
"W",
|
||||||
|
"X",
|
||||||
|
"Y",
|
||||||
|
"Z",
|
||||||
|
"a",
|
||||||
|
"c",
|
||||||
|
"d",
|
||||||
|
"e",
|
||||||
|
"f",
|
||||||
|
"g",
|
||||||
|
"h",
|
||||||
|
"i",
|
||||||
|
"k",
|
||||||
|
"m",
|
||||||
|
"n",
|
||||||
|
"o",
|
||||||
|
"r",
|
||||||
|
"s",
|
||||||
|
"t",
|
||||||
|
"u",
|
||||||
|
"v",
|
||||||
|
"w",
|
||||||
|
"y",
|
||||||
|
"z",
|
||||||
|
"°",
|
||||||
|
"µ",
|
||||||
|
"Ω",
|
||||||
|
]
|
||||||
|
|
||||||
|
display:
|
||||||
|
- platform: mipi_spi
|
||||||
|
id: lcd
|
||||||
|
model: M5CORE2
|
||||||
|
update_interval: 500ms
|
||||||
|
lambda: |-
|
||||||
|
owon_meter.render(it, id(meter_font));
|
||||||
|
|
||||||
|
touchscreen:
|
||||||
|
- platform: ft63x6
|
||||||
|
id: touch
|
||||||
|
display: lcd
|
||||||
|
|
||||||
|
binary_sensor:
|
||||||
|
- platform: touchscreen
|
||||||
|
id: button_a
|
||||||
|
touchscreen_id: touch
|
||||||
|
x_min: 34
|
||||||
|
x_max: 74
|
||||||
|
y_min: 212
|
||||||
|
y_max: 240
|
||||||
|
internal: true
|
||||||
|
on_press:
|
||||||
|
then:
|
||||||
|
- lambda: |-
|
||||||
|
owon_meter.previous_button();
|
||||||
|
- platform: touchscreen
|
||||||
|
id: button_b
|
||||||
|
touchscreen_id: touch
|
||||||
|
x_min: 108
|
||||||
|
x_max: 208
|
||||||
|
y_min: 212
|
||||||
|
y_max: 240
|
||||||
|
internal: true
|
||||||
|
on_click:
|
||||||
|
- min_length: 50ms
|
||||||
|
max_length: 1500ms
|
||||||
|
then:
|
||||||
|
- logger.log:
|
||||||
|
level: INFO
|
||||||
|
format: "OWON short press: %s"
|
||||||
|
args: ["owon_meter.selected_button_name()"]
|
||||||
|
- ble_client.ble_write:
|
||||||
|
id: owon_ble_client
|
||||||
|
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
|
||||||
|
characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb"
|
||||||
|
value: !lambda |-
|
||||||
|
std::vector<uint8_t> data = {owon_meter.selected_button, 0x01};
|
||||||
|
return data;
|
||||||
|
- min_length: 1500ms
|
||||||
|
max_length: 5000ms
|
||||||
|
then:
|
||||||
|
- logger.log:
|
||||||
|
level: INFO
|
||||||
|
format: "OWON long press: %s"
|
||||||
|
args: ["owon_meter.selected_button_name()"]
|
||||||
|
- ble_client.ble_write:
|
||||||
|
id: owon_ble_client
|
||||||
|
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
|
||||||
|
characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb"
|
||||||
|
value: !lambda |-
|
||||||
|
uint8_t press_type = (owon_meter.selected_button == 1 || owon_meter.selected_button == 5) ? 0x01 : 0x00;
|
||||||
|
std::vector<uint8_t> data = {owon_meter.selected_button, press_type};
|
||||||
|
return data;
|
||||||
|
- platform: touchscreen
|
||||||
|
id: button_c
|
||||||
|
touchscreen_id: touch
|
||||||
|
x_min: 242
|
||||||
|
x_max: 282
|
||||||
|
y_min: 212
|
||||||
|
y_max: 240
|
||||||
|
internal: true
|
||||||
|
on_press:
|
||||||
|
then:
|
||||||
|
- lambda: |-
|
||||||
|
owon_meter.next_button();
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
name: "${friendly_name} Connected"
|
||||||
|
lambda: |-
|
||||||
|
return owon_meter.connected;
|
||||||
|
- platform: template
|
||||||
|
name: "${friendly_name} Overload"
|
||||||
|
lambda: |-
|
||||||
|
return owon_meter.overload;
|
||||||
|
- platform: template
|
||||||
|
name: "${friendly_name} Low Battery"
|
||||||
|
lambda: |-
|
||||||
|
return owon_meter.low_battery;
|
||||||
|
|
||||||
|
sensor:
|
||||||
|
- platform: ble_client
|
||||||
|
type: characteristic
|
||||||
|
ble_client_id: owon_ble_client
|
||||||
|
id: owon_notify_source
|
||||||
|
internal: true
|
||||||
|
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
|
||||||
|
characteristic_uuid: "0000fff4-0000-1000-8000-00805f9b34fb"
|
||||||
|
notify: true
|
||||||
|
update_interval: never
|
||||||
|
lambda: |-
|
||||||
|
owon_meter.handle_notify(x);
|
||||||
|
return owon_meter.value();
|
||||||
|
|
||||||
|
- platform: wifi_signal
|
||||||
|
name: "${friendly_name} WiFi Signal"
|
||||||
|
update_interval: 60s
|
||||||
|
|
||||||
|
text_sensor:
|
||||||
|
- platform: template
|
||||||
|
name: "${friendly_name} Reading"
|
||||||
|
update_interval: 1s
|
||||||
|
lambda: |-
|
||||||
|
return owon_meter.reading_text();
|
||||||
|
- platform: template
|
||||||
|
name: "${friendly_name} Unit"
|
||||||
|
update_interval: 1s
|
||||||
|
lambda: |-
|
||||||
|
return std::string(owon_meter.scale()) + owon_meter.unit();
|
||||||
|
- platform: template
|
||||||
|
name: "${friendly_name} Mode"
|
||||||
|
update_interval: 1s
|
||||||
|
lambda: |-
|
||||||
|
return owon_meter.mode_text();
|
||||||
@@ -0,0 +1,567 @@
|
|||||||
|
/*
|
||||||
|
* ESPHome helper for OWON B35T/B35T+ BLE meter on M5Stack Core 1.
|
||||||
|
* Parser is based on the standalone Arduino sketch by Reaper7
|
||||||
|
* (Beerware license, Revision 42) and Dean Cording's owonb35 notes.
|
||||||
|
*/
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstring>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include "esp_heap_caps.h"
|
||||||
|
#include "esp_system.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
#include "esphome/components/display/display.h"
|
||||||
|
|
||||||
|
namespace owon_b35t {
|
||||||
|
|
||||||
|
using esphome::Color;
|
||||||
|
using esphome::display::Display;
|
||||||
|
|
||||||
|
static const char *const TAG = "owon_b35t";
|
||||||
|
|
||||||
|
static const uint8_t ACCU_BMP[32] = {
|
||||||
|
0b00000000, 0b00000000,
|
||||||
|
0b00000000, 0b00000000,
|
||||||
|
0b00000000, 0b00000000,
|
||||||
|
0b11111111, 0b11111110,
|
||||||
|
0b10000000, 0b00000010,
|
||||||
|
0b10000000, 0b00000011,
|
||||||
|
0b10000000, 0b00000011,
|
||||||
|
0b10000000, 0b00000011,
|
||||||
|
0b10000000, 0b00000011,
|
||||||
|
0b10000000, 0b00000011,
|
||||||
|
0b10000000, 0b00000011,
|
||||||
|
0b10000000, 0b00000010,
|
||||||
|
0b11111111, 0b11111110,
|
||||||
|
0b00000000, 0b00000000,
|
||||||
|
0b00000000, 0b00000000,
|
||||||
|
0b00000000, 0b00000000,
|
||||||
|
};
|
||||||
|
|
||||||
|
static const uint8_t BLE_BMP[32] = {
|
||||||
|
0b00000001, 0b10000000,
|
||||||
|
0b00000001, 0b11000000,
|
||||||
|
0b00010001, 0b01100000,
|
||||||
|
0b00011001, 0b00110000,
|
||||||
|
0b00001101, 0b00011000,
|
||||||
|
0b00000111, 0b00110000,
|
||||||
|
0b00000011, 0b01100000,
|
||||||
|
0b00000001, 0b11000000,
|
||||||
|
0b00000001, 0b11000000,
|
||||||
|
0b00000011, 0b01100000,
|
||||||
|
0b00000111, 0b00110000,
|
||||||
|
0b00001101, 0b00011000,
|
||||||
|
0b00011001, 0b00110000,
|
||||||
|
0b00010001, 0b01100000,
|
||||||
|
0b00000001, 0b11000000,
|
||||||
|
0b00000001, 0b10000000,
|
||||||
|
};
|
||||||
|
|
||||||
|
static const uint8_t DIODE_BMP[32] = {
|
||||||
|
0b00001000, 0b00011000,
|
||||||
|
0b00001100, 0b00011000,
|
||||||
|
0b00001110, 0b00011000,
|
||||||
|
0b00001111, 0b00011000,
|
||||||
|
0b00001111, 0b10011000,
|
||||||
|
0b00001111, 0b11011000,
|
||||||
|
0b00001111, 0b11111000,
|
||||||
|
0b11111111, 0b11111111,
|
||||||
|
0b11111111, 0b11111111,
|
||||||
|
0b00001111, 0b11111000,
|
||||||
|
0b00001111, 0b11011000,
|
||||||
|
0b00001111, 0b10011000,
|
||||||
|
0b00001111, 0b00011000,
|
||||||
|
0b00001110, 0b00011000,
|
||||||
|
0b00001100, 0b00011000,
|
||||||
|
0b00001000, 0b00011000,
|
||||||
|
};
|
||||||
|
|
||||||
|
static const uint8_t BUZZ_BMP[32] = {
|
||||||
|
0b00000000, 0b11000000,
|
||||||
|
0b00000001, 0b11000000,
|
||||||
|
0b00000011, 0b11000001,
|
||||||
|
0b00000111, 0b11000001,
|
||||||
|
0b00001111, 0b11000101,
|
||||||
|
0b11111111, 0b11000101,
|
||||||
|
0b11111111, 0b11010101,
|
||||||
|
0b11111111, 0b11010101,
|
||||||
|
0b11111111, 0b11010101,
|
||||||
|
0b11111111, 0b11010101,
|
||||||
|
0b11111111, 0b11000101,
|
||||||
|
0b00001111, 0b11000101,
|
||||||
|
0b00000111, 0b11000001,
|
||||||
|
0b00000011, 0b11000001,
|
||||||
|
0b00000001, 0b11000000,
|
||||||
|
0b00000000, 0b11000000,
|
||||||
|
};
|
||||||
|
|
||||||
|
class Meter {
|
||||||
|
public:
|
||||||
|
static constexpr uint8_t REGPLUSMINUS = 0x00;
|
||||||
|
static constexpr uint8_t FLAGPLUS = 0b00101011;
|
||||||
|
static constexpr uint8_t FLAGMINUS = 0b00101101;
|
||||||
|
static constexpr uint8_t REGDIG1 = 0x01;
|
||||||
|
static constexpr uint8_t REGDIG2 = 0x02;
|
||||||
|
static constexpr uint8_t REGDIG3 = 0x03;
|
||||||
|
static constexpr uint8_t REGDIG4 = 0x04;
|
||||||
|
static constexpr uint8_t REGPOINT = 0x06;
|
||||||
|
static constexpr uint8_t FLAGPOINT0 = 0b00110000;
|
||||||
|
static constexpr uint8_t FLAGPOINT1 = 0b00110001;
|
||||||
|
static constexpr uint8_t FLAGPOINT2 = 0b00110010;
|
||||||
|
static constexpr uint8_t FLAGPOINT3 = 0b00110100;
|
||||||
|
static constexpr uint8_t REGMODE = 0x07;
|
||||||
|
static constexpr uint8_t FLAGMODEHOLD = 0b00000010;
|
||||||
|
static constexpr uint8_t FLAGMODEREL = 0b00000100;
|
||||||
|
static constexpr uint8_t FLAGMODEAC = 0b00001000;
|
||||||
|
static constexpr uint8_t FLAGMODEDC = 0b00010000;
|
||||||
|
static constexpr uint8_t FLAGMODEAUTO = 0b00100000;
|
||||||
|
static constexpr uint8_t REGMINMAX = 0x08;
|
||||||
|
static constexpr uint8_t FLAGMIN = 0b00010000;
|
||||||
|
static constexpr uint8_t FLAGMAX = 0b00100000;
|
||||||
|
static constexpr uint8_t REGSCALE = 0x09;
|
||||||
|
static constexpr uint8_t FLAGSCALEDUTY = 0b00000010;
|
||||||
|
static constexpr uint8_t FLAGSCALEDIODE = 0b00000100;
|
||||||
|
static constexpr uint8_t FLAGSCALEBUZZ = 0b00001000;
|
||||||
|
static constexpr uint8_t FLAGSCALEMEGA = 0b00010000;
|
||||||
|
static constexpr uint8_t FLAGSCALEKILO = 0b00100000;
|
||||||
|
static constexpr uint8_t FLAGSCALEMILLI = 0b01000000;
|
||||||
|
static constexpr uint8_t FLAGSCALEMICRO = 0b10000000;
|
||||||
|
static constexpr uint8_t REGUNIT = 0x0a;
|
||||||
|
static constexpr uint8_t FLAGUNITFAHR = 0b00000001;
|
||||||
|
static constexpr uint8_t FLAGUNITGRAD = 0b00000010;
|
||||||
|
static constexpr uint8_t FLAGUNITNF = 0b00000100;
|
||||||
|
static constexpr uint8_t FLAGUNITHZ = 0b00001000;
|
||||||
|
static constexpr uint8_t FLAGUNITHFE = 0b00010000;
|
||||||
|
static constexpr uint8_t FLAGUNITOHM = 0b00100000;
|
||||||
|
static constexpr uint8_t FLAGUNITAMP = 0b01000000;
|
||||||
|
static constexpr uint8_t FLAGUNITVOLT = 0b10000000;
|
||||||
|
|
||||||
|
bool connected{false};
|
||||||
|
bool write_available{false};
|
||||||
|
bool is_plus{false};
|
||||||
|
bool low_battery{false};
|
||||||
|
bool overload{false};
|
||||||
|
bool has_reading{false};
|
||||||
|
uint8_t selected_button{1};
|
||||||
|
uint32_t last_notify_ms{0};
|
||||||
|
|
||||||
|
bool handle_notify(const std::vector<uint8_t> &data) {
|
||||||
|
if (data.size() > sizeof(this->raw_))
|
||||||
|
return false;
|
||||||
|
if (data.size() == 6 && data[1] >= 0xF0) {
|
||||||
|
memset(this->raw_, 0, sizeof(this->raw_));
|
||||||
|
memcpy(this->raw_, data.data(), data.size());
|
||||||
|
this->is_plus = true;
|
||||||
|
this->parse_plus_();
|
||||||
|
} else if (data.size() == 14 && data[12] == 0x0D && data[13] == 0x0A) {
|
||||||
|
memset(this->value_, 0, sizeof(this->value_));
|
||||||
|
memcpy(this->value_, data.data(), data.size());
|
||||||
|
this->is_plus = false;
|
||||||
|
} else {
|
||||||
|
ESP_LOGW(TAG, "Ignoring unexpected OWON frame length=%u", static_cast<unsigned>(data.size()));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->overload = memcmp(this->value_, OVERLOAD_FRAME, sizeof(OVERLOAD_FRAME)) == 0;
|
||||||
|
this->display_value = this->calc_display_value_();
|
||||||
|
this->base_value = this->calc_base_value_();
|
||||||
|
this->has_reading = true;
|
||||||
|
this->last_notify_ms = millis();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void on_connect() {
|
||||||
|
this->connected = true;
|
||||||
|
this->write_available = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void on_disconnect() {
|
||||||
|
this->connected = false;
|
||||||
|
this->write_available = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
float value() const { return this->display_value; }
|
||||||
|
float value_base() const { return this->base_value; }
|
||||||
|
bool negative() const { return (this->value_[REGPLUSMINUS] & FLAGMINUS) == FLAGMINUS; }
|
||||||
|
bool auto_range() const { return (this->value_[REGMODE] & FLAGMODEAUTO) == FLAGMODEAUTO; }
|
||||||
|
bool hold() const { return (this->value_[REGMODE] & FLAGMODEHOLD) == FLAGMODEHOLD; }
|
||||||
|
bool relative() const { return (this->value_[REGMODE] & FLAGMODEREL) == FLAGMODEREL; }
|
||||||
|
bool ac() const { return (this->value_[REGMODE] & FLAGMODEAC) == FLAGMODEAC; }
|
||||||
|
bool dc() const { return (this->value_[REGMODE] & FLAGMODEDC) == FLAGMODEDC; }
|
||||||
|
bool min_mode() const { return (this->value_[REGMINMAX] & FLAGMIN) == FLAGMIN; }
|
||||||
|
bool max_mode() const { return (this->value_[REGMINMAX] & FLAGMAX) == FLAGMAX; }
|
||||||
|
bool diode() const { return (this->value_[REGSCALE] & FLAGSCALEDIODE) == FLAGSCALEDIODE; }
|
||||||
|
bool continuity() const { return (this->value_[REGSCALE] & FLAGSCALEBUZZ) == FLAGSCALEBUZZ; }
|
||||||
|
|
||||||
|
const char *unit() const {
|
||||||
|
switch (this->value_[REGUNIT]) {
|
||||||
|
case FLAGUNITFAHR: return "°F";
|
||||||
|
case FLAGUNITGRAD: return "°C";
|
||||||
|
case FLAGUNITNF: return "nF";
|
||||||
|
case FLAGUNITHZ: return "Hz";
|
||||||
|
case FLAGUNITHFE: return "hFE";
|
||||||
|
case FLAGUNITOHM: return "Ω";
|
||||||
|
case FLAGUNITAMP: return "A";
|
||||||
|
case FLAGUNITVOLT: return "V";
|
||||||
|
default: return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *scale() const {
|
||||||
|
if ((this->value_[REGSCALE] & FLAGSCALEDUTY) == FLAGSCALEDUTY) return "%";
|
||||||
|
if ((this->value_[REGSCALE] & FLAGSCALEMEGA) == FLAGSCALEMEGA) return "M";
|
||||||
|
if ((this->value_[REGSCALE] & FLAGSCALEKILO) == FLAGSCALEKILO) return "k";
|
||||||
|
if ((this->value_[REGSCALE] & FLAGSCALEMILLI) == FLAGSCALEMILLI) return "m";
|
||||||
|
if ((this->value_[REGSCALE] & FLAGSCALEMICRO) == FLAGSCALEMICRO) return "µ";
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string mode_text() const {
|
||||||
|
std::string out;
|
||||||
|
if (this->dc()) out += "DC ";
|
||||||
|
if (this->ac()) out += "AC ";
|
||||||
|
if (this->auto_range()) out += "AUTO ";
|
||||||
|
if (this->hold()) out += "HOLD ";
|
||||||
|
if (this->relative()) out += "REL ";
|
||||||
|
if (this->min_mode()) out += "MIN ";
|
||||||
|
if (this->max_mode()) out += "MAX ";
|
||||||
|
if (this->diode()) out += "DIODE ";
|
||||||
|
if (this->continuity()) out += "CONT ";
|
||||||
|
if (!out.empty()) out.pop_back();
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string reading_text() const {
|
||||||
|
if (!this->connected) return "Disconnected";
|
||||||
|
if (!this->has_reading) return "Waiting for data";
|
||||||
|
if (this->overload) return "OL " + std::string(this->scale()) + this->unit();
|
||||||
|
char buf[48];
|
||||||
|
snprintf(buf, sizeof(buf), "%s%.4g %s%s", this->negative() ? "-" : "", std::fabs(this->display_value), this->scale(), this->unit());
|
||||||
|
return std::string(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Kind { KIND_OTHER, KIND_VOLTAGE, KIND_CURRENT, KIND_RESISTANCE, KIND_FREQUENCY, KIND_CAPACITANCE, KIND_TEMP_C, KIND_TEMP_F, KIND_DUTY };
|
||||||
|
Kind kind() const {
|
||||||
|
if ((this->value_[REGSCALE] & FLAGSCALEDUTY) == FLAGSCALEDUTY) return KIND_DUTY;
|
||||||
|
switch (this->value_[REGUNIT]) {
|
||||||
|
case FLAGUNITVOLT: return KIND_VOLTAGE;
|
||||||
|
case FLAGUNITAMP: return KIND_CURRENT;
|
||||||
|
case FLAGUNITOHM: return KIND_RESISTANCE;
|
||||||
|
case FLAGUNITHZ: return KIND_FREQUENCY;
|
||||||
|
case FLAGUNITNF: return KIND_CAPACITANCE;
|
||||||
|
case FLAGUNITGRAD: return KIND_TEMP_C;
|
||||||
|
case FLAGUNITFAHR: return KIND_TEMP_F;
|
||||||
|
default: return KIND_OTHER;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *selected_button_name() const {
|
||||||
|
static const char *const names[] = {"SELECT", "RANGE", "HLD/LIG", "REL/BT", "HZ/DUTY", "MAX/MIN"};
|
||||||
|
uint8_t index = this->selected_button;
|
||||||
|
if (index < 1) index = 1;
|
||||||
|
if (index > 6) index = 6;
|
||||||
|
return names[index - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
void previous_button() {
|
||||||
|
if (this->selected_button > 1) this->selected_button--;
|
||||||
|
}
|
||||||
|
void next_button() {
|
||||||
|
if (this->selected_button < 6) this->selected_button++;
|
||||||
|
}
|
||||||
|
|
||||||
|
void render(Display &it, esphome::display::BaseFont *font) {
|
||||||
|
const Color bg(0, 0, 0);
|
||||||
|
const Color fg(210, 210, 210);
|
||||||
|
// Chosen to map to a neutral dark gray in the RGB332 8-bit display palette.
|
||||||
|
const Color inactive(80, 80, 80);
|
||||||
|
const Color yellow(255, 220, 0);
|
||||||
|
const Color blue(0, 80, 255);
|
||||||
|
const Color cyan(0, 255, 255);
|
||||||
|
const Color magenta(255, 0, 255);
|
||||||
|
const Color red(255, 0, 0);
|
||||||
|
const Color green(0, 220, 0);
|
||||||
|
const Color orange(255, 165, 0);
|
||||||
|
|
||||||
|
it.fill(bg);
|
||||||
|
this->draw_icon_(it, 12, 8, 16, 16, ACCU_BMP, this->low_battery ? red : green);
|
||||||
|
this->draw_icon_(it, 46, 8, 16, 16, BLE_BMP, this->connected ? blue : inactive);
|
||||||
|
this->label_(it, font, 86, 8, "AUTO", this->auto_range() ? fg : inactive);
|
||||||
|
this->label_(it, font, 138, 8, "MAX", this->max_mode() ? red : inactive);
|
||||||
|
this->label_(it, font, 178, 8, "MIN", this->min_mode() ? green : inactive);
|
||||||
|
this->label_(it, font, 218, 8, "HOLD", this->hold() ? blue : inactive);
|
||||||
|
this->label_(it, font, 270, 8, "REL", this->relative() ? Color(128, 128, 0) : inactive);
|
||||||
|
|
||||||
|
this->label_(it, font, 8, 66, "DC", this->dc() ? cyan : inactive);
|
||||||
|
this->label_(it, font, 8, 102, "AC", this->ac() ? magenta : inactive);
|
||||||
|
|
||||||
|
if (!this->connected) {
|
||||||
|
this->draw_digits_(it, "----", false, inactive);
|
||||||
|
it.print(160, 148, font, inactive, esphome::display::TextAlign::CENTER, "scan/connect");
|
||||||
|
} else if (!this->has_reading) {
|
||||||
|
this->draw_digits_(it, "8888", false, inactive);
|
||||||
|
it.print(160, 148, font, inactive, esphome::display::TextAlign::CENTER, "waiting");
|
||||||
|
} else if (this->overload) {
|
||||||
|
this->draw_digits_(it, " OL ", false, fg);
|
||||||
|
} else {
|
||||||
|
char d[5];
|
||||||
|
d[0] = this->digit_char_(REGDIG1);
|
||||||
|
d[1] = this->digit_char_(REGDIG2);
|
||||||
|
d[2] = this->digit_char_(REGDIG3);
|
||||||
|
d[3] = this->digit_char_(REGDIG4);
|
||||||
|
d[4] = 0;
|
||||||
|
this->draw_digits_(it, d, this->negative(), fg);
|
||||||
|
this->draw_decimal_points_(it, fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string unit_line = std::string(this->scale()) + this->unit();
|
||||||
|
it.print(270, 140, font, yellow, esphome::display::TextAlign::CENTER, unit_line.c_str());
|
||||||
|
|
||||||
|
this->draw_bargraph_(it, this->has_reading && !this->overload ? this->digits_from_buffer_() : 0, this->has_reading && !this->overload);
|
||||||
|
this->draw_icon_(it, 300, 148, 16, 16, DIODE_BMP, this->diode() ? magenta : inactive);
|
||||||
|
this->draw_icon_(it, 300, 174, 16, 16, BUZZ_BMP, this->continuity() ? orange : inactive);
|
||||||
|
|
||||||
|
it.filled_rectangle(34, 212, 40, 24, this->write_available ? fg : inactive);
|
||||||
|
it.filled_rectangle(108, 212, 100, 24, this->write_available ? fg : inactive);
|
||||||
|
it.filled_rectangle(242, 212, 40, 24, this->write_available ? fg : inactive);
|
||||||
|
it.print(54, 216, font, bg, esphome::display::TextAlign::TOP_CENTER, "<");
|
||||||
|
it.print(158, 216, font, bg, esphome::display::TextAlign::TOP_CENTER, this->selected_button_name());
|
||||||
|
it.print(262, 216, font, bg, esphome::display::TextAlign::TOP_CENTER, ">");
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
uint8_t raw_[14]{};
|
||||||
|
uint8_t value_[14]{};
|
||||||
|
float display_value{NAN};
|
||||||
|
float base_value{NAN};
|
||||||
|
static constexpr uint8_t OVERLOAD_FRAME[5] = {0x2B, 0x3F, 0x30, 0x3A, 0x3F};
|
||||||
|
|
||||||
|
uint16_t digits_from_buffer_() const {
|
||||||
|
uint16_t out = 0;
|
||||||
|
if (this->value_[REGDIG1] >= '0' && this->value_[REGDIG1] <= '9') out += (this->value_[REGDIG1] - '0') * 1000;
|
||||||
|
if (this->value_[REGDIG2] >= '0' && this->value_[REGDIG2] <= '9') out += (this->value_[REGDIG2] - '0') * 100;
|
||||||
|
if (this->value_[REGDIG3] >= '0' && this->value_[REGDIG3] <= '9') out += (this->value_[REGDIG3] - '0') * 10;
|
||||||
|
if (this->value_[REGDIG4] >= '0' && this->value_[REGDIG4] <= '9') out += (this->value_[REGDIG4] - '0');
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
float calc_display_value_() const {
|
||||||
|
if (this->overload) return NAN;
|
||||||
|
|
||||||
|
if (this->is_plus) {
|
||||||
|
uint16_t pair1 = static_cast<uint16_t>(this->raw_[0]) | (static_cast<uint16_t>(this->raw_[1]) << 8);
|
||||||
|
uint8_t decimal = pair1 & 0x07;
|
||||||
|
if (decimal >= 7) return NAN;
|
||||||
|
|
||||||
|
uint16_t pair3 = static_cast<uint16_t>(this->raw_[4]) | (static_cast<uint16_t>(this->raw_[5]) << 8);
|
||||||
|
bool negative = pair3 >= 0x7FFF;
|
||||||
|
uint16_t digits = negative ? (pair3 & 0x7FFF) : pair3;
|
||||||
|
float v = static_cast<float>(digits) / std::pow(10.0f, decimal);
|
||||||
|
return negative ? -v : v;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t decimal = 0;
|
||||||
|
switch (this->value_[REGPOINT] & 0x07) {
|
||||||
|
case 0b001: decimal = 1; break;
|
||||||
|
case 0b010: decimal = 2; break;
|
||||||
|
case 0b100: decimal = 3; break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
float v = static_cast<float>(this->digits_from_buffer_()) / std::pow(10.0f, decimal);
|
||||||
|
return this->negative() ? -v : v;
|
||||||
|
}
|
||||||
|
|
||||||
|
float calc_base_value_() const {
|
||||||
|
if (std::isnan(this->display_value)) return NAN;
|
||||||
|
if (this->value_[REGUNIT] == FLAGUNITNF) return this->display_value * 1e-9f;
|
||||||
|
if ((this->value_[REGSCALE] & FLAGSCALEMEGA) == FLAGSCALEMEGA) return this->display_value * 1e6f;
|
||||||
|
if ((this->value_[REGSCALE] & FLAGSCALEKILO) == FLAGSCALEKILO) return this->display_value * 1e3f;
|
||||||
|
if ((this->value_[REGSCALE] & FLAGSCALEMILLI) == FLAGSCALEMILLI) return this->display_value * 1e-3f;
|
||||||
|
if ((this->value_[REGSCALE] & FLAGSCALEMICRO) == FLAGSCALEMICRO) return this->display_value * 1e-6f;
|
||||||
|
return this->display_value;
|
||||||
|
}
|
||||||
|
|
||||||
|
void parse_plus_() {
|
||||||
|
memset(this->value_, 0, sizeof(this->value_));
|
||||||
|
this->value_[5] = 0x20;
|
||||||
|
this->value_[12] = 0x0D;
|
||||||
|
this->value_[13] = 0x0A;
|
||||||
|
|
||||||
|
uint16_t pair1 = static_cast<uint16_t>(this->raw_[0]) | (static_cast<uint16_t>(this->raw_[1]) << 8);
|
||||||
|
uint8_t function = (pair1 >> 6) & 0x0F;
|
||||||
|
uint8_t scale = (pair1 >> 3) & 0x07;
|
||||||
|
uint8_t decimal = pair1 & 0x07;
|
||||||
|
|
||||||
|
switch (decimal) {
|
||||||
|
case 0: this->value_[REGPOINT] = FLAGPOINT0; break;
|
||||||
|
case 1: this->value_[REGPOINT] = FLAGPOINT3; break;
|
||||||
|
case 2: this->value_[REGPOINT] = FLAGPOINT2; break;
|
||||||
|
case 3: this->value_[REGPOINT] = FLAGPOINT1; break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (function) {
|
||||||
|
case 0: this->value_[REGUNIT] |= FLAGUNITVOLT; this->value_[REGMODE] |= FLAGMODEDC; break;
|
||||||
|
case 1: this->value_[REGUNIT] |= FLAGUNITVOLT; this->value_[REGMODE] |= FLAGMODEAC; break;
|
||||||
|
case 2: this->value_[REGUNIT] |= FLAGUNITAMP; this->value_[REGMODE] |= FLAGMODEDC; break;
|
||||||
|
case 3: this->value_[REGUNIT] |= FLAGUNITAMP; this->value_[REGMODE] |= FLAGMODEAC; break;
|
||||||
|
case 4: this->value_[REGUNIT] |= FLAGUNITOHM; break;
|
||||||
|
case 5: this->value_[REGUNIT] |= FLAGUNITNF; break;
|
||||||
|
case 6: this->value_[REGUNIT] |= FLAGUNITHZ; break;
|
||||||
|
case 7: this->value_[REGSCALE] |= FLAGSCALEDUTY; break;
|
||||||
|
case 8: this->value_[REGUNIT] |= FLAGUNITGRAD; break;
|
||||||
|
case 9: this->value_[REGUNIT] |= FLAGUNITFAHR; break;
|
||||||
|
case 10: this->value_[REGSCALE] |= FLAGSCALEDIODE; break;
|
||||||
|
case 11: this->value_[REGSCALE] |= FLAGSCALEBUZZ; break;
|
||||||
|
case 12: this->value_[REGUNIT] |= FLAGUNITHFE; break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (scale) {
|
||||||
|
case 2: this->value_[REGSCALE] |= FLAGSCALEMICRO; break;
|
||||||
|
case 3: this->value_[REGSCALE] |= FLAGSCALEMILLI; break;
|
||||||
|
case 5: this->value_[REGSCALE] |= FLAGSCALEKILO; break;
|
||||||
|
case 6: this->value_[REGSCALE] |= FLAGSCALEMEGA; break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t pair2 = static_cast<uint16_t>(this->raw_[2]) | (static_cast<uint16_t>(this->raw_[3]) << 8);
|
||||||
|
if (pair2 & (1 << 0)) this->value_[REGMODE] |= FLAGMODEHOLD;
|
||||||
|
if (pair2 & (1 << 1)) this->value_[REGMODE] |= FLAGMODEREL;
|
||||||
|
if (pair2 & (1 << 2)) this->value_[REGMODE] |= FLAGMODEAUTO;
|
||||||
|
this->low_battery = (pair2 & (1 << 3)) != 0;
|
||||||
|
if (pair2 & (1 << 4)) this->value_[REGMINMAX] |= FLAGMIN;
|
||||||
|
if (pair2 & (1 << 5)) this->value_[REGMINMAX] |= FLAGMAX;
|
||||||
|
|
||||||
|
uint16_t pair3 = static_cast<uint16_t>(this->raw_[4]) | (static_cast<uint16_t>(this->raw_[5]) << 8);
|
||||||
|
if (decimal < 7) {
|
||||||
|
uint16_t digits = pair3;
|
||||||
|
if (pair3 < 0x7FFF) {
|
||||||
|
this->value_[REGPLUSMINUS] = FLAGPLUS;
|
||||||
|
} else {
|
||||||
|
this->value_[REGPLUSMINUS] = FLAGMINUS;
|
||||||
|
digits = pair3 & 0x7FFF;
|
||||||
|
}
|
||||||
|
this->value_[REGDIG1] = '0' + ((digits / 1000) % 10);
|
||||||
|
this->value_[REGDIG2] = '0' + ((digits / 100) % 10);
|
||||||
|
this->value_[REGDIG3] = '0' + ((digits / 10) % 10);
|
||||||
|
this->value_[REGDIG4] = '0' + (digits % 10);
|
||||||
|
} else {
|
||||||
|
memcpy(this->value_, OVERLOAD_FRAME, sizeof(OVERLOAD_FRAME));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
char digit_char_(uint8_t reg) const {
|
||||||
|
uint8_t c = this->value_[reg];
|
||||||
|
return (c >= '0' && c <= '9') ? static_cast<char>(c) : ' ';
|
||||||
|
}
|
||||||
|
|
||||||
|
void label_(Display &it, esphome::display::BaseFont *font, int x, int y, const char *text, Color color) {
|
||||||
|
it.print(x, y, font, color, esphome::display::TextAlign::TOP_LEFT, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
void draw_digits_(Display &it, const char *text, bool negative, Color color) {
|
||||||
|
if (negative) this->draw_segment_(it, 8, 88, 26, 9, true, color);
|
||||||
|
constexpr int digit_x = 40;
|
||||||
|
constexpr int digit_y = 35;
|
||||||
|
constexpr int digit_w = 50;
|
||||||
|
constexpr int digit_h = 88;
|
||||||
|
constexpr int digit_distance = 64;
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
this->draw_seven_segment_(it, digit_x + i * digit_distance, digit_y, digit_w, digit_h, text[i], color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void draw_decimal_points_(Display &it, Color color) {
|
||||||
|
uint8_t p = this->value_[REGPOINT];
|
||||||
|
if ((p & FLAGPOINT1) == FLAGPOINT1) it.filled_rectangle(95, 117, 8, 10, color);
|
||||||
|
if ((p & FLAGPOINT2) == FLAGPOINT2) it.filled_rectangle(159, 117, 8, 10, color);
|
||||||
|
if ((p & FLAGPOINT3) == FLAGPOINT3) it.filled_rectangle(223, 117, 8, 10, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
void draw_segment_(Display &it, int x, int y, int w, int h, bool horizontal, Color color) {
|
||||||
|
if (horizontal) {
|
||||||
|
int cap = h / 2;
|
||||||
|
it.filled_rectangle(x + cap, y, w - 2 * cap, h, color);
|
||||||
|
it.filled_triangle(x, y + cap, x + cap, y, x + cap, y + h, color);
|
||||||
|
it.filled_triangle(x + w, y + cap, x + w - cap, y, x + w - cap, y + h, color);
|
||||||
|
} else {
|
||||||
|
int cap = w / 2;
|
||||||
|
it.filled_rectangle(x, y + cap, w, h - 2 * cap, color);
|
||||||
|
it.filled_triangle(x + cap, y, x, y + cap, x + w, y + cap, color);
|
||||||
|
it.filled_triangle(x + cap, y + h, x, y + h - cap, x + w, y + h - cap, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void draw_icon_(Display &it, int x, int y, int w, int h, const uint8_t *data, Color color) {
|
||||||
|
for (int row = 0; row < h; row++) {
|
||||||
|
for (int col = 0; col < w; col++) {
|
||||||
|
uint8_t byte = data[row * ((w + 7) / 8) + (col / 8)];
|
||||||
|
if ((byte & (0x80 >> (col % 8))) != 0) {
|
||||||
|
it.draw_pixel_at(x + col, y + row, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void draw_seven_segment_(Display &it, int x, int y, int w, int h, char ch, Color color) {
|
||||||
|
bool a=false,b=false,c=false,d=false,e=false,f=false,g=false;
|
||||||
|
switch (ch) {
|
||||||
|
case '0': a=b=c=d=e=f=true; break;
|
||||||
|
case '1': b=c=true; break;
|
||||||
|
case '2': a=b=d=e=g=true; break;
|
||||||
|
case '3': a=b=c=d=g=true; break;
|
||||||
|
case '4': b=c=f=g=true; break;
|
||||||
|
case '5': a=c=d=f=g=true; break;
|
||||||
|
case '6': a=c=d=e=f=g=true; break;
|
||||||
|
case '7': a=b=c=true; break;
|
||||||
|
case '8': a=b=c=d=e=f=g=true; break;
|
||||||
|
case '9': a=b=c=d=f=g=true; break;
|
||||||
|
case 'O': a=b=c=d=e=f=true; break;
|
||||||
|
case 'L': d=e=f=true; break;
|
||||||
|
case '-': g=true; break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
int t = 10;
|
||||||
|
int half = h / 2;
|
||||||
|
int gap = 1;
|
||||||
|
|
||||||
|
int top_y = y;
|
||||||
|
int mid_y = y + half - t / 2;
|
||||||
|
int bot_y = y + h - t;
|
||||||
|
|
||||||
|
int upper_v_y = top_y + t + gap;
|
||||||
|
int upper_v_h = mid_y - gap - upper_v_y;
|
||||||
|
int lower_v_y = mid_y + t + gap;
|
||||||
|
int lower_v_h = bot_y - gap - lower_v_y;
|
||||||
|
|
||||||
|
if (a) this->draw_segment_(it, x + t / 2, top_y, w - t, t, true, color);
|
||||||
|
if (b && upper_v_h > 0) this->draw_segment_(it, x + w - t, upper_v_y, t, upper_v_h, false, color);
|
||||||
|
if (c && lower_v_h > 0) this->draw_segment_(it, x + w - t, lower_v_y, t, lower_v_h, false, color);
|
||||||
|
if (d) this->draw_segment_(it, x + t / 2, bot_y, w - t, t, true, color);
|
||||||
|
if (e && lower_v_h > 0) this->draw_segment_(it, x, lower_v_y, t, lower_v_h, false, color);
|
||||||
|
if (f && upper_v_h > 0) this->draw_segment_(it, x, upper_v_y, t, upper_v_h, false, color);
|
||||||
|
if (g) this->draw_segment_(it, x + t / 2, mid_y, w - t, t, true, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
void draw_bargraph_(Display &it, uint16_t digits, bool active) {
|
||||||
|
const Color fg(255, 255, 255);
|
||||||
|
const Color inactive(80, 80, 80);
|
||||||
|
uint16_t mapped = active ? static_cast<uint16_t>(digits * 240 / 6000) : 0;
|
||||||
|
if (mapped > 240) mapped = 240;
|
||||||
|
for (uint16_t i = 0; i <= 240; i += 4) {
|
||||||
|
Color col = (active && i <= mapped) ? fg : inactive;
|
||||||
|
int h = (i % 40 == 0) ? 20 : ((i % 20 == 0) ? 15 : 10);
|
||||||
|
it.vertical_line(40 + i, 185 - h, h, col);
|
||||||
|
}
|
||||||
|
it.horizontal_line(35, 185, 250, inactive);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace owon_b35t
|
||||||
|
|
||||||
|
static owon_b35t::Meter owon_meter;
|
||||||
@@ -0,0 +1,529 @@
|
|||||||
|
# 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:
|
||||||
|
- platform: esphome
|
||||||
|
id: ota_esphome
|
||||||
|
password: !secret ota
|
||||||
|
|
||||||
|
external_components:
|
||||||
|
- source: github://RASPIAUDIO/esphomeLuxe@main
|
||||||
|
components: [es8388]
|
||||||
|
refresh: 0s
|
||||||
|
|
||||||
|
network:
|
||||||
|
enable_ipv6: true
|
||||||
|
|
||||||
|
wifi:
|
||||||
|
ssid: Voltage-legacy
|
||||||
|
password: !secret voltage_legacy_psk
|
||||||
|
#use_address: ${name}.home
|
||||||
|
power_save_mode: high
|
||||||
|
fast_connect: on
|
||||||
|
min_auth_mode: WPA2
|
||||||
|
# Enable fallback hotspot (captive portal) in case wifi connection fails
|
||||||
|
ap:
|
||||||
|
ssid: "Raspiaudio Fallback Hotspot"
|
||||||
|
password: !secret fallback_psk
|
||||||
|
|
||||||
|
esphome:
|
||||||
|
name: ${name}
|
||||||
|
friendly_name: ${friendly_name}
|
||||||
|
min_version: 2025.2.0
|
||||||
|
name_add_mac_suffix: false
|
||||||
|
platformio_options:
|
||||||
|
board_build.flash_mode: dio
|
||||||
|
board_build.arduino.memory_type: qio_opi
|
||||||
|
project:
|
||||||
|
name: raspiaudio.voice-assistant
|
||||||
|
version: "2025.3.1"
|
||||||
|
on_boot:
|
||||||
|
priority: -100.0
|
||||||
|
then:
|
||||||
|
- lambda: id(phase) = 0;
|
||||||
|
- script.execute: update_led
|
||||||
|
|
||||||
|
esp32:
|
||||||
|
board: esp-wrover-kit
|
||||||
|
flash_size: 4MB
|
||||||
|
framework:
|
||||||
|
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: okay_nabu
|
||||||
|
- 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
|
||||||
|
|
||||||
|
#####################
|
||||||
|
# Internal Components
|
||||||
|
output:
|
||||||
|
- platform: gpio
|
||||||
|
id: dac_mute
|
||||||
|
pin:
|
||||||
|
number: GPIO21
|
||||||
|
inverted: true
|
||||||
|
mode:
|
||||||
|
output: true
|
||||||
|
|
||||||
|
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'
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
- 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 voltage
|
||||||
|
device_class: voltage
|
||||||
|
unit_of_measurement: "V"
|
||||||
|
accuracy_decimals: 2
|
||||||
|
state_class: measurement
|
||||||
|
entity_category: diagnostic
|
||||||
|
update_interval: 15s
|
||||||
|
attenuation: auto
|
||||||
|
filters:
|
||||||
|
- multiply: 2
|
||||||
|
- calibrate_linear:
|
||||||
|
- 0.0 -> 0.0
|
||||||
|
# Map measured full charge to real value to compensate divider/ADC bias
|
||||||
|
- 4.58 -> 4.20
|
||||||
|
- exponential_moving_average:
|
||||||
|
alpha: 0.2
|
||||||
|
send_every: 2
|
||||||
|
- 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.20 -> 100.0
|
||||||
|
- 4.13 -> 97.1
|
||||||
|
- 4.10 -> 94.2
|
||||||
|
- 4.07 -> 88.4
|
||||||
|
- 4.05 -> 82.7
|
||||||
|
- 4.04 -> 76.9
|
||||||
|
- 4.04 -> 71.1
|
||||||
|
- 4.01 -> 65.3
|
||||||
|
- 3.99 -> 59.5
|
||||||
|
- 3.95 -> 53.8
|
||||||
|
- 3.93 -> 48.0
|
||||||
|
- 3.91 -> 42.2
|
||||||
|
- 3.88 -> 36.4
|
||||||
|
- 3.86 -> 30.6
|
||||||
|
- 3.84 -> 24.9
|
||||||
|
- 3.82 -> 19.1
|
||||||
|
- 3.76 -> 13.3
|
||||||
|
- 3.73 -> 10.4
|
||||||
|
- 3.70 -> 7.5
|
||||||
|
- 3.64 -> 4.6
|
||||||
|
- 3.50 -> 1.7
|
||||||
|
- 3.00 -> 0.0
|
||||||
|
- lambda: return clamp(x, 0.0f, 100.0f);
|
||||||
|
|
||||||
|
binary_sensor:
|
||||||
|
- platform: gpio
|
||||||
|
pin:
|
||||||
|
number: GPIO27
|
||||||
|
mode:
|
||||||
|
input: true
|
||||||
|
pullup: true
|
||||||
|
id: jack_detect
|
||||||
|
name: Jack Detect
|
||||||
|
internal: true
|
||||||
|
on_press:
|
||||||
|
- script.execute: jack_detect_high
|
||||||
|
on_release:
|
||||||
|
- script.execute: jack_detect_low
|
||||||
|
- platform: gpio
|
||||||
|
pin:
|
||||||
|
number: GPIO19
|
||||||
|
inverted: true
|
||||||
|
mode:
|
||||||
|
input: true
|
||||||
|
pullup: true
|
||||||
|
name: Volume Up
|
||||||
|
on_click:
|
||||||
|
- 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
|
||||||
|
inverted: true
|
||||||
|
mode:
|
||||||
|
input: true
|
||||||
|
pullup: true
|
||||||
|
name: Volume Down
|
||||||
|
on_click:
|
||||||
|
- 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: 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: esp32_rmt_led_strip
|
||||||
|
name: None
|
||||||
|
id: top_led
|
||||||
|
pin: GPIO22
|
||||||
|
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:
|
||||||
|
id: i2s0
|
||||||
|
i2s_lrclk_pin: GPIO25
|
||||||
|
i2s_bclk_pin: GPIO5
|
||||||
|
i2s_mclk_pin: GPIO0
|
||||||
|
|
||||||
|
microphone:
|
||||||
|
- platform: i2s_audio
|
||||||
|
id: luxe_mic
|
||||||
|
i2s_audio_id: i2s0
|
||||||
|
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
|
||||||
|
num_channels: 2
|
||||||
|
buffer_duration: 100ms
|
||||||
|
|
||||||
|
media_player:
|
||||||
|
- platform: speaker
|
||||||
|
name: None
|
||||||
|
id: luxe_media_player
|
||||||
|
internal: false
|
||||||
|
# task_stack_in_psram: true
|
||||||
|
# volume_min: 0.5
|
||||||
|
# volume_max: 0.8
|
||||||
|
announcement_pipeline:
|
||||||
|
speaker: luxe_speaker
|
||||||
|
format: FLAC
|
||||||
|
sample_rate: 48000
|
||||||
|
num_channels: 1
|
||||||
|
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"
|
||||||
|
- micro_wake_word.stop:
|
||||||
|
- 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: jack_detect_high
|
||||||
|
then:
|
||||||
|
- logger.log: "Jack detect high -> route to speaker"
|
||||||
|
- output.turn_off: dac_mute
|
||||||
|
- lambda: |-
|
||||||
|
id(my_es8388).write_byte(0x1D, 0x20);
|
||||||
|
id(my_es8388).write_byte(0x1C, 0x10);
|
||||||
|
id(my_es8388).write_byte(0x04, 0x30);
|
||||||
|
- id: jack_detect_low
|
||||||
|
then:
|
||||||
|
- logger.log: "Jack detect low -> route to line out"
|
||||||
|
- output.turn_on: dac_mute
|
||||||
|
- lambda: |-
|
||||||
|
id(my_es8388).write_byte(0x1D, 0x00);
|
||||||
|
id(my_es8388).write_byte(0x1C, 0x00);
|
||||||
|
id(my_es8388).write_byte(0x04, 0x0C);
|
||||||
|
- id: update_led
|
||||||
|
then:
|
||||||
|
- logger.log: "==>>>update_led"
|
||||||
|
- 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: false
|
||||||
|
- script.execute: update_led
|
||||||
|
|
||||||
|
- id: waiting
|
||||||
|
then:
|
||||||
|
- light.turn_on:
|
||||||
|
id: top_led
|
||||||
|
effect: pulse
|
||||||
|
red: 0%
|
||||||
|
green: 0%
|
||||||
|
blue: 100%
|
||||||
|
brightness: 100%
|
||||||
|
- voice_assistant.stop:
|
||||||
|
- micro_wake_word.start:
|
||||||
|
|
||||||
|
|
||||||
|
- id: listening
|
||||||
|
then:
|
||||||
|
- light.turn_on:
|
||||||
|
id: top_led
|
||||||
|
effect: pulse
|
||||||
|
red: 0%
|
||||||
|
green: 100%
|
||||||
|
blue: 0%
|
||||||
|
brightness: 100%
|
||||||
|
|
||||||
|
- id: answering
|
||||||
|
then:
|
||||||
|
- light.turn_on:
|
||||||
|
id: top_led
|
||||||
|
effect: none
|
||||||
|
red: 100%
|
||||||
|
green: 100%
|
||||||
|
blue: 0%
|
||||||
|
brightness: 100%
|
||||||
|
|
||||||
|
- id: external_player
|
||||||
|
then:
|
||||||
|
- light.turn_on:
|
||||||
|
id: top_led
|
||||||
|
effect: none
|
||||||
|
red: 80%
|
||||||
|
green: 40%
|
||||||
|
blue: 0%
|
||||||
|
|
||||||
|
- id: mute_on
|
||||||
|
then:
|
||||||
|
- media_player.volume_set:
|
||||||
|
volume: '0'
|
||||||
|
- lambda: id(mute) = true;
|
||||||
|
|
||||||
|
- id: mute_off
|
||||||
|
then:
|
||||||
|
- media_player.volume_set:
|
||||||
|
volume: !lambda return(id(Vol));
|
||||||
|
- lambda: id(mute) = false;
|
||||||
@@ -1,8 +1,15 @@
|
|||||||
esphome:
|
substitutions:
|
||||||
name: sleepingroom
|
name: "sleepingroom"
|
||||||
platform: ESP32
|
friendly_name: "Luftqualität Schlafzimmer"
|
||||||
board: nodemcu-32s
|
|
||||||
|
|
||||||
|
esphome:
|
||||||
|
name: ${name}
|
||||||
|
friendly_name: ${friendly_name}
|
||||||
|
name_add_mac_suffix: false
|
||||||
|
project:
|
||||||
|
name: sensor.${name}
|
||||||
|
version: "1.0"
|
||||||
|
min_version: 2021.9.0
|
||||||
on_boot:
|
on_boot:
|
||||||
then:
|
then:
|
||||||
- light.turn_on:
|
- light.turn_on:
|
||||||
@@ -41,12 +48,18 @@ esphome:
|
|||||||
id: pm25_warn
|
id: pm25_warn
|
||||||
state: "grün"
|
state: "grün"
|
||||||
|
|
||||||
|
esp32:
|
||||||
|
board: nodemcu-32s
|
||||||
|
framework:
|
||||||
|
type: esp-idf
|
||||||
|
|
||||||
wifi:
|
wifi:
|
||||||
ssid: "Voltage-legacy"
|
ssid: "Voltage-legacy"
|
||||||
password: !secret voltage_legacy_psk
|
password: !secret voltage_legacy_psk
|
||||||
use_address: sleepingroom.home
|
use_address: ${name}.home
|
||||||
power_save_mode: high
|
power_save_mode: high
|
||||||
fast_connect: on
|
fast_connect: on
|
||||||
|
min_auth_mode: WPA2
|
||||||
|
|
||||||
# Enable fallback hotspot (captive portal) in case wifi connection fails
|
# Enable fallback hotspot (captive portal) in case wifi connection fails
|
||||||
ap:
|
ap:
|
||||||
@@ -66,6 +79,7 @@ api:
|
|||||||
|
|
||||||
# Enable over-the-air updates
|
# Enable over-the-air updates
|
||||||
ota:
|
ota:
|
||||||
|
platform: esphome
|
||||||
password: !secret ota
|
password: !secret ota
|
||||||
|
|
||||||
# Initialize I²C
|
# Initialize I²C
|
||||||
@@ -100,6 +114,7 @@ light:
|
|||||||
|
|
||||||
# Initialize UART for Vindriktning sensor
|
# Initialize UART for Vindriktning sensor
|
||||||
uart:
|
uart:
|
||||||
|
id: uartPM
|
||||||
rx_pin: GPIO26
|
rx_pin: GPIO26
|
||||||
baud_rate: 9600
|
baud_rate: 9600
|
||||||
|
|
||||||
@@ -186,17 +201,19 @@ sensor:
|
|||||||
temperature:
|
temperature:
|
||||||
name: "Schlafzimmer Temperatur"
|
name: "Schlafzimmer Temperatur"
|
||||||
accuracy_decimals: 2
|
accuracy_decimals: 2
|
||||||
|
filters:
|
||||||
|
- offset: -2.5
|
||||||
humidity:
|
humidity:
|
||||||
name: "Schlafzimmer Luftfeuchtigkeit"
|
name: "Schlafzimmer Luftfeuchtigkeit"
|
||||||
accuracy_decimals: 1
|
accuracy_decimals: 1
|
||||||
# SCD30 temp sensor is a bit off
|
# SCD30 temp sensor is a bit off
|
||||||
temperature_offset: 0 °C
|
|
||||||
ambient_pressure_compensation: 1
|
ambient_pressure_compensation: 1
|
||||||
automatic_self_calibration: True
|
automatic_self_calibration: True
|
||||||
address: 0x61
|
address: 0x61
|
||||||
update_interval: 60s
|
update_interval: 10s
|
||||||
# Vindriktning particulate matter sensor
|
# Vindriktning particulate matter sensor
|
||||||
- platform: pm1006
|
- platform: pm1006
|
||||||
|
uart_id: uartPM
|
||||||
pm_2_5:
|
pm_2_5:
|
||||||
name: "Feinstaub PM 2.5µm"
|
name: "Feinstaub PM 2.5µm"
|
||||||
on_value_range:
|
on_value_range:
|
||||||
@@ -221,7 +238,7 @@ sensor:
|
|||||||
- platform: adc
|
- platform: adc
|
||||||
pin: 32
|
pin: 32
|
||||||
name: "Spannung Photodiode"
|
name: "Spannung Photodiode"
|
||||||
attenuation: 11db
|
attenuation: 12db
|
||||||
update_interval: 1s
|
update_interval: 1s
|
||||||
internal: true
|
internal: true
|
||||||
on_value_range:
|
on_value_range:
|
||||||
@@ -245,6 +262,7 @@ remote_receiver:
|
|||||||
|
|
||||||
# IR Transmitter
|
# IR Transmitter
|
||||||
remote_transmitter:
|
remote_transmitter:
|
||||||
|
non_blocking: True
|
||||||
pin: GPIO22
|
pin: GPIO22
|
||||||
# Infrared remotes use a 50% carrier signal
|
# Infrared remotes use a 50% carrier signal
|
||||||
carrier_duty_percent: 50%
|
carrier_duty_percent: 50%
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
network:
|
||||||
|
enable_ipv6: true
|
||||||
|
|
||||||
|
wifi:
|
||||||
|
ssid: "Voltage-legacy"
|
||||||
|
password: !secret voltage_legacy_psk
|
||||||
|
#use_address: bathroom.home
|
||||||
|
power_save_mode: high
|
||||||
|
fast_connect: on
|
||||||
|
min_auth_mode: WPA2
|
||||||
|
|
||||||
|
# Enable fallback hotspot (captive portal) in case wifi connection fails
|
||||||
|
ap:
|
||||||
|
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
|
||||||
@@ -1,14 +1,31 @@
|
|||||||
|
substitutions:
|
||||||
|
name: "geigercounter"
|
||||||
|
friendly_name: "Geigerzähler"
|
||||||
|
|
||||||
esphome:
|
esphome:
|
||||||
name: geigercounter
|
name: ${name}
|
||||||
platform: ESP32
|
friendly_name: ${friendly_name}
|
||||||
|
name_add_mac_suffix: false
|
||||||
|
project:
|
||||||
|
name: sensor.radiation
|
||||||
|
version: "1.0"
|
||||||
|
min_version: 2022.11.0
|
||||||
|
|
||||||
|
esp32:
|
||||||
board: nodemcu-32s
|
board: nodemcu-32s
|
||||||
|
framework:
|
||||||
|
type: esp-idf
|
||||||
|
|
||||||
|
network:
|
||||||
|
enable_ipv6: true
|
||||||
|
|
||||||
wifi:
|
wifi:
|
||||||
ssid: "Voltage-legacy"
|
ssid: "Voltage-legacy"
|
||||||
password: !secret voltage_legacy_psk
|
password: !secret voltage_legacy_psk
|
||||||
use_address: geigercounter.home
|
#use_address: ${name}.home
|
||||||
power_save_mode: high
|
power_save_mode: high
|
||||||
fast_connect: on
|
fast_connect: on
|
||||||
|
min_auth_mode: WPA2
|
||||||
|
|
||||||
# Enable fallback hotspot (captive portal) in case wifi connection fails
|
# Enable fallback hotspot (captive portal) in case wifi connection fails
|
||||||
ap:
|
ap:
|
||||||
@@ -26,6 +43,7 @@ api:
|
|||||||
key: !secret apikey
|
key: !secret apikey
|
||||||
|
|
||||||
ota:
|
ota:
|
||||||
|
platform: esphome
|
||||||
password: !secret ota
|
password: !secret ota
|
||||||
|
|
||||||
spi:
|
spi:
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
substitutions:
|
||||||
|
name: home-assistant-voice-09c0e7
|
||||||
|
friendly_name: Home Assistant Voice Küche
|
||||||
|
packages:
|
||||||
|
Nabu Casa.Home Assistant Voice PE: github://esphome/home-assistant-voice-pe/home-assistant-voice.yaml
|
||||||
|
esphome:
|
||||||
|
name: ${name}
|
||||||
|
name_add_mac_suffix: false
|
||||||
|
friendly_name: ${friendly_name}
|
||||||
|
|
||||||
|
wifi:
|
||||||
|
ssid: "Voltage-legacy"
|
||||||
|
password: !secret voltage_legacy_psk
|
||||||
|
#use_address: ${name}.home
|
||||||
|
#power_save_mode: high
|
||||||
|
#fast_connect: on
|
||||||
|
min_auth_mode: WPA2
|
||||||
|
|
||||||
|
# Enable fallback hotspot (captive portal) in case wifi connection fails
|
||||||
|
ap:
|
||||||
|
ssid: "HA Voice Fallback Hotspot"
|
||||||
|
password: !secret fallback_psk
|
||||||
|
|
||||||
|
# Enable Home Assistant API
|
||||||
|
api:
|
||||||
|
encryption:
|
||||||
|
key: !secret apikey
|
||||||
|
|
||||||
|
ota:
|
||||||
|
platform: esphome
|
||||||
|
password: !secret ota
|
||||||
|
|
||||||
|
http_request:
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
substitutions:
|
||||||
|
name: home-assistant-voice-0a7c6b
|
||||||
|
friendly_name: Home Assistant Voice Wohnzimmer
|
||||||
|
packages:
|
||||||
|
Nabu Casa.Home Assistant Voice PE: github://esphome/home-assistant-voice-pe/home-assistant-voice.yaml
|
||||||
|
esphome:
|
||||||
|
name: ${name}
|
||||||
|
name_add_mac_suffix: false
|
||||||
|
friendly_name: ${friendly_name}
|
||||||
|
|
||||||
|
wifi:
|
||||||
|
ssid: "Voltage-legacy"
|
||||||
|
password: !secret voltage_legacy_psk
|
||||||
|
#use_address: ${name}.home
|
||||||
|
#power_save_mode: high
|
||||||
|
#fast_connect: on
|
||||||
|
min_auth_mode: WPA2
|
||||||
|
|
||||||
|
# Enable fallback hotspot (captive portal) in case wifi connection fails
|
||||||
|
ap:
|
||||||
|
ssid: "HA Voice Fallback Hotspot"
|
||||||
|
password: !secret fallback_psk
|
||||||
|
|
||||||
|
# Enable Home Assistant API
|
||||||
|
api:
|
||||||
|
encryption:
|
||||||
|
key: !secret apikey
|
||||||
|
|
||||||
|
ota:
|
||||||
|
platform: esphome
|
||||||
|
password: !secret ota
|
||||||
|
|
||||||
|
http_request:
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
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: esp-idf
|
||||||
|
|
||||||
|
network:
|
||||||
|
enable_ipv6: true
|
||||||
|
|
||||||
|
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: GPIO3
|
||||||
|
#sda: GPIO13
|
||||||
|
#scl: GPIO14
|
||||||
|
scl: GPIO1
|
||||||
|
scan: true
|
||||||
|
- id: bus_c
|
||||||
|
sda: GPIO26
|
||||||
|
scl: GPIO27
|
||||||
|
|
||||||
|
psram:
|
||||||
|
speed: 80MHz
|
||||||
|
|
||||||
|
# Camera
|
||||||
|
esp32_camera:
|
||||||
|
frame_buffer_location: PSRAM
|
||||||
|
name: ${friendly_name}
|
||||||
|
external_clock:
|
||||||
|
pin: GPIO0
|
||||||
|
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: XGA
|
||||||
|
max_framerate: 24 fps
|
||||||
|
idle_framerate: 0.2 fps
|
||||||
|
jpeg_quality: 15
|
||||||
|
agc_mode: auto
|
||||||
|
agc_gain_ceiling: 8x
|
||||||
|
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
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
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: esp-idf
|
||||||
|
|
||||||
|
network:
|
||||||
|
enable_ipv6: true
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# Initialize I²C
|
||||||
|
i2c:
|
||||||
|
- id: bus_c
|
||||||
|
sda: GPIO26
|
||||||
|
scl: GPIO27
|
||||||
|
|
||||||
|
psram:
|
||||||
|
speed: 80MHz
|
||||||
|
|
||||||
|
# Camera
|
||||||
|
esp32_camera:
|
||||||
|
frame_buffer_location: PSRAM
|
||||||
|
name: ${friendly_name}
|
||||||
|
external_clock:
|
||||||
|
pin: GPIO0
|
||||||
|
frequency: 20MHz
|
||||||
|
i2c_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: XGA
|
||||||
|
max_framerate: 24 fps
|
||||||
|
idle_framerate: 0.2 fps
|
||||||
|
jpeg_quality: 15
|
||||||
|
agc_mode: auto
|
||||||
|
agc_gain_ceiling: 8x
|
||||||
|
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
|
||||||
@@ -0,0 +1,785 @@
|
|||||||
|
/*
|
||||||
|
* ESPHome helper for OWON B35T/B35T+ BLE meter on M5Stack Core 2.
|
||||||
|
* Parser is based on the standalone Arduino sketch by Reaper7
|
||||||
|
* (Beerware license, Revision 42) and Dean Cording's owonb35 notes.
|
||||||
|
* Rendering code and some functionality modified to suppport the
|
||||||
|
* Atorch DL24 dc load.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstring>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include "esp_heap_caps.h"
|
||||||
|
#include "esp_system.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
#include "esphome/components/i2c/i2c.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
#include "esphome/components/display/display.h"
|
||||||
|
|
||||||
|
namespace owon_b35t {
|
||||||
|
|
||||||
|
using esphome::Color;
|
||||||
|
using esphome::display::Display;
|
||||||
|
|
||||||
|
static const char *const TAG = "owon_b35t";
|
||||||
|
static const char *const POWER_TAG = "core2_power";
|
||||||
|
static constexpr uint8_t AXP192_ADDR = 0x34;
|
||||||
|
static esphome::i2c::I2CDevice axp192;
|
||||||
|
static bool axp192_ready = false;
|
||||||
|
|
||||||
|
static bool axp_write(uint8_t reg, uint8_t value) {
|
||||||
|
if (!axp192_ready) return false;
|
||||||
|
bool ok = axp192.write_byte(reg, value);
|
||||||
|
if (!ok) ESP_LOGW(POWER_TAG, "AXP192 write reg 0x%02X failed", reg);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool axp_read(uint8_t reg, uint8_t *value) {
|
||||||
|
if (!axp192_ready) return false;
|
||||||
|
bool ok = axp192.read_byte(reg, value);
|
||||||
|
if (!ok) ESP_LOGW(POWER_TAG, "AXP192 read reg 0x%02X failed", reg);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void axp_update(uint8_t reg, uint8_t clear_mask, uint8_t set_mask) {
|
||||||
|
uint8_t value = 0;
|
||||||
|
if (!axp_read(reg, &value)) return;
|
||||||
|
value = (value & ~clear_mask) | set_mask;
|
||||||
|
axp_write(reg, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint8_t axp_dc_voltage_data(uint16_t millivolts) {
|
||||||
|
if (millivolts < 700) millivolts = 700;
|
||||||
|
if (millivolts > 3500) millivolts = 3500;
|
||||||
|
return static_cast<uint8_t>((millivolts - 700) / 25) & 0x7F;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint8_t axp_ldo_voltage_data(uint16_t millivolts) {
|
||||||
|
if (millivolts < 1800) millivolts = 1800;
|
||||||
|
if (millivolts > 3300) millivolts = 3300;
|
||||||
|
return static_cast<uint8_t>((millivolts - 1800) / 100) & 0x0F;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void core2_axp192_set_lcd_voltage(uint16_t millivolts) {
|
||||||
|
uint8_t value = 0;
|
||||||
|
axp_read(0x27, &value);
|
||||||
|
axp_write(0x27, (value & 0x80) | axp_dc_voltage_data(millivolts)); // DCDC3, LCD backlight
|
||||||
|
}
|
||||||
|
|
||||||
|
static void core2_axp192_set_backlight(float brightness) {
|
||||||
|
if (brightness <= 0.0f) {
|
||||||
|
axp_update(0x12, 0x02, 0x00); // DCDC3 off
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (brightness > 1.0f) brightness = 1.0f;
|
||||||
|
uint16_t millivolts = static_cast<uint16_t>(2400 + brightness * 900);
|
||||||
|
core2_axp192_set_lcd_voltage(millivolts);
|
||||||
|
axp_update(0x12, 0x00, 0x02); // DCDC3 on
|
||||||
|
}
|
||||||
|
|
||||||
|
static void core2_axp192_init(esphome::i2c::I2CBus *bus) {
|
||||||
|
axp192.set_i2c_bus(bus);
|
||||||
|
axp192.set_i2c_address(AXP192_ADDR);
|
||||||
|
axp192_ready = true;
|
||||||
|
ESP_LOGI(POWER_TAG, "Initializing M5Stack Core2 AXP192 LCD power");
|
||||||
|
axp_update(0x30, 0xF9, 0x02); // Disable VBUS current limit, preserve bit 2.
|
||||||
|
axp_update(0x92, 0x07, 0x00); // GPIO1 open-drain output.
|
||||||
|
axp_update(0x93, 0x07, 0x00); // GPIO2 open-drain output.
|
||||||
|
axp_write(0x35, 0xA2); // RTC battery charging.
|
||||||
|
|
||||||
|
uint8_t value = 0;
|
||||||
|
axp_read(0x26, &value);
|
||||||
|
axp_write(0x26, (value & 0x80) | axp_dc_voltage_data(3350)); // DCDC1 ESP32 VDD.
|
||||||
|
core2_axp192_set_lcd_voltage(2800);
|
||||||
|
|
||||||
|
uint8_t ldo2 = axp_ldo_voltage_data(3300);
|
||||||
|
uint8_t ldo3 = axp_ldo_voltage_data(2000);
|
||||||
|
axp_write(0x28, (ldo2 << 4) | ldo3); // LDO2 LCD logic/SD, LDO3 vibrator.
|
||||||
|
axp_update(0x12, 0x00, 0x07); // Enable DCDC1, DCDC3, LDO2.
|
||||||
|
axp_write(0x82, 0xFF); // ADCs on.
|
||||||
|
axp_update(0x95, 0x8D, 0x84); // GPIO4 setup, as M5Core2 library does.
|
||||||
|
axp_write(0x36, 0x4C); // Power key timing.
|
||||||
|
|
||||||
|
// LCD reset through AXP192 GPIO4.
|
||||||
|
axp_update(0x96, 0x02, 0x00);
|
||||||
|
delay(100);
|
||||||
|
axp_update(0x96, 0x00, 0x02);
|
||||||
|
delay(100);
|
||||||
|
|
||||||
|
core2_axp192_set_backlight(1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const uint8_t ACCU_BMP[32] = {
|
||||||
|
0b00000000, 0b00000000,
|
||||||
|
0b00000000, 0b00000000,
|
||||||
|
0b00000000, 0b00000000,
|
||||||
|
0b11111111, 0b11111110,
|
||||||
|
0b10000000, 0b00000010,
|
||||||
|
0b10000000, 0b00000011,
|
||||||
|
0b10000000, 0b00000011,
|
||||||
|
0b10000000, 0b00000011,
|
||||||
|
0b10000000, 0b00000011,
|
||||||
|
0b10000000, 0b00000011,
|
||||||
|
0b10000000, 0b00000011,
|
||||||
|
0b10000000, 0b00000010,
|
||||||
|
0b11111111, 0b11111110,
|
||||||
|
0b00000000, 0b00000000,
|
||||||
|
0b00000000, 0b00000000,
|
||||||
|
0b00000000, 0b00000000,
|
||||||
|
};
|
||||||
|
|
||||||
|
static const uint8_t BLE_BMP[32] = {
|
||||||
|
0b00000001, 0b10000000,
|
||||||
|
0b00000001, 0b11000000,
|
||||||
|
0b00010001, 0b01100000,
|
||||||
|
0b00011001, 0b00110000,
|
||||||
|
0b00001101, 0b00011000,
|
||||||
|
0b00000111, 0b00110000,
|
||||||
|
0b00000011, 0b01100000,
|
||||||
|
0b00000001, 0b11000000,
|
||||||
|
0b00000001, 0b11000000,
|
||||||
|
0b00000011, 0b01100000,
|
||||||
|
0b00000111, 0b00110000,
|
||||||
|
0b00001101, 0b00011000,
|
||||||
|
0b00011001, 0b00110000,
|
||||||
|
0b00010001, 0b01100000,
|
||||||
|
0b00000001, 0b11000000,
|
||||||
|
0b00000001, 0b10000000,
|
||||||
|
};
|
||||||
|
|
||||||
|
static const uint8_t HOLD_BMP[32] = {
|
||||||
|
0b01111111, 0b11111110,
|
||||||
|
0b11111111, 0b11111111,
|
||||||
|
0b11100011, 0b11000111,
|
||||||
|
0b11100011, 0b11000111,
|
||||||
|
0b11100011, 0b11000111,
|
||||||
|
0b11100011, 0b11000111,
|
||||||
|
0b11100011, 0b11000111,
|
||||||
|
0b11100000, 0b00000111,
|
||||||
|
0b11100000, 0b00000111,
|
||||||
|
0b11100011, 0b11000111,
|
||||||
|
0b11100011, 0b11000111,
|
||||||
|
0b11100011, 0b11000111,
|
||||||
|
0b11100011, 0b11000111,
|
||||||
|
0b11100011, 0b11000111,
|
||||||
|
0b11111111, 0b11111111,
|
||||||
|
0b01111111, 0b11111110,
|
||||||
|
};
|
||||||
|
|
||||||
|
static const uint8_t REL_BMP[32] = {
|
||||||
|
0b00000001, 0b00000000,
|
||||||
|
0b00000001, 0b00000000,
|
||||||
|
0b00000011, 0b10000000,
|
||||||
|
0b00000011, 0b10000000,
|
||||||
|
0b00000110, 0b11000000,
|
||||||
|
0b00000110, 0b11000000,
|
||||||
|
0b00001100, 0b01100000,
|
||||||
|
0b00001100, 0b01100000,
|
||||||
|
0b00011000, 0b00110000,
|
||||||
|
0b00011000, 0b00110000,
|
||||||
|
0b00110000, 0b00011000,
|
||||||
|
0b00110000, 0b00011000,
|
||||||
|
0b01100000, 0b00001100,
|
||||||
|
0b01100000, 0b00001100,
|
||||||
|
0b11111111, 0b11111110,
|
||||||
|
0b11111111, 0b11111110,
|
||||||
|
};
|
||||||
|
|
||||||
|
static const uint8_t DIODE_BMP[32] = {
|
||||||
|
0b00001000, 0b00011000,
|
||||||
|
0b00001100, 0b00011000,
|
||||||
|
0b00001110, 0b00011000,
|
||||||
|
0b00001111, 0b00011000,
|
||||||
|
0b00001111, 0b10011000,
|
||||||
|
0b00001111, 0b11011000,
|
||||||
|
0b00001111, 0b11111000,
|
||||||
|
0b11111111, 0b11111111,
|
||||||
|
0b11111111, 0b11111111,
|
||||||
|
0b00001111, 0b11111000,
|
||||||
|
0b00001111, 0b11011000,
|
||||||
|
0b00001111, 0b10011000,
|
||||||
|
0b00001111, 0b00011000,
|
||||||
|
0b00001110, 0b00011000,
|
||||||
|
0b00001100, 0b00011000,
|
||||||
|
0b00001000, 0b00011000,
|
||||||
|
};
|
||||||
|
|
||||||
|
static const uint8_t BUZZ_BMP[32] = {
|
||||||
|
0b00000000, 0b11000000,
|
||||||
|
0b00000001, 0b11000000,
|
||||||
|
0b00000011, 0b11000001,
|
||||||
|
0b00000111, 0b11000001,
|
||||||
|
0b00001111, 0b11000101,
|
||||||
|
0b11111111, 0b11000101,
|
||||||
|
0b11111111, 0b11010101,
|
||||||
|
0b11111111, 0b11010101,
|
||||||
|
0b11111111, 0b11010101,
|
||||||
|
0b11111111, 0b11010101,
|
||||||
|
0b11111111, 0b11000101,
|
||||||
|
0b00001111, 0b11000101,
|
||||||
|
0b00000111, 0b11000001,
|
||||||
|
0b00000011, 0b11000001,
|
||||||
|
0b00000001, 0b11000000,
|
||||||
|
0b00000000, 0b11000000,
|
||||||
|
};
|
||||||
|
|
||||||
|
class Meter {
|
||||||
|
public:
|
||||||
|
static constexpr uint8_t REGPLUSMINUS = 0x00;
|
||||||
|
static constexpr uint8_t FLAGPLUS = 0b00101011;
|
||||||
|
static constexpr uint8_t FLAGMINUS = 0b00101101;
|
||||||
|
static constexpr uint8_t REGDIG1 = 0x01;
|
||||||
|
static constexpr uint8_t REGDIG2 = 0x02;
|
||||||
|
static constexpr uint8_t REGDIG3 = 0x03;
|
||||||
|
static constexpr uint8_t REGDIG4 = 0x04;
|
||||||
|
static constexpr uint8_t REGPOINT = 0x06;
|
||||||
|
static constexpr uint8_t FLAGPOINT0 = 0b00110000;
|
||||||
|
static constexpr uint8_t FLAGPOINT1 = 0b00110001;
|
||||||
|
static constexpr uint8_t FLAGPOINT2 = 0b00110010;
|
||||||
|
static constexpr uint8_t FLAGPOINT3 = 0b00110100;
|
||||||
|
static constexpr uint8_t REGMODE = 0x07;
|
||||||
|
static constexpr uint8_t FLAGMODEHOLD = 0b00000010;
|
||||||
|
static constexpr uint8_t FLAGMODEREL = 0b00000100;
|
||||||
|
static constexpr uint8_t FLAGMODEAC = 0b00001000;
|
||||||
|
static constexpr uint8_t FLAGMODEDC = 0b00010000;
|
||||||
|
static constexpr uint8_t FLAGMODEAUTO = 0b00100000;
|
||||||
|
static constexpr uint8_t REGMINMAX = 0x08;
|
||||||
|
static constexpr uint8_t FLAGMIN = 0b00010000;
|
||||||
|
static constexpr uint8_t FLAGMAX = 0b00100000;
|
||||||
|
static constexpr uint8_t REGSCALE = 0x09;
|
||||||
|
static constexpr uint8_t FLAGSCALEDUTY = 0b00000010;
|
||||||
|
static constexpr uint8_t FLAGSCALEDIODE = 0b00000100;
|
||||||
|
static constexpr uint8_t FLAGSCALEBUZZ = 0b00001000;
|
||||||
|
static constexpr uint8_t FLAGSCALEMEGA = 0b00010000;
|
||||||
|
static constexpr uint8_t FLAGSCALEKILO = 0b00100000;
|
||||||
|
static constexpr uint8_t FLAGSCALEMILLI = 0b01000000;
|
||||||
|
static constexpr uint8_t FLAGSCALEMICRO = 0b10000000;
|
||||||
|
static constexpr uint8_t REGUNIT = 0x0a;
|
||||||
|
static constexpr uint8_t FLAGUNITFAHR = 0b00000001;
|
||||||
|
static constexpr uint8_t FLAGUNITGRAD = 0b00000010;
|
||||||
|
static constexpr uint8_t FLAGUNITNF = 0b00000100;
|
||||||
|
static constexpr uint8_t FLAGUNITHZ = 0b00001000;
|
||||||
|
static constexpr uint8_t FLAGUNITHFE = 0b00010000;
|
||||||
|
static constexpr uint8_t FLAGUNITOHM = 0b00100000;
|
||||||
|
static constexpr uint8_t FLAGUNITAMP = 0b01000000;
|
||||||
|
static constexpr uint8_t FLAGUNITVOLT = 0b10000000;
|
||||||
|
|
||||||
|
bool connected{false};
|
||||||
|
bool write_available{false};
|
||||||
|
bool is_plus{false};
|
||||||
|
bool low_battery{false};
|
||||||
|
bool overload{false};
|
||||||
|
bool has_reading{false};
|
||||||
|
uint8_t selected_button{1};
|
||||||
|
uint32_t last_notify_ms{0};
|
||||||
|
|
||||||
|
bool handle_notify(const std::vector<uint8_t> &data) {
|
||||||
|
if (data.size() > sizeof(this->raw_))
|
||||||
|
return false;
|
||||||
|
if (data.size() == 6 && data[1] >= 0xF0) {
|
||||||
|
memset(this->raw_, 0, sizeof(this->raw_));
|
||||||
|
memcpy(this->raw_, data.data(), data.size());
|
||||||
|
this->is_plus = true;
|
||||||
|
this->parse_plus_();
|
||||||
|
} else if (data.size() == 14 && data[12] == 0x0D && data[13] == 0x0A) {
|
||||||
|
memset(this->value_, 0, sizeof(this->value_));
|
||||||
|
memcpy(this->value_, data.data(), data.size());
|
||||||
|
this->is_plus = false;
|
||||||
|
} else {
|
||||||
|
ESP_LOGW(TAG, "Ignoring unexpected OWON frame length=%u", static_cast<unsigned>(data.size()));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->overload = memcmp(this->value_, OVERLOAD_FRAME, sizeof(OVERLOAD_FRAME)) == 0;
|
||||||
|
this->display_value = this->calc_display_value_();
|
||||||
|
this->base_value = this->calc_base_value_();
|
||||||
|
this->has_reading = true;
|
||||||
|
this->last_notify_ms = millis();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void on_connect() {
|
||||||
|
this->connected = true;
|
||||||
|
this->write_available = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void on_disconnect() {
|
||||||
|
this->connected = false;
|
||||||
|
this->write_available = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
float value() const { return this->display_value; }
|
||||||
|
float value_base() const { return this->base_value; }
|
||||||
|
bool negative() const { return (this->value_[REGPLUSMINUS] & FLAGMINUS) == FLAGMINUS; }
|
||||||
|
bool auto_range() const { return (this->value_[REGMODE] & FLAGMODEAUTO) == FLAGMODEAUTO; }
|
||||||
|
bool hold() const { return (this->value_[REGMODE] & FLAGMODEHOLD) == FLAGMODEHOLD; }
|
||||||
|
bool relative() const { return (this->value_[REGMODE] & FLAGMODEREL) == FLAGMODEREL; }
|
||||||
|
bool ac() const { return (this->value_[REGMODE] & FLAGMODEAC) == FLAGMODEAC; }
|
||||||
|
bool dc() const { return (this->value_[REGMODE] & FLAGMODEDC) == FLAGMODEDC; }
|
||||||
|
bool min_mode() const { return (this->value_[REGMINMAX] & FLAGMIN) == FLAGMIN; }
|
||||||
|
bool max_mode() const { return (this->value_[REGMINMAX] & FLAGMAX) == FLAGMAX; }
|
||||||
|
bool diode() const { return (this->value_[REGSCALE] & FLAGSCALEDIODE) == FLAGSCALEDIODE; }
|
||||||
|
bool continuity() const { return (this->value_[REGSCALE] & FLAGSCALEBUZZ) == FLAGSCALEBUZZ; }
|
||||||
|
|
||||||
|
const char *unit() const {
|
||||||
|
switch (this->value_[REGUNIT]) {
|
||||||
|
case FLAGUNITFAHR: return "°F";
|
||||||
|
case FLAGUNITGRAD: return "°C";
|
||||||
|
case FLAGUNITNF: return "nF";
|
||||||
|
case FLAGUNITHZ: return "Hz";
|
||||||
|
case FLAGUNITHFE: return "hFE";
|
||||||
|
case FLAGUNITOHM: return "Ω";
|
||||||
|
case FLAGUNITAMP: return "A";
|
||||||
|
case FLAGUNITVOLT: return "V";
|
||||||
|
default: return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *scale() const {
|
||||||
|
if ((this->value_[REGSCALE] & FLAGSCALEDUTY) == FLAGSCALEDUTY) return "%";
|
||||||
|
if ((this->value_[REGSCALE] & FLAGSCALEMEGA) == FLAGSCALEMEGA) return "M";
|
||||||
|
if ((this->value_[REGSCALE] & FLAGSCALEKILO) == FLAGSCALEKILO) return "k";
|
||||||
|
if ((this->value_[REGSCALE] & FLAGSCALEMILLI) == FLAGSCALEMILLI) return "m";
|
||||||
|
if ((this->value_[REGSCALE] & FLAGSCALEMICRO) == FLAGSCALEMICRO) return "µ";
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string mode_text() const {
|
||||||
|
std::string out;
|
||||||
|
if (this->dc()) out += "DC ";
|
||||||
|
if (this->ac()) out += "AC ";
|
||||||
|
if (this->auto_range()) out += "AUTO ";
|
||||||
|
if (this->hold()) out += "HOLD ";
|
||||||
|
if (this->relative()) out += "REL ";
|
||||||
|
if (this->min_mode()) out += "MIN ";
|
||||||
|
if (this->max_mode()) out += "MAX ";
|
||||||
|
if (this->diode()) out += "DIODE ";
|
||||||
|
if (this->continuity()) out += "CONT ";
|
||||||
|
if (!out.empty()) out.pop_back();
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string reading_text() const {
|
||||||
|
if (!this->connected) return "Disconnected";
|
||||||
|
if (!this->has_reading) return "Waiting for data";
|
||||||
|
if (this->overload) return "OL " + std::string(this->scale()) + this->unit();
|
||||||
|
char buf[48];
|
||||||
|
snprintf(buf, sizeof(buf), "%s%.4g %s%s", this->negative() ? "-" : "", std::fabs(this->display_value), this->scale(), this->unit());
|
||||||
|
return std::string(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Kind { KIND_OTHER, KIND_VOLTAGE, KIND_CURRENT, KIND_RESISTANCE, KIND_FREQUENCY, KIND_CAPACITANCE, KIND_TEMP_C, KIND_TEMP_F, KIND_DUTY };
|
||||||
|
Kind kind() const {
|
||||||
|
if ((this->value_[REGSCALE] & FLAGSCALEDUTY) == FLAGSCALEDUTY) return KIND_DUTY;
|
||||||
|
switch (this->value_[REGUNIT]) {
|
||||||
|
case FLAGUNITVOLT: return KIND_VOLTAGE;
|
||||||
|
case FLAGUNITAMP: return KIND_CURRENT;
|
||||||
|
case FLAGUNITOHM: return KIND_RESISTANCE;
|
||||||
|
case FLAGUNITHZ: return KIND_FREQUENCY;
|
||||||
|
case FLAGUNITNF: return KIND_CAPACITANCE;
|
||||||
|
case FLAGUNITGRAD: return KIND_TEMP_C;
|
||||||
|
case FLAGUNITFAHR: return KIND_TEMP_F;
|
||||||
|
default: return KIND_OTHER;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *selected_button_name() const {
|
||||||
|
static const char *const names[] = {"SELECT", "RANGE", "HLD/LIG", "REL/BT", "HZ/DUTY", "MAX/MIN"};
|
||||||
|
uint8_t index = this->selected_button;
|
||||||
|
if (index < 1) index = 1;
|
||||||
|
if (index > 6) index = 6;
|
||||||
|
return names[index - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
void previous_button() {
|
||||||
|
if (this->selected_button > 1) this->selected_button--;
|
||||||
|
}
|
||||||
|
void next_button() {
|
||||||
|
if (this->selected_button < 6) this->selected_button++;
|
||||||
|
}
|
||||||
|
|
||||||
|
void render(esphome::display::Display &it, esphome::display::BaseFont *font,
|
||||||
|
esphome::display::BaseFont *value_font, int display_page = 0,
|
||||||
|
bool atorch_connected = false,
|
||||||
|
float atorch_voltage = NAN, float atorch_current = NAN, float atorch_power = NAN,
|
||||||
|
float atorch_capacity = NAN, float atorch_energy = NAN, float atorch_temperature = NAN) {
|
||||||
|
const Color bg(0, 0, 0);
|
||||||
|
const Color fg(210, 210, 210);
|
||||||
|
// Chosen to map to a neutral dark gray in the RGB332 8-bit display palette.
|
||||||
|
const Color inactive(80, 80, 80);
|
||||||
|
const Color yellow(255, 220, 0);
|
||||||
|
const Color blue(0, 80, 255);
|
||||||
|
const Color cyan(0, 255, 255);
|
||||||
|
const Color magenta(255, 0, 255);
|
||||||
|
const Color red(255, 0, 0);
|
||||||
|
const Color green(0, 220, 0);
|
||||||
|
const Color orange(255, 165, 0);
|
||||||
|
|
||||||
|
if (display_page == 0) {
|
||||||
|
// --- PAGE 1: OWON Multimeter ---
|
||||||
|
it.fill(bg);
|
||||||
|
bool status_active = this->connected && this->has_reading;
|
||||||
|
this->draw_icon_(it, 12, 8, 16, 16, ACCU_BMP, status_active ? (this->low_battery ? red : green) : inactive);
|
||||||
|
this->draw_icon_(it, 46, 8, 16, 16, BLE_BMP, this->connected ? blue : inactive);
|
||||||
|
this->label_(it, font, 86, 8, "AUTO", status_active && this->auto_range() ? fg : inactive);
|
||||||
|
this->label_(it, font, 138, 8, "MAX", status_active && this->max_mode() ? red : inactive);
|
||||||
|
this->label_(it, font, 178, 8, "MIN", status_active && this->min_mode() ? green : inactive);
|
||||||
|
this->draw_icon_(it, 218, 8, 16, 16, HOLD_BMP, status_active && this->hold() ? blue : inactive);
|
||||||
|
this->draw_icon_(it, 244, 8, 16, 16, REL_BMP, status_active && this->relative() ? Color(128, 128, 0) : inactive);
|
||||||
|
this->draw_icon_(it, 270, 8, 16, 16, DIODE_BMP, status_active && this->diode() ? magenta : inactive);
|
||||||
|
this->draw_icon_(it, 296, 8, 16, 16, BUZZ_BMP, status_active && this->continuity() ? orange : inactive);
|
||||||
|
|
||||||
|
this->label_(it, font, 8, 104, "DC", status_active && this->dc() ? cyan : inactive);
|
||||||
|
this->label_(it, font, 8, 124, "AC", status_active && this->ac() ? magenta : inactive);
|
||||||
|
|
||||||
|
if (!this->connected) {
|
||||||
|
this->draw_digits_(it, "----", false, inactive);
|
||||||
|
it.print(160, 148, font, inactive, esphome::display::TextAlign::CENTER, "scan/connect");
|
||||||
|
} else if (!this->has_reading) {
|
||||||
|
this->draw_digits_(it, "8888", false, inactive);
|
||||||
|
it.print(160, 148, font, inactive, esphome::display::TextAlign::CENTER, "waiting");
|
||||||
|
} else if (this->overload) {
|
||||||
|
this->draw_digits_(it, " OL ", false, fg);
|
||||||
|
} else {
|
||||||
|
char d[5];
|
||||||
|
d[0] = this->digit_char_(REGDIG1);
|
||||||
|
d[1] = this->digit_char_(REGDIG2);
|
||||||
|
d[2] = this->digit_char_(REGDIG3);
|
||||||
|
d[3] = this->digit_char_(REGDIG4);
|
||||||
|
d[4] = 0;
|
||||||
|
this->draw_digits_(it, d, this->negative(), fg);
|
||||||
|
this->draw_decimal_points_(it, fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status_active) {
|
||||||
|
std::string unit_line = std::string(this->scale()) + this->unit();
|
||||||
|
it.print(316, 141, font, yellow, esphome::display::TextAlign::BOTTOM_RIGHT, unit_line.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool bargraph_active = status_active && !this->overload;
|
||||||
|
this->draw_bargraph_(it, bargraph_active ? this->digits_from_buffer_() : 0, bargraph_active);
|
||||||
|
it.filled_rectangle(34, 212, 40, 24, this->write_available ? fg : inactive);
|
||||||
|
it.filled_rectangle(108, 212, 100, 24, this->write_available ? fg : inactive);
|
||||||
|
it.filled_rectangle(242, 212, 40, 24, this->write_available ? fg : inactive);
|
||||||
|
it.print(54, 216, font, bg, esphome::display::TextAlign::TOP_CENTER, "<");
|
||||||
|
it.print(158, 216, font, bg, esphome::display::TextAlign::TOP_CENTER, this->selected_button_name());
|
||||||
|
it.print(262, 216, font, bg, esphome::display::TextAlign::TOP_CENTER, ">");
|
||||||
|
} else {
|
||||||
|
// --- PAGE 2: Atorch DL24 DC load ---
|
||||||
|
it.fill(bg);
|
||||||
|
const Color panel_active(18, 24, 32);
|
||||||
|
const Color panel_inactive(12, 12, 12);
|
||||||
|
const Color panel_dim(10, 14, 20);
|
||||||
|
const Color border_active(55, 70, 86);
|
||||||
|
const Color border_inactive(42, 42, 42);
|
||||||
|
const Color white(245, 245, 245);
|
||||||
|
const Color dim_value(110, 110, 110);
|
||||||
|
const Color dim_accent(65, 65, 65);
|
||||||
|
const Color panel = atorch_connected ? panel_active : panel_inactive;
|
||||||
|
const Color border = atorch_connected ? border_active : border_inactive;
|
||||||
|
const Color value_color = atorch_connected ? white : dim_value;
|
||||||
|
const Color voltage_color = atorch_connected ? cyan : dim_accent;
|
||||||
|
const Color current_color = atorch_connected ? orange : dim_accent;
|
||||||
|
const Color power_color = atorch_connected ? yellow : dim_accent;
|
||||||
|
const Color temp_color = atorch_connected ? magenta : dim_accent;
|
||||||
|
const Color header_color = atorch_connected ? cyan : dim_value;
|
||||||
|
|
||||||
|
char voltage_text[24];
|
||||||
|
char current_text[24];
|
||||||
|
char power_text[24];
|
||||||
|
char capacity_text[24];
|
||||||
|
char energy_text[24];
|
||||||
|
char temperature_text[24];
|
||||||
|
this->format_metric_(voltage_text, sizeof(voltage_text), atorch_voltage, "V", 2);
|
||||||
|
this->format_metric_(current_text, sizeof(current_text), atorch_current, "A", 3);
|
||||||
|
this->format_metric_(power_text, sizeof(power_text), atorch_power, "W", 2);
|
||||||
|
this->format_metric_(capacity_text, sizeof(capacity_text), atorch_capacity, "Ah", 3);
|
||||||
|
this->format_metric_(energy_text, sizeof(energy_text), atorch_energy, "Wh", 3);
|
||||||
|
this->format_metric_(temperature_text, sizeof(temperature_text), atorch_temperature, "°C", 1);
|
||||||
|
|
||||||
|
it.filled_rectangle(0, 0, 320, 30, atorch_connected ? panel_dim : panel_inactive);
|
||||||
|
it.print(10, 7, font, header_color, esphome::display::TextAlign::TOP_LEFT, "ATORCH DL24");
|
||||||
|
if (atorch_connected) {
|
||||||
|
this->draw_icon_(it, 152, 7, 16, 16, BLE_BMP, blue);
|
||||||
|
}
|
||||||
|
it.print(310, 7, font, inactive, esphome::display::TextAlign::TOP_RIGHT, "DC LOAD");
|
||||||
|
|
||||||
|
this->draw_metric_card_(it, font, value_font, 10, 42, 145, 76, "VOLTAGE", voltage_text, voltage_color, panel, border, value_color);
|
||||||
|
this->draw_metric_card_(it, font, value_font, 165, 42, 145, 76, "CURRENT", current_text, current_color, panel, border, value_color);
|
||||||
|
this->draw_metric_card_(it, font, value_font, 10, 128, 145, 76, "POWER", power_text, power_color, panel, border, value_color);
|
||||||
|
this->draw_metric_card_(it, font, value_font, 165, 128, 145, 76, "TEMP", temperature_text, temp_color, panel, border, value_color);
|
||||||
|
|
||||||
|
it.filled_rectangle(10, 212, 300, 22, atorch_connected ? panel_dim : panel_inactive);
|
||||||
|
it.filled_rectangle(10, 212, 300, 1, border);
|
||||||
|
it.filled_rectangle(10, 233, 300, 1, border);
|
||||||
|
it.filled_rectangle(10, 212, 1, 22, border);
|
||||||
|
it.filled_rectangle(309, 212, 1, 22, border);
|
||||||
|
it.print(20, 216, font, inactive, esphome::display::TextAlign::TOP_LEFT, "CAP");
|
||||||
|
it.print(64, 216, font, value_color, esphome::display::TextAlign::TOP_LEFT, capacity_text);
|
||||||
|
it.print(170, 216, font, inactive, esphome::display::TextAlign::TOP_LEFT, "ENERGY");
|
||||||
|
it.print(306, 216, font, value_color, esphome::display::TextAlign::TOP_RIGHT, energy_text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
uint8_t raw_[14]{};
|
||||||
|
uint8_t value_[14]{};
|
||||||
|
float display_value{NAN};
|
||||||
|
float base_value{NAN};
|
||||||
|
static constexpr uint8_t OVERLOAD_FRAME[5] = {0x2B, 0x3F, 0x30, 0x3A, 0x3F};
|
||||||
|
|
||||||
|
uint16_t digits_from_buffer_() const {
|
||||||
|
uint16_t out = 0;
|
||||||
|
if (this->value_[REGDIG1] >= '0' && this->value_[REGDIG1] <= '9') out += (this->value_[REGDIG1] - '0') * 1000;
|
||||||
|
if (this->value_[REGDIG2] >= '0' && this->value_[REGDIG2] <= '9') out += (this->value_[REGDIG2] - '0') * 100;
|
||||||
|
if (this->value_[REGDIG3] >= '0' && this->value_[REGDIG3] <= '9') out += (this->value_[REGDIG3] - '0') * 10;
|
||||||
|
if (this->value_[REGDIG4] >= '0' && this->value_[REGDIG4] <= '9') out += (this->value_[REGDIG4] - '0');
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
float calc_display_value_() const {
|
||||||
|
if (this->overload) return NAN;
|
||||||
|
|
||||||
|
if (this->is_plus) {
|
||||||
|
uint16_t pair1 = static_cast<uint16_t>(this->raw_[0]) | (static_cast<uint16_t>(this->raw_[1]) << 8);
|
||||||
|
uint8_t decimal = pair1 & 0x07;
|
||||||
|
if (decimal >= 7) return NAN;
|
||||||
|
|
||||||
|
uint16_t pair3 = static_cast<uint16_t>(this->raw_[4]) | (static_cast<uint16_t>(this->raw_[5]) << 8);
|
||||||
|
bool negative = pair3 >= 0x7FFF;
|
||||||
|
uint16_t digits = negative ? (pair3 & 0x7FFF) : pair3;
|
||||||
|
float v = static_cast<float>(digits) / std::pow(10.0f, decimal);
|
||||||
|
return negative ? -v : v;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t decimal = 0;
|
||||||
|
switch (this->value_[REGPOINT] & 0x07) {
|
||||||
|
case 0b001: decimal = 1; break;
|
||||||
|
case 0b010: decimal = 2; break;
|
||||||
|
case 0b100: decimal = 3; break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
float v = static_cast<float>(this->digits_from_buffer_()) / std::pow(10.0f, decimal);
|
||||||
|
return this->negative() ? -v : v;
|
||||||
|
}
|
||||||
|
|
||||||
|
float calc_base_value_() const {
|
||||||
|
if (std::isnan(this->display_value)) return NAN;
|
||||||
|
if (this->value_[REGUNIT] == FLAGUNITNF) return this->display_value * 1e-9f;
|
||||||
|
if ((this->value_[REGSCALE] & FLAGSCALEMEGA) == FLAGSCALEMEGA) return this->display_value * 1e6f;
|
||||||
|
if ((this->value_[REGSCALE] & FLAGSCALEKILO) == FLAGSCALEKILO) return this->display_value * 1e3f;
|
||||||
|
if ((this->value_[REGSCALE] & FLAGSCALEMILLI) == FLAGSCALEMILLI) return this->display_value * 1e-3f;
|
||||||
|
if ((this->value_[REGSCALE] & FLAGSCALEMICRO) == FLAGSCALEMICRO) return this->display_value * 1e-6f;
|
||||||
|
return this->display_value;
|
||||||
|
}
|
||||||
|
|
||||||
|
void parse_plus_() {
|
||||||
|
memset(this->value_, 0, sizeof(this->value_));
|
||||||
|
this->value_[5] = 0x20;
|
||||||
|
this->value_[12] = 0x0D;
|
||||||
|
this->value_[13] = 0x0A;
|
||||||
|
|
||||||
|
uint16_t pair1 = static_cast<uint16_t>(this->raw_[0]) | (static_cast<uint16_t>(this->raw_[1]) << 8);
|
||||||
|
uint8_t function = (pair1 >> 6) & 0x0F;
|
||||||
|
uint8_t scale = (pair1 >> 3) & 0x07;
|
||||||
|
uint8_t decimal = pair1 & 0x07;
|
||||||
|
|
||||||
|
switch (decimal) {
|
||||||
|
case 0: this->value_[REGPOINT] = FLAGPOINT0; break;
|
||||||
|
case 1: this->value_[REGPOINT] = FLAGPOINT3; break;
|
||||||
|
case 2: this->value_[REGPOINT] = FLAGPOINT2; break;
|
||||||
|
case 3: this->value_[REGPOINT] = FLAGPOINT1; break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (function) {
|
||||||
|
case 0: this->value_[REGUNIT] |= FLAGUNITVOLT; this->value_[REGMODE] |= FLAGMODEDC; break;
|
||||||
|
case 1: this->value_[REGUNIT] |= FLAGUNITVOLT; this->value_[REGMODE] |= FLAGMODEAC; break;
|
||||||
|
case 2: this->value_[REGUNIT] |= FLAGUNITAMP; this->value_[REGMODE] |= FLAGMODEDC; break;
|
||||||
|
case 3: this->value_[REGUNIT] |= FLAGUNITAMP; this->value_[REGMODE] |= FLAGMODEAC; break;
|
||||||
|
case 4: this->value_[REGUNIT] |= FLAGUNITOHM; break;
|
||||||
|
case 5: this->value_[REGUNIT] |= FLAGUNITNF; break;
|
||||||
|
case 6: this->value_[REGUNIT] |= FLAGUNITHZ; break;
|
||||||
|
case 7: this->value_[REGSCALE] |= FLAGSCALEDUTY; break;
|
||||||
|
case 8: this->value_[REGUNIT] |= FLAGUNITGRAD; break;
|
||||||
|
case 9: this->value_[REGUNIT] |= FLAGUNITFAHR; break;
|
||||||
|
case 10: this->value_[REGSCALE] |= FLAGSCALEDIODE; break;
|
||||||
|
case 11: this->value_[REGSCALE] |= FLAGSCALEBUZZ; break;
|
||||||
|
case 12: this->value_[REGUNIT] |= FLAGUNITHFE; break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (scale) {
|
||||||
|
case 2: this->value_[REGSCALE] |= FLAGSCALEMICRO; break;
|
||||||
|
case 3: this->value_[REGSCALE] |= FLAGSCALEMILLI; break;
|
||||||
|
case 5: this->value_[REGSCALE] |= FLAGSCALEKILO; break;
|
||||||
|
case 6: this->value_[REGSCALE] |= FLAGSCALEMEGA; break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t pair2 = static_cast<uint16_t>(this->raw_[2]) | (static_cast<uint16_t>(this->raw_[3]) << 8);
|
||||||
|
if (pair2 & (1 << 0)) this->value_[REGMODE] |= FLAGMODEHOLD;
|
||||||
|
if (pair2 & (1 << 1)) this->value_[REGMODE] |= FLAGMODEREL;
|
||||||
|
if (pair2 & (1 << 2)) this->value_[REGMODE] |= FLAGMODEAUTO;
|
||||||
|
this->low_battery = (pair2 & (1 << 3)) != 0;
|
||||||
|
if (pair2 & (1 << 4)) this->value_[REGMINMAX] |= FLAGMIN;
|
||||||
|
if (pair2 & (1 << 5)) this->value_[REGMINMAX] |= FLAGMAX;
|
||||||
|
|
||||||
|
uint16_t pair3 = static_cast<uint16_t>(this->raw_[4]) | (static_cast<uint16_t>(this->raw_[5]) << 8);
|
||||||
|
if (decimal < 7) {
|
||||||
|
uint16_t digits = pair3;
|
||||||
|
if (pair3 < 0x7FFF) {
|
||||||
|
this->value_[REGPLUSMINUS] = FLAGPLUS;
|
||||||
|
} else {
|
||||||
|
this->value_[REGPLUSMINUS] = FLAGMINUS;
|
||||||
|
digits = pair3 & 0x7FFF;
|
||||||
|
}
|
||||||
|
this->value_[REGDIG1] = '0' + ((digits / 1000) % 10);
|
||||||
|
this->value_[REGDIG2] = '0' + ((digits / 100) % 10);
|
||||||
|
this->value_[REGDIG3] = '0' + ((digits / 10) % 10);
|
||||||
|
this->value_[REGDIG4] = '0' + (digits % 10);
|
||||||
|
} else {
|
||||||
|
memcpy(this->value_, OVERLOAD_FRAME, sizeof(OVERLOAD_FRAME));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
char digit_char_(uint8_t reg) const {
|
||||||
|
uint8_t c = this->value_[reg];
|
||||||
|
return (c >= '0' && c <= '9') ? static_cast<char>(c) : ' ';
|
||||||
|
}
|
||||||
|
|
||||||
|
void format_metric_(char *buffer, size_t size, float value, const char *unit, uint8_t decimals) const {
|
||||||
|
if (!std::isfinite(value)) {
|
||||||
|
snprintf(buffer, size, "-- %s", unit);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
char format[12];
|
||||||
|
snprintf(format, sizeof(format), "%%.%uf %%s", decimals);
|
||||||
|
snprintf(buffer, size, format, value, unit);
|
||||||
|
}
|
||||||
|
|
||||||
|
void draw_metric_card_(Display &it, esphome::display::BaseFont *label_font, esphome::display::BaseFont *value_font,
|
||||||
|
int x, int y, int w, int h, const char *title, const char *value,
|
||||||
|
Color accent, Color fill, Color border, Color value_color) {
|
||||||
|
const Color bg(0, 0, 0);
|
||||||
|
const Color inactive(90, 100, 110);
|
||||||
|
it.filled_rectangle(x, y, w, h, fill);
|
||||||
|
it.filled_rectangle(x, y, w, 2, accent);
|
||||||
|
it.filled_rectangle(x, y + h - 1, w, 1, border);
|
||||||
|
it.filled_rectangle(x, y, 1, h, border);
|
||||||
|
it.filled_rectangle(x + w - 1, y, 1, h, border);
|
||||||
|
it.print(x + 10, y + 9, label_font, inactive, esphome::display::TextAlign::TOP_LEFT, title);
|
||||||
|
it.print(x + w / 2, y + 43, value_font, value_color, esphome::display::TextAlign::CENTER, value);
|
||||||
|
it.filled_rectangle(x + 10, y + h - 11, w - 20, 3, bg);
|
||||||
|
it.filled_rectangle(x + 10, y + h - 11, w - 20, 1, accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
void label_(Display &it, esphome::display::BaseFont *font, int x, int y, const char *text, Color color) {
|
||||||
|
it.print(x, y, font, color, esphome::display::TextAlign::TOP_LEFT, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
void draw_digits_(Display &it, const char *text, bool negative, Color color) {
|
||||||
|
if (negative) this->draw_segment_(it, 8, 83, 26, 9, true, color);
|
||||||
|
constexpr int digit_x = 40;
|
||||||
|
constexpr int digit_y = 35;
|
||||||
|
constexpr int digit_w = 50;
|
||||||
|
constexpr int digit_h = 106;
|
||||||
|
constexpr int digit_distance = 64;
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
this->draw_seven_segment_(it, digit_x + i * digit_distance, digit_y, digit_w, digit_h, text[i], color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void draw_decimal_points_(Display &it, Color color) {
|
||||||
|
uint8_t p = this->value_[REGPOINT];
|
||||||
|
if ((p & FLAGPOINT1) == FLAGPOINT1) it.filled_rectangle(95, 135, 8, 10, color);
|
||||||
|
if ((p & FLAGPOINT2) == FLAGPOINT2) it.filled_rectangle(159, 135, 8, 10, color);
|
||||||
|
if ((p & FLAGPOINT3) == FLAGPOINT3) it.filled_rectangle(223, 135, 8, 10, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
void draw_segment_(Display &it, int x, int y, int w, int h, bool horizontal, Color color) {
|
||||||
|
if (horizontal) {
|
||||||
|
int cap = h / 2;
|
||||||
|
it.filled_rectangle(x + cap, y, w - 2 * cap, h +1, color);
|
||||||
|
it.filled_triangle(x, y + cap, x + cap, y, x + cap, y + h, color);
|
||||||
|
it.filled_triangle(x + w, y + cap, x + w - cap, y, x + w - cap, y + h, color);
|
||||||
|
} else {
|
||||||
|
int cap = w / 2;
|
||||||
|
it.filled_rectangle(x, y + cap, w +1, h - 2 * cap, color);
|
||||||
|
it.filled_triangle(x + cap, y, x, y + cap, x + w, y + cap, color);
|
||||||
|
it.filled_triangle(x + cap, y + h, x, y + h - cap, x + w, y + h - cap, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void draw_icon_(Display &it, int x, int y, int w, int h, const uint8_t *data, Color color) {
|
||||||
|
for (int row = 0; row < h; row++) {
|
||||||
|
for (int col = 0; col < w; col++) {
|
||||||
|
uint8_t byte = data[row * ((w + 7) / 8) + (col / 8)];
|
||||||
|
if ((byte & (0x80 >> (col % 8))) != 0) {
|
||||||
|
it.draw_pixel_at(x + col, y + row, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void draw_seven_segment_(Display &it, int x, int y, int w, int h, char ch, Color color) {
|
||||||
|
bool a=false,b=false,c=false,d=false,e=false,f=false,g=false;
|
||||||
|
switch (ch) {
|
||||||
|
case '0': a=b=c=d=e=f=true; break;
|
||||||
|
case '1': b=c=true; break;
|
||||||
|
case '2': a=b=d=e=g=true; break;
|
||||||
|
case '3': a=b=c=d=g=true; break;
|
||||||
|
case '4': b=c=f=g=true; break;
|
||||||
|
case '5': a=c=d=f=g=true; break;
|
||||||
|
case '6': a=c=d=e=f=g=true; break;
|
||||||
|
case '7': a=b=c=true; break;
|
||||||
|
case '8': a=b=c=d=e=f=g=true; break;
|
||||||
|
case '9': a=b=c=d=f=g=true; break;
|
||||||
|
case 'O': a=b=c=d=e=f=true; break;
|
||||||
|
case 'L': d=e=f=true; break;
|
||||||
|
case '-': g=true; break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
int t = 10;
|
||||||
|
int half = h / 2;
|
||||||
|
int gap = 1;
|
||||||
|
|
||||||
|
int top_y = y;
|
||||||
|
int mid_y = y + half - t / 2;
|
||||||
|
int bot_y = y + h - t;
|
||||||
|
|
||||||
|
int upper_v_y = top_y + t + gap;
|
||||||
|
int upper_v_h = mid_y - gap - upper_v_y;
|
||||||
|
int lower_v_y = mid_y + t + gap;
|
||||||
|
int lower_v_h = bot_y - gap - lower_v_y;
|
||||||
|
|
||||||
|
if (a) this->draw_segment_(it, x + t / 2, top_y, w - t, t, true, color);
|
||||||
|
if (b && upper_v_h > 0) this->draw_segment_(it, x + w - t, upper_v_y, t, upper_v_h, false, color);
|
||||||
|
if (c && lower_v_h > 0) this->draw_segment_(it, x + w - t, lower_v_y, t, lower_v_h, false, color);
|
||||||
|
if (d) this->draw_segment_(it, x + t / 2, bot_y, w - t, t, true, color);
|
||||||
|
if (e && lower_v_h > 0) this->draw_segment_(it, x, lower_v_y, t, lower_v_h, false, color);
|
||||||
|
if (f && upper_v_h > 0) this->draw_segment_(it, x, upper_v_y, t, upper_v_h, false, color);
|
||||||
|
if (g) this->draw_segment_(it, x + t / 2, mid_y, w - t, t, true, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
void draw_bargraph_(Display &it, uint16_t digits, bool active) {
|
||||||
|
const Color fg(255, 255, 255);
|
||||||
|
const Color inactive(80, 80, 80);
|
||||||
|
uint16_t mapped = active ? static_cast<uint16_t>(digits * 240 / 6000) : 0;
|
||||||
|
if (mapped > 240) mapped = 240;
|
||||||
|
for (uint16_t i = 0; i <= 240; i += 4) {
|
||||||
|
Color col = (active && i <= mapped) ? fg : inactive;
|
||||||
|
int h = (i % 40 == 0) ? 20 : ((i % 20 == 0) ? 15 : 10);
|
||||||
|
it.vertical_line(40 + i, 185 - h, h, col);
|
||||||
|
}
|
||||||
|
it.horizontal_line(35, 185, 250, inactive);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace owon_b35t
|
||||||
|
|
||||||
|
static owon_b35t::Meter owon_meter;
|
||||||
@@ -0,0 +1,628 @@
|
|||||||
|
# Connects to an OWON BT35T/BT35T+ multimeter and an Atorch DL24 dc-load and mirrors their
|
||||||
|
# displays. Also sends those values to Home Assistant for logging and dashboard display.
|
||||||
|
# Also exposes actionable buttons to HA to control the devices.
|
||||||
|
#
|
||||||
|
# Derived work based on https://github.com/reaper7/M5Stack_BLE_client_Owon_B35T by reaper7.
|
||||||
|
# AI (ChatGPT (GPT5.5) has been used to adopt the Arduino sketch to ESPHome
|
||||||
|
# Ported to M5Stack Core2 due to memory constraints.
|
||||||
|
# Integrated Atorch BLE proxy functionality from https://github.com/syssi/esphome-atorch-dl24 by syssi.
|
||||||
|
# Soft buttons work as indicated for OWON meter. Left/right so select action, middle to execute.
|
||||||
|
# Long-press is supported. Display switches to last connected device, tap center of screen to
|
||||||
|
# switch. The dc-load has no on-device buttons, but both are available from HA.
|
||||||
|
|
||||||
|
substitutions:
|
||||||
|
name: "lab-ble-proxy"
|
||||||
|
friendly_name: "Lab BLE Proxy"
|
||||||
|
device_description: "M5Stack Core2 BLE client for OWON B35T/B35T+ multimeter and Atorch DL24 DC load"
|
||||||
|
owon_mac_address: !secret owon_b35t_mac_address
|
||||||
|
dl24_mac_address: !secret dl24_mac_address
|
||||||
|
external_components_source: github://syssi/esphome-atorch-dl24@main
|
||||||
|
atorch_project_version: "2.1.0"
|
||||||
|
|
||||||
|
esphome:
|
||||||
|
name: ${name}
|
||||||
|
friendly_name: ${friendly_name}
|
||||||
|
comment: ${device_description}
|
||||||
|
min_version: 2024.6.0
|
||||||
|
includes:
|
||||||
|
- lab-ble-proxy.h
|
||||||
|
on_boot:
|
||||||
|
priority: 850
|
||||||
|
then:
|
||||||
|
- lambda: |-
|
||||||
|
owon_b35t::core2_axp192_init(id(core2_i2c));
|
||||||
|
project:
|
||||||
|
name: "custom.lab-ble-proxy-m5stack-core2"
|
||||||
|
version: "1.0"
|
||||||
|
|
||||||
|
esp32:
|
||||||
|
board: m5stack-core2
|
||||||
|
flash_size: 16MB
|
||||||
|
framework:
|
||||||
|
type: esp-idf
|
||||||
|
advanced:
|
||||||
|
minimum_chip_revision: "3.1"
|
||||||
|
sram1_as_iram: true
|
||||||
|
|
||||||
|
psram:
|
||||||
|
mode: quad
|
||||||
|
speed: 80MHz
|
||||||
|
|
||||||
|
external_components:
|
||||||
|
- source: ${external_components_source}
|
||||||
|
refresh: 0s
|
||||||
|
|
||||||
|
logger:
|
||||||
|
level: INFO
|
||||||
|
|
||||||
|
api:
|
||||||
|
encryption:
|
||||||
|
key: !secret apikey
|
||||||
|
|
||||||
|
ota:
|
||||||
|
platform: esphome
|
||||||
|
password: !secret ota
|
||||||
|
|
||||||
|
network:
|
||||||
|
enable_ipv6: true
|
||||||
|
|
||||||
|
wifi:
|
||||||
|
ssid: "Voltage-legacy"
|
||||||
|
password: !secret voltage_legacy_psk
|
||||||
|
#use_address: ${name}.home
|
||||||
|
power_save_mode: none
|
||||||
|
fast_connect: on
|
||||||
|
min_auth_mode: WPA2
|
||||||
|
ap:
|
||||||
|
ssid: "Lab BLE Proxy Fallback Hotspot"
|
||||||
|
password: !secret fallback_psk
|
||||||
|
|
||||||
|
captive_portal:
|
||||||
|
|
||||||
|
globals:
|
||||||
|
- id: display_page
|
||||||
|
type: int
|
||||||
|
initial_value: "0"
|
||||||
|
restore_value: no
|
||||||
|
- id: atorch_connected
|
||||||
|
type: bool
|
||||||
|
initial_value: "false"
|
||||||
|
restore_value: no
|
||||||
|
|
||||||
|
script:
|
||||||
|
- id: wake_backlight
|
||||||
|
mode: restart
|
||||||
|
then:
|
||||||
|
- script.stop: backlight_idle
|
||||||
|
- light.turn_on:
|
||||||
|
id: backlight
|
||||||
|
brightness: 100%
|
||||||
|
- id: backlight_idle
|
||||||
|
mode: restart
|
||||||
|
then:
|
||||||
|
- delay: 2min
|
||||||
|
- if:
|
||||||
|
condition:
|
||||||
|
lambda: |-
|
||||||
|
return !owon_meter.connected && !id(atorch_connected);
|
||||||
|
then:
|
||||||
|
- light.turn_on:
|
||||||
|
id: backlight
|
||||||
|
brightness: 50%
|
||||||
|
- delay: 3min
|
||||||
|
- if:
|
||||||
|
condition:
|
||||||
|
lambda: |-
|
||||||
|
return !owon_meter.connected && !id(atorch_connected);
|
||||||
|
then:
|
||||||
|
- light.turn_off: backlight
|
||||||
|
|
||||||
|
interval:
|
||||||
|
- interval: 10s
|
||||||
|
then:
|
||||||
|
- lambda: |-
|
||||||
|
ESP_LOGI("mem", "heap free=%u min_free=%u internal_free=%u internal_largest=%u dma_free=%u dma_largest=%u psram_free=%u psram_largest=%u",
|
||||||
|
static_cast<unsigned>(esp_get_free_heap_size()),
|
||||||
|
static_cast<unsigned>(esp_get_minimum_free_heap_size()),
|
||||||
|
static_cast<unsigned>(heap_caps_get_free_size(MALLOC_CAP_INTERNAL)),
|
||||||
|
static_cast<unsigned>(heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL)),
|
||||||
|
static_cast<unsigned>(heap_caps_get_free_size(MALLOC_CAP_DMA)),
|
||||||
|
static_cast<unsigned>(heap_caps_get_largest_free_block(MALLOC_CAP_DMA)),
|
||||||
|
static_cast<unsigned>(heap_caps_get_free_size(MALLOC_CAP_SPIRAM)),
|
||||||
|
static_cast<unsigned>(heap_caps_get_largest_free_block(MALLOC_CAP_SPIRAM)));
|
||||||
|
|
||||||
|
esp32_ble_tracker:
|
||||||
|
scan_parameters:
|
||||||
|
active: true
|
||||||
|
continuous: true
|
||||||
|
|
||||||
|
ble_client:
|
||||||
|
- mac_address: ${owon_mac_address}
|
||||||
|
id: owon_ble_client
|
||||||
|
on_connect:
|
||||||
|
then:
|
||||||
|
- script.execute: wake_backlight
|
||||||
|
- lambda: |-
|
||||||
|
owon_meter.on_connect();
|
||||||
|
id(display_page) = 0;
|
||||||
|
id(lcd).update();
|
||||||
|
on_disconnect:
|
||||||
|
then:
|
||||||
|
- lambda: |-
|
||||||
|
owon_meter.on_disconnect();
|
||||||
|
if (id(display_page) == 0 && id(atorch_connected)) {
|
||||||
|
id(display_page) = 1;
|
||||||
|
}
|
||||||
|
id(lcd).update();
|
||||||
|
- if:
|
||||||
|
condition:
|
||||||
|
lambda: |-
|
||||||
|
return !owon_meter.connected && !id(atorch_connected);
|
||||||
|
then:
|
||||||
|
- script.execute: backlight_idle
|
||||||
|
|
||||||
|
- mac_address: ${dl24_mac_address}
|
||||||
|
id: atorch_ble_client
|
||||||
|
on_connect:
|
||||||
|
then:
|
||||||
|
- script.execute: wake_backlight
|
||||||
|
- lambda: |-
|
||||||
|
id(atorch_connected) = true;
|
||||||
|
id(display_page) = 1;
|
||||||
|
id(lcd).update();
|
||||||
|
on_disconnect:
|
||||||
|
then:
|
||||||
|
- lambda: |-
|
||||||
|
id(atorch_connected) = false;
|
||||||
|
if (id(display_page) == 1 && owon_meter.connected) {
|
||||||
|
id(display_page) = 0;
|
||||||
|
}
|
||||||
|
id(lcd).update();
|
||||||
|
- if:
|
||||||
|
condition:
|
||||||
|
lambda: |-
|
||||||
|
return !owon_meter.connected && !id(atorch_connected);
|
||||||
|
then:
|
||||||
|
- script.execute: backlight_idle
|
||||||
|
|
||||||
|
atorch_dl24:
|
||||||
|
- id: atorch0
|
||||||
|
ble_client_id: atorch_ble_client
|
||||||
|
check_crc: false
|
||||||
|
throttle: 0s
|
||||||
|
|
||||||
|
spi:
|
||||||
|
clk_pin: GPIO18
|
||||||
|
mosi_pin: GPIO23
|
||||||
|
|
||||||
|
i2c:
|
||||||
|
id: core2_i2c
|
||||||
|
sda: GPIO21
|
||||||
|
scl: GPIO22
|
||||||
|
scan: true
|
||||||
|
|
||||||
|
output:
|
||||||
|
- platform: template
|
||||||
|
type: float
|
||||||
|
id: lcd_backlight
|
||||||
|
write_action:
|
||||||
|
- lambda: |-
|
||||||
|
owon_b35t::core2_axp192_set_backlight(state);
|
||||||
|
|
||||||
|
light:
|
||||||
|
- platform: monochromatic
|
||||||
|
output: lcd_backlight
|
||||||
|
name: "${friendly_name} Backlight"
|
||||||
|
id: backlight
|
||||||
|
restore_mode: ALWAYS_ON
|
||||||
|
|
||||||
|
font:
|
||||||
|
- file: "fonts/Roboto-Regular.ttf"
|
||||||
|
id: meter_font
|
||||||
|
size: 15
|
||||||
|
glyphs:
|
||||||
|
[
|
||||||
|
" ",
|
||||||
|
"!",
|
||||||
|
"%",
|
||||||
|
"+",
|
||||||
|
"-",
|
||||||
|
".",
|
||||||
|
"/",
|
||||||
|
":",
|
||||||
|
"0",
|
||||||
|
"1",
|
||||||
|
"2",
|
||||||
|
"3",
|
||||||
|
"4",
|
||||||
|
"5",
|
||||||
|
"6",
|
||||||
|
"7",
|
||||||
|
"8",
|
||||||
|
"9",
|
||||||
|
"<",
|
||||||
|
">",
|
||||||
|
"A",
|
||||||
|
"B",
|
||||||
|
"C",
|
||||||
|
"D",
|
||||||
|
"E",
|
||||||
|
"F",
|
||||||
|
"G",
|
||||||
|
"H",
|
||||||
|
"I",
|
||||||
|
"L",
|
||||||
|
"M",
|
||||||
|
"N",
|
||||||
|
"O",
|
||||||
|
"P",
|
||||||
|
"R",
|
||||||
|
"S",
|
||||||
|
"T",
|
||||||
|
"U",
|
||||||
|
"V",
|
||||||
|
"W",
|
||||||
|
"X",
|
||||||
|
"Y",
|
||||||
|
"Z",
|
||||||
|
"a",
|
||||||
|
"b",
|
||||||
|
"c",
|
||||||
|
"d",
|
||||||
|
"e",
|
||||||
|
"f",
|
||||||
|
"g",
|
||||||
|
"h",
|
||||||
|
"i",
|
||||||
|
"k",
|
||||||
|
"l",
|
||||||
|
"m",
|
||||||
|
"n",
|
||||||
|
"o",
|
||||||
|
"p",
|
||||||
|
"r",
|
||||||
|
"s",
|
||||||
|
"t",
|
||||||
|
"u",
|
||||||
|
"v",
|
||||||
|
"w",
|
||||||
|
"y",
|
||||||
|
"z",
|
||||||
|
"°",
|
||||||
|
"µ",
|
||||||
|
"Ω",
|
||||||
|
]
|
||||||
|
|
||||||
|
- file: "fonts/Roboto-Medium.ttf"
|
||||||
|
id: atorch_value_font
|
||||||
|
size: 22
|
||||||
|
glyphs:
|
||||||
|
[
|
||||||
|
" ",
|
||||||
|
"+",
|
||||||
|
"-",
|
||||||
|
".",
|
||||||
|
"0",
|
||||||
|
"1",
|
||||||
|
"2",
|
||||||
|
"3",
|
||||||
|
"4",
|
||||||
|
"5",
|
||||||
|
"6",
|
||||||
|
"7",
|
||||||
|
"8",
|
||||||
|
"9",
|
||||||
|
"A",
|
||||||
|
"C",
|
||||||
|
"V",
|
||||||
|
"W",
|
||||||
|
"h",
|
||||||
|
"°",
|
||||||
|
]
|
||||||
|
|
||||||
|
display:
|
||||||
|
- platform: mipi_spi
|
||||||
|
id: lcd
|
||||||
|
model: M5CORE2
|
||||||
|
update_interval: 500ms
|
||||||
|
lambda: |-
|
||||||
|
owon_meter.render(
|
||||||
|
it,
|
||||||
|
id(meter_font),
|
||||||
|
id(atorch_value_font),
|
||||||
|
id(display_page),
|
||||||
|
id(atorch_connected),
|
||||||
|
id(atorch_voltage).has_state() ? id(atorch_voltage).state : NAN,
|
||||||
|
id(atorch_current).has_state() ? id(atorch_current).state : NAN,
|
||||||
|
id(atorch_power).has_state() ? id(atorch_power).state : NAN,
|
||||||
|
id(atorch_capacity).has_state() ? id(atorch_capacity).state : NAN,
|
||||||
|
id(atorch_energy_calculated).has_state() ? id(atorch_energy_calculated).state : NAN,
|
||||||
|
id(atorch_temperature).has_state() ? id(atorch_temperature).state : NAN
|
||||||
|
);
|
||||||
|
|
||||||
|
touchscreen:
|
||||||
|
- platform: ft63x6
|
||||||
|
id: touch
|
||||||
|
display: lcd
|
||||||
|
on_touch:
|
||||||
|
then:
|
||||||
|
- script.execute: wake_backlight
|
||||||
|
- if:
|
||||||
|
condition:
|
||||||
|
lambda: |-
|
||||||
|
return !owon_meter.connected && !id(atorch_connected);
|
||||||
|
then:
|
||||||
|
- script.execute: backlight_idle
|
||||||
|
|
||||||
|
binary_sensor:
|
||||||
|
- platform: touchscreen
|
||||||
|
touchscreen_id: touch
|
||||||
|
id: btn_toggle_page
|
||||||
|
x_min: 70
|
||||||
|
x_max: 250
|
||||||
|
y_min: 45
|
||||||
|
y_max: 195
|
||||||
|
on_press:
|
||||||
|
then:
|
||||||
|
- lambda: |-
|
||||||
|
id(display_page) = 1 - id(display_page);
|
||||||
|
id(lcd).update();
|
||||||
|
- platform: touchscreen
|
||||||
|
id: button_a
|
||||||
|
touchscreen_id: touch
|
||||||
|
x_min: 34
|
||||||
|
x_max: 74
|
||||||
|
y_min: 212
|
||||||
|
y_max: 240
|
||||||
|
internal: true
|
||||||
|
on_press:
|
||||||
|
then:
|
||||||
|
- lambda: |-
|
||||||
|
owon_meter.previous_button();
|
||||||
|
- platform: touchscreen
|
||||||
|
id: button_b
|
||||||
|
touchscreen_id: touch
|
||||||
|
x_min: 108
|
||||||
|
x_max: 208
|
||||||
|
y_min: 212
|
||||||
|
y_max: 240
|
||||||
|
internal: true
|
||||||
|
on_click:
|
||||||
|
- min_length: 50ms
|
||||||
|
max_length: 1500ms
|
||||||
|
then:
|
||||||
|
- logger.log:
|
||||||
|
level: INFO
|
||||||
|
format: "OWON short press: %s"
|
||||||
|
args: ["owon_meter.selected_button_name()"]
|
||||||
|
- ble_client.ble_write:
|
||||||
|
id: owon_ble_client
|
||||||
|
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
|
||||||
|
characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb"
|
||||||
|
value: !lambda |-
|
||||||
|
std::vector<uint8_t> data = {owon_meter.selected_button, 0x01};
|
||||||
|
return data;
|
||||||
|
- min_length: 1500ms
|
||||||
|
max_length: 5000ms
|
||||||
|
then:
|
||||||
|
- logger.log:
|
||||||
|
level: INFO
|
||||||
|
format: "OWON long press: %s"
|
||||||
|
args: ["owon_meter.selected_button_name()"]
|
||||||
|
- ble_client.ble_write:
|
||||||
|
id: owon_ble_client
|
||||||
|
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
|
||||||
|
characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb"
|
||||||
|
value: !lambda |-
|
||||||
|
uint8_t press_type = (owon_meter.selected_button == 1 || owon_meter.selected_button == 5) ? 0x01 : 0x00;
|
||||||
|
std::vector<uint8_t> data = {owon_meter.selected_button, press_type};
|
||||||
|
return data;
|
||||||
|
- platform: touchscreen
|
||||||
|
id: button_c
|
||||||
|
touchscreen_id: touch
|
||||||
|
x_min: 242
|
||||||
|
x_max: 282
|
||||||
|
y_min: 212
|
||||||
|
y_max: 240
|
||||||
|
internal: true
|
||||||
|
on_press:
|
||||||
|
then:
|
||||||
|
- lambda: |-
|
||||||
|
owon_meter.next_button();
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
name: "${friendly_name} OWON Connected"
|
||||||
|
icon: "mdi:bluetooth-connect"
|
||||||
|
lambda: |-
|
||||||
|
return owon_meter.connected;
|
||||||
|
- platform: template
|
||||||
|
name: "${friendly_name} OWON Overload"
|
||||||
|
icon: "mdi:debug-step-over"
|
||||||
|
lambda: |-
|
||||||
|
return owon_meter.overload;
|
||||||
|
- platform: template
|
||||||
|
name: "${friendly_name} OWON Low Battery"
|
||||||
|
icon: "mdi:battery-low"
|
||||||
|
lambda: |-
|
||||||
|
return owon_meter.low_battery;
|
||||||
|
- platform: template
|
||||||
|
name: "${friendly_name} Atorch Connected"
|
||||||
|
device_class: connectivity
|
||||||
|
icon: "mdi:bluetooth-connect"
|
||||||
|
lambda: |-
|
||||||
|
return id(atorch_connected);
|
||||||
|
|
||||||
|
sensor:
|
||||||
|
- platform: ble_client
|
||||||
|
type: characteristic
|
||||||
|
ble_client_id: owon_ble_client
|
||||||
|
id: owon_notify_source
|
||||||
|
internal: true
|
||||||
|
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
|
||||||
|
characteristic_uuid: "0000fff4-0000-1000-8000-00805f9b34fb"
|
||||||
|
notify: true
|
||||||
|
update_interval: never
|
||||||
|
lambda: |-
|
||||||
|
owon_meter.handle_notify(x);
|
||||||
|
return owon_meter.value();
|
||||||
|
|
||||||
|
- platform: wifi_signal
|
||||||
|
name: "${friendly_name} WiFi Signal"
|
||||||
|
update_interval: 60s
|
||||||
|
|
||||||
|
- platform: atorch_dl24
|
||||||
|
atorch_dl24_id: atorch0
|
||||||
|
voltage:
|
||||||
|
name: "${friendly_name} Atorch Voltage"
|
||||||
|
id: atorch_voltage
|
||||||
|
current:
|
||||||
|
name: "${friendly_name} Atorch Current"
|
||||||
|
id: atorch_current
|
||||||
|
power:
|
||||||
|
name: "${friendly_name} Atorch Power"
|
||||||
|
id: atorch_power
|
||||||
|
capacity:
|
||||||
|
name: "${friendly_name} Atorch Capacity"
|
||||||
|
id: atorch_capacity
|
||||||
|
temperature:
|
||||||
|
name: "${friendly_name} Atorch Temperature"
|
||||||
|
id: atorch_temperature
|
||||||
|
dim_backlight:
|
||||||
|
name: "${friendly_name} Atorch Dim Backlight"
|
||||||
|
id: atorch_dim_backlight
|
||||||
|
|
||||||
|
- platform: integration
|
||||||
|
name: "${friendly_name} Atorch Energy"
|
||||||
|
id: atorch_energy_calculated
|
||||||
|
sensor: atorch_power
|
||||||
|
time_unit: h
|
||||||
|
unit_of_measurement: "Wh"
|
||||||
|
icon: "mdi:lightning-bolt"
|
||||||
|
device_class: energy
|
||||||
|
state_class: total_increasing
|
||||||
|
accuracy_decimals: 3
|
||||||
|
restore: true
|
||||||
|
integration_method: trapezoid
|
||||||
|
|
||||||
|
text_sensor:
|
||||||
|
- platform: template
|
||||||
|
name: "${friendly_name} OWON Reading"
|
||||||
|
update_interval: 1s
|
||||||
|
icon: "mdi:gauge"
|
||||||
|
lambda: |-
|
||||||
|
return owon_meter.reading_text();
|
||||||
|
- platform: template
|
||||||
|
name: "${friendly_name} OWON Unit"
|
||||||
|
update_interval: 1s
|
||||||
|
icon: "mdi:ruler"
|
||||||
|
lambda: |-
|
||||||
|
return std::string(owon_meter.scale()) + owon_meter.unit();
|
||||||
|
- platform: template
|
||||||
|
name: "${friendly_name} OWON Mode"
|
||||||
|
icon: "mdi:knob"
|
||||||
|
update_interval: 1s
|
||||||
|
lambda: |-
|
||||||
|
return owon_meter.mode_text();
|
||||||
|
|
||||||
|
button:
|
||||||
|
- platform: atorch_dl24
|
||||||
|
atorch_dl24_id: atorch0
|
||||||
|
reset_energy:
|
||||||
|
name: "${friendly_name} Atorch Reset Energy"
|
||||||
|
on_press:
|
||||||
|
- sensor.integration.reset: atorch_energy_calculated
|
||||||
|
reset_capacity:
|
||||||
|
name: "${friendly_name} Atorch Reset Capacity"
|
||||||
|
reset_runtime:
|
||||||
|
name: "${friendly_name} Atorch Reset Runtime"
|
||||||
|
reset_all:
|
||||||
|
name: "${friendly_name} Atorch Reset All"
|
||||||
|
on_press:
|
||||||
|
- sensor.integration.reset: atorch_energy_calculated
|
||||||
|
usb_plus:
|
||||||
|
name: "${friendly_name} Atorch Plus"
|
||||||
|
usb_minus:
|
||||||
|
name: "${friendly_name} Atorch Minus"
|
||||||
|
setup:
|
||||||
|
name: "${friendly_name} Atorch Setup"
|
||||||
|
enter:
|
||||||
|
name: "${friendly_name} Atorch Enter"
|
||||||
|
- platform: template
|
||||||
|
name: "OWON SELECT"
|
||||||
|
id: owon_btn_select
|
||||||
|
on_press:
|
||||||
|
- ble_client.ble_write:
|
||||||
|
id: owon_ble_client
|
||||||
|
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
|
||||||
|
characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb"
|
||||||
|
value: !lambda "return std::vector<uint8_t>({1, 0x01});"
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
name: "OWON RANGE"
|
||||||
|
id: owon_btn_range
|
||||||
|
on_press:
|
||||||
|
- ble_client.ble_write:
|
||||||
|
id: owon_ble_client
|
||||||
|
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
|
||||||
|
characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb"
|
||||||
|
value: !lambda "return std::vector<uint8_t>({2, 0x01});"
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
name: "OWON HOLD "
|
||||||
|
id: owon_btn_hold
|
||||||
|
on_press:
|
||||||
|
- ble_client.ble_write:
|
||||||
|
id: owon_ble_client
|
||||||
|
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
|
||||||
|
characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb"
|
||||||
|
value: !lambda "return std::vector<uint8_t>({3, 0x01});"
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
name: "OWON LIGHT"
|
||||||
|
id: owon_btn_light
|
||||||
|
on_press:
|
||||||
|
- ble_client.ble_write:
|
||||||
|
id: owon_ble_client
|
||||||
|
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
|
||||||
|
characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb"
|
||||||
|
value: !lambda "return std::vector<uint8_t>({3, 0x00});"
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
name: "OWON RELATIVE"
|
||||||
|
id: owon_btn_rel
|
||||||
|
on_press:
|
||||||
|
- ble_client.ble_write:
|
||||||
|
id: owon_ble_client
|
||||||
|
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
|
||||||
|
characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb"
|
||||||
|
value: !lambda "return std::vector<uint8_t>({4, 0x01});"
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
name: "OWON BT"
|
||||||
|
id: owon_btn_bt
|
||||||
|
on_press:
|
||||||
|
- ble_client.ble_write:
|
||||||
|
id: owon_ble_client
|
||||||
|
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
|
||||||
|
characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb"
|
||||||
|
value: !lambda "return std::vector<uint8_t>({4, 0x00});"
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
name: "OWON HZ | DUTY"
|
||||||
|
id: owon_btn_hz
|
||||||
|
on_press:
|
||||||
|
- ble_client.ble_write:
|
||||||
|
id: owon_ble_client
|
||||||
|
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
|
||||||
|
characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb"
|
||||||
|
value: !lambda "return std::vector<uint8_t>({5, 0x01});"
|
||||||
|
|
||||||
|
- platform: template
|
||||||
|
name: "OWON MAX | MIN"
|
||||||
|
id: owon_btn_maxmin
|
||||||
|
on_press:
|
||||||
|
- ble_client.ble_write:
|
||||||
|
id: owon_ble_client
|
||||||
|
service_uuid: "0000fff0-0000-1000-8000-00805f9b34fb"
|
||||||
|
characteristic_uuid: "0000fff3-0000-1000-8000-00805f9b34fb"
|
||||||
|
value: !lambda "return std::vector<uint8_t>({6, 0x01});"
|
||||||
@@ -1,7 +1,15 @@
|
|||||||
|
substitutions:
|
||||||
|
name: "livingroom"
|
||||||
|
friendly_name: "Luftqualität Wohnzimmer"
|
||||||
|
|
||||||
esphome:
|
esphome:
|
||||||
name: livingroom
|
name: ${name}
|
||||||
platform: ESP32
|
friendly_name: ${friendly_name}
|
||||||
board: nodemcu-32s
|
name_add_mac_suffix: false
|
||||||
|
project:
|
||||||
|
name: sensor.${name}
|
||||||
|
version: "1.1"
|
||||||
|
min_version: 2021.9.0
|
||||||
|
|
||||||
on_boot:
|
on_boot:
|
||||||
then:
|
then:
|
||||||
@@ -41,12 +49,23 @@ esphome:
|
|||||||
id: pm25_warn
|
id: pm25_warn
|
||||||
state: "grün"
|
state: "grün"
|
||||||
|
|
||||||
|
esp32:
|
||||||
|
board: nodemcu-32s
|
||||||
|
framework:
|
||||||
|
type: esp-idf
|
||||||
|
advanced:
|
||||||
|
minimum_chip_revision: "3.0"
|
||||||
|
|
||||||
|
network:
|
||||||
|
enable_ipv6: true
|
||||||
|
|
||||||
wifi:
|
wifi:
|
||||||
ssid: "Voltage-legacy"
|
ssid: "Voltage-legacy"
|
||||||
password: !secret voltage_legacy_psk
|
password: !secret voltage_legacy_psk
|
||||||
use_address: livingroom.home
|
# use_address: ${name}.home
|
||||||
power_save_mode: high
|
power_save_mode: high
|
||||||
fast_connect: on
|
fast_connect: on
|
||||||
|
min_auth_mode: WPA2
|
||||||
|
|
||||||
# Enable fallback hotspot (captive portal) in case wifi connection fails
|
# Enable fallback hotspot (captive portal) in case wifi connection fails
|
||||||
ap:
|
ap:
|
||||||
@@ -66,6 +85,7 @@ api:
|
|||||||
|
|
||||||
# Enable over-the-air updates
|
# Enable over-the-air updates
|
||||||
ota:
|
ota:
|
||||||
|
platform: esphome
|
||||||
password: !secret ota
|
password: !secret ota
|
||||||
|
|
||||||
# Initialize I²C
|
# Initialize I²C
|
||||||
@@ -100,6 +120,7 @@ light:
|
|||||||
|
|
||||||
# Initialize UART for Vindriktning sensor
|
# Initialize UART for Vindriktning sensor
|
||||||
uart:
|
uart:
|
||||||
|
id: uartPM
|
||||||
rx_pin: GPIO22
|
rx_pin: GPIO22
|
||||||
baud_rate: 9600
|
baud_rate: 9600
|
||||||
|
|
||||||
@@ -186,15 +207,16 @@ sensor:
|
|||||||
temperature:
|
temperature:
|
||||||
name: "Wohnzimmer Temperatur"
|
name: "Wohnzimmer Temperatur"
|
||||||
accuracy_decimals: 2
|
accuracy_decimals: 2
|
||||||
|
filters:
|
||||||
|
- offset: -2.5
|
||||||
humidity:
|
humidity:
|
||||||
name: "Wohnzimmer Luftfeuchtigkeit"
|
name: "Wohnzimmer Luftfeuchtigkeit"
|
||||||
accuracy_decimals: 1
|
accuracy_decimals: 1
|
||||||
# SCD30 temp sensor is a bit off
|
# SCD30 temp sensor is a bit off
|
||||||
temperature_offset: 0 °C
|
|
||||||
ambient_pressure_compensation: 1
|
ambient_pressure_compensation: 1
|
||||||
automatic_self_calibration: True
|
automatic_self_calibration: True
|
||||||
address: 0x61
|
address: 0x61
|
||||||
update_interval: 60s
|
update_interval: 10s
|
||||||
# Vindriktning particulate matter sensor
|
# Vindriktning particulate matter sensor
|
||||||
- platform: pm1006
|
- platform: pm1006
|
||||||
pm_2_5:
|
pm_2_5:
|
||||||
@@ -221,7 +243,7 @@ sensor:
|
|||||||
- platform: adc
|
- platform: adc
|
||||||
pin: 34
|
pin: 34
|
||||||
name: "Spannung Photodiode"
|
name: "Spannung Photodiode"
|
||||||
attenuation: 11db
|
attenuation: 12db
|
||||||
update_interval: 1s
|
update_interval: 1s
|
||||||
internal: true
|
internal: true
|
||||||
on_value_range:
|
on_value_range:
|
||||||
|
|||||||
@@ -1,34 +1,43 @@
|
|||||||
|
# Source: https://github.com/esphome/wake-word-voice-assistants/blob/main/m5stack-atom-echo/m5stack-atom-echo.yaml
|
||||||
|
|
||||||
substitutions:
|
substitutions:
|
||||||
name: "m5stack-atom-echo"
|
name: m5stack-atom-echo
|
||||||
friendly_name: "M5Stack Atom Echo"
|
friendly_name: M5Stack Atom Echo
|
||||||
|
micro_wake_word_model: okay_nabu # alexa, hey_jarvis, hey_mycroft are also supported
|
||||||
|
|
||||||
esphome:
|
esphome:
|
||||||
name: ${name}
|
name: ${name}
|
||||||
friendly_name: ${friendly_name}
|
|
||||||
name_add_mac_suffix: False
|
name_add_mac_suffix: False
|
||||||
project:
|
friendly_name: ${friendly_name}
|
||||||
name: m5stack.atom-echo
|
min_version: 2025.2.0
|
||||||
version: "1.0"
|
|
||||||
min_version: 2023.5.0
|
|
||||||
|
|
||||||
esp32:
|
esp32:
|
||||||
board: m5stack-atom
|
board: m5stack-atom
|
||||||
framework:
|
framework:
|
||||||
type: arduino
|
type: esp-idf
|
||||||
|
|
||||||
# Enable Home Assistant API
|
# Enable Home Assistant API
|
||||||
api:
|
api:
|
||||||
encryption:
|
encryption:
|
||||||
key: !secret apikey
|
key: !secret apikey
|
||||||
|
|
||||||
ota:
|
ota:
|
||||||
|
- platform: esphome
|
||||||
|
id: ota_esphome
|
||||||
password: !secret ota
|
password: !secret ota
|
||||||
|
|
||||||
|
network:
|
||||||
|
enable_ipv6: true
|
||||||
|
|
||||||
wifi:
|
wifi:
|
||||||
|
on_connect:
|
||||||
|
- delay: 5s # Gives time for improv results to be transmitted
|
||||||
ssid: Voltage-legacy
|
ssid: Voltage-legacy
|
||||||
password: !secret voltage_legacy_psk
|
password: !secret voltage_legacy_psk
|
||||||
use_address: m5stack-atom-echo.home
|
#use_address: ${name}.home
|
||||||
power_save_mode: high
|
power_save_mode: high
|
||||||
fast_connect: on
|
fast_connect: on
|
||||||
|
min_auth_mode: WPA2
|
||||||
# Enable fallback hotspot (captive portal) in case wifi connection fails
|
# Enable fallback hotspot (captive portal) in case wifi connection fails
|
||||||
ap:
|
ap:
|
||||||
ssid: "Raspiaudio Fallback Hotspot"
|
ssid: "Raspiaudio Fallback Hotspot"
|
||||||
@@ -36,14 +45,15 @@ wifi:
|
|||||||
|
|
||||||
logger:
|
logger:
|
||||||
|
|
||||||
dashboard_import:
|
|
||||||
package_import_url: github://esphome/media-players/m5stack-atom-echo.yaml@main
|
|
||||||
|
|
||||||
captive_portal:
|
captive_portal:
|
||||||
|
|
||||||
improv_serial:
|
button:
|
||||||
|
- platform: factory_reset
|
||||||
|
id: factory_reset_btn
|
||||||
|
name: Factory reset
|
||||||
|
|
||||||
i2s_audio:
|
i2s_audio:
|
||||||
|
- id: i2s_audio_bus
|
||||||
i2s_lrclk_pin: GPIO33
|
i2s_lrclk_pin: GPIO33
|
||||||
i2s_bclk_pin: GPIO19
|
i2s_bclk_pin: GPIO19
|
||||||
|
|
||||||
@@ -54,95 +64,301 @@ microphone:
|
|||||||
adc_type: external
|
adc_type: external
|
||||||
pdm: true
|
pdm: true
|
||||||
|
|
||||||
voice_assistant:
|
speaker:
|
||||||
microphone: echo_microphone
|
- platform: i2s_audio
|
||||||
speaker: atom_echo_speaker
|
id: echo_speaker
|
||||||
on_start:
|
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:
|
- light.turn_on:
|
||||||
id: led
|
id: led
|
||||||
blue: 100%
|
blue: 100%
|
||||||
red: 0%
|
red: 0%
|
||||||
green: 0%
|
green: 0%
|
||||||
|
brightness: 100%
|
||||||
effect: none
|
effect: none
|
||||||
|
on_idle:
|
||||||
|
- script.execute: start_wake_word
|
||||||
|
|
||||||
|
voice_assistant:
|
||||||
|
id: va
|
||||||
|
microphone: echo_microphone
|
||||||
|
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: "Slow Pulse"
|
||||||
|
on_stt_vad_end:
|
||||||
|
- light.turn_on:
|
||||||
|
id: led
|
||||||
|
blue: 100%
|
||||||
|
red: 0%
|
||||||
|
green: 0%
|
||||||
|
effect: "Fast Pulse"
|
||||||
on_tts_start:
|
on_tts_start:
|
||||||
- light.turn_on:
|
- light.turn_on:
|
||||||
id: led
|
id: led
|
||||||
blue: 0%
|
blue: 100%
|
||||||
red: 0%
|
red: 0%
|
||||||
green: 100%
|
green: 0%
|
||||||
|
brightness: 100%
|
||||||
effect: none
|
effect: none
|
||||||
on_tts_end:
|
|
||||||
- light.turn_on:
|
|
||||||
id: led
|
|
||||||
blue: 0%
|
|
||||||
red: 0%
|
|
||||||
green: 100%
|
|
||||||
effect: pulse
|
|
||||||
on_end:
|
on_end:
|
||||||
- delay: 1s
|
- delay: 100ms
|
||||||
- wait_until:
|
- script.execute: start_wake_word
|
||||||
not:
|
|
||||||
speaker.is_playing: atom_echo_speaker
|
|
||||||
- light.turn_off: led
|
|
||||||
on_error:
|
on_error:
|
||||||
- light.turn_on:
|
- light.turn_on:
|
||||||
id: led
|
id: led
|
||||||
blue: 0%
|
|
||||||
red: 100%
|
red: 100%
|
||||||
green: 0%
|
green: 0%
|
||||||
|
blue: 0%
|
||||||
|
brightness: 100%
|
||||||
effect: none
|
effect: none
|
||||||
- delay: 1s
|
- delay: 2s
|
||||||
|
- script.execute: reset_led
|
||||||
|
on_client_connected:
|
||||||
|
- delay: 2s # Give the api server time to settle
|
||||||
|
- script.execute: start_wake_word
|
||||||
|
on_client_disconnected:
|
||||||
|
- voice_assistant.stop:
|
||||||
|
- micro_wake_word.stop:
|
||||||
|
on_timer_finished:
|
||||||
|
- voice_assistant.stop:
|
||||||
|
- micro_wake_word.stop:
|
||||||
|
- wait_until:
|
||||||
|
not:
|
||||||
|
microphone.is_capturing:
|
||||||
|
- switch.turn_on: timer_ringing
|
||||||
|
- light.turn_on:
|
||||||
|
id: led
|
||||||
|
red: 0%
|
||||||
|
green: 100%
|
||||||
|
blue: 0%
|
||||||
|
brightness: 100%
|
||||||
|
effect: "Fast Pulse"
|
||||||
|
- wait_until:
|
||||||
|
- switch.is_off: timer_ringing
|
||||||
- light.turn_off: led
|
- light.turn_off: led
|
||||||
|
- switch.turn_off: timer_ringing
|
||||||
|
|
||||||
binary_sensor:
|
binary_sensor:
|
||||||
|
# button does the following:
|
||||||
|
# short click - stop a timer
|
||||||
|
# if no timer then restart either microwakeword or voice assistant continuous
|
||||||
- platform: gpio
|
- platform: gpio
|
||||||
pin:
|
pin:
|
||||||
number: GPIO39
|
number: GPIO39
|
||||||
inverted: true
|
inverted: true
|
||||||
name: Button
|
name: Button
|
||||||
|
disabled_by_default: true
|
||||||
|
entity_category: diagnostic
|
||||||
id: echo_button
|
id: echo_button
|
||||||
on_multi_click:
|
on_multi_click:
|
||||||
- timing:
|
- timing:
|
||||||
- ON FOR AT MOST 350ms
|
- ON for at least 50ms
|
||||||
- OFF FOR AT LEAST 10ms
|
- OFF for at least 50ms
|
||||||
then:
|
then:
|
||||||
- media_player.toggle: media_out
|
- if:
|
||||||
|
condition:
|
||||||
|
switch.is_on: timer_ringing
|
||||||
|
then:
|
||||||
|
- switch.turn_off: timer_ringing
|
||||||
|
else:
|
||||||
|
- script.execute: start_wake_word
|
||||||
- timing:
|
- timing:
|
||||||
- ON FOR AT LEAST 350ms
|
- ON for at least 10s
|
||||||
then:
|
then:
|
||||||
- voice_assistant.start:
|
- button.press: factory_reset_btn
|
||||||
- timing:
|
|
||||||
- ON FOR AT LEAST 350ms
|
|
||||||
- OFF FOR AT LEAST 10ms
|
|
||||||
then:
|
|
||||||
- voice_assistant.stop:
|
|
||||||
|
|
||||||
media_player:
|
|
||||||
- platform: i2s_audio
|
|
||||||
id: media_out
|
|
||||||
name: None
|
|
||||||
dac_type: external
|
|
||||||
i2s_dout_pin: GPIO22
|
|
||||||
mode: mono
|
|
||||||
|
|
||||||
speaker:
|
|
||||||
- platform: i2s_audio
|
|
||||||
id: atom_echo_speaker
|
|
||||||
dac_type: external
|
|
||||||
i2s_dout_pin: GPIO22
|
|
||||||
mode: mono
|
|
||||||
|
|
||||||
light:
|
light:
|
||||||
- platform: esp32_rmt_led_strip
|
- platform: esp32_rmt_led_strip
|
||||||
id: led
|
id: led
|
||||||
name: None
|
name: None
|
||||||
|
disabled_by_default: true
|
||||||
|
entity_category: config
|
||||||
pin: GPIO27
|
pin: GPIO27
|
||||||
default_transition_length: 0s
|
default_transition_length: 0s
|
||||||
chipset: SK6812
|
chipset: SK6812
|
||||||
num_leds: 1
|
num_leds: 1
|
||||||
rgb_order: grb
|
rgb_order: grb
|
||||||
rmt_channel: 0
|
|
||||||
effects:
|
effects:
|
||||||
- pulse:
|
- pulse:
|
||||||
|
name: "Slow Pulse"
|
||||||
transition_length: 250ms
|
transition_length: 250ms
|
||||||
update_interval: 250ms
|
update_interval: 250ms
|
||||||
|
min_brightness: 50%
|
||||||
|
max_brightness: 100%
|
||||||
|
- pulse:
|
||||||
|
name: "Fast Pulse"
|
||||||
|
transition_length: 100ms
|
||||||
|
update_interval: 100ms
|
||||||
|
min_brightness: 50%
|
||||||
|
max_brightness: 100%
|
||||||
|
|
||||||
|
script:
|
||||||
|
- id: reset_led
|
||||||
|
then:
|
||||||
|
- if:
|
||||||
|
condition:
|
||||||
|
- lambda: return id(wake_word_engine_location).state == "On device";
|
||||||
|
- switch.is_on: use_listen_light
|
||||||
|
then:
|
||||||
|
- light.turn_on:
|
||||||
|
id: led
|
||||||
|
red: 100%
|
||||||
|
green: 89%
|
||||||
|
blue: 71%
|
||||||
|
brightness: 60%
|
||||||
|
effect: none
|
||||||
|
else:
|
||||||
|
- if:
|
||||||
|
condition:
|
||||||
|
- lambda: return id(wake_word_engine_location).state != "On device";
|
||||||
|
- switch.is_on: use_listen_light
|
||||||
|
then:
|
||||||
|
- light.turn_on:
|
||||||
|
id: led
|
||||||
|
red: 0%
|
||||||
|
green: 100%
|
||||||
|
blue: 100%
|
||||||
|
brightness: 60%
|
||||||
|
effect: none
|
||||||
|
else:
|
||||||
|
- light.turn_off: led
|
||||||
|
- id: start_wake_word
|
||||||
|
then:
|
||||||
|
- wait_until:
|
||||||
|
and:
|
||||||
|
- media_player.is_idle:
|
||||||
|
- speaker.is_stopped:
|
||||||
|
- if:
|
||||||
|
condition:
|
||||||
|
lambda: return id(wake_word_engine_location).state == "On device";
|
||||||
|
then:
|
||||||
|
- voice_assistant.stop
|
||||||
|
- micro_wake_word.stop:
|
||||||
|
- delay: 1s
|
||||||
|
- script.execute: reset_led
|
||||||
|
- script.wait: reset_led
|
||||||
|
- micro_wake_word.start:
|
||||||
|
else:
|
||||||
|
- if:
|
||||||
|
condition: voice_assistant.is_running
|
||||||
|
then:
|
||||||
|
- voice_assistant.stop:
|
||||||
|
- script.execute: reset_led
|
||||||
|
- voice_assistant.start_continuous:
|
||||||
|
|
||||||
|
switch:
|
||||||
|
- platform: template
|
||||||
|
name: Use listen light
|
||||||
|
id: use_listen_light
|
||||||
|
optimistic: true
|
||||||
|
restore_mode: RESTORE_DEFAULT_ON
|
||||||
|
entity_category: config
|
||||||
|
on_turn_on:
|
||||||
|
- script.execute: reset_led
|
||||||
|
on_turn_off:
|
||||||
|
- script.execute: reset_led
|
||||||
|
- platform: template
|
||||||
|
id: timer_ringing
|
||||||
|
optimistic: true
|
||||||
|
restore_mode: ALWAYS_OFF
|
||||||
|
on_turn_off:
|
||||||
|
# Turn off the repeat mode and disable the pause between playlist items
|
||||||
|
- lambda: |-
|
||||||
|
id(echo_media_player)
|
||||||
|
->make_call()
|
||||||
|
.set_command(media_player::MediaPlayerCommand::MEDIA_PLAYER_COMMAND_REPEAT_OFF)
|
||||||
|
.set_announcement(true)
|
||||||
|
.perform();
|
||||||
|
id(echo_media_player)->set_playlist_delay_ms(speaker::AudioPipelineType::ANNOUNCEMENT, 0);
|
||||||
|
# Stop playing the alarm
|
||||||
|
- media_player.stop:
|
||||||
|
announcement: true
|
||||||
|
on_turn_on:
|
||||||
|
# Turn on the repeat mode and pause for 1000 ms between playlist items/repeats
|
||||||
|
- lambda: |-
|
||||||
|
id(echo_media_player)
|
||||||
|
->make_call()
|
||||||
|
.set_command(media_player::MediaPlayerCommand::MEDIA_PLAYER_COMMAND_REPEAT_ONE)
|
||||||
|
.set_announcement(true)
|
||||||
|
.perform();
|
||||||
|
id(echo_media_player)->set_playlist_delay_ms(speaker::AudioPipelineType::ANNOUNCEMENT, 1000);
|
||||||
|
- media_player.speaker.play_on_device_media_file:
|
||||||
|
media_file: timer_finished_wave_file
|
||||||
|
announcement: true
|
||||||
|
- delay: 15min
|
||||||
|
- switch.turn_off: timer_ringing
|
||||||
|
|
||||||
|
select:
|
||||||
|
- platform: template
|
||||||
|
entity_category: config
|
||||||
|
name: Wake word engine location
|
||||||
|
id: wake_word_engine_location
|
||||||
|
optimistic: true
|
||||||
|
restore_value: true
|
||||||
|
options:
|
||||||
|
- In Home Assistant
|
||||||
|
- On device
|
||||||
|
initial_option: On device
|
||||||
|
on_value:
|
||||||
|
- if:
|
||||||
|
condition:
|
||||||
|
lambda: return x == "In Home Assistant";
|
||||||
|
then:
|
||||||
|
- micro_wake_word.stop
|
||||||
|
- delay: 500ms
|
||||||
|
- lambda: id(va).set_use_wake_word(true);
|
||||||
|
- voice_assistant.start_continuous:
|
||||||
|
- if:
|
||||||
|
condition:
|
||||||
|
lambda: return x == "On device";
|
||||||
|
then:
|
||||||
|
- lambda: id(va).set_use_wake_word(false);
|
||||||
|
- voice_assistant.stop
|
||||||
|
- delay: 500ms
|
||||||
|
- micro_wake_word.start
|
||||||
|
|
||||||
|
micro_wake_word:
|
||||||
|
on_wake_word_detected:
|
||||||
|
- voice_assistant.start:
|
||||||
|
wake_word: !lambda return wake_word;
|
||||||
|
vad:
|
||||||
|
models:
|
||||||
|
- model: ${micro_wake_word_model}
|
||||||
|
|||||||
@@ -1,208 +0,0 @@
|
|||||||
substitutions:
|
|
||||||
name: "raspiaudio-muse-luxe"
|
|
||||||
friendly_name: "RaspiAudio Muse Luxe"
|
|
||||||
|
|
||||||
# Enable Home Assistant API
|
|
||||||
api:
|
|
||||||
encryption:
|
|
||||||
key: !secret apikey
|
|
||||||
ota:
|
|
||||||
password: !secret ota
|
|
||||||
|
|
||||||
wifi:
|
|
||||||
ssid: Voltage-legacy
|
|
||||||
password: !secret voltage_legacy_psk
|
|
||||||
use_address: raspiaudio-muse-luxe.home
|
|
||||||
power_save_mode: high
|
|
||||||
fast_connect: on
|
|
||||||
# Enable fallback hotspot (captive portal) in case wifi connection fails
|
|
||||||
ap:
|
|
||||||
ssid: "Raspiaudio Fallback Hotspot"
|
|
||||||
password: !secret fallback_psk
|
|
||||||
|
|
||||||
esphome:
|
|
||||||
name: ${name}
|
|
||||||
friendly_name: ${friendly_name}
|
|
||||||
name_add_mac_suffix: false
|
|
||||||
project:
|
|
||||||
name: raspiaudio.muse-luxe
|
|
||||||
version: "1.0"
|
|
||||||
min_version: 2023.5.0
|
|
||||||
on_boot:
|
|
||||||
then:
|
|
||||||
- media_player.volume_set:
|
|
||||||
id: luxe_out
|
|
||||||
volume: 50%
|
|
||||||
|
|
||||||
esp32:
|
|
||||||
board: esp-wrover-kit
|
|
||||||
framework:
|
|
||||||
type: arduino
|
|
||||||
|
|
||||||
logger:
|
|
||||||
|
|
||||||
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:
|
|
||||||
number: GPIO21
|
|
||||||
inverted: true
|
|
||||||
|
|
||||||
speaker:
|
|
||||||
- platform: i2s_audio
|
|
||||||
id: luxe_out_speaker
|
|
||||||
dac_type: external
|
|
||||||
i2s_dout_pin: GPIO26
|
|
||||||
mode: stereo
|
|
||||||
|
|
||||||
microphone:
|
|
||||||
- platform: i2s_audio
|
|
||||||
id: luxe_microphone
|
|
||||||
i2s_din_pin: GPIO35
|
|
||||||
adc_type: external
|
|
||||||
pdm: false
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
sensor:
|
|
||||||
- platform: adc
|
|
||||||
pin: GPIO33
|
|
||||||
name: Battery
|
|
||||||
icon: "mdi:battery-outline"
|
|
||||||
device_class: voltage
|
|
||||||
state_class: measurement
|
|
||||||
entity_category: diagnostic
|
|
||||||
unit_of_measurement: V
|
|
||||||
update_interval: 15s
|
|
||||||
accuracy_decimals: 3
|
|
||||||
attenuation: 11db
|
|
||||||
raw: true
|
|
||||||
filters:
|
|
||||||
- multiply: 0.00173913 # 2300 -> 4, for attenuation 11db, based on Olivier's code
|
|
||||||
- exponential_moving_average:
|
|
||||||
alpha: 0.2
|
|
||||||
send_every: 2
|
|
||||||
- delta: 0.002
|
|
||||||
|
|
||||||
binary_sensor:
|
|
||||||
- platform: gpio
|
|
||||||
pin:
|
|
||||||
number: GPIO19
|
|
||||||
inverted: true
|
|
||||||
mode:
|
|
||||||
input: true
|
|
||||||
pullup: true
|
|
||||||
name: Volume Up
|
|
||||||
on_click:
|
|
||||||
- media_player.volume_up: luxe_out
|
|
||||||
- platform: gpio
|
|
||||||
pin:
|
|
||||||
number: GPIO32
|
|
||||||
inverted: true
|
|
||||||
mode:
|
|
||||||
input: true
|
|
||||||
pullup: true
|
|
||||||
name: Volume Down
|
|
||||||
on_click:
|
|
||||||
- media_player.volume_down: luxe_out
|
|
||||||
- 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:
|
|
||||||
|
|
||||||
light:
|
|
||||||
- platform: fastled_clockless
|
|
||||||
name: None
|
|
||||||
id: top_led
|
|
||||||
pin: GPIO22
|
|
||||||
chipset: SK6812
|
|
||||||
num_leds: 1
|
|
||||||
rgb_order: grb
|
|
||||||
gamma_correct: 2.8
|
|
||||||
effects:
|
|
||||||
- pulse:
|
|
||||||
transition_length: 250ms
|
|
||||||
update_interval: 250ms
|
|
||||||
@@ -0,0 +1,510 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
network:
|
||||||
|
enable_ipv6: true
|
||||||
|
|
||||||
|
wifi:
|
||||||
|
ssid: "Voltage-legacy"
|
||||||
|
password: !secret voltage_legacy_psk
|
||||||
|
#use_address: riden-labornetzteil-18a.home
|
||||||
|
power_save_mode: high
|
||||||
|
fast_connect: on
|
||||||
|
min_auth_mode: WPA2
|
||||||
|
|
||||||
|
# Enable fallback hotspot (captive portal) in case wifi connection fails
|
||||||
|
ap:
|
||||||
|
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"
|
||||||
@@ -0,0 +1,510 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
network:
|
||||||
|
enable_ipv6: true
|
||||||
|
|
||||||
|
wifi:
|
||||||
|
ssid: "Voltage-legacy"
|
||||||
|
password: !secret voltage_legacy_psk
|
||||||
|
#use_address: riden-labornetzteil-6a.home
|
||||||
|
power_save_mode: high
|
||||||
|
fast_connect: on
|
||||||
|
min_auth_mode: WPA2
|
||||||
|
|
||||||
|
# Enable fallback hotspot (captive portal) in case wifi connection fails
|
||||||
|
ap:
|
||||||
|
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"
|
||||||
@@ -1,14 +1,31 @@
|
|||||||
|
substitutions:
|
||||||
|
name: "serverroom"
|
||||||
|
friendly_name: "Serverraum Luft"
|
||||||
|
|
||||||
esphome:
|
esphome:
|
||||||
name: serverroom
|
name: ${name}
|
||||||
platform: ESP32
|
friendly_name: ${friendly_name}
|
||||||
board: nodemcu-32s
|
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: esp-idf
|
||||||
|
|
||||||
|
network:
|
||||||
|
enable_ipv6: true
|
||||||
|
|
||||||
wifi:
|
wifi:
|
||||||
ssid: "Voltage-legacy"
|
ssid: "Voltage-legacy"
|
||||||
password: !secret voltage_legacy_psk
|
password: !secret voltage_legacy_psk
|
||||||
use_address: serverroom.home
|
#use_address: serverroom.home
|
||||||
power_save_mode: high
|
power_save_mode: high
|
||||||
fast_connect: on
|
fast_connect: on
|
||||||
|
min_auth_mode: WPA2
|
||||||
|
|
||||||
# Enable fallback hotspot (captive portal) in case wifi connection fails
|
# Enable fallback hotspot (captive portal) in case wifi connection fails
|
||||||
ap:
|
ap:
|
||||||
@@ -22,23 +39,23 @@ logger:
|
|||||||
|
|
||||||
# Enable Home Assistant API
|
# Enable Home Assistant API
|
||||||
api:
|
api:
|
||||||
password: !secret api
|
|
||||||
encryption:
|
encryption:
|
||||||
key: !secret apikey
|
key: !secret apikey
|
||||||
|
|
||||||
ota:
|
ota:
|
||||||
|
platform: esphome
|
||||||
password: !secret ota
|
password: !secret ota
|
||||||
|
|
||||||
# Initialize I²C
|
# Initialize I²C
|
||||||
i2c:
|
i2c:
|
||||||
- id: bus_a
|
- id: bus_a
|
||||||
sda: 32
|
sda: 4
|
||||||
scl: 25
|
scl: 3
|
||||||
scan: true
|
scan: true
|
||||||
|
|
||||||
# DHT22 sensor
|
# BMP280 sensor
|
||||||
sensor:
|
sensor:
|
||||||
- platform: bmp280
|
- platform: bmp280_i2c
|
||||||
i2c_id: bus_a
|
i2c_id: bus_a
|
||||||
temperature:
|
temperature:
|
||||||
name: "Serveraum Temperatur"
|
name: "Serveraum Temperatur"
|
||||||
|
|||||||
@@ -1,321 +0,0 @@
|
|||||||
##############################################################################
|
|
||||||
# Globals Glances parameters
|
|
||||||
##############################################################################
|
|
||||||
|
|
||||||
[global]
|
|
||||||
# Does Glances should check if a newer version is available on PyPI ?
|
|
||||||
check_update=false
|
|
||||||
# History size (maximum number of values)
|
|
||||||
# Default glances history size to 0
|
|
||||||
# https://github.com/hassio-addons/addon-glances/issues/61
|
|
||||||
# https://github.com/nicolargo/glances/issues/1341
|
|
||||||
history_size=0
|
|
||||||
|
|
||||||
##############################################################################
|
|
||||||
# User interface
|
|
||||||
##############################################################################
|
|
||||||
|
|
||||||
[outputs]
|
|
||||||
# Theme name for the Curses interface: black or white
|
|
||||||
curse_theme=black
|
|
||||||
# Limit the number of processes to display in the WebUI
|
|
||||||
max_processes_display=30
|
|
||||||
|
|
||||||
##############################################################################
|
|
||||||
# plugins
|
|
||||||
##############################################################################
|
|
||||||
|
|
||||||
[quicklook]
|
|
||||||
# Define CPU, MEM and SWAP thresholds in %
|
|
||||||
cpu_careful=50
|
|
||||||
cpu_warning=70
|
|
||||||
cpu_critical=90
|
|
||||||
mem_careful=50
|
|
||||||
mem_warning=70
|
|
||||||
mem_critical=90
|
|
||||||
swap_careful=50
|
|
||||||
swap_warning=70
|
|
||||||
swap_critical=90
|
|
||||||
|
|
||||||
[cpu]
|
|
||||||
# Default values if not defined: 50/70/90 (except for iowait)
|
|
||||||
user_careful=50
|
|
||||||
user_warning=70
|
|
||||||
user_critical=90
|
|
||||||
#user_log=False
|
|
||||||
#user_critical_action=echo {{user}} {{value}} {{max}} > /tmp/cpu.alert
|
|
||||||
system_careful=50
|
|
||||||
system_warning=70
|
|
||||||
system_critical=90
|
|
||||||
steal_careful=50
|
|
||||||
steal_warning=70
|
|
||||||
steal_critical=90
|
|
||||||
#steal_log=True
|
|
||||||
# I/O wait percentage should be lower than 1/# (Logical CPU cores)
|
|
||||||
# Leave commented to just use the default config (1/#-20% / 1/#-10% / 1/#)
|
|
||||||
#iowait_careful=30
|
|
||||||
#iowait_warning=40
|
|
||||||
#iowait_critical=50
|
|
||||||
# Context switch limit (core / second)
|
|
||||||
# Leave commented to just use the default config (critical is 50000*# (Logical CPU cores)
|
|
||||||
#ctx_switches_careful=10000
|
|
||||||
#ctx_switches_warning=12000
|
|
||||||
#ctx_switches_critical=14000
|
|
||||||
|
|
||||||
[percpu]
|
|
||||||
# Define CPU thresholds in %
|
|
||||||
# Default values if not defined: 50/70/90
|
|
||||||
user_careful=50
|
|
||||||
user_warning=70
|
|
||||||
user_critical=90
|
|
||||||
iowait_careful=50
|
|
||||||
iowait_warning=70
|
|
||||||
iowait_critical=90
|
|
||||||
system_careful=50
|
|
||||||
system_warning=70
|
|
||||||
system_critical=90
|
|
||||||
|
|
||||||
[gpu]
|
|
||||||
# Default processor values if not defined: 50/70/90
|
|
||||||
proc_careful=50
|
|
||||||
proc_warning=70
|
|
||||||
proc_critical=90
|
|
||||||
# Default memory values if not defined: 50/70/90
|
|
||||||
mem_careful=50
|
|
||||||
mem_warning=70
|
|
||||||
mem_critical=90
|
|
||||||
|
|
||||||
[mem]
|
|
||||||
# Define RAM thresholds in %
|
|
||||||
# Default values if not defined: 50/70/90
|
|
||||||
careful=50
|
|
||||||
#careful_action_repeat=echo {{percent}} >> /tmp/memory.alert
|
|
||||||
warning=70
|
|
||||||
critical=90
|
|
||||||
|
|
||||||
[memswap]
|
|
||||||
# Define SWAP thresholds in %
|
|
||||||
# Default values if not defined: 50/70/90
|
|
||||||
careful=50
|
|
||||||
warning=70
|
|
||||||
critical=90
|
|
||||||
|
|
||||||
[load]
|
|
||||||
# Define LOAD thresholds
|
|
||||||
# Value * number of cores
|
|
||||||
# Default values if not defined: 0.7/1.0/5.0 per number of cores
|
|
||||||
# Source: http://blog.scoutapp.com/articles/2009/07/31/understanding-load-averages
|
|
||||||
# http://www.linuxjournal.com/article/9001
|
|
||||||
careful=0.7
|
|
||||||
warning=1.0
|
|
||||||
critical=5.0
|
|
||||||
#log=False
|
|
||||||
|
|
||||||
[network]
|
|
||||||
# Default bitrate thresholds in % of the network interface speed
|
|
||||||
# Default values if not defined: 70/80/90
|
|
||||||
rx_careful=70
|
|
||||||
rx_warning=80
|
|
||||||
rx_critical=90
|
|
||||||
tx_careful=70
|
|
||||||
tx_warning=80
|
|
||||||
tx_critical=90
|
|
||||||
# Define the list of hidden network interfaces (comma-separated regexp)
|
|
||||||
#hide=docker.*,lo
|
|
||||||
# WLAN 0 alias
|
|
||||||
#wlan0_alias=Wireless IF
|
|
||||||
# It is possible to overwrite the bitrate thresholds per interface
|
|
||||||
# WLAN 0 Default limits (in bits per second aka bps) for interface bitrate
|
|
||||||
#wlan0_rx_careful=4000000
|
|
||||||
#wlan0_rx_warning=5000000
|
|
||||||
#wlan0_rx_critical=6000000
|
|
||||||
#wlan0_rx_log=True
|
|
||||||
#wlan0_tx_careful=700000
|
|
||||||
#wlan0_tx_warning=900000
|
|
||||||
#wlan0_tx_critical=1000000
|
|
||||||
#wlan0_tx_log=True
|
|
||||||
|
|
||||||
[wifi]
|
|
||||||
# Define the list of hidden wireless network interfaces (comma-separated regexp)
|
|
||||||
hide=lo,docker.*
|
|
||||||
# Define SIGNAL thresholds in db (lower is better...)
|
|
||||||
# Based on: http://serverfault.com/questions/501025/industry-standard-for-minimum-wifi-signal-strength
|
|
||||||
careful=-65
|
|
||||||
warning=-75
|
|
||||||
critical=-85
|
|
||||||
|
|
||||||
[diskio]
|
|
||||||
# Define the list of hidden disks (comma-separated regexp)
|
|
||||||
#hide=sda2,sda5,loop.*
|
|
||||||
hide=loop.*
|
|
||||||
# Alias for sda1
|
|
||||||
#sda1_alias=IntDisk
|
|
||||||
|
|
||||||
[fs]
|
|
||||||
# Define the list of hidden file system (comma-separated regexp)
|
|
||||||
#hide=/boot.*
|
|
||||||
# Define filesystem space thresholds in %
|
|
||||||
# Default values if not defined: 50/70/90
|
|
||||||
# It is also possible to define per mount point value
|
|
||||||
# Example: /_careful=40
|
|
||||||
careful=50
|
|
||||||
warning=70
|
|
||||||
critical=90
|
|
||||||
# Allow additional file system types (comma-separated FS type)
|
|
||||||
#allow=zfs
|
|
||||||
|
|
||||||
[folders]
|
|
||||||
# Define a folder list to monitor
|
|
||||||
# The list is composed of items (list_#nb <= 10)
|
|
||||||
# An item is defined by:
|
|
||||||
# * path: absolute path
|
|
||||||
# * careful: optional careful threshold (in MB)
|
|
||||||
# * warning: optional warning threshold (in MB)
|
|
||||||
# * critical: optional critical threshold (in MB)
|
|
||||||
folder_1_path=/config
|
|
||||||
folder_1_careful=4096
|
|
||||||
folder_1_warning=8196
|
|
||||||
folder_1_critical=12228
|
|
||||||
folder_2_path=/share
|
|
||||||
folder_2_careful=4096
|
|
||||||
folder_2_warning=8196
|
|
||||||
folder_2_critical=12228
|
|
||||||
folder_3_path=/backup
|
|
||||||
folder_3_careful=4096
|
|
||||||
folder_3_warning=8196
|
|
||||||
folder_3_critical=12228
|
|
||||||
folder_4_path=/media
|
|
||||||
folder_4_careful=4096
|
|
||||||
folder_4_warning=8196
|
|
||||||
folder_4_critical=12228
|
|
||||||
|
|
||||||
[sensors]
|
|
||||||
# Sensors core thresholds (in Celsius...)
|
|
||||||
# Default values if not defined: 60/70/80
|
|
||||||
temperature_core_careful=60
|
|
||||||
temperature_core_warning=70
|
|
||||||
temperature_core_critical=80
|
|
||||||
# Temperatures threshold in °C for hddtemp
|
|
||||||
# Default values if not defined: 45/52/60
|
|
||||||
temperature_hdd_careful=45
|
|
||||||
temperature_hdd_warning=52
|
|
||||||
temperature_hdd_critical=60
|
|
||||||
# Battery threshold in %
|
|
||||||
battery_careful=80
|
|
||||||
battery_warning=90
|
|
||||||
battery_critical=95
|
|
||||||
# Sensors alias
|
|
||||||
#temp1_alias=Motherboard 0
|
|
||||||
#temp2_alias=Motherboard 1
|
|
||||||
#core 0_alias=CPU Core 0
|
|
||||||
#core 1_alias=CPU Core 1
|
|
||||||
|
|
||||||
[processlist]
|
|
||||||
# Define CPU/MEM (per process) thresholds in %
|
|
||||||
# Default values if not defined: 50/70/90
|
|
||||||
cpu_careful=50
|
|
||||||
cpu_warning=70
|
|
||||||
cpu_critical=90
|
|
||||||
mem_careful=50
|
|
||||||
mem_warning=70
|
|
||||||
mem_critical=90
|
|
||||||
#
|
|
||||||
# Nice priorities range from -20 to 19.
|
|
||||||
# Configure nice levels using a comma separated list.
|
|
||||||
#
|
|
||||||
# Nice: Example 1, non-zero is warning (default behavior)
|
|
||||||
nice_warning=-20,-19,-18,-17,-16,-15,-14,-13,-12,-11,-10,-9,-8,-7,-6,-5,-4,-3,-2,-1,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19
|
|
||||||
#
|
|
||||||
# Nice: Example 2, low priority processes escalate from careful to critical
|
|
||||||
#nice_careful=1,2,3,4,5,6,7,8,9
|
|
||||||
#nice_warning=10,11,12,13,14
|
|
||||||
#nice_critical=15,16,17,18,19
|
|
||||||
|
|
||||||
[ports]
|
|
||||||
# Ports scanner plugin configuration
|
|
||||||
# Interval in second between two scans
|
|
||||||
refresh=30
|
|
||||||
# Set the default timeout (in second) for a scan (can be overwritten in the scan list)
|
|
||||||
timeout=3
|
|
||||||
# If port_default_gateway is True, add the default gateway on top of the scan list
|
|
||||||
port_default_gateway=True
|
|
||||||
#
|
|
||||||
# Define the scan list (1 < x < 255)
|
|
||||||
# port_x_host (name or IP) is mandatory
|
|
||||||
# port_x_port (TCP port number) is optional (if not set, use ICMP)
|
|
||||||
# port_x_description is optional (if not set, define to host:port)
|
|
||||||
# port_x_timeout is optional and overwrite the default timeout value
|
|
||||||
# port_x_rtt_warning is optional and defines the warning threshold in ms
|
|
||||||
#
|
|
||||||
#port_1_host=192.168.0.1
|
|
||||||
#port_1_port=80
|
|
||||||
#port_1_description=Home Box
|
|
||||||
#port_1_timeout=1
|
|
||||||
#port_2_host=www.free.fr
|
|
||||||
#port_2_description=My ISP
|
|
||||||
#port_3_host=www.google.com
|
|
||||||
#port_3_description=Internet ICMP
|
|
||||||
#port_3_rtt_warning=1000
|
|
||||||
#port_4_description=Internet Web
|
|
||||||
#port_4_host=www.google.com
|
|
||||||
#port_4_port=80
|
|
||||||
#port_4_rtt_warning=1000
|
|
||||||
#
|
|
||||||
# Define Web (URL) monitoring list (1 < x < 255)
|
|
||||||
# web_x_url is the URL to monitor (example: http://my.site.com/folder)
|
|
||||||
# web_x_description is optional (if not set, define to URL)
|
|
||||||
# web_x_timeout is optional and overwrite the default timeout value
|
|
||||||
# web_x_rtt_warning is optional and defines the warning respond time in ms (approximatively)
|
|
||||||
#
|
|
||||||
#web_1_url=https://blog.nicolargo.com
|
|
||||||
#web_1_description=My Blog
|
|
||||||
#web_1_rtt_warning=3000
|
|
||||||
#web_2_url=https://github.com
|
|
||||||
#web_3_url=http://www.google.fr
|
|
||||||
#web_3_description=Google Fr
|
|
||||||
#web_4_url=https://blog.nicolargo.com/nonexist
|
|
||||||
#web_4_description=Intranet
|
|
||||||
|
|
||||||
[docker]
|
|
||||||
# Thresholds for CPU and MEM (in %)
|
|
||||||
cpu_careful=50
|
|
||||||
cpu_warning=70
|
|
||||||
cpu_critical=90
|
|
||||||
mem_careful=20
|
|
||||||
mem_warning=50
|
|
||||||
mem_critical=70
|
|
||||||
#
|
|
||||||
# Per container thresholds
|
|
||||||
#containername_cpu_careful=10
|
|
||||||
#containername_cpu_warning=20
|
|
||||||
#containername_cpu_critical=30
|
|
||||||
#
|
|
||||||
# By default, Glances only display running containers
|
|
||||||
# Set the following key to True to display all containers
|
|
||||||
all=False
|
|
||||||
|
|
||||||
##############################################################################
|
|
||||||
# Client/server
|
|
||||||
##############################################################################
|
|
||||||
|
|
||||||
[serverlist]
|
|
||||||
# Define the static servers list
|
|
||||||
#server_1_name=localhost
|
|
||||||
#server_1_alias=My local PC
|
|
||||||
#server_1_port=61209
|
|
||||||
#server_2_name=localhost
|
|
||||||
#server_2_port=61235
|
|
||||||
#server_3_name=192.168.0.17
|
|
||||||
#server_3_alias=Another PC on my network
|
|
||||||
#server_3_port=61209
|
|
||||||
#server_4_name=pasbon
|
|
||||||
#server_4_port=61237
|
|
||||||
|
|
||||||
[passwords]
|
|
||||||
# Define the passwords list
|
|
||||||
# Syntax: host=password
|
|
||||||
# Where: host is the hostname
|
|
||||||
# password is the clear password
|
|
||||||
# Additionally (and optionally) a default password could be defined
|
|
||||||
#localhost=abc
|
|
||||||
#default=defaultpassword
|
|
||||||
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 623 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 685 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 373 KiB |
|
After Width: | Height: | Size: 1.2 MiB |