4 | Awtrix Light Display
5 |
21 |
22 |
23 |
24 |
25 |
109 |
110 |
111 |
--------------------------------------------------------------------------------
/blueprints/automation/awtrix_hvac.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | blueprint:
3 | name: AWTRIX HVAC 🥵 🌡️ 🥶
4 | description: >
5 | Monitor the status of your HVAC system with Awtrix
6 |
7 |
8 | This automation requires two icons to exist (you can select the name below).
9 |
10 |
11 | If you want to use the icons I developed run:
12 |
13 | 
14 |
15 | bash -c "$(curl -fsSL https://raw.githubusercontent.com/jeeftor/HomeAssistant/master/icons/upload_icon.sh)"
16 |
17 | And select the IP_ADDRESS of your awtrix device and `hvac` as the icon set to upload
18 |
19 | domain: automation
20 | input:
21 | awtrix:
22 | name: AWTRIX Device
23 | description: Select the Awtrix light
24 | selector:
25 | device:
26 | filter:
27 | integration: mqtt
28 | manufacturer: Blueforcer
29 | model: AWTRIX 3
30 | multiple: true
31 | hvac:
32 | name: Climate Device / HVAC
33 | description: HVAC Device (Ecobee, Nest etc)
34 | selector:
35 | entity:
36 | filter:
37 | domain: climate
38 | heat_icon:
39 | name: Heating icon name
40 | selector:
41 | text:
42 | default: heat
43 | cool_icon:
44 | name: Cooling icon name
45 | selector:
46 | text:
47 | default: cool
48 | app_name:
49 | name: Awtrix Applicaiton name
50 | description: This is the app name listed in the MQTT topic - it should be unique
51 | selector:
52 | text:
53 | default: jeef_hvac
54 |
55 | mode: restart
56 | variables:
57 | device_ids: !input awtrix
58 | app_name: !input app_name
59 | devices_topics: >-
60 | {%- macro get_device_topic(device_id) %}
61 | {{- states((device_entities(device_id) | select('search','device_topic') | list)[0]) }}
62 | {%- endmacro %}
63 |
64 | {%- set ns = namespace(devices=[]) %}
65 | {%- for device_id in device_ids %}
66 | {%- set device=get_device_topic(device_id)|replace(' ','') %}
67 | {% set ns.devices = ns.devices + [ device ~ '/custom/' ~ app_name] %}
68 | {%- endfor %}
69 | {{ ns.devices | reject('match','unavailable') | list}}
70 |
71 | climate: !input hvac
72 | mode: "{{states(climate)}}"
73 | target_temp: "{{ iif(state_attr('climate.downstairs', 'temperature'),state_attr('climate.downstairs', 'temperature'),0,state_attr('climate.downstairs', 'current_temperature')) | round(0)}}"
74 | temp_current: "{{state_attr(climate, 'current_temperature')}}"
75 | temp_current_ceil: "{{ temp_current | round(0,'ceil') }}"
76 | temp_current_floor: "{{ temp_current | round(0,'floor') }}"
77 |
78 | is_running: "{{ not state_attr('climate.downstairs', 'temperature') is none }}"
79 | is_ac_on: "{{ mode == 'cool' and (target_temp < temp_current) }}"
80 | is_heat_on: "{{ mode == 'heat' and (target_temp > temp_current) }}"
81 |
82 | payload: >-
83 | {%- if not is_running %}
84 | {}
85 | {%- elif mode == 'heat' and is_heat_on%}
86 | { "icon": "heat",
87 | "text": [
88 | {"t":"{{temp_current_floor}}", "c":"#ffffff"},
89 | {"t":">","c":"#9c9d97"},
90 | {"t":"{{target_temp}}", "c":"#ffffff"}
91 | ] }
92 | {% elif mode == 'cool' and is_ac_on %}
93 | { "icon": "cool",
94 | "text": [
95 | {"t":"{{temp_current_ceil}}", "c":"#ffffff"},
96 | {"t":">","c":"#9c9d97"},
97 | {"t":"{{target_temp}}", "c":"#ffffff"}
98 | ] }
99 | {% else %}
100 | {}
101 | {% endif %}
102 |
103 | trigger:
104 | - trigger: time_pattern
105 | seconds: /5
106 |
107 | condition: []
108 | action:
109 | - repeat:
110 | for_each: "{{ devices_topics }}"
111 | sequence:
112 | - action: mqtt.publish
113 | data:
114 | qos: 0
115 | retain: false
116 | topic: "{{ repeat.item }}"
117 | payload: >
118 | {{payload}}
119 |
--------------------------------------------------------------------------------
/icons/upload_icon.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e
3 |
4 | # Colors
5 | RED='\033[0;31m'
6 | GREEN='\033[0;32m'
7 | YELLOW='\033[0;33m'
8 | NC='\033[0m' # No Color
9 |
10 | # Check if 'jq' is installed
11 | check_jq() {
12 | if ! command -v jq >/dev/null 2>&1; then
13 | echo -e "${RED}Error: 'jq' is not installed. Please install 'jq' to run this script.${NC}"
14 | exit 1
15 | fi
16 | }
17 |
18 | # List icon directories
19 | list_icon_directories() {
20 | local OWNER="jeeftor"
21 | local REPO="HomeAssistant"
22 | local BRANCH="master"
23 | local DIRECTORY="icons"
24 |
25 | # Make the API request to list the directory contents
26 | local response
27 | response=$(curl -s "https://api.github.com/repos/$OWNER/$REPO/contents/$DIRECTORY?ref=$BRANCH")
28 |
29 | # Extract directory names
30 | local directories=($(echo "$response" | jq -r '.[] | select(.type == "dir") | .name'))
31 |
32 | # Return the list of directories
33 | echo "${directories[@]}"
34 | }
35 |
36 | # List icons within a directory
37 | list_icons() {
38 | local OWNER="jeeftor"
39 | local REPO="HomeAssistant"
40 | local BRANCH="master"
41 | local DIRECTORY="icons/$1"
42 |
43 | # Make the API request to list the directory contents
44 | local response
45 | response=$(curl -L -s "https://api.github.com/repos/$OWNER/$REPO/contents/$DIRECTORY?ref=$BRANCH")
46 |
47 | # Extract URLs of files with the .gif extension
48 | local icons=($(echo "$response" | jq -r '.[] | select(.type == "file" and (.name | test("\\.gif$"))) | .download_url'))
49 |
50 | # Return the list of icons
51 | echo "${icons[@]}"
52 | }
53 |
54 | # Verify if a file is a valid GIF
55 | verify_gif() {
56 | local FILE_NAME="$1"
57 |
58 | if file -b --mime-type "$FILE_NAME" | grep -q '^image/gif$'; then
59 | # File is a valid GIF
60 | return 0
61 | else
62 | echo -e "${RED}Error: File $FILE_NAME is NOT a valid GIF file.${NC}"
63 | return 1
64 | fi
65 | }
66 |
67 | # Upload an icon to a clock device
68 | upload_icon() {
69 | local IP_ADDRESS="$1"
70 | local ICON_NAME="$2"
71 | local FILE_NAME="$3"
72 |
73 | URL="http://$IP_ADDRESS/edit"
74 | TEMP_FILE=".$FILE_NAME"
75 |
76 | BASE_URL="https://raw.githubusercontent.com/jeeftor/HomeAssistant/master/icons/"
77 | GIF_FILE="$BASE_URL/$ICON_NAME"
78 |
79 | curl -L -s -X GET "$GIF_FILE" -o "$TEMP_FILE"
80 |
81 | if verify_gif "$TEMP_FILE"; then
82 | curl -X POST -F "file=@$TEMP_FILE;filename=/ICONS/$FILE_NAME" "$URL"
83 | echo -e "${GREEN}Uploaded icon:${NC} $FILE_NAME${NC}"
84 | else
85 | echo -e "${RED}Error: File $FILE_NAME does not appear to be a valid GIF file.${NC}"
86 | echo -e "${RED}Try yourself with:${NC} curl -L -s -X GET \"$GIF_FILE\" -o $TEMP_FILE"
87 | fi
88 |
89 | rm -f "$TEMP_FILE"
90 | }
91 |
92 | # Prompt for IP address if not provided as a command-line argument
93 | prompt_ip_address() {
94 | if [ -z "$1" ]; then
95 | read -rp "Enter the IP address: " IP_ADDRESS
96 | else
97 | IP_ADDRESS="$1"
98 | fi
99 |
100 | # Validate IP address format
101 | if ! [[ $IP_ADDRESS =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
102 | echo -e "${RED}Error: Invalid IP address format.${NC}"
103 | exit 1
104 | fi
105 | }
106 |
107 | # Main script logic
108 | main() {
109 | # Check if 'jq' is installed
110 | check_jq
111 |
112 | # Prompt for IP address
113 | prompt_ip_address "$1"
114 |
115 | # List icon directories
116 | echo -e "${GREEN}Available icon directories:${NC}"
117 | directories=($(list_icon_directories))
118 |
119 | # Prompt for directory selection
120 | PS3="Select a directory: "
121 | select DIRECTORY_NAME in "${directories[@]}"; do
122 | if [[ -n $DIRECTORY_NAME ]]; then
123 | break
124 | else
125 | echo -e "${YELLOW}Invalid selection. Please try again.${NC}"
126 | fi
127 | done
128 |
129 | # Example usage
130 | ICONS=($(list_icons "$DIRECTORY_NAME"))
131 |
132 | echo -e "${YELLOW}Downloading icons...${NC}"
133 |
134 | for ICON_URL in "${ICONS[@]}"; do
135 | ICON_NAME=$(basename "$ICON_URL")
136 |
137 | upload_icon "$IP_ADDRESS" "$DIRECTORY_NAME/$ICON_NAME" "$ICON_NAME"
138 | done
139 | }
140 |
141 | # Execute the main script
142 | main "$@"
143 |
--------------------------------------------------------------------------------
/icons/dev/ansiPreview.py:
--------------------------------------------------------------------------------
1 | import requests
2 | import time
3 | from PIL import Image, ImageDraw
4 | from typing import List
5 | import asyncio
6 | import sys
7 | import os
8 |
9 |
10 | class ScreenCapture:
11 | def __init__(
12 | self,
13 | endpoint_url: str,
14 | width: int,
15 | height: int,
16 | initial_duration: int,
17 | ) -> None:
18 | self.endpoint_url = endpoint_url
19 | self.width = width
20 | self.height = height
21 | self.initial_duration = initial_duration
22 |
23 | async def capture_frame(self) -> None:
24 | """Capture a frame from the endpoint and display a live preview."""
25 | frame_count = 0
26 | while True:
27 | response = requests.get(self.endpoint_url)
28 |
29 | # Check if the request was successful
30 | if response.status_code == 200:
31 | # Get the RGB565 color values as a list
32 | rgb565_values = response.json()
33 |
34 | # Create a new PIL image of the original dimensions (32x8)
35 | image = Image.new("RGB", (self.width, self.height))
36 | draw = ImageDraw.Draw(image)
37 |
38 | # Set the color of each pixel in the image
39 | for y in range(self.height):
40 | for x in range(self.width):
41 | # Convert the decimal RGB565 value to RGB888
42 | rgb565 = rgb565_values[y * self.width + x]
43 | red = (rgb565 & 0xFF0000) >> 16
44 | green = (rgb565 & 0x00FF00) >> 8
45 | blue = rgb565 & 0x0000FF
46 |
47 | # Draw a pixel with the converted RGB value
48 | draw.point((x, y), fill=(red, green, blue))
49 |
50 | # Scale the image to the desired dimensions (256x64)
51 | scaled_image = image.resize(
52 | (self.width * 4, self.height * 4), resample=Image.NEAREST
53 | )
54 |
55 | # Print the frame count and live preview
56 | self.print_live_preview(frame_count, image)
57 |
58 | frame_count += 1
59 |
60 | await asyncio.sleep(0.05) # Delay between frame captures
61 |
62 | def print_live_preview(self, frame_count: int, image: Image.Image) -> None:
63 | """Print the live preview of the image, clearing the console screen."""
64 | os.system("cls" if os.name == "nt" else "clear")
65 | print(f"\033[32mFrames Shown: {frame_count}\033[0m")
66 | width, height = image.size
67 | for y in range(height):
68 | for x in range(width):
69 | r, g, b = image.getpixel((x, y))
70 | sys.stdout.write(f"\033[48;2;{r};{g};{b}m \033[0m")
71 | sys.stdout.write("\n")
72 | print("\033[32mctrl+c to exit\033[0m")
73 |
74 |
75 | async def capture_loop(screen_capture: ScreenCapture) -> None:
76 | await screen_capture.capture_frame()
77 |
78 |
79 | def main() -> None:
80 | import argparse
81 |
82 | parser = argparse.ArgumentParser(description="Awtrix Clock Screen Capture")
83 | parser.add_argument(
84 | "--ip",
85 | type=str,
86 | help="The IP address of your Awtrix Clock",
87 | )
88 |
89 | args = parser.parse_args()
90 |
91 | # Prompt user for the IP address if not provided through command line argument
92 | if args.ip is None:
93 | endpoint_ip = input("What is the IP for your Awtrix Clock: ")
94 | else:
95 | endpoint_ip = args.ip
96 |
97 | # Endpoint URL
98 | endpoint_url = f"http://{endpoint_ip}/api/screen"
99 |
100 | # Image dimensions
101 | width = 32
102 | height = 8
103 |
104 | # GIF parameters
105 | initial_duration = 50 # in milliseconds
106 |
107 | # Create ScreenCapture instance
108 | screen_capture = ScreenCapture(endpoint_url, width, height, initial_duration)
109 |
110 | # Create the event loop
111 | loop = asyncio.get_event_loop()
112 |
113 | # Run the capture loop
114 | try:
115 | loop.run_until_complete(capture_loop(screen_capture))
116 | except KeyboardInterrupt:
117 | pass
118 |
119 | # Close the event loop
120 | loop.close()
121 |
122 |
123 | if __name__ == "__main__":
124 | main()
125 |
--------------------------------------------------------------------------------
/blueprints/automation/awtrix_battery_monitor.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | blueprint:
3 | name: AWTRIX 📱️ Mobile App - Device 🔋️ Battery Monitor 🪫️
4 | description: >
5 | This blueprint will print out the battery status of a device available to home assistant.
6 | It uses a custom icon set you need to install.
7 |
8 | You can find all the icons here: https://github.com/jeeftor/HomeAssistant/tree/master/icons/phone
9 |
10 | ### On Battery Icons
11 |
12 | 
13 |
14 | ### Charging Icons
15 |
16 | 
17 |
18 | domain: automation
19 | input:
20 | awtrix:
21 | name: AWTRIX Device
22 | description: Select the Awtrix light
23 | selector:
24 | device:
25 | filter:
26 | integration: mqtt
27 | manufacturer: Blueforcer
28 | model: AWTRIX 3
29 | multiple: true
30 | app_name:
31 | name: Awtrix Applicaiton name
32 | description: This is the app name listed in the MQTT topic - it should be unique
33 | selector:
34 | text:
35 | default: phone_battery
36 | battery:
37 | name: Phone or device
38 | description: A phone connected via the mobile app
39 | selector:
40 | entity:
41 | multiple: false
42 | filter:
43 | - integration: mobile_app
44 | device_class: battery
45 | # multiple: false
46 | message_text:
47 | name: Text to Display
48 | description: This is the text to dispally on the screen
49 | selector:
50 | text:
51 | default: iphone
52 | push_icon:
53 | name: Icon Mode
54 | description: >
55 | Please select the pushIcon setting for the icon
56 |
57 | - `0` Icon doesn't move
58 |
59 | - `1` Icon moves with text and will not appear again
60 |
61 | - `2` Icon moves with text but appears again when the text starts to scroll again
62 | selector:
63 | select:
64 | options:
65 | - label: Icon doesn't move (default)
66 | value: "0"
67 | - label: Icon moves with text and will not appear again
68 | value: "1"
69 | - label: Icon moves with text but appears again when the text starts to scroll again
70 | value: "2"
71 | show_below:
72 | name: Show Below x percent
73 | description: Only show the application on the clock when the battery level is below the specified percent
74 |
75 | selector:
76 | number:
77 | min: 1
78 | max: 101
79 | unit_of_measurement: "percent"
80 |
81 | default: 101
82 |
83 | mode: restart
84 | variables:
85 | device_ids: !input awtrix
86 | app_name: !input app_name
87 | show_below: !input show_below
88 | devices_topics: >-
89 | {%- macro get_device_topic(device_id) %}
90 | {{- states((device_entities(device_id) | select('search','device_topic') | list)[0]) }}
91 | {%- endmacro %}
92 |
93 | {%- set ns = namespace(devices=[]) %}
94 | {%- for device_id in device_ids %}
95 | {%- set device=get_device_topic(device_id)|replace(' ','') %}
96 | {% set ns.devices = ns.devices + [ device ~ '/custom/' ~ app_name] %}
97 | {%- endfor %}
98 | {{ ns.devices | reject('match','unavailable') | list}}
99 |
100 | battery_sensor: !input battery
101 | base_icon: "{{states[battery_sensor] }}"
102 | message_text: !input message_text
103 | push_icon: !input push_icon
104 | payload: >-
105 | {"icon":"{{ states[battery_sensor].attributes.icon
106 | | replace('mdi:','')
107 | | replace('90','80')
108 | | replace('70','60')
109 | | replace('50','40')
110 | | replace('30','20')}}",
111 | "text":"{{message_text}}",
112 | "pushIcon":{{push_icon}},
113 | "progress":"{{states[battery_sensor].state}}","pushIcon":1}
114 |
115 | trigger:
116 | - trigger: time_pattern
117 | minutes: /1
118 |
119 | condition: []
120 | action:
121 | - repeat:
122 | for_each: "{{ devices_topics }}"
123 | sequence:
124 | - action: mqtt.publish
125 | data:
126 | qos: 0
127 | retain: false
128 | topic: "{{ repeat.item }}"
129 | payload: >
130 | {{payload}}
131 |
--------------------------------------------------------------------------------
/blueprints/automation/state-monitor.yaml:
--------------------------------------------------------------------------------
1 | blueprint:
2 | name: A State Monitor with Actionable Notifications
3 | description: >
4 | Send notifications to a mobile app at regular intervals when a device reaches a specified state.
5 | Includes actionable notifications to close covers if the entity is a cover.
6 | Messages will arrive in the form of "[ENTITY NAME] is [Condition Name] for X hrs".
7 | domain: automation
8 | input:
9 | monitored_entity:
10 | name: Monitored Entity
11 | description: The entity to monitor for state changes
12 | selector:
13 | entity:
14 | target_state:
15 | name: Target State
16 | description: >
17 | Leave this blank to use the default. The default states are as follows:
18 | - `cover`: **`open`**
19 | - `binary_sensor`: **`on`**
20 | selector:
21 | text:
22 | default: ""
23 | message_state:
24 | name: Condition Name
25 | description: The text to include in notifications (e.g., "Open" or "Still Open")
26 | selector:
27 | text:
28 | default: "Open"
29 | notify_device:
30 | name: Notification Device
31 | description: The mobile app device to send notifications to
32 | selector:
33 | device:
34 | filter:
35 | integration: mobile_app
36 | interval_minutes:
37 | name: Notification Interval (Minutes)
38 | description: How often to send notifications while in target state
39 | default: 30
40 | selector:
41 | number:
42 | min: 1
43 | max: 180
44 | unit_of_measurement: minutes
45 |
46 | variables:
47 | interval_minutes: !input interval_minutes
48 | monitored_entity: !input monitored_entity
49 | notify_device: !input notify_device
50 | message_state: !input message_state
51 | entity_name: "{{ state_attr(monitored_entity, 'friendly_name') or monitored_entity.split('.')[1] | replace('_', ' ') | title }}"
52 | # Derive notify_service from device ID
53 | notify_service: >-
54 | {% set devices = device_entities(notify_device) | list %}
55 | notify.mobile_app_{{devices[0].split('.')[1]}}
56 | unique_tag: "{{ monitored_entity | replace('.', '_') }}_state_monitor"
57 | default_state: >-
58 | {% if monitored_entity.split('.')[0] == 'cover' %}
59 | open
60 | {% elif monitored_entity.split('.')[0] == 'binary_sensor' %}
61 | on
62 | {% else %}
63 | on
64 | {% endif %}
65 | ts: !input target_state
66 | target_state: >-
67 | {% if not ts or ts == '' %}
68 | {{ default_state | trim }}
69 | {% else %}
70 | {{ ts | trim }}
71 | {% endif %}
72 | is_cover: >-
73 | {% if monitored_entity.split('.')[0] == 'cover' %}
74 | true
75 | {% else %}
76 | false
77 | {% endif %}
78 | actions: >-
79 | {% if is_cover == 'true' %}
80 | [{"action": "CLOSE_{{ unique_tag }}",
81 | "title": "Close {{ entity_name }}",
82 | "service": "cover.close_cover",
83 | "service_data": {"entity_id": "{{ monitored_entity }}"}}]
84 | {% else %}
85 | []
86 | {% endif %}
87 |
88 | trigger:
89 | - trigger: state
90 | entity_id: !input monitored_entity
91 | - trigger: homeassistant
92 | event: start
93 | - trigger: event
94 | event_type: automation_reloaded
95 |
96 | action:
97 | - choose:
98 | - conditions:
99 | - condition: state
100 | entity_id: !input monitored_entity
101 | state: "{{ target_state }}"
102 | sequence:
103 | - variables:
104 | message: "{{ entity_name }} is now {{ message_state }}"
105 | - action: "{{ notify_service }}"
106 | data:
107 | title: "{{ entity_name }}"
108 | message: "{{ message }}"
109 | data:
110 | tag: "{{ unique_tag }}"
111 | actions: "{{ actions }}"
112 | - repeat:
113 | while:
114 | - condition: state
115 | entity_id: !input monitored_entity
116 | state: "{{ target_state }}"
117 | sequence:
118 | - delay:
119 | minutes: "{{ interval_minutes }}"
120 | - variables:
121 | last_changed_str: >-
122 | {{ relative_time(states[monitored_entity].last_changed | default(0))
123 | | replace('hours', 'hr')
124 | | replace('hour', 'hr')
125 | | replace('seconds', 'sec')
126 | | replace('minutes', 'min')
127 | | replace('minute', 'min') }}
128 | message: "{{ entity_name }} is {{ message_state }} for {{ last_changed_str }}"
129 | - action: "{{ notify_service }}"
130 | data:
131 | title: "{{ entity_name }}"
132 | message: "{{ message }}"
133 | data:
134 | tag: "{{ unique_tag }}"
135 | actions: "{{ actions }}"
136 | default:
137 | - action: "{{ notify_service }}"
138 | data:
139 | message: "clear_notification"
140 | data:
141 | tag: "{{ unique_tag }}"
142 |
143 | mode: restart
144 |
--------------------------------------------------------------------------------
/blueprints/automation/climate_alert.yaml:
--------------------------------------------------------------------------------
1 | blueprint:
2 | name: Climate Alert
3 | description: Notification
4 | domain: automation
5 | input:
6 | climate:
7 | name: Climate Entity
8 | selector:
9 | entity:
10 | filter:
11 | domain: climate
12 |
13 | # mode_heat:
14 | # name: Mode Heat
15 | # default: false
16 | # description: Run the automation when the heat is on
17 | # selector:
18 | # boolean:
19 | # mode_cool:
20 | # name: Mode Cool
21 | # default: false
22 | # description: Run the automation when the aircondition is on
23 | # selector:
24 | # boolean:
25 | # mode_heat_cool:
26 | # name: Mode Heat_cool
27 | # default: false
28 | # description: Run the automation when hvac mode is automatic (heat/cool)
29 | # selector:
30 | # boolean:
31 | temp:
32 | name: Trigger Temp
33 | description: Temp value to trigger the automation. In `heat` mode the target temp must be above this value to trigger the automation, where as in `cool` mode it must be below.
34 | default: 70
35 | selector:
36 | number:
37 | min: 32
38 | max: 100
39 | unit_of_measurement: "°"
40 | mode: box
41 | mode:
42 | description: "Select whether to trigger when Heating or Cooling"
43 | selector:
44 | select:
45 | options:
46 | - "heat"
47 | - "cool"
48 |
49 | notify_device:
50 | name: Device to notify
51 | description: Device needs to run the official Home Assistant app to receive notifications
52 | selector:
53 | device:
54 | filter:
55 | integration: mobile_app
56 |
57 | variables:
58 | mode: !input mode
59 | temp: !input temp
60 | climate: !input climate
61 |
62 | trigger:
63 | - trigger: state
64 | entity_id: !input climate
65 | attribute: temperature
66 |
67 | action:
68 | - alias: "choose alias (name)"
69 | choose:
70 | - conditions:
71 | - condition: or
72 | conditions:
73 | - alias: "Too Hot - Heat"
74 | condition: template
75 | value_template: "{{ mode=='heat' and (states(climate) == 'heat') and (state_attr(climate, 'temperature') > temp)}}"
76 | - alias: "Too Hot - Heat + AC"
77 | condition: template
78 | value_template: "{{ mode=='heat' and (states(climate) == 'heat_cool') and (state_attr(climate, 'temperature') > temp)}}"
79 | sequence:
80 | - device_id: !input notify_device
81 | domain: mobile_app
82 | type: notify
83 | message: >-
84 | 🔥️ Heat set to {{ int(state_attr('climate.downstairs','temperature'))}}° 🌡️
85 | data:
86 | actions:
87 | - action: DO_NOTHING
88 | title: Leave it as-is!
89 | icon: sfsymbols:checkmark.circle
90 | destructive: false
91 | - action: SET_TO_TEMP
92 | title: Set it to {{ temp }}
93 | destructive: false
94 | icon: sfsymbols::arrow.down.circle
95 | - conditions:
96 | - condition: or
97 | conditions:
98 | - alias: "Too Cold - AC"
99 | condition: template
100 | value_template: "{{ mode=='cool' and (states(climate) == 'cool') and (state_attr(climate, 'temperature') < temp)}}"
101 | - alias: "Too Cold - AC + Heat"
102 | condition: template
103 | value_template: "{{ mode=='cool' and (states(climate) == 'heat_cool') and (state_attr(climate, 'temperature') < temp)}}"
104 | sequence:
105 | - device_id: !input notify_device
106 | domain: mobile_app
107 | type: notify
108 | message: >-
109 | 🥶️ AC set to {{ int(state_attr('climate.downstairs','temperature'))}}° ❄️
110 | data:
111 | actions:
112 | - action: DO_NOTHING
113 | title: Leave it as-is!
114 | icon: sfsymbols:checkmark.circle
115 | destructive: false
116 | - action: SET_TO_TEMP
117 | title: Set it to {{ temp }}
118 | destructive: false
119 | icon: sfsymbols::arrow.up.circle
120 | - alias: Wait for Input
121 | wait_for_trigger:
122 | - trigger: event
123 | event_type: mobile_app_notification_action
124 | event_data:
125 | action: LEAVE_AS_IS
126 | - trigger: event
127 | event_type: mobile_app_notification_action
128 | event_data:
129 | action: SET_TO_TEMP
130 | - alias: "Change Climate"
131 | choose:
132 | - alias: "Set to target temp"
133 | conditions: "{{ wait.trigger.event.data.action == 'SET_TO_TEMP' }}"
134 | sequence:
135 | - action: climate.set_temperature
136 | target:
137 | entity_id: !input climate
138 | data:
139 | temperature: !input temp
140 | - alias: "Target minus 1"
141 | conditions: "{{ wait.trigger.event.data.action == 'SET_TO_TEMP_MINUS_ONE' }}"
142 | sequence:
143 | - action: climate.set_temperature
144 | target:
145 | entity_id: !input climate
146 | data:
147 | temperature: "{{ int(temp) - 1 }}"
148 | - alias: "Target plus 1"
149 | conditions: "{{ wait.trigger.event.data.action == 'SET_TO_TEMP_PLUS_ONE' }}"
150 | sequence:
151 | - action: climate.set_temperature
152 | target:
153 | entity_id: !input climate
154 | data:
155 | temperature: "{{ int(temp) + 1 }}"
156 |
157 | # - conditions:
158 | # condition
159 | # sequence:
160 | # action
161 | # default:
162 | # action
163 | mode: restart
164 |
--------------------------------------------------------------------------------
/scripts/inovelli_led_set_defaults.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | # Incorporates default LED color + dimmer brightness.
3 | # Originally in a script by Kevin Schlichter https://github.com/kschlichter/Home-Assistant-Inovelli-Red-Dimmer-Switch
4 | variables:
5 | # REQUIRED to be one of these options: "zwave", "ozw", "zwave_js"
6 | zwave_integration: "zwave_js"
7 |
8 | # * Strongly recommended -> Use the passed model type ("dimmer", "switch", "combo_light") when present.
9 | # * If not present, then attempt to identify the type using the "product_name" attribute (which is only
10 | # unfortunately only available in the original zwave integration).
11 | # * Finally, assume the model type is "dimmer".
12 | model: |
13 | {% if model is string %}
14 | {{ model }}
15 | {%- elif state_attr(entity_id, 'product_name') is string %}
16 | {%- if 'LZW31' in state_attr(entity_id, 'product_name') %}
17 | dimmer
18 | {%- elif 'LZW36' in state_attr(entity_id, 'product_name') %}
19 | combo_light
20 | {%- else %}
21 | switch
22 | {%- endif %}
23 | {%- else %}
24 | dimmer
25 | {%- endif %}
26 | node_id: '{{ state_attr(entity_id,"node_id") }}'
27 | color: '{{ color|default("Blue") }}'
28 | level_on: '{{ level_on|default("10") }}'
29 | level_off: '{{ level_off|default("3") }}'
30 | parameters:
31 | dimmer:
32 | color: 13
33 | level_on: 14
34 | level_off: 15
35 | combo_light:
36 | color: 18
37 | level_on: 19
38 | level_off: 22
39 | combo_fan:
40 | color: 20
41 | level_on: 21
42 | level_off: 23
43 | switch:
44 | color: 5
45 | level_on: 6
46 | level_off: 7
47 | colors:
48 | "Off": 0
49 | "Red": 1
50 | "Orange": 21
51 | "Yellow": 42
52 | "Green": 85
53 | "Cyan": 127
54 | "Teal": 145
55 | "Blue": 170
56 | "Purple": 195
57 | "Light Pink": 220
58 | "Pink": 234
59 | sequence:
60 | - variables:
61 | color: '{{ colors[color|title] }}'
62 | color_parameter: '{{ parameters[model]["color"] }}'
63 | level_on_parameter: '{{ parameters[model]["level_on"] }}'
64 | level_off_parameter: '{{ parameters[model]["level_off"] }}'
65 |
66 | # - service: script.debug
67 | # data:
68 | # id: 02
69 | # message: |
70 | # node_id: '{{ node_id }}'
71 | # parameter: '{{ parameters[model]["color"] }}'
72 | # color: '{{ color }}'
73 | # level_on: '{{ level_on }}'
74 | # level_off: '{{ level_off }}'
75 |
76 | - choose:
77 | # The Z-Wave JS integration requires this service call.
78 | - conditions:
79 | - '{{ zwave_integration == "zwave_js" }}'
80 | sequence:
81 | - service: zwave_js.set_config_parameter
82 | target:
83 | entity_id: '{{ entity_id }}'
84 | data:
85 | parameter: '{{ color_parameter }}'
86 | value: '{{ color }}'
87 |
88 | - service: zwave_js.set_config_parameter
89 | target:
90 | entity_id: '{{ entity_id }}'
91 | data:
92 | parameter: '{{ level_on_parameter }}'
93 | value: '{{ level_on }}'
94 |
95 | # - service: ozw.set_config_parameter
96 | - service: zwave_js.set_config_parameter
97 | target:
98 | entity_id: '{{ entity_id }}'
99 | data:
100 | parameter: '{{ level_off_parameter }}'
101 | value: '{{ level_off }}'
102 |
103 | # The OZW integration requires this service call.
104 | - conditions:
105 | - '{{ zwave_integration == "ozw" }}'
106 | sequence:
107 | - service: ozw.set_config_parameter
108 | data:
109 | node_id: '{{ node_id }}'
110 | parameter: '{{ color_parameter }}'
111 | value: '{{ color }}'
112 |
113 | - service: ozw.set_config_parameter
114 | data:
115 | node_id: "{{ node_id }}"
116 | parameter: '{{ level_on_parameter }}'
117 | value: '{{ level_on }}'
118 |
119 | # - service: ozw.set_config_parameter
120 | - service: zwave_js.set_config_parameter
121 | data:
122 | node_id: "{{ node_id }}"
123 | parameter: '{{ level_off_parameter }}'
124 | value: '{{ level_off }}'
125 |
126 | # The Z-wave integration requires this service call.
127 | default:
128 | - service: zwave.set_config_parameter
129 | data:
130 | node_id: '{{ node_id }}'
131 | parameter: '{{ color_parameter }}'
132 | size: 2
133 | value: '{{ color }}'
134 |
135 | - service: zwave.set_config_parameter
136 | data:
137 | node_id: "{{ node_id }}"
138 | parameter: '{{ level_on_parameter }}'
139 | size: 1
140 | value: '{{ level_on }}'
141 |
142 | - service: zwave.set_config_parameter
143 | data:
144 | node_id: "{{ node_id }}"
145 | parameter: '{{ level_off_parameter }}'
146 | size: 1
147 | value: '{{ level_off }}'
148 |
149 | fields:
150 | entity_id:
151 | description: Light or switch which represents
152 | example: light.red_series_dimmer
153 | selector:
154 | entity:
155 | #integration: zwave_js
156 | model:
157 | description: 'Device type: dimmer (default), switch, combo_light'
158 | example: dimmer
159 | selector:
160 | select:
161 | options:
162 | - dimmer
163 | - switch
164 | - combo_light
165 | color:
166 | description: 'Choose a color.'
167 | example: purple
168 | selector:
169 | select:
170 | options:
171 | - "Off"
172 | - Red
173 | - Orange
174 | - Yellow
175 | - Green
176 | - Cyan
177 | - Teal
178 | - Blue
179 | - Purple
180 | - Light Pink
181 | - Pink
182 | level_on:
183 | description: LED Brightness when on
184 | example: 10
185 | level_off:
186 | description: LED Brightness when off
187 | example: 3
--------------------------------------------------------------------------------
/blueprints/automation/fireplace_sound.yml:
--------------------------------------------------------------------------------
1 | ---
2 | blueprint:
3 | name: Fireplace 🔥️ Sounds 🎶️
4 | description: >
5 | This blueprint will let you tie together a speaker(s) and a fireplace so you can get nice fireplace sounds when the fire is on
6 | domain: automation
7 | input:
8 | fireplace_sensor:
9 | name: Fireplace Sensor
10 | description: Sensor that has the on/off state of the fireplace
11 | selector:
12 | entity:
13 | filter:
14 | domain:
15 | - binary_sensor
16 | - input_boolean
17 | player:
18 | name: Where to play fireplace sounds
19 | selector:
20 | entity:
21 | filter:
22 | domain:
23 | - media_player
24 | start_vol:
25 | name: Volume settings
26 | description: At what volume should the fire sound play
27 | selector:
28 | number:
29 | min: 0
30 | max: 1
31 | step: .1
32 | default: .27
33 | end_vol:
34 | name: Volume settings
35 | description: What level do you wish to return the volume to after you stop playing the fire sounds.
36 | selector:
37 | number:
38 | min: 0
39 | max: 1
40 | step: .1
41 | default: .27
42 |
43 | media:
44 | name: Select a sound
45 | description: Due to limitations in HA you will also have to re-select the media player above
46 | selector:
47 | media:
48 | mode: queued
49 |
50 | variables:
51 | fireplace_sensor: !input fireplace_sensor
52 | fireplace_state: "{{states(fireplace_sensor)}}"
53 | is_fireplace_on: "{{is_state(fireplace_sensor,'on')}}"
54 |
55 | media: !input media
56 | player: !input player
57 | start_vol: !input start_vol
58 | end_vol: !input end_vol
59 | fire_content_id: "{{ media['media_content_id'] }}"
60 | fire_content_substr: "{{ media['media_content_id'] | replace('media-source://media_source/','') }}"
61 | current_content_id: "{{ state_attr(player,'media_content_id') }}"
62 | is_fire_sound: >
63 | {%- if current_content_id %}
64 | {{ fire_content_substr in current_content_id }}
65 | {%- else %}
66 | {{ false }}
67 | {%- endif %}
68 |
69 | is_playing: "{{is_state(player,'playing')}}"
70 | is_playing_fire_sound: "{{ is_playing and is_fire_sound}}"
71 | action_to_take: >-
72 | {%- if is_fireplace_on and not is_playing %}
73 | {%- if trigger.id == 'stopped' and is_fire_sound %}
74 | do_nothing
75 | {%- else %}
76 | play_music
77 | {%- endif %}
78 | {#- If fireplace is off and we were playing the fire music -#}
79 | {%- elif not is_fireplace_on and (is_playing and is_fire_sound) %}
80 | stop_music
81 | {%- else %}
82 | do_nothing
83 | {%- endif %}
84 |
85 | last_state: >
86 | {{ trigger }}
87 | bool1: "{{ 'from_state' in trigger }}"
88 | bool2: "{{ 'entity_id' in trigger }}"
89 | bool3: "{{ 'id' in trigger }}"
90 | bool4: "{{ 'to_state' in trigger }}"
91 |
92 | dev: >
93 | {%- if 'from_state' in trigger -%}
94 | From: {{trigger.from_state}}
95 | {%- endif -%}
96 | {%- if 'to_state' in trigger -%}
97 | To: {{trigger.to_state}}
98 | {%- endif -%}
99 | trigger:
100 | - trigger: state
101 | entity_id: !input fireplace_sensor
102 | id: fireplace_state
103 | - trigger: state
104 | # Stop playing Music
105 | entity_id: !input player
106 | id: music_stop
107 | from: "playing"
108 | for:
109 | seconds: 1
110 | # - platform: time_pattern
111 | # id: time_pattern
112 | # seconds: /5
113 | # minutes: /5
114 | # - platform: state
115 | # # Start playing music... not sure why i need this one?
116 | # entity_id: !input player
117 | # id: media_start
118 | # to: "playing"
119 |
120 | # Don't do anything for do_nothing
121 | condition: []
122 |
123 | action:
124 | # Debug stuff
125 | - action: mqtt.publish
126 | data:
127 | qos: 0
128 | retain: false
129 | topic: "dev"
130 | payload: >
131 | bool1: {{bool1}}
132 | bool2: {{bool2}}
133 | bool3: {{bool3}}
134 | bool4: {{bool4}}
135 |
136 | - action: mqtt.publish
137 | data:
138 | qos: 0
139 | retain: false
140 | topic: "dev"
141 | payload: "{{dev}}"
142 | # payload: "{{trigger | to_json}}"
143 |
144 | - choose:
145 | # - conditions:
146 | # - condition: trigger
147 | # id: stop_5_seconds
148 | # sequence: []
149 | # # Not playing music, and the fire is on
150 | - conditions:
151 | - condition: template
152 | value_template: "{{action_to_take == 'play_music'}}"
153 | sequence:
154 | # Unjoin the groups
155 | - action: media_player.unjoin
156 | target:
157 | entity_id: "{{player}}"
158 | # set volume
159 | - action: media_player.volume_set
160 | data:
161 | volume_level: "{{start_vol}}"
162 | target:
163 | entity_id: "{{player}}"
164 | # play
165 | - action: media_player.play_media
166 | data:
167 | media_content_id: "{{fire_content_id}}"
168 | media_content_type: music
169 | target:
170 | entity_id: "{{player}}"
171 | # Repeat ON
172 | - action: media_player.repeat_set
173 | data:
174 | repeat: one
175 | target:
176 | entity_id: "{{player}}"
177 |
178 | # Stop the MUSIC
179 | - conditions:
180 | - condition: template
181 | value_template: "{{action_to_take == 'stop_music'}}"
182 | sequence:
183 | # Pause/Stop the current "stuff" the current
184 | - action: media_player.media_stop
185 | target:
186 | entity_id: "{{player}}"
187 | - action: media_player.volume_set
188 | data:
189 | volume_level: "{{end_vol}}"
190 | target:
191 | entity_id: "{{player}}"
192 |
193 | - conditions:
194 | - condition: template
195 | value_template: "{{action_to_take == 'do_nothing'}}"
196 | sequence: []
197 |
--------------------------------------------------------------------------------
/icons/dev/gifMaker.py:
--------------------------------------------------------------------------------
1 | import requests
2 | import time
3 | from PIL import Image, ImageDraw
4 | from typing import List
5 | import asyncio
6 | import sys
7 | import os
8 | from datetime import timedelta
9 |
10 |
11 | class ScreenCapture:
12 | def __init__(
13 | self,
14 | endpoint_url: str,
15 | width: int,
16 | height: int,
17 | gif_filename: str,
18 | initial_duration: int,
19 | max_duration: int,
20 | ) -> None:
21 | self.endpoint_url = endpoint_url
22 | self.width = width
23 | self.height = height
24 | self.gif_filename = gif_filename
25 | self.initial_duration = initial_duration
26 | self.max_duration = max_duration
27 | self.gif_frames: List[Image.Image] = []
28 |
29 | async def capture_frame(self) -> None:
30 | """Capture a frame from the endpoint and add it to the GIF frames."""
31 | frame_count = 0
32 | while True:
33 | response = requests.get(self.endpoint_url)
34 |
35 | # Check if the request was successful
36 | if response.status_code == 200:
37 | # Get the RGB565 color values as a list
38 | rgb565_values = response.json()
39 |
40 | # Create a new PIL image of the original dimensions (32x8)
41 | image = Image.new("RGB", (self.width, self.height))
42 | draw = ImageDraw.Draw(image)
43 |
44 | # Set the color of each pixel in the image
45 | for y in range(self.height):
46 | for x in range(self.width):
47 | # Convert the decimal RGB565 value to RGB888
48 | rgb565 = rgb565_values[y * self.width + x]
49 | red = (rgb565 & 0xFF0000) >> 16
50 | green = (rgb565 & 0x00FF00) >> 8
51 | blue = rgb565 & 0x0000FF
52 |
53 | # Draw a pixel with the converted RGB value
54 | draw.point((x, y), fill=(red, green, blue))
55 |
56 | # Scale the image to the desired dimensions (256x64)
57 | scaled_image = image.resize(
58 | (self.width * 4, self.height * 4), resample=Image.NEAREST
59 | )
60 |
61 | # Add the current frame to the GIF
62 | self.gif_frames.append(scaled_image)
63 | frame_count += 1
64 |
65 | # Print the frame count and live preview
66 | self.print_live_preview(frame_count, image)
67 |
68 | await asyncio.sleep(0.05) # Delay between frame captures
69 |
70 | def print_live_preview(self, frame_count: int, image: Image.Image) -> None:
71 | """Print the live preview of the image, clearing the console screen."""
72 | os.system("cls" if os.name == "nt" else "clear")
73 | print(f"\033[32mFrames captured: {frame_count}\033[0m")
74 | width, height = image.size
75 | for y in range(height):
76 | for x in range(width):
77 | r, g, b = image.getpixel((x, y))
78 | sys.stdout.write(f"\033[48;2;{r};{g};{b}m \033[0m")
79 | sys.stdout.write("\n")
80 | print("\033[32mctrl+c to exit\033[0m")
81 |
82 | def save_as_gif(self) -> None:
83 | """Save the captured frames as a GIF and print the duration."""
84 | if len(self.gif_frames) > 0:
85 | # Calculate the total duration of the GIF
86 | total_duration = len(self.gif_frames) * self.initial_duration
87 |
88 | # Save the frames as a GIF
89 | self.gif_frames[0].save(
90 | self.gif_filename,
91 | format="GIF",
92 | append_images=self.gif_frames[1:],
93 | save_all=True,
94 | duration=self.initial_duration,
95 | loop=0,
96 | )
97 |
98 | # Print the duration
99 | duration_str = str(timedelta(milliseconds=total_duration))
100 | print(f"\nGIF saved successfully. Duration: {duration_str}")
101 | else:
102 | print("\nNo frames captured. GIF not saved.")
103 |
104 |
105 | async def capture_loop(screen_capture: ScreenCapture) -> None:
106 | await screen_capture.capture_frame()
107 |
108 |
109 | def main() -> None:
110 | import argparse
111 |
112 | parser = argparse.ArgumentParser(description="Awtrix Clock Screen Capture")
113 | parser.add_argument(
114 | "--ip",
115 | type=str,
116 | help="The IP address of your Awtrix Clock",
117 | )
118 |
119 | args = parser.parse_args()
120 |
121 | # Prompt user for the IP address if not provided through command line argument
122 | if args.ip is None:
123 | endpoint_ip = input("What is the IP for your Awtrix Clock: ")
124 | else:
125 | endpoint_ip = args.ip
126 |
127 | # Endpoint URL
128 | endpoint_url = f"http://{endpoint_ip}/api/screen"
129 |
130 | # Image dimensions
131 | width = 32
132 | height = 8
133 |
134 | # GIF parameters
135 | gif_filename = "output.gif"
136 | initial_duration = 50 # in milliseconds
137 | max_duration = 500 # in milliseconds
138 |
139 | # Create ScreenCapture instance
140 | screen_capture = ScreenCapture(
141 | endpoint_url, width, height, gif_filename, initial_duration, max_duration
142 | )
143 |
144 | # Prompt user to start capturing
145 | input("Press Enter to start capturing frames...")
146 |
147 | # Start time
148 | start_time = time.time()
149 |
150 | # Create the event loop
151 | loop = asyncio.get_event_loop()
152 |
153 | # Run the capture loop
154 | try:
155 | loop.run_until_complete(capture_loop(screen_capture))
156 | except KeyboardInterrupt:
157 | pass
158 |
159 | # Save frames as GIF
160 | screen_capture.save_as_gif()
161 |
162 | # End time
163 | end_time = time.time()
164 |
165 | # Calculate capture duration
166 | capture_duration = end_time - start_time
167 | print(f"Capture duration: {capture_duration:.2f} seconds")
168 |
169 |
170 | if __name__ == "__main__":
171 | main()
172 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # HomeAssistant
2 |
3 | This archive contains various blueprints for Home Assistant
4 |
5 | |Bluperint|Description|Import|Preview|
6 | |-----------|-----------|-------|----|
7 | | Awtrix 🔋️ Battery Monitor 🪫️|Monitors the battery status of a mobile device|[](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fraw.githubusercontent.com%2Fjeeftor%2FHomeAssistant%2Fmaster%2Fblueprints%2Fautomation%2Fawtrix_battery_monitor.yaml)|
8 | |Awtrix 🚪️ Door Status Monitor 🔍️|Icon based monitor for Binary/Sensor (open/close) status monitoring. Generally used for doors and windows|[](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fraw.githubusercontent.com%2Fjeeftor%2FHomeAssistant%2Fmaster%2Fblueprints%2Fautomation%2Fawtrix_door_status.yaml) |
9 | |Awtrix HVAC 🥵 🌡️ 🥶| See the current heating/cooling mode |[](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fraw.githubusercontent.com%2Fjeeftor%2FHomeAssistant%2Fmaster%2Fblueprints%2Fautomation%2Fawtrix_hvac.yaml)|
10 | | Awtrix Weather ⛈️ + Forecast + 🌕️ | Super weather blueprint - Conditions + Forecast + Sunrise/Set + MoonPhase| [](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fraw.githubusercontent.com%2Fjeeftor%2FHomeAssistant%2Fmaster%2Fblueprints%2Fautomation%2Fawtrix_weatherflow.yaml) |  |
11 | | Awtrix UV ☀️ Humidity💧️ | See Humidity & Current UV Index | [](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fraw.githubusercontent.com%2Fjeeftor%2FHomeAssistant%2Fmaster%2Fblueprints%2Fautomation%2Fawtrix_uv_hum.yaml)||
12 | | Awtrix AQI IQAir/AirNow.gov 🌬️ | Give current AQI + Forecast | [](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fraw.githubusercontent.com%2Fjeeftor%2FHomeAssistant%2Fmaster%2Fblueprints%2Fautomation%2Fawtrix_aqi.yaml)||
13 | | Awtrix Pollen 🥀️| Parses IQAir's pollen data into a nice picture. (Only works if IQAir supports pollen forecasts in your region) | [](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fraw.githubusercontent.com%2Fjeeftor%2FHomeAssistant%2Fmaster%2Fblueprints%2Fautomation%2Fawtrix_pollen.yaml)||
14 | | AWTRIX Pollen (Template 🇪🇺️) 🥀️| Allows you to define a custom Template Sensor for Pollen Data | [](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fraw.githubusercontent.com%2Fjeeftor%2FHomeAssistant%2Fmaster%2Fblueprints%2Fautomation%2Fawtrix_pollen_template.yaml)||
15 | | Fireplace 🔥️ Sounds 🎶️ | Play a fireplace sound when fireplace is on| [](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fraw.githubusercontent.com%2Fjeeftor%2FHomeAssistant%2Fmaster%2Fblueprints%2Fautomation%2Ffireplace_sound.yml)|
16 | | Hue Remote | Simulate the state change feature of a hue remote in software | [](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fraw.githubusercontent.com%2Fjeeftor%2FHomeAssistant%2Fmaster%2Fblueprints%2Fautomation%2Fhue-dimmer.yaml) |
17 | | Climate Alert | Get an actionable notification if somebody sets the heat too high | [](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fraw.githubusercontent.com%2Fjeeftor%2FHomeAssistant%2Fmaster%2Fblueprints%2Fautomation%2Fclimate_alert.yaml) |
18 | | Calendar 📅️ Indicator 🚥️ | Tie indicator lights to calendar events | [](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fraw.githubusercontent.com%2Fjeeftor%2FHomeAssistant%2Fmaster%2Fblueprints%2Fautomation%2Fawtrix_indicator.yaml) |  |
19 |
20 | # Getting the ICONS
21 |
22 | ```bash
23 | # If you runt his script it will help upload icons to your Awtrix device
24 | bash -c "$(curl -fsSL https://raw.githubusercontent.com/jeeftor/HomeAssistant/master/icons/upload_icon.sh)"
25 |
26 | # Or you can run
27 | bash -c "$(curl -fsSL https://raw.githubusercontent.com/jeeftor/HomeAssistant/master/icons/upload_icon.sh)" -- IP_ADDRESS_OF_CLOCK
28 | ```
29 |
30 | ### Innoveli
31 |
32 | It was built out with the following devices:
33 |
34 | * Inovelli Red Dimmer
35 | * August SmartLock Pro (zwave)
36 | * MyQ Garage Door opener
37 | * ZwaveJS
38 |
39 | In order to set the LEDs i forked scripts from @brianhanifin's [Home-Assistant-Config](https://github.com/brianhanifin/Home-Assistant-Config) repo
40 |
41 |
42 |
--------------------------------------------------------------------------------
/blueprints/automation/awtrix_uv_hum.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | blueprint:
3 | name: AWTRIX UV☀️ Humidity💧️
4 | description: >
5 | Display both a humidity value and the UV index on the awtrix.
6 |
7 | ### Notes about UV
8 |
9 | The Weatherflow UV readings seem to be instantaneous so it may be best to create an average sensor for the readings if you don't want to see wild fluctuations. You can do this by adding the following sensor to your `sensors:` block in `configuration.yaml`
10 |
11 | - platform: statistics
12 | name: "Weatherflow uv index 10 minute average"
13 | entity_id: sensor.weatherflow_uv_index
14 | state_characteristic: mean
15 | max_age:
16 | minutes: 10
17 |
18 | domain: automation
19 | input:
20 | awtrix:
21 | name: AWTRIX Device
22 | description: Select the Awtrix light
23 | selector:
24 | device:
25 | filter:
26 | integration: mqtt
27 | manufacturer: Blueforcer
28 | model: AWTRIX 3
29 | multiple: true
30 | hum:
31 | name: Humidity Sensor
32 | description: >
33 |
34 | A sensor that provides a humidity value
35 |
36 | - `sensor.weatherflow_relative_humidity`
37 |
38 | - `sensor.openweathermap_humidity`
39 |
40 | selector:
41 | entity:
42 | uv:
43 | name: UV Index
44 | description: >
45 |
46 | A sesnor that provides a uv index like:
47 |
48 | - `sensor.weatherflow_uv_index`
49 |
50 | - `sensor.openweathermap_uv_index`
51 |
52 | selector:
53 | entity:
54 | show_forecast:
55 | name: Show Hourly UV Forecast
56 | description: If you have a valid data source to obtain an hourly UV forecast you should use it
57 | selector:
58 | boolean:
59 | default: true
60 |
61 | forecast:
62 | name: Hourly UV Forecast
63 | description: >
64 | Select a forecast with `uv_index` available
65 |
66 | This integration has been tested with:
67 |
68 | - HACS [Weatherflow](https://github.com/briis/hass-weatherflow) integration
69 |
70 | - NOTE: [Openweather](https://www.home-assistant.io/integrations/openweathermap/) DOES NOT HAVE AN HOURLY UV FORECAST
71 | selector:
72 | entity:
73 | filter:
74 | domain:
75 | - weather
76 | multiple: false
77 |
78 | mode: restart
79 | variables:
80 | app_topic: jeef_uv_hum
81 | device_ids: !input awtrix
82 | devices_topics: >-
83 | {%- macro get_device_topic(device_id) %}
84 | {{- states((device_entities(device_id) | select('search','device_topic') | list)[0]) }}
85 | {%- endmacro %}
86 |
87 | {%- set ns = namespace(devices=[]) %}
88 | {%- for device_id in device_ids %}
89 | {%- set device = get_device_topic(device_id)|replace(' ','') %}
90 | {% set ns.devices = ns.devices + [ device ~ '/custom/' ~ app_topic] %}
91 | {%- endfor %}
92 | {{ ns.devices | reject('match','unavailable') | list}}
93 |
94 | humidity_sensor: !input hum
95 | uv_sensor: !input uv
96 |
97 | humidity: "{{states(humidity_sensor) | round}}"
98 | uv: "{{states(uv_sensor) | round}}"
99 | forecast_var: !input forecast
100 | forecast_field: "uv_index"
101 | show_forecast: !input show_forecast
102 |
103 | trigger:
104 | - trigger: time_pattern
105 | minutes: "/5"
106 |
107 | condition: []
108 | action:
109 | - action: weather.get_forecasts
110 | target:
111 | entity_id: "{{forecast_var}}"
112 | data:
113 | type: hourly
114 | response_variable: forecast_response
115 |
116 | - repeat:
117 | for_each: "{{ devices_topics }}"
118 | sequence:
119 | - action: mqtt.publish
120 | data:
121 | qos: 0
122 | retain: false
123 | topic: "{{ repeat.item }}"
124 | payload: >
125 | {%- set forecast = forecast_response[forecast_var]['forecast'] -%}
126 |
127 | {%- macro get_uv_color(index) -%} {%- if index | float > 10 -%}
128 | #EE82EE
129 | {%- elif index | float >= 8 -%}
130 | #FF0000
131 | {%- elif index | float >= 6 -%}
132 | #FF8C00
133 | {%- elif index | float >= 3 -%}
134 | #FFFF00
135 | {%- else -%}
136 | #00FF00
137 | {%- endif -%}
138 | {%- endmacro -%}
139 |
140 | {%- macro draw_uv_forecast_h(x,y,hours,height) %}
141 | {%- for hour in range(hours) %}
142 | {%- set uv_index = forecast[hour][forecast_field] |int %}
143 | {%- set uv_color = get_uv_color(uv_index) %}
144 | {%- if height == 0 %}
145 | {"dp": [{{x+hour}},{{y}},"{{uv_color }}"]}
146 | {%- else %}
147 | {"dl": [{{x+hour}},{{y}},{{x+hour}},{{y - height}},"{{uv_color}}"]}
148 | {%- endif %}
149 | {%- if hour+1 != hours %},{%endif%}
150 | {%- endfor %}
151 | {%- endmacro %}
152 |
153 | {%- macro get_uv_color(index) -%} {%- if index | float > 10 -%}
154 | #EE82EE
155 | {%- elif index | float >= 8 -%}
156 | #FF0000
157 | {%- elif index | float >= 6 -%}
158 | #FF8C00
159 | {%- elif index | float >= 3 -%}
160 | #FFFF00
161 | {%- else -%}
162 | #00FF00
163 | {%- endif -%}
164 | {%- endmacro -%}
165 | {% set uv_color = get_uv_color(uv) %}
166 |
167 |
168 |
169 |
170 | {#- Calculate UV Offset -#}
171 | {%- if uv|float >= 10-%}
172 | {%- set uv_offset = "23,1" -%}
173 | {%- else %}
174 | {%- set uv_offset = "25,1" -%}
175 | {%- endif %}
176 |
177 | {% macro draw_uv() %}
178 | {"dt":[{{uv_offset}},"{{uv}}","{{uv_color}}"]}
179 | {% endmacro %}
180 |
181 | {%- macro draw_u(x,y,color) -%}
182 | {"dl":[{{x}},{{y}},{{x}},{{y+2}},"{{color}}"]},
183 | {"dl":[{{x}},{{y+2}},{{x+2}},{{y+2}},"{{color}}"]},
184 | {"dl":[{{x+2}},{{y}},{{x+2}},{{y+2}},"{{color}}"]}
185 | {%- endmacro -%}
186 |
187 | {%- macro draw_v(x,y,color) -%}
188 | {"dl":[{{x}},{{y}},{{x+1}},{{y+2}},"{{color}}"]},
189 | {"dl":[{{x+2}},{{y}},{{x+1}},{{y+2}},"{{color}}"]}
190 | {%- endmacro -%}
191 |
192 | {
193 | "draw": [
194 | {"db":[0,1,8,8,[0,0,15657727,0,0,0,0,65536,0,16777215,15132415,12961791,0,0,0,0,16711422,15657982,15198207,12895742,11315711,65536,0,0,15723775,15132415,15066623,11381503,11185150,0,256,0,15592191,12961534,12961535,11315452,8224464,0,0,2,2,11315966,11315965,8158669,0,0,0,0,0,0,0,0,256,0,0,0,0,65793,0,65536,0,2,256,2]]},
195 | {"dt":[6,2,"{{humidity}}%"]},
196 | {{draw_u(19,1,"#00ffaa")}},
197 | {{draw_v(19,5,"#00ffaa")}},
198 | {%- if show_forecast %}{{ draw_uv_forecast_h(22,7,8,0) }},{%- endif %}
199 | {{draw_uv()}}
200 | ],
201 | "lifetime": 620,
202 | "lifetimeMode":1
203 | }
204 |
--------------------------------------------------------------------------------
/icons/dev/gifConverter.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import os
3 | from PIL import Image
4 |
5 |
6 | class GifConverter:
7 | """
8 | A class that converts a GIF image into SVG files and generates Markdown file referencing the SVG files as images.
9 | """
10 |
11 | DEFAULT_CELL_SIZE = 16
12 |
13 | def __init__(self, gif_path: str, output_dir: str, cell_size: int):
14 | """
15 | Initializes the GifConverter class.
16 |
17 | Args:
18 | gif_path (str): Path to the GIF image.
19 | output_dir (str): Output directory for the SVG files and Markdown file.
20 | cell_size (int): Size of each cell in the SVG representation.
21 | """
22 | self.gif_path = gif_path
23 | self.output_dir = output_dir
24 | self.cell_size = cell_size
25 | self.base_name = os.path.splitext(os.path.basename(self.gif_path))[
26 | 0
27 | ] # Extract base name
28 | self.gif = None
29 |
30 | def convert_to_svg(self) -> list[str]:
31 | """
32 | Converts the GIF image to a series of SVG files.
33 |
34 | Returns:
35 | list[str]: List of file paths to the generated SVG files.
36 | """
37 | # Open the GIF image
38 | self.gif = Image.open(self.gif_path)
39 |
40 | # Create the output directory if it doesn't exist
41 | os.makedirs(self.output_dir, exist_ok=True)
42 |
43 | # Get the base name of the input file
44 | base_name = os.path.splitext(os.path.basename(self.gif_path))[0]
45 |
46 | # Convert each frame to SVG
47 | svg_file_paths = []
48 | for frame_index in range(self.gif.n_frames):
49 | # Select the current frame
50 | self.gif.seek(frame_index)
51 | frame = self.gif.convert("RGBA")
52 |
53 | # Get the dimensions of the frame
54 | width, height = frame.size
55 |
56 | # Calculate the cell size
57 | if self.cell_size is None:
58 | self.cell_size = self.DEFAULT_CELL_SIZE
59 |
60 | # Create an SVG file path for the current frame
61 | svg_path = os.path.join(
62 | self.output_dir, f"{base_name}_frame_{frame_index}.svg"
63 | )
64 | svg_file_paths.append(svg_path)
65 |
66 | # Open the SVG file
67 | with open(svg_path, "w") as svg_file:
68 | # Write the SVG header
69 | svg_file.write(
70 | f'\n")
102 |
103 | print(f"SVG file '{svg_path}' has been created successfully.")
104 |
105 | return svg_file_paths
106 |
107 | def generate_markdown(self, svg_file_paths: list[str]) -> None:
108 | """
109 | Generates a Markdown file with image references to the SVG files.
110 |
111 | Args:
112 | svg_file_paths (list[str]): List of file paths to the SVG files.
113 | """
114 | markdown_path = os.path.join(self.output_dir, f"{self.base_name}.md")
115 |
116 | with open(markdown_path, "w") as markdown_file:
117 | # Write the Markdown syntax for each SVG file as an image tag with a title
118 | for frame_index, svg_file_path in enumerate(svg_file_paths):
119 | image_name = f"{self.base_name}_frame_{frame_index}.svg"
120 | title = f"Frame {frame_index}"
121 | markdown_file.write(f'\n')
122 |
123 | print(f"Markdown file '{markdown_path}' has been created successfully.")
124 |
125 | def gif_to_html(self) -> None:
126 | """
127 | Converts the GIF image to an HTML file.
128 |
129 | Args:
130 | gif_path (str): Path to the GIF image.
131 | cell_size (int): Size of each cell in the HTML table.
132 | """
133 | # Open the GIF image
134 | gif = Image.open(self.gif_path)
135 |
136 | # Generate the output HTML file path
137 | output_path = os.path.join(self.output_dir, f"{self.base_name}.html")
138 |
139 | # Create the HTML file
140 | with open(output_path, "w") as html_file:
141 | # Write the HTML header
142 | html_file.write("\n")
143 | html_file.write("\n")
144 |
145 | # Iterate over each frame in the GIF
146 | for frame_index in range(gif.n_frames):
147 | # Select the current frame
148 | gif.seek(frame_index)
149 | frame = gif.convert("RGBA")
150 |
151 | # Get the dimensions of the frame
152 | width, height = frame.size
153 |
154 | # Calculate the cell size
155 | if self.cell_size is None:
156 | cell_size = GifConverter.DEFAULT_CELL_SIZE
157 |
158 | # Write the frame number as the title
159 | html_file.write(f"
Frame Number: {frame_index}
\n")
160 |
161 | # Write the table start tag
162 | html_file.write('
\n')
163 |
164 | # Iterate over each pixel in the frame
165 | for y in range(height):
166 | # Write the table row start tag
167 | html_file.write("
\n")
168 | for x in range(width):
169 | # Get the color of the current pixel
170 | r, g, b, a = frame.getpixel((x, y))
171 |
172 | # Check if the pixel is transparent or matches the background color
173 | if a == 0 or (r, g, b, a) == gif.info.get("background", ()):
174 | # Set the pixel color to black
175 | r, g, b = 0, 0, 0
176 |
177 | # Convert RGB values to hexadecimal
178 | hex_color = f"#{r:02x}{g:02x}{b:02x}"
179 |
180 | # Write the table cell with the updated pixel color
181 | html_file.write(
182 | f'
\n'
183 | )
184 |
185 | # Write the table row end tag
186 | html_file.write("
\n")
187 |
188 | # Write the table end tag
189 | html_file.write("
\n")
190 |
191 | # Add a line break between frames
192 | html_file.write(" \n")
193 |
194 | # Write the HTML footer
195 | html_file.write("\n")
196 | html_file.write("\n")
197 |
198 | print(f'HTML file "{output_path}" has been created successfully.')
199 |
200 |
201 | def main() -> None:
202 | """
203 | Main function for executing the GIF to SVG conversion and generating Markdown and HTML files.
204 | """
205 | if len(sys.argv) < 2:
206 | print("Please provide the path to the GIF image.")
207 | return
208 |
209 | gif_path = sys.argv[1]
210 | cell_size = None
211 | if len(sys.argv) >= 3:
212 | cell_size = int(sys.argv[2])
213 |
214 | converter = GifConverter(gif_path, "output", cell_size)
215 | svg_file_paths = converter.convert_to_svg()
216 | converter.generate_markdown(svg_file_paths)
217 | converter.gif_to_html()
218 |
219 |
220 | if __name__ == "__main__":
221 | main()
222 |
--------------------------------------------------------------------------------
/blueprints/automation/inovelli_door_cover_notification.yaml:
--------------------------------------------------------------------------------
1 | # This blueprint requires the use of two external scripts that must be setup with your configuration. They are available from https://github.com/brianhanifin/Home-Assistant-Config
2 | #
3 | # https://github.com/brianhanifin/Home-Assistant-Config/blob/master/scripts/notifications/inovelli_led/inovelli_led.yaml
4 | # https://github.com/brianhanifin/Home-Assistant-Config/blob/master/scripts/notifications/inovelli_led/inovelli_led_set_defaults.yaml
5 | #
6 | # The "jist" of this blueprint is that it will monitor the status of two items (a lock and a cover) and give you a 4x matrix of color options
7 | # So you can have your inovelli swtich (at a glance) tell you whats up
8 | #
9 | #
10 | #
11 |
12 | blueprint:
13 | name: Inovelli Red Series LZW31-SN Dimmer - LED Notifications based on combo of Door and Garage
14 | description: The idea is to matrix a Door lock status and a garage door (cover) status to give you different light effects. Also it will fire off a moible phone notificaiton. For this blueprint to work, however, you MUST have two separate inovelli scripts installed (inovelli_led.yaml & inovelli_led_set_defaults.yaml).
15 |
16 | domain: automation
17 | input:
18 | lock:
19 | name: Door Lock
20 | description: The lock that we want to trigger on
21 | default: lock.august_smart_lock_pro_3rd_gen
22 | selector:
23 | entity:
24 | domain: lock
25 | cover:
26 | name: Garage Door
27 | default: cover.garage_door
28 | selector:
29 | entity:
30 | domain: cover
31 | light:
32 | name: Inovelli Light
33 | description: zwave_js supported Inovelli lights will show up here
34 | default: light.red_series_dimmer
35 | selector:
36 | entity:
37 | integration: zwave_js
38 | # manufacturer: Inovelli
39 | domain: light
40 |
41 | effect:
42 | description: 'Choose a state transition effect.'
43 | selector:
44 | select:
45 | options:
46 | - "Off"
47 | - Solid
48 | - Chase
49 | - Fast Blink
50 | - Slow Blink
51 | - Blink
52 | - Pulse
53 | - Breath
54 | duration:
55 | description: 'How long should the effect run?'
56 | selector:
57 | select:
58 | options:
59 | - "Off"
60 | - 1 Second
61 | - 2 Seconds
62 | - 3 Seconds
63 | - 4 Seconds
64 | - 5 Seconds
65 | - 6 Seconds
66 | - 7 Seconds
67 | - 8 Seconds
68 | - 9 Seconds
69 | - 10 Seconds
70 | - 15 Seconds
71 | - 20 Seconds
72 | - 25 Seconds
73 | - 30 Seconds
74 | - 35 Seconds
75 | - 40 Seconds
76 | - 45 Seconds
77 | - 50 Seconds
78 | - 55 Seconds
79 | - 60 Seconds
80 | - 2 Minutes
81 | - 3 Minutes
82 | - 4 Minutes
83 | # - 10 Minutes
84 | # - 15 Minutes
85 | # - 30 Minutes
86 | # - 45 Minutes
87 |
88 | door_locked_garage_closed_color:
89 | name: Door Locked / Garage Close Color
90 | default: Green
91 | selector:
92 | select:
93 | options:
94 | - "Off"
95 | - Red
96 | - Orange
97 | - Yellow
98 | - Green
99 | - Cyan
100 | - Teal
101 | - Blue
102 | - Purple
103 | - Light Pink
104 | - Pink
105 | door_locked_garage_open_color:
106 | name: Door Locked / Garage Open
107 | description: Pick a color for the combo of an locked door and an open garage
108 | default: Yellow
109 | selector:
110 | select:
111 | options:
112 | - "Off"
113 | - Red
114 | - Orange
115 | - Yellow
116 | - Green
117 | - Cyan
118 | - Teal
119 | - Blue
120 | - Purple
121 | - Light Pink
122 | - Pink
123 | door_unlocked_garage_closed_color:
124 | name: Door Unlocked / Garage Close Color
125 | default: Purple
126 | selector:
127 | select:
128 | options:
129 | - "Off"
130 | - Red
131 | - Orange
132 | - Yellow
133 | - Green
134 | - Cyan
135 | - Teal
136 | - Blue
137 | - Purple
138 | - Light Pink
139 | - Pink
140 | door_unlocked_garage_open_color:
141 | name: Door Unlocked / Garage Open
142 | description: Pick a color for the combo of an unlocked door and an open garage
143 | default: Red
144 | selector:
145 | select:
146 | options:
147 | - "Off"
148 | - Red
149 | - Orange
150 | - Yellow
151 | - Green
152 | - Cyan
153 | - Teal
154 | - Blue
155 | - Purple
156 | - Light Pink
157 | - Pink
158 | notify_device:
159 | name: Device to notify
160 | description: Device needs to run the official Home Assistant app to receive notifications
161 | selector:
162 | device:
163 | integration: mobile_app
164 | mode: restart
165 |
166 |
167 | trigger:
168 | - platform: state
169 | entity_id: !input cover
170 | to: 'open'
171 | - platform: state
172 | entity_id: !input cover
173 | to: 'closed'
174 | - platform: state
175 | entity_id: !input lock
176 | to: 'locked'
177 | - platform: state
178 | entity_id: !input lock
179 | to: 'unlocked'
180 |
181 | action:
182 | - choose:
183 | - conditions: #Door Locked / Garage Locked
184 | - condition: state
185 | entity_id: !input cover
186 | state: closed
187 | - condition: state
188 | entity_id: !input lock
189 | state: locked
190 | sequence:
191 | - service: script.inovelli_led_set_defaults
192 | data:
193 | entity_id: !input light
194 | color: !input door_locked_garage_closed_color
195 | - service: script.inovelli_led
196 | data:
197 | entity_id: !input light
198 | color: !input door_locked_garage_closed_color
199 | effect: !input effect
200 | duration: !input duration
201 | - device_id: !input notify_device
202 | domain: mobile_app
203 | type: notify
204 | message: All Closed
205 | - conditions: # Door Unlocked / Garage Locked
206 | - condition: state
207 | entity_id: !input cover
208 | state: closed
209 | - condition: state
210 | entity_id: !input lock
211 | state: unlocked
212 | sequence:
213 | - service: script.inovelli_led_set_defaults
214 | data:
215 | entity_id: !input light
216 | color: !input door_unlocked_garage_closed_color
217 | - service: script.inovelli_led
218 | data:
219 | entity_id: !input light
220 | color: !input door_unlocked_garage_closed_color
221 | effect: !input effect
222 | duration: !input duration
223 | - device_id: !input notify_device
224 | domain: mobile_app
225 | type: notify
226 | message: Garage Closed - Door Unlocked
227 | - conditions: # Door Locked / Garage Open
228 | - condition: state
229 | entity_id: !input cover
230 | state: open
231 | - condition: state
232 | entity_id: !input lock
233 | state: locked
234 | sequence:
235 | - service: script.inovelli_led_set_defaults
236 | data:
237 | entity_id: !input light
238 | color: !input door_locked_garage_open_color
239 | - service: script.inovelli_led
240 | data:
241 | entity_id: !input light
242 | color: !input door_locked_garage_open_color
243 | effect: !input effect
244 | duration: !input duration
245 | - device_id: !input notify_device
246 | domain: mobile_app
247 | type: notify
248 | message: Garage Open - Door Locked
249 | - conditions: # Door Open / Garage Open
250 | - condition: state
251 | entity_id: !input cover
252 | state: open
253 | - condition: state
254 | entity_id: !input lock
255 | state: unlocked
256 | sequence:
257 | - service: script.inovelli_led_set_defaults
258 | data:
259 | entity_id: !input light
260 | color: !input door_unlocked_garage_open_color
261 | - service: script.inovelli_led
262 | data:
263 | entity_id: !input light
264 | color: !input door_unlocked_garage_open_color
265 | effect: !input effect
266 | duration: !input duration
267 | - device_id: !input notify_device
268 | domain: mobile_app
269 | type: notify
270 | message: Garage Open - Door Unlocked
271 |
--------------------------------------------------------------------------------
/blueprints/automation/awtrix_aqi.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | blueprint:
3 | name: AWTRIX AQI IQAir/AirNow.gov 🌬️
4 | description: >
5 | Blueprint to show Air Quality Forecast + Air Quality from AirNow.gov + IQAir
6 |
7 | In my limited research IQAir offers a scrapable AQI forecast, but primarily focused on 2.5PPM air quality. AirNow.gov has a worse update rate but it offers an Ozone forecast - so it may be useful to have both sensors up
8 |
9 | In order to use this blueprint you'll have to install a HACS component (see below) to scrape the IQAir webpage
10 |
11 |
12 | ### Requirements
13 |
14 | - [HACS Multiscraper](https://github.com/danieldotnl/ha-multiscrape)
15 |
16 | - [AirNow - or equivalent](https://www.home-assistant.io/integrations/airnow/)
17 |
18 | - IQAir Sensor: (example for chicago below)
19 |
20 |
21 | multiscrape:
22 | - name: IQAir Scraper
23 | resource: https://www.iqair.com/usa/illinois/chicago
24 | scan_interval: 600
25 | log_response: true
26 | sensor:
27 | - unique_id: iq_air_aqi
28 | name: IQAir MultiScrape
29 | icon: >-
30 | {%- if value | int < 50 %}
31 | mdi:gauge-empty
32 | {%- elif value | int < 100 %}
33 | mdi:gauge-low
34 | {%- elif value | int < 200 %}
35 | mdi:gauge
36 | {%- else %}
37 | mdi:gauge-full
38 | {%-endif%}
39 | select_list: ".pollutant-level-wrapper b"
40 | value_template: "{{value.split(',')[4] | int }}"
41 | select: "forecast"
42 | attributes:
43 | - name: Main Pollutant
44 | select: ".aqi-overview-detail__main-pollution-table td:last-child"
45 | - name: Raw Data
46 | select_list: ".pollutant-level-wrapper b"
47 | - name: History
48 | select_list: ".pollutant-level-wrapper b"
49 | value_template: "{{value.split(',')[0:4] | reverse | map('int') | list }}"
50 | - name: Forecast
51 | select_list: ".pollutant-level-wrapper b"
52 | value_template: "{{value.split(',')[5:] | map('int') | list}}"
53 |
54 | domain: automation
55 | input:
56 | awtrix:
57 | name: AWTRIX Device
58 | description: Select the Awtrix light
59 | selector:
60 | device:
61 | integration: mqtt
62 | manufacturer: Blueforcer
63 | model: AWTRIX 3
64 | multiple: true
65 | airnow_sensor:
66 | name: Airnow.Gov AQI
67 | description: IQAir and Airnow have different readings so we use both
68 | selector:
69 | entity:
70 | filter:
71 | domain:
72 | - sensor
73 | iqair_sensor:
74 | name: IQAir Custom multi-scrape sensor
75 | description: >-
76 | You need to configure a custom sensor with the HACS Multiscrape plugin:
77 |
78 | Replace `<>` with correct webpage you want to scrape
79 |
80 |
81 | - name: IQAir Scraper
82 | resource: <>
83 | scan_interval: 600
84 | log_response: true
85 | sensor:
86 | - unique_id: iq_air_aqi
87 | name: IQAir Now
88 | icon: >-
89 | {%- if value | int < 50 %}
90 | mdi:gauge-empty
91 | {%- elif value | int < 100 %}
92 | mdi:gauge-low
93 | {%- elif value | int < 200 %}
94 | mdi:gauge
95 | {%- else %}
96 | mdi:gauge-full
97 | {%-endif%}
98 | select_list: ".pollutant-level-wrapper b"
99 | value_template: "{{value.split(',')[4] | int }}"
100 | select: "forecast"
101 | attributes:
102 | - name: Main Pollutant
103 | select: ".aqi-overview-detail__main-pollution-table td:last-child"
104 | - name: Raw Data
105 | select_list: ".pollutant-level-wrapper b"
106 | - name: History
107 | select_list: ".pollutant-level-wrapper b"
108 | value_template: "{{value.split(',')[0:4] | reverse | map('int') | list }}"
109 | - name: Forecast
110 | select_list: ".pollutant-level-wrapper b"
111 | value_template: "{{value.split(',')[5:] | map('int') | list}}"
112 | selector:
113 | entity:
114 | domain:
115 | - sensor
116 |
117 | app_name:
118 | name: Awtrix Applicaiton name
119 | description: This is the app name listed in the MQTT topic - it should be unique
120 | selector:
121 | text:
122 | default: iq_air
123 |
124 | mode: restart
125 | variables:
126 | device_ids: !input awtrix
127 | app_topic: !input app_name
128 | aqi_icon: >-
129 | {"db": [0, 0, 8, 8, [5029628, 5029628, 0, 0, 0, 0, 0, 0, 5029628, 0, 5029628, 0, 16777215, 0, 0, 0, 5029628, 5029628, 5029628, 16777215, 0, 16777215, 0, 2425087, 5029628, 0, 5029628, 16777215, 0, 16777215, 0, 2425087, 5029628, 0, 5029628, 16777215, 0, 16777215, 0, 2425087, 0, 0, 0, 0, 16777215, 16777215, 16777215, 2425087, 0, 0, 0, 0, 0, 0, 0, 2425087, 327172, 16580100, 16557572, 16515588, 10224284, 7602692, 0, 0]]}
130 |
131 | # Generate history stuff
132 | sensor_prefix: "iqair"
133 | iqair_sensor: !input iqair_sensor
134 | airnow_sensor: !input airnow_sensor
135 |
136 | iq_air_aqi: "{{ states('sensor.iq_air_aqi',-1) | int | round(0)}}"
137 | iq_air_forecast_1: "{{(state_attr(iqair_sensor, 'forecast') | from_json)[0] | int}}"
138 | iq_air_forecast_2: "{{(state_attr(iqair_sensor, 'forecast') | from_json)[1] | int}}"
139 | iq_air_forecast_3: "{{(state_attr(iqair_sensor, 'forecast') | from_json)[2] | int}}"
140 | iq_air_forecast_4: "{{(state_attr(iqair_sensor, 'forecast') | from_json)[3] | int}}"
141 | iq_air_forecast_5: "{{(state_attr(iqair_sensor, 'forecast') | from_json)[4] | int}}"
142 | airnow_aqi: "{{ states(airnow_sensor) | round(0) }}"
143 |
144 | forecast: >-
145 | {%- macro aqi_line(x, value) %}
146 | {"dl":
147 | {%- if value >= 301 -%}
148 | [{{x}},7,{{x+1}},7,"#750000"]
149 | {%- elif value >= 201 -%}
150 | [{{x}},7,{{x+1}},7,"#9a009a"]
151 | {%- elif value >= 151 -%}
152 | [{{x}},7,{{x+1}},7,"#ff0000"]
153 | {%- elif value >= 101 -%}
154 | [{{x}},7,{{x+1}},7,"#ffa500"]
155 | {%- elif value >= 51 -%}
156 | [{{x}},7,{{x+1}},7,"#FFFF00"]
157 | {%- else -%}
158 | [{{x}},7,{{x+1}},7,"#00FF00"]
159 | {%- endif -%}
160 | }
161 | {%- endmacro %}
162 |
163 | {{aqi_line(10,iq_air_aqi)}},
164 | {{aqi_line(13, iq_air_forecast_1) }},
165 | {{aqi_line(16, iq_air_forecast_2) }},
166 | {{aqi_line(19, iq_air_forecast_3) }},
167 | {{aqi_line(22, iq_air_forecast_4) }},
168 | {{aqi_line(25, iq_air_forecast_5) }}
169 |
170 | payload: >-
171 | {%- macro get_index_color(value) %}
172 | {%- if value >= 301 %}
173 | {{- "#750000" -}}
174 | {%- elif value >= 201 %}
175 | {{- "#9a009a" -}}
176 | {%- elif value >= 151 %}
177 | {{- "#ff0000" -}}
178 | {%- elif value >= 101 %}
179 | {{- "#ffa500" -}}
180 | {%- elif value >= 51 %}
181 | {{- "#FFFF00" -}}
182 | {%- else %}
183 | {{- "#00FF00" -}}
184 | {%- endif %}
185 | {%- endmacro %}
186 |
187 | {%- set iqair = iq_air_aqi %}
188 | {%- set airnow = airnow_aqi %}
189 |
190 | {
191 | "lifetime": 600,
192 | "lifetimeMode":1,
193 | "draw": [
194 | {{aqi_icon}},
195 | {%- if iqair < 10 %}
196 | {"dt": [12,1,"{{iqair}}","{{get_index_color(iqair)}}"]},
197 | {%- elif iqair < 100 -%}
198 | {"dt": [10,1,"{{iqair}}","{{get_index_color(iqair)}}"]},
199 | {%- else %}
200 | {"dt": [9,1,"{{iqair}}","{{get_index_color(iqair)}}"]},
201 | {%- endif %}
202 |
203 | {%- if airnow < 10 %}
204 | {"dt": [21,1,"{{airnow}}","{{get_index_color(airnow)}}"]},
205 | {%- elif airnow < 100 -%}
206 | {"dt": [21,1,"{{airnow}}","{{get_index_color(airnow)}}"]},
207 | {%- else %}
208 | {"dt": [21,1,"{{airnow}}","{{get_index_color(airnow)}}"]},
209 | {%- endif %}
210 |
211 | {{forecast}}
212 | ]}
213 | message_topics: >-
214 | {%- macro get_device_topic(device_id) %}
215 | {{ states((device_entities(device_id) | select('search','device_topic') | list)[0]) }}
216 | {%- endmacro %}
217 |
218 | {%- set ns = namespace(devices=[]) %}
219 | {%- for device_id in device_ids %}
220 | {%- set device=get_device_topic(device_id)|replace(' ','') %}
221 | {% set ns.devices = ns.devices + [ device ~ '/custom/' ~ 'jeef_' ~ app_topic] %}
222 | {%- endfor %}
223 | {{ ns.devices | reject('match','unavailable') | list}}
224 | trigger:
225 | - platform: time_pattern
226 | seconds: /5
227 | condition: []
228 | action:
229 | - repeat:
230 | for_each: "{{ message_topics }}"
231 | sequence:
232 | - service: mqtt.publish
233 | data:
234 | qos: 0
235 | retain: false
236 | topic: "{{ repeat.item }}"
237 | payload: >
238 | {{payload}}
239 |
--------------------------------------------------------------------------------
/blueprints/automation/hue-dimmer.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | blueprint:
3 | name: Hue Dimmer 🎛️ Hue Dimmer 🔅️ Action 💡️ (Zigbee2MQTT)
4 | description: >
5 | ## Overview
6 |
7 | This blueprint will enable a [Hue Dimmer Remote](https://www.zigbee2mqtt.io/devices/324131092621.html) (connected via Zigbee2MQTT) to function as a scene controller.
8 |
9 | ```
10 | ┌───────┐
11 | │┌─────┐│
12 | ││ ON ││ Press ON to cycle through Scenes and Scripts
13 | │├─────┤│
14 | ││ * ││
15 | │├─────┤│
16 | ││ * ││
17 | │├─────┤│
18 | ││ OFF ││ Press OFF to cycle through Scenes and Scripts
19 | │└─────┘│
20 | └───────┘
21 | ```
22 |
23 | ## Requirements
24 |
25 | The main requirement of this blueprint is the existance of an **Input Text Helper** named: `zigbee2mqtt_json`
26 |
27 | ## Details
28 |
29 | At each press of the `ON` or `OFF` button the automation will send a request to change to a specific scene as well as launch a specific script. If either of these scenes or scripts exists they will be activated.
30 |
31 | Both Scenes an Scripts follow a specific naming convention:
32 |
33 | For example if you have a device called `Basement Remote` (in Z2M) and it will look for the following scenes/scripts:
34 |
35 |
36 | When iterating through presses of the `ON` button:
37 |
38 |
39 | - `script.basement_remote_on_0` / `scene.basement_remote_on_0`
40 | - `script.basement_remote_on_1` / `scene.basement_remote_on_1`
41 | - `script.basement_remote_on_2` / `scene.basement_remote_on_2`
42 |
43 |
44 | When iterating through the `OFF` button:
45 |
46 | - `script.basement_remote_off_0` / `scene.basement_remote_off_0`
47 | - `script.basement_remote_off_1` / `scene.basement_remote_off_1`
48 | - `script.basement_remote_off_2` / `scene.basement_remote_off_2`
49 |
50 | **⚠️ NOTE ⚠️: The blueprint requires the following input text helper: `input_text.zigbee2mqtt_json` to exist and have a default value of `{}`**
51 |
52 | domain: automation
53 | # icon: "mdi:remote"
54 | input:
55 | device_topic:
56 | name: Z2M Device Name
57 | description: "This will be used to calculate the topic that Z2M is publishg to. So for example if you have the topic `zigbee2mqtt/Basement Remote/action` your device name woudl be: `Basement Remote`"
58 | selector:
59 | text:
60 | on_count:
61 | name: ON count
62 | description: >
63 | The maximum number of scenes available to cycle through.
64 |
65 |
66 | **⚠️NOTE:⚠️** this is a `0` based value so if you have `5` scenes it will look for scenes `0` through `4`
67 | default: 5
68 | selector:
69 | number:
70 | min: 1
71 | max: 10
72 | step: 1
73 | mode: box
74 | off_count:
75 | name: OFF count
76 | description: >
77 | The maximum number of scenes available to cycle through.
78 |
79 |
80 | **⚠️NOTE:⚠️** this is a `0` based value so if you have `5` scenes it will look for scenes `0` through `4`
81 | default: 5
82 | selector:
83 | number:
84 | min: 1
85 | max: 10
86 | step: 1
87 | mode: box
88 |
89 | mode: queued
90 | max: 10
91 |
92 | variables:
93 | # input_text.zigbee2mqtt_json
94 | # current_scene: "{{ state_attr('input_number.remote_basement_scene_selector', 'value') | int }}"
95 | device_topic: !input device_topic
96 | # my_light: !input light_entity
97 |
98 | # Parse the existing helper - if its empty, unset or actually has valid data this should work
99 | stored_json_text: '{{ iif(states(''input_text.zigbee2mqtt_json'') in [None, '''',''{}''], dict({device_topic:{"state":"off","scene":0}}) | to_json, states(''input_text.zigbee2mqtt_json'')) }}'
100 |
101 | # Extract light state
102 | light_state: "{{ (stored_json_text.get(device_topic, dict())).get('state','off') }}"
103 | # Calculate current scene ID a s well as the "rollover mod values for an on or off press"
104 | current_scene: "{{ (stored_json_text.get(device_topic, dict())).get('scene',0) | int }}"
105 |
106 | # Assuming a secondary On or Off is pressed - calculate what the new ID woudl be taking into account rollover
107 | on_count: !input on_count
108 | off_count: !input off_count
109 | on_scene_id: "{{ ((current_scene + 1) % (on_count | int)) | string}}"
110 | off_scene_id: "{{ ((current_scene + 1) % (off_count | int)) | string}}"
111 |
112 | # Extract the command from MQTT payload - and construct the trigger key which is made up
113 | # of the last stored light state + the command
114 | command: "{{ trigger.payload.split('_')[0] }}"
115 | trigger_key: "{{light_state}}:{{command}}"
116 |
117 | # Construct various dictionaries entries for this automation
118 | # Off/On Cycle will use their according data values as well
119 | dict_base: "{{dict({device_topic:{'state':command, 'scene':0}})}}"
120 | dict_on: "{{ dict({device_topic:{'state':command, 'scene':on_scene_id}}) }} "
121 | dict_off: "{{ dict({device_topic:{'state':command, 'scene':off_scene_id}}) }} "
122 |
123 | # These dictionaries above will be combined with a filtered dictionary
124 | # We just want to update the existing dictionaries which is supposed done by making a new dictionary
125 | # out of a filtered dicctionary and one of the oens above
126 | dict_filtered: "{{ dict( (stored_json_text).items() | rejectattr('0','eq',device_topic) | list ) }}"
127 |
128 | # Build out JSON Packets
129 | #
130 | # If we have a state transition we'll send the base_json packet
131 | # if we are Cycling we send either on_json or off_json accordingly
132 | base_json: " {{ dict(dict_base, **dict_filtered) }}"
133 | on_json: " {{ dict(dict_on, **dict_filtered)}}"
134 | off_json: " {{ dict(dict_off, **dict_filtered)}}"
135 |
136 | # Generate Strings arrays
137 | strings_on: "{{[device_topic | lower | replace(' ','_'), command, on_scene_id] }}"
138 | strings_off: "{{[device_topic | lower | replace(' ','_'), command, off_scene_id] }}"
139 |
140 | # Build out the prefix for actions
141 | prefix: "{{ device_topic | lower | replace(' ','_') ~ '_' ~ command ~ '_'}}"
142 | scene_dict: "{% if light_state == 'on' %}{{ dict({'on': on_scene_id, 'off': '0'}) }}{% else %}{{ dict({'on': '0', 'off': off_scene_id}) }}{% endif %}"
143 | scene: "{{ dict({'on':('scene.' ~ (strings_on | join('_'))),'off':('scene.' ~ (strings_off | join('_'))) }) }}"
144 | script: "{{ dict({'on':('script.' ~ (strings_on | join('_'))),'off':('script.' ~ (strings_off | join('_'))) }) }}"
145 |
146 | # Make a list of all scenes/scripts that exist
147 | # this will be used later for a boolean check
148 | all_scenes: "{{ states.scene | map(attribute='entity_id') | list }}"
149 | all_scripts: "{{ states.script | map(attribute='entity_id') | list }}"
150 |
151 | trigger:
152 | - platform: mqtt
153 | topic: zigbee2mqtt/+/action
154 |
155 | condition:
156 | - alias: "Trigger on correct action"
157 | condition: template
158 | value_template: "{{ (trigger.topic == 'zigbee2mqtt/' + device_topic + '/action') and (trigger.payload in ['on_press','off_press'])}}"
159 |
160 | action:
161 | - service: input_text.set_value
162 | target:
163 | entity_id: input_text.zigbee2mqtt_json
164 | data:
165 | value: "{{ base_json }}"
166 | - choose:
167 | # ON
168 | - conditions:
169 | - condition: template
170 | alias: "ON"
171 | value_template: "{{ trigger_key == 'off:on' }}"
172 | sequence:
173 | - alias: "Set Base JSON"
174 | service: input_text.set_value
175 | target:
176 | entity_id: input_text.zigbee2mqtt_json
177 | data:
178 | value: "{{base_json}}"
179 |
180 | # ON_CYCLE
181 | - conditions:
182 | - condition: template
183 | alias: "ON_CYCLE"
184 | value_template: "{{ trigger_key == 'on:on' }}"
185 | sequence:
186 | - alias: "Increment Scene"
187 | service: input_text.set_value
188 | target:
189 | entity_id: input_text.zigbee2mqtt_json
190 | data:
191 | value: "{{on_json}}"
192 |
193 | # OFF
194 | - conditions:
195 | - condition: template
196 | alias: "OFF"
197 | value_template: "{{ trigger_key == 'on:off' }}"
198 | sequence:
199 | - alias: "Set Base JSON"
200 | service: input_text.set_value
201 | target:
202 | entity_id: input_text.zigbee2mqtt_json
203 | data:
204 | value: "{{base_json}}"
205 |
206 | # OFF_CYCLE
207 | - conditions:
208 | - condition: template
209 | alias: "OFF_CYCLE"
210 | value_template: "{{ trigger_key == 'off:off' }}"
211 | sequence:
212 | - alias: "Increment Scene"
213 | service: input_text.set_value
214 | target:
215 | entity_id: input_text.zigbee2mqtt_json
216 | data:
217 | value: "{{off_json}}"
218 | - parallel:
219 | - sequence:
220 | - condition: template
221 | value_template: "{{ 'scene.' ~ prefix ~ scene_dict[command] in all_scenes }}"
222 | - service: scene.turn_on
223 | data:
224 | transition: 1
225 | target:
226 | entity_id: "scene.{{prefix ~ scene_dict[command]}}"
227 | - sequence:
228 | - condition: template
229 | value_template: "{{ 'script.' ~ prefix ~ scene_dict[command] in all_scripts }}"
230 | - service: "script.{{prefix ~ scene_dict[command]}}"
231 |
--------------------------------------------------------------------------------
/icons/dev/awtrixPreview.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Awtrix Light Capture
5 |
51 |
207 |
208 |
209 |
Awtrix Light Capture
210 |
211 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
266 |
267 |
268 |
--------------------------------------------------------------------------------
/scripts/inovelli_led.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | # Calculation References:
3 | # https://nathanfiscus.github.io/inovelli-notification-calc/
4 | # https://community.inovelli.com/t/home-assistant-2nd-gen-switch-rgb-working/168/62
5 | # https://docs.google.com/spreadsheets/d/1bEpujdvBPZY9Fl61PZLUWuHanD2VAkRORFZ5D9xjLzA/edit?usp=sharing
6 | #
7 | # Changes:
8 | # July 22, 2020: Incorporating changes from Kevin Schlichter.
9 | # https://github.com/kschlichter/Home-Assistant-Inovelli-Red-Dimmer-Switch
10 | #
11 | # September 17, 2020: There are some massive improvements to my version of this code. Here are the highlights:
12 | # 1. Choose - using the recently added choose: feature a separate call has been created for the Z-wave and OZW
13 | # versions of the service call.
14 | # 2. Variables - using 0.115’s new variables: feature the variables sent each service call only have to be
15 | # calculated once.
16 | # 3. Supported Features - I realized that I could probably tell the difference between modules using the
17 | # “supported_features” attribute of each switch. For example my dimmer’s supported features is “33”.
18 | #
19 | # September 18, 2020: Added "model" parameter with options of dimmer, switch, combo_light, combo_fan. This replaces
20 | # supported_features as the combo fan/light switch also had the same supported_features value.
21 | #
22 | # February 24, 2021: Added support for Z-Wave JS in place of OpenZwave (ozw).
23 | # The ozw code is remarked out for those that still need it.
24 | #
25 | # February 27, 2021:
26 | # 1. Added zwave_integration at top of "variables:" section to allow users to define which integration is
27 | # installed ("zwave", "ozw", "zwave_js"). I just don't see a simple way to auto-detect this.
28 | # 2. Added a comment describing the "model" variable.
29 | # 3. Replaced personal "script.debug" service call with universal "persistent_notification.create".
30 | # Unremarking these lines could help you troubleshoot why something isn't working as expected.
31 | # 4. Updated broken spreadsheet link with public copy stored in my Google Docs account.
32 | # Thanks for the heads up Kevin Schlichter!
33 | #
34 | # March 26, 2021
35 | # 1. Added fields to help users experimenting in the Services Developer Tool.
36 | #
37 | # April 3, 2021
38 | # Incorporated @firstof9's changes:
39 | # 1. Set execution mode to "parallel" to all this script to potentially run on more than one devices simultaneously.
40 | # 2. Implement ZWave JS's new zwave_js.bulk_set_partial_config_parameters command.
41 | # Source: https://gist.github.com/firstof9/b88d072a81c54b314fe7ddb901fc5c29
42 | #
43 | mode: parallel
44 | variables:
45 | # REQUIRED to be one of these options: "zwave", "ozw", "zwave_js"
46 | # Advanced: If you'd like to have your device list filtered in the Services Developer Tool,
47 | # then unremark out "integration: zwave_js" under the fields section, and change the integration
48 | # name to match.
49 | zwave_integration: "zwave_js"
50 |
51 | # * Strongly recommended -> Use the passed model type ("dimmer", "switch", "combo_light") when present.
52 | # * If not present, then attempt to identify the type using the "product_name" attribute (which is only
53 | # unfortunately only available in the original zwave integration).
54 | # * Finally, assume the model type is "dimmer".
55 | model: |
56 | {% if model is string %}
57 | {{ model }}
58 | {%- elif state_attr(entity_id, 'product_name') is string %}
59 | {%- if 'LZW31' in state_attr(entity_id, 'product_name') %}
60 | dimmer
61 | {%- elif 'LZW36' in state_attr(entity_id, 'product_name') %}
62 | combo_light
63 | {%- else %}
64 | switch
65 | {%- endif %}
66 | {%- else %}
67 | dimmer
68 | {%- endif %}
69 | parameters:
70 | dimmer: 16
71 | combo_light: 24
72 | combo_fan: 25
73 | switch: 8
74 | node_id: '{{ state_attr(entity_id,"node_id") }}'
75 | color: |
76 | {%- if color is not number %}
77 | {{ color|default("Yellow")|title }}
78 | {%- else %}
79 | {{ color|int }}
80 | {% endif %}
81 | # 1-10
82 | level: "{{ level|default(4)|int }}"
83 | duration: '{{ duration|default("Indefinitely")|title }}'
84 | effect: '{{ effect|default("Blink")|title }}'
85 | colors:
86 | "Off": 0
87 | "Red": 1
88 | "Orange": 21
89 | "Yellow": 42
90 | "Green": 85
91 | "Cyan": 127
92 | "Teal": 145
93 | "Blue": 170
94 | "Purple": 195
95 | "Light Pink": 220
96 | "Pink": 234
97 | durations:
98 | "Off": 0
99 | "1 Second": 1
100 | "2 Seconds": 2
101 | "3 Seconds": 3
102 | "4 Seconds": 4
103 | "5 Seconds": 5
104 | "6 Seconds": 6
105 | "7 Seconds": 7
106 | "8 Seconds": 8
107 | "9 Seconds": 9
108 | "10 Seconds": 10
109 | "15 Seconds": 15
110 | "20 Seconds": 20
111 | "25 Seconds": 25
112 | "30 Seconds": 30
113 | "35 Seconds": 35
114 | "40 Seconds": 40
115 | "45 Seconds": 45
116 | "50 Seconds": 50
117 | "55 Seconds": 55
118 | "60 Seconds": 60
119 | "2 Minutes": 62
120 | "3 Minutes": 63
121 | "4 Minutes": 64
122 | "10 Minutes": 70
123 | "15 Minutes": 75
124 | "30 Minutes": 90
125 | "45 Minutes": 105
126 | "1 Hour": 120
127 | "2 Hours": 122
128 | "Indefinitely": 255
129 | effects_dimmer:
130 | "Off": 0
131 | "Solid": 1
132 | "Chase": 2
133 | "Fast Blink": 3
134 | "Slow Blink": 4
135 | "Blink": 4
136 | "Pulse": 5
137 | "Breath": 5
138 | effects_switch:
139 | "Off": 0
140 | "Solid": 1
141 | "Fast Blink": 2
142 | "Slow Blink": 3
143 | "Blink": 3
144 | "Pulse": 4
145 | "Breath": 4
146 | sequence:
147 | # Preform the Inovelli math.
148 | - variables:
149 | parameter: "{{ parameters[model|lower] }}"
150 | color: "{{ colors[color|title]|int }}"
151 | duration: "{{ durations[duration|title] }}"
152 | effect: |
153 | {% if model == "switch" %}
154 | {{- effects_switch[effect|title] }}
155 | {%- else %}
156 | {{- effects_dimmer[effect|title] }}
157 | {% endif %}
158 | inovelli_math: |
159 | {%- if effect|int > 0 %}
160 | {{ color|int + (level|int * 256) + (duration|int * 65536) + (effect|int * 16777216) }}
161 | {%- else %}
162 | 0
163 | {% endif %}
164 | # Unremark to provide an notification with troubleshooting information.
165 | # - service: persistent_notification.create
166 | # data:
167 | # title: "DEBUG: script.inovelli_led"
168 | # notification_id: "inovelli_led"
169 | # message: |
170 | # zwave_integration: {{ zwave_integration }}
171 | # model: {{ model }}
172 | # color: '{{ color|title }}'
173 | # level: '{{ level }}'
174 | # duration: '{{ duration|title }}'
175 | # effect: '{{ effect|title }}'
176 | # node_id: '{{ node_id }}'
177 | # parameter: '{{ parameter }}'
178 | # value: '{{ inovelli_math }}'
179 |
180 | - choose:
181 | # The Z-Wave JS integration requires this service call.
182 | - conditions:
183 | - '{{ zwave_integration == "zwave_js" }}'
184 | sequence:
185 | # Clear the previous effect.
186 | - service: zwave_js.bulk_set_partial_config_parameters
187 | target:
188 | entity_id: "{{ entity_id }}"
189 | data:
190 | parameter: "{{ parameter }}"
191 | value: 0
192 |
193 | # Start the new effect.
194 | - service: zwave_js.bulk_set_partial_config_parameters
195 | target:
196 | entity_id: "{{ entity_id }}"
197 | data:
198 | parameter: "{{ parameter }}"
199 | value: "{{ inovelli_math }}"
200 |
201 | # The OZW integration requires this service call.
202 | - conditions:
203 | - '{{ zwave_integration == "ozw" }}'
204 | sequence:
205 | # Clear the previous effect.
206 | - service: ozw.set_config_parameter
207 | data:
208 | node_id: "{{ node_id }}"
209 | parameter: "{{ parameter }}"
210 | value: 0
211 |
212 | # Start the new effect.
213 | - service: ozw.set_config_parameter
214 | data:
215 | node_id: "{{ node_id }}"
216 | parameter: "{{ parameter }}"
217 | value: "{{ inovelli_math }}"
218 |
219 | # The Z-wave integration requires this service call.
220 | default:
221 | # Clear the previous effect.
222 | - service: zwave.set_config_parameter
223 | data:
224 | node_id: "{{ node_id }}"
225 | parameter: "{{ parameter }}"
226 | size: 4
227 | value: 0
228 |
229 | # Start the new effect.
230 | - service: zwave.set_config_parameter
231 | data:
232 | node_id: "{{ node_id }}"
233 | parameter: "{{ parameter }}"
234 | size: 4
235 | value: "{{ inovelli_math }}"
236 |
237 | fields:
238 | entity_id:
239 | description: Light or switch which represents
240 | example: light.red_series_dimmer
241 | selector:
242 | entity:
243 | #integration: zwave_js
244 | model:
245 | description: 'Device type: dimmer (default), switch, combo_light'
246 | example: dimmer
247 | selector:
248 | select:
249 | options:
250 | - dimmer
251 | - switch
252 | - combo_light
253 | color:
254 | description: 'Choose a color.'
255 | example: purple
256 | selector:
257 | select:
258 | options:
259 | - "Off"
260 | - Red
261 | - Orange
262 | - Yellow
263 | - Green
264 | - Cyan
265 | - Teal
266 | - Blue
267 | - Purple
268 | - Light Pink
269 | - Pink
270 | effect:
271 | description: 'Choose an effect.'
272 | example: blink
273 | selector:
274 | select:
275 | options:
276 | - "Off"
277 | - Solid
278 | - Chase
279 | - Fast Blink
280 | - Slow Blink
281 | - Blink
282 | - Pulse
283 | - Breath
284 | duration:
285 | description: 'How long should the effect run?'
286 | example: 10 seconds
287 | selector:
288 | select:
289 | options:
290 | - "Off"
291 | - 1 Second
292 | - 2 Seconds
293 | - 3 Seconds
294 | - 4 Seconds
295 | - 5 Seconds
296 | - 6 Seconds
297 | - 7 Seconds
298 | - 8 Seconds
299 | - 9 Seconds
300 | - 10 Seconds
301 | - 15 Seconds
302 | - 20 Seconds
303 | - 25 Seconds
304 | - 30 Seconds
305 | - 35 Seconds
306 | - 40 Seconds
307 | - 45 Seconds
308 | - 50 Seconds
309 | - 55 Seconds
310 | - 60 Seconds
311 | - 2 Minutes
312 | - 3 Minutes
313 | - 4 Minutes
314 | - 10 Minutes
315 | - 15 Minutes
316 | - 30 Minutes
317 | - 45 Minutes
318 | - 1 Hour
319 | - 2 Hours
320 | - Indefinitely
--------------------------------------------------------------------------------
/icons/dev/makeRGB.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from PIL import Image
3 | import json
4 | import os
5 |
6 |
7 | class RGBMaker:
8 | def __init__(self, filename):
9 | self.filename = filename
10 | self.stripped_filename = self.strip_extension_and_path(filename)
11 | self.width = None
12 | self.height = None
13 |
14 | def strip_extension_and_path(self, filename):
15 | base_filename = os.path.basename(filename)
16 | stripped_filename = os.path.splitext(base_filename)[0]
17 | return stripped_filename
18 |
19 | def frame_to_rgb(self, frame, background_value=0):
20 | # Convert the frame image to RGBA mode
21 | rgba_frame = frame.convert("RGBA")
22 |
23 | # Get the dimensions of the frame
24 | width, height = rgba_frame.size
25 |
26 | # Create an empty list to store the RGB888 pixel values
27 | rgb888_data = []
28 |
29 | # Iterate over each pixel in the frame
30 | for y in range(height):
31 | for x in range(width):
32 | # Get the RGBA values of the pixel in the frame
33 | r, g, b, a = rgba_frame.getpixel((x, y))
34 |
35 | # Replace the background color with the provided background value
36 | if a == 0:
37 | r, g, b = background_value, background_value, background_value
38 |
39 | # Convert the RGB values to RGB565 format
40 | rgb565 = ((r & 0b11111000) << 8) | ((g & 0b11111100) << 3) | (b >> 3)
41 | # Convert the RGB values to RGB888 format
42 | rgb888 = (r << 16) | (g << 8) | b
43 |
44 | # Append the RGB888 value to the list
45 | rgb888_data.append(rgb888)
46 |
47 | return rgb888_data
48 |
49 | def clean_frame(self, frame):
50 | # Clean the frame to ensure all frames have the same size and black background
51 | cleaned_frame = Image.new("RGBA", frame.size, (0, 0, 0))
52 | cleaned_frame.paste(frame, (0, 0), frame)
53 | return cleaned_frame
54 |
55 | def make_rgb(self, background_value=0):
56 | # Open the GIF image
57 | try:
58 | gif_image = Image.open(self.filename)
59 | except IOError:
60 | print(
61 | f"Error: Unable to open '{self.filename}'. Please make sure it is a valid GIF image."
62 | )
63 | return []
64 |
65 | # Create an empty list to store the RGB888 pixel values
66 | rgb888_data_frames = []
67 |
68 | self.width, self.height = gif_image.size
69 |
70 | # Iterate over each frame in the GIF image
71 | try:
72 | while True:
73 | # Get the current frame
74 | frame = gif_image.convert("RGBA")
75 |
76 | # Clean the frame to ensure all frames have the same size and black background
77 | cleaned_frame = self.clean_frame(frame)
78 |
79 | # Convert the cleaned frame to RGB888 format
80 | rgb888_data = self.frame_to_rgb(cleaned_frame, background_value)
81 |
82 | # Append the current frame's RGB888 data to the list of frames
83 | rgb888_data_frames.append(rgb888_data)
84 |
85 | # Move to the next frame
86 | gif_image.seek(gif_image.tell() + 1)
87 |
88 | except EOFError:
89 | pass
90 |
91 | return rgb888_data_frames
92 |
93 | def print_color_palette_from_data(self, data, background_value=0):
94 | print("Color Palette From Data:")
95 | unique_colors = set(data) # Get unique colors
96 |
97 | # Add the background value to the unique colors set
98 | unique_colors.add(background_value)
99 |
100 | # Count the occurrences of each color
101 | color_counts = {color: data.count(color) for color in unique_colors}
102 |
103 | for rgb888 in unique_colors:
104 | # Extract the RGB components from RGB888 format
105 | r = (rgb888 >> 16) & 0xFF
106 | g = (rgb888 >> 8) & 0xFF
107 | b = rgb888 & 0xFF
108 |
109 | # Convert the RGB components to hexadecimal
110 | hex_value = "0x{:04X}".format(rgb888)
111 |
112 | # Format the columns for alignment
113 | swatch_col = "\033[48;2;{};{};{}m \033[0m".format(r, g, b)
114 | rgb888_col = "{:<7}".format(rgb888)
115 | hex_col = "{:<9}".format(hex_value)
116 | count_col = "{:<5}".format(color_counts[rgb888])
117 |
118 | # Print the color information, count, and ASCII swatch
119 | print(
120 | "Swatch: {} RGB888: {} Hex: {} Count: {}".format(
121 | swatch_col, rgb888_col, hex_col, count_col
122 | )
123 | )
124 | print("\n")
125 |
126 | def print_color_palette_from_image(self):
127 | # Open the image file
128 | try:
129 | image = Image.open(self.filename)
130 | except IOError:
131 | print(
132 | f"Error: Unable to open '{self.filename}'. Please make sure it is a valid image."
133 | )
134 | return
135 |
136 | # Check if the image has a color palette
137 | if image.mode == "P":
138 | # Get the color palette
139 | palette = image.getpalette()
140 |
141 | print("Color Palette From Image:")
142 | unique_colors = set()
143 |
144 | # Iterate over the palette and extract unique colors
145 | for i in range(0, len(palette), 3):
146 | # Get the RGB values
147 | r, g, b = palette[i : i + 3]
148 |
149 | # Convert RGB to RGB888 format
150 | rgb888 = ((r & 0b11111000) << 8) | ((g & 0b11111100) << 3) | (b >> 3)
151 |
152 | # Add the RGB888 value to the set of unique colors
153 | unique_colors.add(rgb888)
154 |
155 | # Print the unique colors and their ASCII swatches
156 | self.print_color_palette_from_data(unique_colors)
157 | else:
158 | print("The image does not have a color palette.")
159 |
160 | def replace(self, data, old_value, new_value):
161 | # Replace the old value with the new value in the data list
162 | return [new_value if value == old_value else value for value in data]
163 |
164 | def print_ascii_swatches(self, rgb888_data):
165 | print("ASCII Swatches:")
166 |
167 | for y in range(self.height):
168 | for x in range(self.width):
169 | rgb888 = rgb888_data[y * self.width + x]
170 |
171 | # Extract the RGB components from RGB888 format
172 | r = (rgb888 >> 16) & 0xFF
173 | g = (rgb888 >> 8) & 0xFF
174 | b = rgb888 & 0xFF
175 |
176 | # Determine the ANSI color code based on the RGB components
177 | ansi_color_code = 16 + (36 * (r // 51)) + (6 * (g // 51)) + (b // 51)
178 |
179 | # Print the ANSI color escape sequence and a space to represent the swatch
180 | print("\033[48;5;{}m \033[0m".format(ansi_color_code), end="")
181 |
182 | print() # Move to the next line after printing each row
183 |
184 | def print_rgb888_data(self, rgb888_data, colors=True):
185 | print("RGB888 Data ({}x{}):".format(self.width, self.height))
186 |
187 | max_value_length = len(
188 | str(max(rgb888_data))
189 | ) # Calculate the maximum length of RGB888 values
190 |
191 | for i in range(0, len(rgb888_data), self.width):
192 | row = rgb888_data[i: i + self.width]
193 | row_str = ""
194 |
195 | for value in row:
196 | value_str = "{:{}}".format(value, max_value_length)
197 |
198 | if colors:
199 | # Extract the RGB components from RGB888 format
200 | r = (value >> 16) & 0xFF
201 | g = (value >> 8) & 0xFF
202 | b = value & 0xFF
203 |
204 | # Convert RGB565 to RGB888 format
205 | r = (r | (r >> 5)) & 0xFF
206 | g = (g | (g >> 6)) & 0xFF
207 | b = (b | (b >> 5)) & 0xFF
208 |
209 | # Determine the ANSI color code based on the RGB components
210 | ansi_color_code = (
211 | 16 + (36 * (r // 51)) + (6 * (g // 51)) + (b // 51)
212 | )
213 |
214 | # Create the color escape sequence
215 | color_escape = f"\033[38;5;{ansi_color_code}m"
216 |
217 | value_str = f"{color_escape}{value_str}\033[0m"
218 |
219 | row_str += value_str + ", "
220 |
221 | row_str = row_str.rstrip(", ")
222 |
223 | if i < len(rgb888_data) - self.width:
224 | row_str += ","
225 | print(row_str)
226 |
227 | def generate_test_data(self, rgb888_data):
228 | test_data = {
229 | "stack": False,
230 | "draw": [{"db": [0, 0, self.width, self.height, rgb888_data]}],
231 | }
232 | return test_data
233 |
234 | def generate_macro(self, rgb888_data, frame_index):
235 | macro_name = f"{self.stripped_filename}_{frame_index}"
236 |
237 | header = "{%- macro " + macro_name + "(x,y) %}"
238 |
239 | body = (
240 | '{"db": [{{x}}, {{y}}, '
241 | + str(self.width)
242 | + ", "
243 | + str(self.height)
244 | + ", "
245 | + str(rgb888_data)
246 | + "]}"
247 | )
248 | footer = "{%- endmacro %}"
249 |
250 | return f"{header}\n{body}\n{footer}\n"
251 |
252 |
253 | def main():
254 | if len(sys.argv) < 2:
255 | print("Please provide the filenames of the GIF images.")
256 | sys.exit(1)
257 |
258 | gif_filenames = sys.argv[1:]
259 |
260 | for gif_filename in gif_filenames:
261 | rgb_maker = RGBMaker(gif_filename)
262 |
263 | # Call the make_rgb function with the GIF filename
264 | rgb888_data_frames = rgb_maker.make_rgb()
265 |
266 | if not rgb888_data_frames:
267 | continue
268 |
269 | frame_count = len(rgb888_data_frames)
270 |
271 | # Print the color palette and RGB565 data for each frame
272 | print(f"--- Color Palette and RGB565 Data for {gif_filename} ---")
273 |
274 | for frame_index, rgb888_data in enumerate(rgb888_data_frames):
275 | print(f"Frame {frame_index + 1} of {frame_count}:")
276 | rgb_maker.print_color_palette_from_data(rgb888_data)
277 | rgb_maker.print_ascii_swatches(rgb888_data)
278 |
279 | print(f"--- Raw Data for Frame {frame_index + 1} of {frame_count} ---")
280 | rgb_maker.print_rgb888_data(rgb888_data, colors=True)
281 |
282 | # Generate the test data dictionary for the frame
283 | test_data = rgb_maker.generate_test_data(rgb888_data)
284 | test_data_json = json.dumps(test_data)
285 |
286 | macro = rgb_maker.generate_macro(
287 | rgb888_data=rgb888_data, frame_index=frame_index
288 | )
289 |
290 | print("\n--- Drawing Macro Frame {frame_index + 1} of {frame_count} ---")
291 | print(macro)
292 |
293 | print("--- Test Data JSON for Frame {frame_index + 1} of {frame_count} ---")
294 | print(test_data_json)
295 | print()
296 |
297 |
298 | if __name__ == "__main__":
299 | main()
300 |
--------------------------------------------------------------------------------
/blueprints/automation/awtrix_calendar_countdown.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | blueprint:
3 | name: AWTRIX Calendar 📅️ Countdown Timer
4 | description: >
5 | Monitors the state of a calendar and sets the indicator based on the status of events
6 |
7 | 
8 | domain: automation
9 | input:
10 | awtrix:
11 | name: AWTRIX Device
12 | description: Select the Awtrix light
13 | selector:
14 | device:
15 | filter:
16 | integration: mqtt
17 | manufacturer: Blueforcer
18 | model: AWTRIX 3
19 | multiple: true
20 | calendar:
21 | name: Calendar
22 | selector:
23 | entity:
24 | filter:
25 | domain: calendar
26 |
27 | mode: queued
28 | variables:
29 | calendar: !input calendar
30 | cal_state: "{{states(calendar)}}"
31 |
32 | device_ids: !input awtrix
33 | devices_topics: >-
34 | {%- macro get_device_topic(device_id) %}
35 | {{- states((device_entities(device_id) | select('search','device_topic') | list)[0]) }}
36 | {%- endmacro %}
37 |
38 | {%- set ns = namespace(devices=[]) %}
39 | {%- for device_id in device_ids %}
40 | {%- set device=get_device_topic(device_id)|replace(' ','') %}
41 | {% set ns.devices = ns.devices + [ device ~ '/indicator' ~ light_suffix] %}
42 | {%- endfor %}
43 | {{ ns.devices | reject('match','unavailable') | list}}
44 |
45 | trigger:
46 | - trigger: time_pattern
47 | seconds: /1
48 | id: temporal
49 |
50 | action:
51 | - action: calendar.get_events
52 | target: "{{ calendar }}"
53 | data:
54 | duration:
55 | hours: 8
56 | minutes: 0
57 | secons: 0
58 | response_variable: v2_events
59 |
60 | - action: calendar.list_events
61 | data:
62 | duration:
63 | hours: 8
64 | minutes: 0
65 | seconds: 0
66 | target:
67 | entity_id: "{{ calendar }}"
68 | response_variable: cal_events
69 | - variables:
70 | event_list: >
71 | {%- set time_period_phrases = [{'language': 'en','phrases':{ 'year': ['year', 'years', 'yr'], 'month': ['month', 'months', 'mth'], 'week': ['week', 'weeks', 'wk'], 'day': ['day', 'days', 'day'], 'hour': ['hour', 'hours', 'hr'], 'minute': ['minute', 'minutes', 'min'], 'second': ['second', 'seconds', 'sec'], 'combine': ' and ', 'error': 'Incorrect date'}}]%}
72 |
73 | {#
74 | macro to split a timedelta in years, months, weeks, days, hours, minutes, seconds
75 | used by the relative time plus macro, set up as a separator macro so it can be reused
76 | #}
77 | {%- macro time_split(date, time, compare_date) -%}
78 | {# set defaults for variables #}
79 | {%- set date = date | as_local -%}
80 | {%- set time = time | default(true) | bool(true) -%}
81 | {%- set n = compare_date if compare_date is defined else now() -%}
82 | {%- set n = n if time else today_at() -%}
83 | {%- set a = [n, date] | max -%}
84 | {%- set b = [n, date] | min -%}
85 | {#- set time periods in seconds #}
86 | {%- set m, h, d, w = 60, 3600, 86400, 604800 -%}
87 | {#- set numer of years, and set n to value using this number of years #}
88 | {%- set yrs = a.year - b.year - (1 if a.replace(year=b.year) < b else 0) -%}
89 | {%- set a = a.replace(year=a.year - yrs) -%}
90 | {#- set numer of months, and set n to value using this number of months #}
91 | {%- set mth = (a.month - b.month - (1 if a.day < b.day else 0) + 12) % 12 -%}
92 | {%- set month_new = (((a.month - mth) + 12) % 12) | default(12, true) -%}
93 | {%- set day_max = ((a.replace(day=1, month=month_new) + timedelta(days=31)).replace(day=1) - timedelta(days=1)).day -%}
94 | {%- set a_temp = a.replace(month=month_new, day=[a.day, day_max]|min) -%}
95 | {%- set a = a_temp if a_temp <= a else a_temp.replace(year=a.year-1) -%}
96 | {#- set other time period variables #}
97 | {%- set s = (a - b).total_seconds() -%}
98 | {%- set wks = (s // w) | int -%}
99 | {%- set day = ((s - wks * w) // d) | int -%}
100 | {%- set hrs = ((s - wks * w - day * d) // h) | int -%}
101 | {%- set min = ((s - wks * w - day * d - hrs * h) // m) | int -%}
102 | {%- set sec = (s - wks * w - day * d - hrs * h - min * m) | int -%}
103 | {# output result #}
104 | {{- dict(y=yrs, mo=mth, w=wks, d=day, h=hrs, m=min, s=sec) | to_json -}}
105 | {%- endmacro -%}
106 |
107 | {# macro to output a timedelta in a readable format #}
108 | {%- macro relative_time_plus(date, parts, week, time, abbr, language, compare_date, verbose) -%}
109 | {#- set defaults for input if not entered #}
110 | {%- set date = date | as_datetime if date is string or date is number else date -%}
111 | {%- set compare_date = compare_date if compare_date is defined else now() -%}
112 | {%- set compare_date = compare_date | as_datetime if compare_date is string or compare_date is number else compare_date -%}
113 | {%- set phrases = time_period_phrases -%}
114 | {#- select correct phrases bases on language input #}
115 | {%- set language = language | default() -%}
116 | {%- set languages = phrases | map(attribute='language') | list -%}
117 | {%- set language = iif(language in languages, language, languages | first) -%}
118 | {%- set phr = phrases | selectattr('language', 'eq', language) | map(attribute='phrases') | list | first -%}
119 | {#- check for valid datetime (using as_timestamp) #}
120 | {%- if as_timestamp(date, default='error') != 'error' -%}
121 | {%- set date = date | as_local -%}
122 | {%- set parts = parts | default(1) | int(1) -%}
123 | {%- set week = week | default(true) | bool(true) -%}
124 | {%- set time = time | default(true) | bool(true) -%}
125 | {%- set abbr = abbr | default(false) | bool(false) or verbose | default(false) | bool(false) -%}
126 | {%- set language = language | default('first') -%}
127 | {%- set date = date if time else today_at().replace(year=date.year, month=date.month, day=date.day) -%}
128 | {%- set tp = time_split(date, time, compare_date) | from_json -%}
129 | {#- create mapping #}
130 | {%- set wk = tp.w if week else 0 -%}
131 | {%- set dy = tp.d if week else tp.d + tp.w * 7 -%}
132 | {%- set dur = dict(
133 | yrs = dict(a=tp.y, d=phr.year[2] if abbr else phr.year[1] if tp.y > 1 else phr.year[0]),
134 | mth = dict(a=tp.mo, d=phr.month[2] if abbr else phr.month[1] if tp.mo > 1 else phr.month[0]),
135 | wks = dict(a=wk, d=phr.week[2] if abbr else phr.week[1] if wk > 1 else phr.week[0]),
136 | day = dict(a=dy, d=phr.day[2] if abbr else phr.day[1] if dy > 1 else phr.day[0]),
137 | hrs = dict(a=tp.h, d=phr.hour[2] if abbr else phr.hour[1] if tp.h > 1 else phr.hour[0]),
138 | min = dict(a=tp.m, d=phr.minute[2] if abbr else phr.minute[1] if tp.m > 1 else phr.minute[0]),
139 | sec = dict(a=tp.s, d=phr.second[2] if abbr else phr.second[1] if tp.s > 1 else phr.second[0])
140 | )
141 | -%}
142 | {#- find first non zero time period #}
143 | {%- set first = dur.items() | rejectattr('1.a', 'eq', 0) | map(attribute='0') | first -%}
144 | {#- set variable to reject weeks if set and find index of first non zero time period #}
145 | {%- set week_reject = 'wks' if not week -%}
146 | {%- set index = (dur.keys() | reject('eq', week_reject) | list).index(first) -%}
147 | {#-select non zero items based on input #}
148 | {%- set items = (dur.keys() | reject('eq', week_reject) | list)[index:index + parts] -%}
149 | {%- set selection = dur.items() | selectattr('0', 'in', items) | rejectattr('1.a', 'eq', 0) | list -%}
150 | {#- create list of texts per selected time period #}
151 | {%- set ns = namespace(text = []) -%}
152 | {%- for i in selection -%}
153 | {%- set ns.text = ns.text + [ i[1].a ~ ' ' ~ i[1].d] -%}
154 | {%- endfor -%}
155 | {#- join texts in a string, using phr.combine for the last item #}
156 | {{- ns.text[:-1] | join(', ') ~ phr.combine ~ ns.text[-1] if ns.text | count > 1 else ns.text | first -}}
157 | {%- else -%}
158 | {{- phr.error -}}
159 | {%- endif -%}
160 | {%- endmacro -%}
161 | {%- macro event_to_dict(event) %}
162 | {%- set now_ts = now() | as_timestamp -%}
163 | {%- set start_ts = event.start | as_timestamp | int %}
164 | {%- set end_ts = event.end | as_timestamp | int %}
165 | {%- set rel_start = relative_time_plus(event.start,3) %}
166 | {%- set rel_end = relative_time_plus(event.end,3) %}
167 | {%- set has_started = iif(now_ts > start_ts,1,0) -%}
168 | {%- set has_ended = iif(now_ts > end_ts,1,0) -%}
169 | {%- set start_sec = ((start_ts - now_ts) ) | int -%}
170 | {%- set end_sec = ((end_ts - now_ts) ) | int -%}
171 | {%- set start_min = ((start_ts - now_ts) / 60) | int -%}
172 | {%- set end_min = ((end_ts - now_ts) / 60 ) | int -%}
173 |
174 | {%- set starts_in_1 = iif((0 < start_min) and (start_min <= 1),1,0) -%}
175 | {%- set starts_in_5 = iif((0 < start_min) and (start_min <= 5),1,0) -%}
176 | {%- set starts_in_10 = iif((0 < start_min) and (start_min <= 10),1,0) -%}
177 | {
178 | "summary": "{{event.summary}}"
179 | ,"location":{{ ((event.location | replace('\n', ' ')) | to_json)}}
180 | ,"start_ts":{{start_ts}}
181 | ,"end_ts":{{end_ts}}
182 | ,"rel_start":"{{rel_start}}"
183 | ,"rel_end":"{{rel_end}}"
184 | ,"has_started":{{has_started}}
185 | ,"has_ended":{{has_ended}}
186 | ,"start_sec":{{ start_sec}}
187 | ,"end_sec":{{ end_sec}}
188 | ,"start_min":{{ start_min}}
189 | ,"end_min":{{ end_min}}
190 |
191 | ,"starts_in_1": {{starts_in_1}}
192 | ,"starts_in_5": {{starts_in_5}}
193 | ,"starts_in_10": {{starts_in_10}}
194 |
195 | }
196 | {%- endmacro %}
197 | {%- set ns = namespace(parsed_events=[]) %}
198 | {%- for event in cal_events.events %}
199 | {%- set e_dict = event_to_dict(event) %}
200 |
201 | {%- set ns.parsed_events = ns.parsed_events + [ (e_dict | from_json )] %}
202 | {%- endfor -%}
203 | {{ns.parsed_events | sort(attribute='start_ts')}}
204 | locations: >-
205 | {%- set ns = namespace(locations=[]) %}
206 | {%- for event in cal_events.events %}
207 | {% set loc = {"locations": ((event.location | replace('\n', ' ')) | to_json) } %}
208 | {%- set ns.locations = ns.locations + [loc] + [loc] %}
209 | {%- endfor -%}
210 | {{ns.locations}}
211 |
212 | current_events: >-
213 | {%- set ns = namespace(current_events=[]) %}
214 | {%- for event in event_list %}
215 |
216 | {%- if event['start_min'] <= 0 %}
217 | {%- set ns.current_events = ns.current_events + [ event ] %}
218 | {%- endif %}
219 | {%- endfor -%}
220 | {{ns.current_events}}
221 | current_events_count: "{{current_events | length }}"
222 | current_events_text: "{{ current_events | map(attribute='summary') | join(' / ') }}"
223 | time_remaining: >-
224 | {%- if current_events_count > 0 %}
225 | {%- macro convert_s(seconds) -%}
226 | {% set hours = (seconds / 60 / 60 ) % 60 -%}
227 | {% set remaining_minutes = (seconds / 60) % 60 -%}
228 | {% set seconds = (seconds % 60) -%}
229 | {{ "%02d:%02d:%02d" | format(hours, remaining_minutes, seconds) }}
230 | {%- endmacro %}
231 | {{convert_s(current_events[0]['end_sec'])}}
232 | {% endif %}
233 |
234 | progress: >-
235 | {%- set total_time = (-1 * current_events[0]['start_min']) + current_events[0]['end_min'] %}
236 | {%- set done = (-1 * current_events[0]['start_min']) %}
237 | {{done/total_time * 100}}
238 |
239 | payload: >-
240 |
241 | {"stack":false, "text":"{{time_remaining}}", "progress":"{{progress}}" }
242 |
243 | - action: mqtt.publish
244 | data:
245 | qos: 0
246 | topic: awtrix_1/notify
247 | payload: >-
248 | {{payload}}
249 |
--------------------------------------------------------------------------------