├── .devcontainer
└── devcontainer.json
├── .gitattributes
├── .github
├── ISSUE_TEMPLATE
│ ├── feature_request.md
│ └── issue.md
├── dependabot.yml
└── workflows
│ ├── cron.yaml
│ ├── pull.yml
│ └── push.yml
├── .gitignore
├── .vscode
├── launch.json
├── settings.json
└── tasks.json
├── LICENSE
├── README.md
├── custom_components
├── __init__.py
└── qr_generator
│ ├── __init__.py
│ ├── config_flow.py
│ ├── const.py
│ ├── icons.json
│ ├── image.py
│ ├── manifest.json
│ ├── services.py
│ ├── services.yaml
│ ├── strings.json
│ └── translations
│ ├── en.json
│ └── sk.json
├── hacs.json
├── requirements_dev.txt
├── requirements_test.txt
├── setup.cfg
└── tests
├── __init__.py
├── conftest.py
├── test_config_flow.py
└── test_image.py
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | // See https://aka.ms/vscode-remote/devcontainer.json for format details.
2 | {
3 | "image": "python:3.13",
4 | "name": "QR-Code Generator integration development",
5 | "context": "..",
6 | "appPort": [
7 | "9123:8123"
8 | ],
9 | "extensions": [
10 | "ms-python.python",
11 | "github.vscode-pull-request-github",
12 | "ryanluker.vscode-coverage-gutters",
13 | "ms-python.vscode-pylance"
14 | ],
15 | "settings": {
16 | "files.eol": "\n",
17 | "editor.tabSize": 4,
18 | "terminal.integrated.shell.linux": "/bin/bash",
19 | "python.pythonPath": "/usr/bin/python3",
20 | "python.analysis.autoSearchPaths": false,
21 | "python.linting.pylintEnabled": true,
22 | "python.linting.enabled": true,
23 | "python.formatting.provider": "black",
24 | "editor.formatOnPaste": false,
25 | "editor.formatOnSave": true,
26 | "editor.formatOnType": true,
27 | "files.trimTrailingWhitespace": true
28 | }
29 | }
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto eol=lf
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 |
5 | ---
6 |
7 | **Is your feature request related to a problem? Please describe.**
8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
9 |
10 | **Describe the solution you'd like**
11 | A clear and concise description of what you want to happen.
12 |
13 | **Describe alternatives you've considered**
14 | A clear and concise description of any alternative solutions or features you've considered.
15 |
16 | **Additional context**
17 | Add any other context or screenshots about the feature request here.
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/issue.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Issue
3 | about: Create a report to help us improve
4 |
5 | ---
6 |
7 |
16 |
17 | ## Version of the custom_component
18 |
21 |
22 | ## Describe the bug
23 | A clear and concise description of what the bug is.
24 |
25 |
26 | ## Debug log
27 |
28 |
29 |
30 | ```text
31 |
32 | Add your logs here.
33 |
34 | ```
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: github-actions
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | - package-ecosystem: pip
8 | directory: "/"
9 | schedule:
10 | interval: daily
--------------------------------------------------------------------------------
/.github/workflows/cron.yaml:
--------------------------------------------------------------------------------
1 | name: Cron actions
2 |
3 | on:
4 | schedule:
5 | - cron: '0 0 * * *'
6 |
7 | jobs:
8 | validate:
9 | runs-on: "ubuntu-latest"
10 | name: Validate
11 | steps:
12 | - uses: "actions/checkout@v4"
13 |
14 | - name: HACS validation
15 | uses: "hacs/action@main"
16 | with:
17 | category: "integration"
18 | ignore: brands
19 |
20 | - name: Hassfest validation
21 | uses: "home-assistant/actions/hassfest@master"
--------------------------------------------------------------------------------
/.github/workflows/pull.yml:
--------------------------------------------------------------------------------
1 | name: Pull actions
2 |
3 | on:
4 | pull_request:
5 |
6 | jobs:
7 | validate:
8 | runs-on: "ubuntu-latest"
9 | name: Validate
10 | steps:
11 | - uses: "actions/checkout@v4"
12 |
13 | - name: HACS validation
14 | uses: "hacs/action@main"
15 | with:
16 | category: "integration"
17 | ignore: brands
18 |
19 | - name: Hassfest validation
20 | uses: "home-assistant/actions/hassfest@master"
21 |
22 | style:
23 | runs-on: "ubuntu-latest"
24 | name: Check style formatting
25 | steps:
26 | - uses: "actions/checkout@v4"
27 | - uses: "actions/setup-python@v5"
28 | with:
29 | python-version: "3.x"
30 | - run: python3 -m pip install black
31 | - run: black .
32 |
33 | tests:
34 | runs-on: "ubuntu-latest"
35 | name: Run tests
36 | steps:
37 | - name: Check out code from GitHub
38 | uses: "actions/checkout@v4"
39 | - name: Setup Python
40 | uses: "actions/setup-python@v5"
41 | with:
42 | python-version: "3.13"
43 | - name: Install requirements
44 | run: python3 -m pip install -r requirements_test.txt
45 | - name: Run tests
46 | run: |
47 | pytest \
48 | -qq \
49 | --timeout=9 \
50 | --durations=10 \
51 | --asyncio-mode=auto \
52 | -n auto \
53 | --cov custom_components.qr_generator \
54 | -o console_output_style=count \
55 | -p no:sugar \
56 | tests
57 |
--------------------------------------------------------------------------------
/.github/workflows/push.yml:
--------------------------------------------------------------------------------
1 | name: Push actions
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | - dev
8 |
9 | jobs:
10 | validate:
11 | runs-on: "ubuntu-latest"
12 | name: Validate
13 | steps:
14 | - uses: "actions/checkout@v4"
15 |
16 | - name: HACS validation
17 | uses: "hacs/action@main"
18 | with:
19 | category: "integration"
20 | ignore: brands
21 |
22 | - name: Hassfest validation
23 | uses: "home-assistant/actions/hassfest@master"
24 |
25 | style:
26 | runs-on: "ubuntu-latest"
27 | name: Check style formatting
28 | steps:
29 | - uses: "actions/checkout@v4"
30 | - uses: "actions/setup-python@v5"
31 | with:
32 | python-version: "3.x"
33 | - run: python3 -m pip install black
34 | - run: black .
35 |
36 | tests:
37 | runs-on: "ubuntu-latest"
38 | name: Run tests
39 | steps:
40 | - name: Check out code from GitHub
41 | uses: "actions/checkout@v4"
42 | - name: Setup Python
43 | uses: "actions/setup-python@v5"
44 | with:
45 | python-version: "3.13"
46 | - name: Install requirements
47 | run: python3 -m pip install -r requirements_test.txt
48 | - name: Run tests
49 | run: |
50 | pytest \
51 | -qq \
52 | --timeout=9 \
53 | --durations=10 \
54 | --asyncio-mode=auto \
55 | -n auto \
56 | --cov custom_components.qr_generator \
57 | -o console_output_style=count \
58 | -p no:sugar \
59 | tests
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__
2 | pythonenv*
3 | venv
4 | .venv
5 | .coverage
6 | .idea
7 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
3 | "version": "0.2.0",
4 | "configurations": [
5 | {
6 | // Example of attaching to local debug server
7 | "name": "Python: Attach Local",
8 | "type": "python",
9 | "request": "attach",
10 | "port": 5678,
11 | "host": "localhost",
12 | "pathMappings": [
13 | {
14 | "localRoot": "${workspaceFolder}",
15 | "remoteRoot": "."
16 | }
17 | ]
18 | },
19 | {
20 | // Example of attaching to my production server
21 | "name": "Python: Attach Remote",
22 | "type": "python",
23 | "request": "attach",
24 | "port": 5678,
25 | "host": "homeassistant.local",
26 | "pathMappings": [
27 | {
28 | "localRoot": "${workspaceFolder}",
29 | "remoteRoot": "/usr/src/homeassistant"
30 | }
31 | ]
32 | }
33 | ]
34 | }
35 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "python.linting.pylintEnabled": true,
3 | "python.linting.enabled": true,
4 | "python.pythonPath": "/usr/local/bin/python",
5 | "files.associations": {
6 | "*.yaml": "home-assistant"
7 | }
8 | }
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "label": "Run Home Assistant on port 9123",
6 | "type": "shell",
7 | "command": "container start",
8 | "problemMatcher": []
9 | },
10 | {
11 | "label": "Run Home Assistant configuration against /config",
12 | "type": "shell",
13 | "command": "container check",
14 | "problemMatcher": []
15 | },
16 | {
17 | "label": "Upgrade Home Assistant to latest dev",
18 | "type": "shell",
19 | "command": "container install",
20 | "problemMatcher": []
21 | },
22 | {
23 | "label": "Install a specific version of Home Assistant",
24 | "type": "shell",
25 | "command": "container set-version",
26 | "problemMatcher": []
27 | }
28 | ]
29 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Maximilian Maier
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Home Assistant integration to create a QR-Code
2 | [![GitHub Release][releases-shield]][releases]
3 | [![hacs][hacsbadge]][hacs]
4 |
5 | This integration allows to create QR Codes with static or dynamic content.
6 |
7 | ## Features
8 | * Static content
9 | * Dynamic content with the help of Templates
10 | * Custom QR-Code color
11 | * Custom QR-Code background color
12 | * Error correction adjustment
13 |
14 | {% if not installed %}
15 | ## Installation
16 |
17 | The easiest way to install is through HACS. This integration is already included in the HACS default repositories.
18 |
19 | 1. In Home Assistant, select HACS -> Integrations -> + Explore and Download Repositories. Search for QR-Code Generator in the list and add it.
20 | 2. Restart Home Assistant
21 | 3. Set up and configure the integration: [](https://my.home-assistant.io/redirect/config_flow_start/?domain=qr_generator)
22 |
23 | ## Manual Installation
24 |
25 | Copy the `custom_components/qr_generator` directory to your `custom_components` folder. Restart Home Assistant, and add the integration from the integrations page.
26 |
27 | {% endif %}
28 |
29 | ## Example configuration
30 |
31 | ### For wifi access
32 |
33 | Name: `My wifi access`
34 | Content: `WIFI:T:WPA2;S:MyNetworkName;P:ThisIsMyPassword;H:true;`
35 |
36 | ### For wifi access with template
37 |
38 | Name: `My wifi access`
39 | Content: `WIFI:T:WPA2;S:MyNetworkName;P:{{states("input_text.wlan_password")}};H:true;`
40 |
41 | > To update the QR code, simply change the value of `input_text.wlan_password`.
42 |
43 | ## Details
44 |
45 | ### Options
46 |
47 | | Options | Description | Default
48 | | ------------ | -------------------------------------- | -------- |
49 | | `Name` | *(str)* Name of the QR Code. | - |
50 | | `Content` | *(str)* Content of the QR Code. Can be a template. | - |
51 | | `Color` | *(str)* Color of the QR Code in hex. Supports transparency (#RRGGBBAA). | #000 |
52 | | `Background color` | *(str)* Color of the background in hex. Supports transparency (#RRGGBBAA). | #FFF |
53 | | `Scale` | *(int)* Scale of the QR Code. | 10 |
54 | | `Border` | *(int)* Thickness of the QR Code border in "QR Code boxes". | 2 |
55 | | `Error correction` | *(str)* Strength of error correction. Possible values:
L - About 7% or less errors can be corrected.
M - About 15% or less errors can be corrected.
Q - About 25% or less errors can be corrected.
H - About 30% or less errors can be corrected. | H |
56 |
57 | ### ATTRIBUTES
58 |
59 | | Attribute | Description |
60 | | ------------ | -------------------------------------- |
61 | | `text` | *(str)* Rendered content of the QR Code. |
62 | | `color` | *(str)* Color of the QR Code in hex. |
63 | | `background_color` | *(str)* Color of the background in hex.|
64 | | `scale` | *(int)* Scale of the QR Code. |
65 | | `border` | *(int)* Thickness of the QR Code border in "QR Code boxes". |
66 | | `error_correction` | *(str)* Strength of error correction. Possible values:
L
M
Q
H |
67 |
68 | [hacs]: https://hacs.xyz
69 | [hacsbadge]: https://img.shields.io/badge/HACS-Default-41BDF5.svg?style=for-the-badge
70 | [releases-shield]: https://img.shields.io/github/v/release/DeerMaximum/QR-Code-Generator.svg?style=for-the-badge
71 | [releases]: https://github.com/DeerMaximum/QR-Code-Generator/releases
72 |
--------------------------------------------------------------------------------
/custom_components/__init__.py:
--------------------------------------------------------------------------------
1 | """Custom components module."""
2 |
--------------------------------------------------------------------------------
/custom_components/qr_generator/__init__.py:
--------------------------------------------------------------------------------
1 | """The QR Generator integration."""
2 | from types import MappingProxyType
3 |
4 | from homeassistant.config_entries import ConfigEntry
5 | from homeassistant.const import Platform
6 | from homeassistant.core import HomeAssistant
7 |
8 | from .const import DOMAIN
9 | from .services import register_services
10 |
11 | PLATFORMS: list[str] = [Platform.IMAGE]
12 |
13 |
14 | async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
15 | """Set up platform from a ConfigEntry."""
16 | if hass.data.get(DOMAIN) is None:
17 | hass.data.setdefault(DOMAIN, {})
18 |
19 | entry.async_on_unload(entry.add_update_listener(_async_update_listener))
20 |
21 | await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
22 |
23 | register_services(hass)
24 |
25 | return True
26 |
27 |
28 | async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
29 | """Unload a config entry."""
30 | return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
31 |
32 |
33 | async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
34 | """Handle options update."""
35 | hass.config_entries.async_update_entry(entry, data=entry.options)
36 | await hass.config_entries.async_reload(entry.entry_id)
37 |
--------------------------------------------------------------------------------
/custom_components/qr_generator/config_flow.py:
--------------------------------------------------------------------------------
1 | """Config flow for the QR Generator integration."""
2 | from __future__ import annotations
3 |
4 | import re
5 | from typing import Any
6 |
7 | import voluptuous as vol
8 |
9 | from homeassistant import config_entries
10 | from homeassistant.const import CONF_NAME, CONF_VALUE_TEMPLATE
11 | from homeassistant.core import callback
12 | from homeassistant.data_entry_flow import FlowResult
13 | from homeassistant.exceptions import TemplateError
14 | from homeassistant.helpers import config_validation as cv, template
15 |
16 | from .const import (
17 | CONF_ADVANCED,
18 | CONF_BACKGROUND_COLOR,
19 | CONF_BORDER,
20 | CONF_COLOR,
21 | CONF_ERROR_CORRECTION,
22 | CONF_SCALE,
23 | DEFAULT_BACKGROUND_COLOR,
24 | DEFAULT_BORDER,
25 | DEFAULT_COLOR,
26 | DEFAULT_ERROR_CORRECTION,
27 | DEFAULT_SCALE,
28 | DOMAIN,
29 | ERROR_CORRECTION_LEVEL,
30 | HEX_COLOR_REGEX,
31 | )
32 |
33 |
34 | def get_schema(config: dict[str, Any] | None = None) -> vol.Schema:
35 | """Generate the schema."""
36 | if not config:
37 | config = {}
38 |
39 | default_name: str = config.get(CONF_NAME, "")
40 | default_value: str = config.get(CONF_VALUE_TEMPLATE, "")
41 |
42 | return vol.Schema(
43 | {
44 | vol.Required(CONF_NAME, default=default_name): cv.string,
45 | vol.Required(CONF_VALUE_TEMPLATE, default=default_value): cv.string,
46 | vol.Optional(CONF_ADVANCED, default=False): cv.boolean,
47 | }
48 | )
49 |
50 |
51 | def get_schema_advanced(config: dict[str, Any] | None = None) -> vol.Schema:
52 | """Generate the schema for the advanced settings."""
53 | if not config:
54 | config = {}
55 |
56 | default_color: str = config.get(CONF_COLOR, DEFAULT_COLOR)
57 | default_scale: int = config.get(CONF_SCALE, DEFAULT_SCALE)
58 | default_border: int = config.get(CONF_BORDER, DEFAULT_BORDER)
59 | default_error_correction: str = config.get(
60 | CONF_ERROR_CORRECTION, DEFAULT_ERROR_CORRECTION
61 | )
62 | default_background_color: str = config.get(
63 | CONF_BACKGROUND_COLOR, DEFAULT_BACKGROUND_COLOR
64 | )
65 |
66 | return vol.Schema(
67 | {
68 | vol.Required(CONF_COLOR, default=default_color): cv.string,
69 | vol.Required(
70 | CONF_BACKGROUND_COLOR, default=default_background_color
71 | ): cv.string,
72 | vol.Required(CONF_SCALE, default=default_scale): cv.positive_int,
73 | vol.Required(CONF_BORDER, default=default_border): cv.positive_int,
74 | vol.Required(
75 | CONF_ERROR_CORRECTION, default=default_error_correction
76 | ): vol.In(ERROR_CORRECTION_LEVEL),
77 | }
78 | )
79 |
80 |
81 | class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
82 | """Handle a config flow for QR Generator."""
83 |
84 | VERSION: int = 1
85 |
86 | override_config: dict[str, Any] = {}
87 |
88 | def __init__(self) -> None:
89 | """Initialize the config flow."""
90 | super().__init__()
91 |
92 | self.config: dict[str, Any] = self.override_config
93 |
94 | async def async_step_user(
95 | self: ConfigFlow,
96 | user_input: dict[str, Any] | None = None,
97 | ) -> FlowResult:
98 | """Handle the initial step."""
99 | errors: dict[str, Any] = {}
100 |
101 | if user_input is not None and not errors:
102 | try:
103 | template.Template( # type: ignore[no-untyped-call]
104 | user_input[CONF_VALUE_TEMPLATE], self.hass
105 | ).async_render()
106 | except TemplateError:
107 | errors["base"] = "invalid_template"
108 | return self.async_show_form(
109 | step_id="user", data_schema=get_schema(), errors=errors
110 | )
111 |
112 | self.config = user_input
113 |
114 | if user_input[CONF_ADVANCED]:
115 | return await self.async_step_advanced()
116 |
117 | return self.async_create_entry(
118 | title=self.config[CONF_NAME], data=self.config
119 | )
120 |
121 | return self.async_show_form(
122 | step_id="user", data_schema=get_schema(), errors=errors
123 | )
124 |
125 | async def async_step_advanced(
126 | self, user_input: dict[str, Any] | None = None
127 | ) -> FlowResult:
128 | """Step for advanced settings."""
129 | errors: dict[str, Any] = {}
130 |
131 | if user_input is not None and not errors:
132 | regex = re.compile(HEX_COLOR_REGEX)
133 |
134 | if regex.match(user_input[CONF_COLOR]) and regex.match(
135 | user_input[CONF_BACKGROUND_COLOR]
136 | ):
137 | self.config.update(user_input)
138 |
139 | return self.async_create_entry(
140 | title=self.config[CONF_NAME], data=self.config
141 | )
142 |
143 | errors["base"] = "invalid_color"
144 |
145 | return self.async_show_form(
146 | step_id="advanced", data_schema=get_schema_advanced(), errors=errors
147 | )
148 |
149 | @staticmethod
150 | @callback
151 | def async_get_options_flow(
152 | config_entry: config_entries.ConfigEntry,
153 | ) -> OptionsFlowHandler:
154 | """Get the options flow for this handler."""
155 | return OptionsFlowHandler(config_entry)
156 |
157 |
158 | class OptionsFlowHandler(config_entries.OptionsFlow):
159 | """Handle a option flow for QR Generator."""
160 |
161 | def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
162 | """Initialize options flow."""
163 | self.config_entry = config_entry
164 | self.data = dict(self.config_entry.data)
165 |
166 | async def async_step_init(
167 | self, user_input: dict[str, Any] | None = None
168 | ) -> FlowResult:
169 | """Handle options flow."""
170 | errors: dict[str, Any] = {}
171 |
172 | if user_input is not None and not errors:
173 |
174 | try:
175 | template.Template( # type: ignore[no-untyped-call]
176 | user_input[CONF_VALUE_TEMPLATE], self.hass
177 | ).async_render()
178 | except TemplateError:
179 | errors["base"] = "invalid_template"
180 | return self.async_show_form(
181 | step_id="init", data_schema=get_schema(self.data), errors=errors
182 | )
183 |
184 | self.data.update(user_input)
185 |
186 | if user_input[CONF_ADVANCED]:
187 | return await self.async_step_advanced()
188 |
189 | return self.async_create_entry(title="", data=self.data)
190 |
191 | return self.async_show_form(
192 | step_id="init", data_schema=get_schema(self.data), errors=errors
193 | )
194 |
195 | async def async_step_advanced(
196 | self, user_input: dict[str, Any] | None = None
197 | ) -> FlowResult:
198 | """Step for advanced settings."""
199 | errors: dict[str, Any] = {}
200 |
201 | if user_input is not None and not errors:
202 | regex = re.compile(HEX_COLOR_REGEX)
203 |
204 | if regex.match(user_input[CONF_COLOR]) and regex.match(
205 | user_input[CONF_BACKGROUND_COLOR]
206 | ):
207 | self.data.update(user_input)
208 |
209 | return self.async_create_entry(title="", data=self.data)
210 |
211 | errors["base"] = "invalid_color"
212 |
213 | return self.async_show_form(
214 | step_id="advanced",
215 | data_schema=get_schema_advanced(self.data),
216 | errors=errors,
217 | )
218 |
--------------------------------------------------------------------------------
/custom_components/qr_generator/const.py:
--------------------------------------------------------------------------------
1 | """Constants for the QR Generator integration."""
2 | from __future__ import annotations
3 |
4 | from logging import Logger, getLogger
5 |
6 | _LOGGER: Logger = getLogger(__package__)
7 |
8 | DOMAIN: str = "qr_generator"
9 |
10 | # Regex to mach color with the #RRGGBBAA format and it short forms
11 | HEX_COLOR_REGEX: str = (
12 | r"^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3}|[A-Fa-f0-9]{4}|[A-Fa-f0-9]{8})$"
13 | )
14 |
15 | ERROR_CORRECTION_LEVEL = ["L", "M", "Q", "H"]
16 |
17 | CONF_COLOR: str = "color"
18 | CONF_SCALE: str = "scale"
19 | CONF_BORDER: str = "border"
20 | CONF_ADVANCED: str = "advanced"
21 | CONF_ERROR_CORRECTION: str = "error_correction"
22 | CONF_BACKGROUND_COLOR: str = "background_color"
23 |
24 | DEFAULT_COLOR: str = "#000000"
25 | DEFAULT_SCALE: int = 10
26 | DEFAULT_BORDER: int = 2
27 | DEFAULT_ERROR_CORRECTION: str = "H"
28 | DEFAULT_BACKGROUND_COLOR: str = "#FFFFFF"
29 |
30 | ATTR_TEXT: str = "text"
31 | ATTR_COLOR: str = CONF_COLOR
32 | ATTR_SCALE: str = CONF_SCALE
33 | ATTR_BORDER: str = CONF_BORDER
34 | ATTR_ERROR_CORRECTION: str = CONF_ERROR_CORRECTION
35 | ATTR_BACKGROUND_COLOR: str = CONF_BACKGROUND_COLOR
36 |
37 | ATTR_FILENAME: str = "filename"
38 |
--------------------------------------------------------------------------------
/custom_components/qr_generator/icons.json:
--------------------------------------------------------------------------------
1 | {
2 | "services": {
3 | "save": "mdi:content-save"
4 | }
5 | }
--------------------------------------------------------------------------------
/custom_components/qr_generator/image.py:
--------------------------------------------------------------------------------
1 | """QR Generator image platform."""
2 |
3 | from __future__ import annotations
4 |
5 | import io
6 | from typing import Any
7 |
8 | from PIL import ImageColor
9 | import pyqrcode
10 |
11 | from homeassistant.util import dt as dt_util
12 | from homeassistant.components.image import ImageEntity
13 | from homeassistant.config_entries import ConfigEntry
14 | from homeassistant.const import CONF_NAME, CONF_VALUE_TEMPLATE
15 | from homeassistant.core import HomeAssistant, callback, Event, EventStateChangedData
16 | from homeassistant.helpers import template
17 | from homeassistant.helpers.entity_platform import AddEntitiesCallback
18 | from homeassistant.helpers.event import async_track_state_change_event
19 |
20 | from .const import (
21 | _LOGGER,
22 | ATTR_BACKGROUND_COLOR,
23 | ATTR_BORDER,
24 | ATTR_COLOR,
25 | ATTR_ERROR_CORRECTION,
26 | ATTR_SCALE,
27 | ATTR_TEXT,
28 | CONF_BACKGROUND_COLOR,
29 | CONF_BORDER,
30 | CONF_COLOR,
31 | CONF_ERROR_CORRECTION,
32 | CONF_SCALE,
33 | DEFAULT_BACKGROUND_COLOR,
34 | DEFAULT_BORDER,
35 | DEFAULT_COLOR,
36 | DEFAULT_ERROR_CORRECTION,
37 | DEFAULT_SCALE,
38 | )
39 |
40 |
41 | async def async_setup_entry(
42 | hass: HomeAssistant,
43 | config_entry: ConfigEntry,
44 | async_add_entities: AddEntitiesCallback,
45 | ) -> None:
46 | """Set up entries."""
47 |
48 | entity = QRImage(config_entry, hass)
49 |
50 | async_add_entities([entity])
51 |
52 |
53 | class QRImage(ImageEntity):
54 | """Representation of a QR code."""
55 |
56 | def __init__(self, entry: ConfigEntry, hass: HomeAssistant) -> None:
57 | """Initialize the camera."""
58 | super().__init__(hass)
59 |
60 | self.hass: HomeAssistant = hass
61 |
62 | self.image: io.BytesIO = io.BytesIO()
63 |
64 | self.value_template: str = entry.data[CONF_VALUE_TEMPLATE]
65 | self.template = template.Template(self.value_template, self.hass) # type: ignore[no-untyped-call]
66 | self.rendered_template: template.RenderInfo = template.RenderInfo(self.template)
67 |
68 | self.color_hex = entry.data.get(CONF_COLOR, DEFAULT_COLOR)
69 | self.color = ImageColor.getcolor(self.color_hex, "RGBA")
70 |
71 | self.background_color_hex = entry.data.get(
72 | CONF_BACKGROUND_COLOR, DEFAULT_BACKGROUND_COLOR
73 | )
74 | self.background_color = ImageColor.getcolor(self.background_color_hex, "RGBA")
75 |
76 | self.scale: int = entry.data.get(CONF_SCALE, DEFAULT_SCALE)
77 | self.border: int = entry.data.get(CONF_BORDER, DEFAULT_BORDER)
78 | self.error_correction: str = entry.data.get(
79 | CONF_ERROR_CORRECTION, DEFAULT_ERROR_CORRECTION
80 | )
81 |
82 | self._attr_name: str = entry.data[CONF_NAME]
83 | self._attr_unique_id: str = f"{entry.entry_id}-qr-code"
84 | self._attr_content_type: str = "image/png"
85 |
86 | def _render(self) -> None:
87 | """Render template."""
88 | self.rendered_template = self.template.async_render_to_info()
89 |
90 | async def async_added_to_hass(self) -> None:
91 | """Register callbacks."""
92 |
93 | self._render()
94 | await self._refresh()
95 |
96 | @callback
97 | async def _update(event: Event[EventStateChangedData]) -> None:
98 | """Handle state changes."""
99 | old_state = event.data["old_state"]
100 | new_state = event.data["new_state"]
101 |
102 | if old_state is None or new_state is None:
103 | return
104 |
105 | if old_state.state == new_state.state:
106 | return
107 |
108 | self._render()
109 | await self._refresh()
110 |
111 | async_track_state_change_event(
112 | self.hass, list(self.rendered_template.entities), _update
113 | )
114 |
115 | async def async_image(self) -> bytes | None:
116 | """Return bytes of image."""
117 | return self.image.getvalue()
118 |
119 | async def _refresh(self) -> None:
120 | """Create the QR code."""
121 |
122 | _LOGGER.debug('Print "%s" with: %s', self.name, self.rendered_template.result())
123 |
124 | code = pyqrcode.create(
125 | self.rendered_template.result(), error=self.error_correction
126 | )
127 |
128 | self.image = io.BytesIO()
129 | code.png(
130 | self.image,
131 | scale=self.scale,
132 | module_color=self.color,
133 | background=self.background_color,
134 | quiet_zone=self.border,
135 | )
136 |
137 | self._attr_image_last_updated = dt_util.utcnow()
138 | self.async_write_ha_state()
139 |
140 | @property
141 | def extra_state_attributes(self) -> dict[str, Any]:
142 | """Return extra attributes of the sensor."""
143 | return {
144 | ATTR_TEXT: self.rendered_template.result(),
145 | ATTR_COLOR: self.color_hex,
146 | ATTR_BACKGROUND_COLOR: self.background_color_hex,
147 | ATTR_SCALE: self.scale,
148 | ATTR_BORDER: self.border,
149 | ATTR_ERROR_CORRECTION: self.error_correction,
150 | }
151 |
--------------------------------------------------------------------------------
/custom_components/qr_generator/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "domain": "qr_generator",
3 | "name": "QR-Code Generator",
4 | "codeowners": ["@DeerMaximum"],
5 | "config_flow": true,
6 | "dependencies": [],
7 | "documentation": "https://github.com/DeerMaximum/QR-Code-Generator",
8 | "iot_class": "calculated",
9 | "issue_tracker" : "https://github.com/DeerMaximum/QR-Code-Generator/issues",
10 | "loggers": ["qr_generator"],
11 | "requirements": ["pyqrcode==1.2.1", "pypng==0.0.21"],
12 | "version" : "2.1.16"
13 | }
14 |
--------------------------------------------------------------------------------
/custom_components/qr_generator/services.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from homeassistant.components.image import ImageEntity
4 | from homeassistant.const import ATTR_ENTITY_ID
5 | from homeassistant.core import HomeAssistant, ServiceCall
6 | from homeassistant.exceptions import HomeAssistantError
7 | from homeassistant.helpers.entity_platform import async_get_platforms
8 |
9 | from custom_components.qr_generator import DOMAIN
10 | import voluptuous as vol
11 | from homeassistant.helpers import config_validation as cv
12 |
13 | from custom_components.qr_generator.const import ATTR_FILENAME, _LOGGER
14 |
15 |
16 | def register_services(hass: HomeAssistant) -> None:
17 | """Register the services."""
18 |
19 | async def save_image(service_call: ServiceCall) -> None:
20 | """Save the image to path."""
21 | entity_id = service_call.data[ATTR_ENTITY_ID]
22 | filepath = service_call.data[ATTR_FILENAME]
23 |
24 | if not hass.config.is_allowed_path(filepath):
25 | raise HomeAssistantError(
26 | f"Cannot write `{filepath}`, no access to path; `allowlist_external_dirs` may need to be adjusted in `configuration.yaml`"
27 | )
28 |
29 | platforms = async_get_platforms(hass, DOMAIN)
30 |
31 | if len(platforms) < 1:
32 | raise HomeAssistantError(
33 | f"Integration not found: {DOMAIN}"
34 | )
35 |
36 | entity: ImageEntity | None = None
37 |
38 | for platform in platforms:
39 | entity_tmp: ImageEntity | None = platform.entities.get(entity_id, None)
40 | if entity_tmp is not None:
41 | entity = entity_tmp
42 | break
43 |
44 | if not entity:
45 | raise HomeAssistantError(
46 | f"Could not find entity {entity_id} from integration {DOMAIN}"
47 | )
48 |
49 | image = await entity.async_image()
50 |
51 | def _write_image(to_file: str, image_data: bytes) -> None:
52 | """Executor helper to write image."""
53 | os.makedirs(os.path.dirname(to_file), exist_ok=True)
54 | with open(to_file, "wb") as img_file:
55 | img_file.write(image_data)
56 |
57 | try:
58 | await hass.async_add_executor_job(_write_image, filepath, image)
59 | except OSError as err:
60 | _LOGGER.error("Can't write image to file: %s", err)
61 |
62 | hass.services.async_register(DOMAIN, "save", save_image, vol.Schema({
63 | vol.Required(ATTR_ENTITY_ID) : cv.entity_id,
64 | vol.Required(ATTR_FILENAME): cv.string
65 | }))
66 |
67 |
68 |
--------------------------------------------------------------------------------
/custom_components/qr_generator/services.yaml:
--------------------------------------------------------------------------------
1 | save:
2 | name: Save current image
3 | description: Save the current image to a path
4 | fields:
5 | entity_id:
6 | name: Entity
7 | description: Identifier of the image entity.
8 | example: "image.qr_code"
9 | required: true
10 | selector:
11 | entity:
12 | integration: qr_generator
13 | domain: image
14 | filename:
15 | required: true
16 | name: Filename
17 | description: Target filename.
18 | example: "/tmp/snapshot_{{ entity_id.name }}.mp4"
19 | selector:
20 | text:
--------------------------------------------------------------------------------
/custom_components/qr_generator/strings.json:
--------------------------------------------------------------------------------
1 | {
2 | "config": {
3 | "step": {
4 | "user": {
5 | "title": "Create QR Code",
6 | "data": {
7 | "name": "Name",
8 | "value_template": "Content (text or template)",
9 | "advanced": "Advanced settings"
10 | }
11 | },
12 | "advanced": {
13 | "title": "Advanced settings",
14 | "data": {
15 | "color": "Color",
16 | "scale": "Scale",
17 | "border": "Border",
18 | "error_correction": "Error correction:",
19 | "background_color": "Background color"
20 | }
21 | }
22 | },
23 | "error": {
24 | "unknown": "[%key:common::config_flow::error::unknown%]",
25 | "invalid_template": "Invalid template",
26 | "invalid_color": "Invalid color"
27 | }
28 | },
29 | "options": {
30 | "step": {
31 | "init": {
32 | "title": "Edit QR Code",
33 | "data": {
34 | "name": "Name",
35 | "value_template": "Content (text or template)",
36 | "advanced": "Advanced settings"
37 | }
38 | },
39 | "advanced": {
40 | "title": "Advanced settings",
41 | "data": {
42 | "color": "Color",
43 | "scale": "Scale",
44 | "border": "Border",
45 | "error_correction": "Error correction:",
46 | "background_color": "Background color"
47 | }
48 | }
49 | },
50 | "error": {
51 | "unknown": "[%key:common::config_flow::error::unknown%]",
52 | "invalid_template": "Invalid template",
53 | "invalid_color": "Invalid color"
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/custom_components/qr_generator/translations/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "config": {
3 | "error": {
4 | "invalid_color": "Invalid color",
5 | "invalid_template": "Invalid template",
6 | "unknown": "Unexpected error"
7 | },
8 | "step": {
9 | "advanced": {
10 | "data": {
11 | "background_color": "Background color",
12 | "border": "Border",
13 | "color": "Color",
14 | "error_correction": "Error correction:",
15 | "scale": "Scale"
16 | },
17 | "title": "Advanced settings"
18 | },
19 | "user": {
20 | "data": {
21 | "advanced": "Advanced settings",
22 | "name": "Name",
23 | "value_template": "Content (text or template)"
24 | },
25 | "title": "Create QR Code"
26 | }
27 | }
28 | },
29 | "options": {
30 | "error": {
31 | "invalid_color": "Invalid color",
32 | "invalid_template": "Invalid template",
33 | "unknown": "Unexpected error"
34 | },
35 | "step": {
36 | "advanced": {
37 | "data": {
38 | "background_color": "Background color",
39 | "border": "Border",
40 | "color": "Color",
41 | "error_correction": "Error correction:",
42 | "scale": "Scale"
43 | },
44 | "title": "Advanced settings"
45 | },
46 | "init": {
47 | "data": {
48 | "advanced": "Advanced settings",
49 | "name": "Name",
50 | "value_template": "Content (text or template)"
51 | },
52 | "title": "Edit QR Code"
53 | }
54 | }
55 | }
56 | }
--------------------------------------------------------------------------------
/custom_components/qr_generator/translations/sk.json:
--------------------------------------------------------------------------------
1 | {
2 | "config": {
3 | "error": {
4 | "invalid_color": "Neplatná farba",
5 | "invalid_template": "Neplatná šablóna",
6 | "unknown": "Neočakávaná chyba"
7 | },
8 | "step": {
9 | "advanced": {
10 | "data": {
11 | "background_color": "Farba pozadia",
12 | "border": "Okraj",
13 | "color": "Farba",
14 | "error_correction": "Korekcia chyby:",
15 | "scale": "Mierka"
16 | },
17 | "title": "Pokročilé nastavenia"
18 | },
19 | "user": {
20 | "data": {
21 | "advanced": "Pokročilé nastavenia",
22 | "name": "Názov",
23 | "value_template": "Obsah (text alebo šablóna)"
24 | },
25 | "title": "Vytvoriť QR kód"
26 | }
27 | }
28 | },
29 | "options": {
30 | "error": {
31 | "invalid_color": "Neplatná farba",
32 | "invalid_template": "Neplatná šablóna",
33 | "unknown": "Neočakávaná chyba"
34 | },
35 | "step": {
36 | "advanced": {
37 | "data": {
38 | "background_color": "Farba pozadia",
39 | "border": "Okraj",
40 | "color": "Farba",
41 | "error_correction": "Korekcia chyby:",
42 | "scale": "Mierka"
43 | },
44 | "title": "Pokročilé nastavenia"
45 | },
46 | "init": {
47 | "data": {
48 | "advanced": "Pokročilé nastavenia",
49 | "name": "Názov",
50 | "value_template": "Obsah (text alebo šablóna)"
51 | },
52 | "title": "Upraviť QR kód"
53 | }
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/hacs.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "QR-Code Generator",
3 | "render_readme": true
4 | }
--------------------------------------------------------------------------------
/requirements_dev.txt:
--------------------------------------------------------------------------------
1 | homeassistant
--------------------------------------------------------------------------------
/requirements_test.txt:
--------------------------------------------------------------------------------
1 | pyqrcode==1.2.1
2 | pypng==0.20220715.0
3 |
4 | #Test will fail witout
5 | aiohttp_cors
6 |
7 | pytest-homeassistant-custom-component==0.13.224
8 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [flake8]
2 | exclude = .venv,.git,.tox,docs,venv,bin,lib,deps,build
3 | doctests = True
4 | # To work with Black
5 | max-line-length = 88
6 | # E501: line too long
7 | # W503: Line break occurred before a binary operator
8 | # E203: Whitespace before ':'
9 | # D202 No blank lines allowed after function docstring
10 | # W504 line break after binary operator
11 | ignore =
12 | E501,
13 | W503,
14 | E203,
15 | D202,
16 | W504
17 |
18 | [isort]
19 | # https://github.com/timothycrosley/isort
20 | # https://github.com/timothycrosley/isort/wiki/isort-Settings
21 | # splits long import on multiple lines indented by 4 spaces
22 | multi_line_output = 3
23 | include_trailing_comma=True
24 | force_grid_wrap=0
25 | use_parentheses=True
26 | line_length=88
27 | indent = " "
28 | # by default isort don't check module indexes
29 | not_skip = __init__.py
30 | # will group `import x` and `from x import` of the same module.
31 | force_sort_within_sections = true
32 | sections = FUTURE,STDLIB,INBETWEENS,THIRDPARTY,FIRSTPARTY,LOCALFOLDER
33 | default_section = THIRDPARTY
34 | known_first_party = custom_components.qr_generator, tests
35 | combine_as_imports = true
36 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
1 | """Test for the QR Generator integration."""
2 |
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | """Fixtures for testing."""
2 | import pytest
3 |
4 |
5 | @pytest.fixture(autouse=True)
6 | def auto_enable_custom_integrations(enable_custom_integrations):
7 | """Enable enable_custom_integrations"""
8 | yield
9 |
--------------------------------------------------------------------------------
/tests/test_config_flow.py:
--------------------------------------------------------------------------------
1 | """Test the QR Generator config flow."""
2 | from __future__ import annotations
3 |
4 | from typing import Any
5 | from unittest.mock import patch
6 |
7 | import pytest
8 | from homeassistant.config_entries import SOURCE_USER
9 | from homeassistant.const import CONF_NAME, CONF_VALUE_TEMPLATE
10 | from homeassistant.core import HomeAssistant
11 | from homeassistant.data_entry_flow import FlowResultType
12 | from pytest_homeassistant_custom_component.common import MockConfigEntry
13 |
14 | from custom_components.qr_generator.config_flow import ConfigFlow
15 | from custom_components.qr_generator.const import (
16 | CONF_ADVANCED,
17 | CONF_BACKGROUND_COLOR,
18 | CONF_BORDER,
19 | CONF_COLOR,
20 | CONF_ERROR_CORRECTION,
21 | CONF_SCALE,
22 | DEFAULT_BACKGROUND_COLOR,
23 | DEFAULT_BORDER,
24 | DEFAULT_COLOR,
25 | DEFAULT_ERROR_CORRECTION,
26 | DEFAULT_SCALE,
27 | DOMAIN,
28 | )
29 |
30 | DUMMY_DATA_SIMPLE: dict[str, Any] = {
31 | CONF_NAME: "Test QR Code",
32 | CONF_VALUE_TEMPLATE: "Sample content",
33 | CONF_ADVANCED: False,
34 | }
35 | DUMMY_DATA_SIMPLE_ADVANCED: dict[str, Any] = {
36 | CONF_NAME: "Test QR Code",
37 | CONF_VALUE_TEMPLATE: "Sample content",
38 | CONF_ADVANCED: True,
39 | }
40 |
41 | DUMMY_DATA_SIMPLE_INVALID_TEMPLATE: dict[str, Any] = {
42 | CONF_NAME: "Test QR Code",
43 | CONF_VALUE_TEMPLATE: "{{novalid template}}",
44 | CONF_ADVANCED: False,
45 | }
46 |
47 | DUMMY_DATA_ADVANCED: dict[str, Any] = {
48 | CONF_COLOR: DEFAULT_COLOR,
49 | CONF_SCALE: DEFAULT_SCALE,
50 | CONF_BORDER: DEFAULT_BORDER,
51 | CONF_ERROR_CORRECTION: DEFAULT_ERROR_CORRECTION,
52 | CONF_BACKGROUND_COLOR: DEFAULT_BACKGROUND_COLOR,
53 | }
54 |
55 | DUMMY_DATA_ADVANCED_INVALID_COLOR: dict[str, Any] = {
56 | CONF_COLOR: "black",
57 | CONF_SCALE: DEFAULT_SCALE,
58 | CONF_BORDER: DEFAULT_BORDER,
59 | CONF_ERROR_CORRECTION: DEFAULT_ERROR_CORRECTION,
60 | CONF_BACKGROUND_COLOR: DEFAULT_BACKGROUND_COLOR,
61 | }
62 |
63 | DUMMY_ENTRY: dict[str, Any] = {
64 | CONF_NAME: "Test QR Code",
65 | CONF_VALUE_TEMPLATE: "Sample content",
66 | CONF_ADVANCED: True,
67 | CONF_COLOR: DEFAULT_COLOR,
68 | CONF_SCALE: DEFAULT_SCALE,
69 | CONF_BORDER: DEFAULT_BORDER,
70 | CONF_ERROR_CORRECTION: DEFAULT_ERROR_CORRECTION,
71 | CONF_BACKGROUND_COLOR: DEFAULT_BACKGROUND_COLOR,
72 | }
73 |
74 | DUMMY_ENTRY_CHANGE: dict[str, Any] = {
75 | CONF_VALUE_TEMPLATE: "New test",
76 | CONF_ADVANCED: False,
77 | }
78 |
79 | DUMMY_ENTRY_ADVANCED_CHANGE: dict[str, Any] = {
80 | CONF_SCALE: 50,
81 | }
82 |
83 | DUMMY_ENTRY_UPDATED: dict[str, Any] = {
84 | CONF_NAME: "Test QR Code",
85 | CONF_VALUE_TEMPLATE: "New test",
86 | CONF_ADVANCED: False,
87 | CONF_COLOR: DEFAULT_COLOR,
88 | CONF_SCALE: DEFAULT_SCALE,
89 | CONF_BORDER: DEFAULT_BORDER,
90 | CONF_ERROR_CORRECTION: DEFAULT_ERROR_CORRECTION,
91 | CONF_BACKGROUND_COLOR: DEFAULT_BACKGROUND_COLOR,
92 | }
93 |
94 | DUMMY_ENTRY_ADVANCED_UPDATED: dict[str, Any] = {
95 | CONF_NAME: "Test QR Code",
96 | CONF_VALUE_TEMPLATE: "Sample content",
97 | CONF_ADVANCED: True,
98 | CONF_COLOR: DEFAULT_COLOR,
99 | CONF_SCALE: 50,
100 | CONF_BORDER: DEFAULT_BORDER,
101 | CONF_ERROR_CORRECTION: DEFAULT_ERROR_CORRECTION,
102 | CONF_BACKGROUND_COLOR: DEFAULT_BACKGROUND_COLOR,
103 | }
104 |
105 | @pytest.mark.asyncio
106 | async def test_show_set_form(hass: HomeAssistant) -> None:
107 | """Test that the setup form is served."""
108 |
109 | result: dict[str, Any] = await hass.config_entries.flow.async_init(
110 | DOMAIN, context={"source": SOURCE_USER}
111 | )
112 |
113 | assert result["type"] == FlowResultType.FORM
114 | assert result["step_id"] == "user"
115 |
116 |
117 | @pytest.mark.asyncio
118 | async def test_step_user(hass: HomeAssistant) -> None:
119 | """Test starting a flow by user with valid values."""
120 |
121 | result: dict[str, Any] = await hass.config_entries.flow.async_init(
122 | DOMAIN, context={"source": SOURCE_USER}, data=DUMMY_DATA_SIMPLE
123 | )
124 |
125 | assert result["type"] == FlowResultType.CREATE_ENTRY
126 | assert result["title"] == DUMMY_DATA_SIMPLE[CONF_NAME]
127 |
128 |
129 | @pytest.mark.asyncio
130 | async def test_step_user_template_error(hass: HomeAssistant) -> None:
131 | """Test starting a flow by user with an invalid template."""
132 |
133 | result: dict[str, Any] = await hass.config_entries.flow.async_init(
134 | DOMAIN, context={"source": SOURCE_USER}, data=DUMMY_DATA_SIMPLE_INVALID_TEMPLATE
135 | )
136 |
137 | assert result["type"] == FlowResultType.FORM
138 | assert result["step_id"] == "user"
139 | assert result["errors"] == {"base": "invalid_template"}
140 |
141 |
142 | @pytest.mark.asyncio
143 | async def test_show_set_form_advanced_from_user(hass: HomeAssistant) -> None:
144 | """Test that the advanced form is served as a step."""
145 |
146 | result: dict[str, Any] = await hass.config_entries.flow.async_init(
147 | DOMAIN, context={"source": "user"}, data=DUMMY_DATA_SIMPLE_ADVANCED
148 | )
149 |
150 | assert result["type"] == FlowResultType.FORM
151 | assert result["step_id"] == "advanced"
152 |
153 |
154 | @pytest.mark.asyncio
155 | async def test_show_set_form_advanced(hass: HomeAssistant) -> None:
156 | """Test that the advanced form is served."""
157 |
158 | result: dict[str, Any] = await hass.config_entries.flow.async_init(
159 | DOMAIN, context={"source": "advanced"}
160 | )
161 |
162 | assert result["type"] == FlowResultType.FORM
163 | assert result["step_id"] == "advanced"
164 |
165 |
166 | @pytest.mark.asyncio
167 | async def test_step_advanced(hass: HomeAssistant) -> None:
168 | """Test starting a flow by advanced with valid values."""
169 |
170 | with patch.object(ConfigFlow, "override_config", DUMMY_DATA_SIMPLE):
171 |
172 | result: dict[str, Any] = await hass.config_entries.flow.async_init(
173 | DOMAIN, context={"source": "advanced"}, data=DUMMY_DATA_ADVANCED
174 | )
175 |
176 | assert result["type"] == FlowResultType.CREATE_ENTRY
177 | assert result["title"] == DUMMY_DATA_SIMPLE[CONF_NAME]
178 |
179 |
180 | @pytest.mark.asyncio
181 | async def test_step_advanced_invalid_color(hass: HomeAssistant) -> None:
182 | """Test starting a flow by advanced with an invalid color."""
183 |
184 | with patch.object(ConfigFlow, "override_config", DUMMY_DATA_SIMPLE):
185 |
186 | result: dict[str, Any] = await hass.config_entries.flow.async_init(
187 | DOMAIN,
188 | context={"source": "advanced"},
189 | data=DUMMY_DATA_ADVANCED_INVALID_COLOR,
190 | )
191 |
192 | assert result["type"] == FlowResultType.FORM
193 | assert result["step_id"] == "advanced"
194 | assert result["errors"] == {"base": "invalid_color"}
195 |
196 |
197 | @pytest.mark.asyncio
198 | async def test_options_flow_init(hass: HomeAssistant) -> None:
199 | """Test config flow options."""
200 | config_entry = MockConfigEntry(
201 | domain=DOMAIN,
202 | title=DUMMY_ENTRY[CONF_NAME],
203 | data=DUMMY_ENTRY,
204 | )
205 | config_entry.add_to_hass(hass)
206 |
207 | with patch("custom_components.qr_generator.async_setup_entry", return_value=True):
208 | result = await hass.config_entries.options.async_init(config_entry.entry_id)
209 |
210 | await hass.config_entries.async_setup(config_entry.entry_id)
211 | await hass.async_block_till_done()
212 |
213 | assert result["type"] == FlowResultType.FORM
214 | assert result["step_id"] == "init"
215 |
216 | result = await hass.config_entries.options.async_configure(
217 | result["flow_id"],
218 | user_input=DUMMY_ENTRY_CHANGE,
219 | )
220 |
221 | assert result["type"] == FlowResultType.CREATE_ENTRY
222 | assert dict(config_entry.options) == DUMMY_ENTRY_UPDATED
223 |
224 |
225 | @pytest.mark.asyncio
226 | async def test_options_flow_invalid_template(hass: HomeAssistant) -> None:
227 | """Test config flow options with invalid template."""
228 | config_entry = MockConfigEntry(
229 | domain=DOMAIN,
230 | title=DUMMY_ENTRY[CONF_NAME],
231 | data=DUMMY_ENTRY,
232 | )
233 | config_entry.add_to_hass(hass)
234 |
235 | with patch("custom_components.qr_generator.async_setup_entry", return_value=True):
236 | result = await hass.config_entries.options.async_init(config_entry.entry_id)
237 |
238 | await hass.config_entries.async_setup(config_entry.entry_id)
239 | await hass.async_block_till_done()
240 |
241 | assert result["type"] == FlowResultType.FORM
242 | assert result["step_id"] == "init"
243 |
244 | result = await hass.config_entries.options.async_configure(
245 | result["flow_id"],
246 | user_input=DUMMY_DATA_SIMPLE_INVALID_TEMPLATE,
247 | )
248 |
249 | assert result["type"] == FlowResultType.FORM
250 | assert result["step_id"] == "init"
251 | assert result["errors"] == {"base": "invalid_template"}
252 |
253 |
254 | @pytest.mark.asyncio
255 | async def test_options_flow_to_advanced(hass: HomeAssistant) -> None:
256 | """Test config flow options."""
257 | config_entry = MockConfigEntry(
258 | domain=DOMAIN,
259 | title=DUMMY_ENTRY[CONF_NAME],
260 | data=DUMMY_ENTRY,
261 | )
262 | config_entry.add_to_hass(hass)
263 |
264 | with patch("custom_components.qr_generator.async_setup_entry", return_value=True):
265 | result = await hass.config_entries.options.async_init(config_entry.entry_id)
266 |
267 | await hass.config_entries.async_setup(config_entry.entry_id)
268 | await hass.async_block_till_done()
269 |
270 | assert result["type"] == FlowResultType.FORM
271 | assert result["step_id"] == "init"
272 |
273 | result = await hass.config_entries.options.async_configure(
274 | result["flow_id"],
275 | user_input=DUMMY_DATA_SIMPLE_ADVANCED,
276 | )
277 |
278 | assert result["type"] == FlowResultType.FORM
279 | assert result["step_id"] == "advanced"
280 | assert result["errors"] == {}
281 |
282 |
283 | @pytest.mark.asyncio
284 | async def test_options_flow_advanced(hass: HomeAssistant) -> None:
285 | """Test config flow options."""
286 | config_entry = MockConfigEntry(
287 | domain=DOMAIN,
288 | title=DUMMY_ENTRY[CONF_NAME],
289 | data=DUMMY_ENTRY,
290 | )
291 | config_entry.add_to_hass(hass)
292 |
293 | with patch("custom_components.qr_generator.async_setup_entry", return_value=True):
294 | result = await hass.config_entries.options.async_init(config_entry.entry_id)
295 |
296 | await hass.config_entries.async_setup(config_entry.entry_id)
297 | await hass.async_block_till_done()
298 |
299 | assert result["type"] == FlowResultType.FORM
300 | assert result["step_id"] == "init"
301 |
302 | result = await hass.config_entries.options.async_configure(
303 | result["flow_id"],
304 | user_input=DUMMY_DATA_SIMPLE_ADVANCED,
305 | )
306 |
307 | assert result["type"] == FlowResultType.FORM
308 | assert result["step_id"] == "advanced"
309 | assert result["errors"] == {}
310 |
311 | result = await hass.config_entries.options.async_configure(
312 | result["flow_id"],
313 | user_input=DUMMY_ENTRY_ADVANCED_CHANGE,
314 | )
315 |
316 | assert result["type"] == FlowResultType.CREATE_ENTRY
317 | assert dict(config_entry.options) == DUMMY_ENTRY_ADVANCED_UPDATED
318 |
319 |
320 | @pytest.mark.asyncio
321 | async def test_options_flow_advanced_invalid_color(hass: HomeAssistant) -> None:
322 | """Test config flow options with invalid template."""
323 | config_entry = MockConfigEntry(
324 | domain=DOMAIN,
325 | title=DUMMY_ENTRY[CONF_NAME],
326 | data=DUMMY_ENTRY,
327 | )
328 | config_entry.add_to_hass(hass)
329 |
330 | with patch("custom_components.qr_generator.async_setup_entry", return_value=True):
331 | result = await hass.config_entries.options.async_init(config_entry.entry_id)
332 |
333 | await hass.config_entries.async_setup(config_entry.entry_id)
334 | await hass.async_block_till_done()
335 |
336 | assert result["type"] == FlowResultType.FORM
337 | assert result["step_id"] == "init"
338 |
339 | result = await hass.config_entries.options.async_configure(
340 | result["flow_id"],
341 | user_input=DUMMY_DATA_SIMPLE_ADVANCED,
342 | )
343 |
344 | assert result["type"] == FlowResultType.FORM
345 | assert result["step_id"] == "advanced"
346 | assert result["errors"] == {}
347 |
348 | result = await hass.config_entries.options.async_configure(
349 | result["flow_id"],
350 | user_input=DUMMY_DATA_ADVANCED_INVALID_COLOR,
351 | )
352 |
353 | assert result["type"] == FlowResultType.FORM
354 | assert result["step_id"] == "advanced"
355 | assert result["errors"] == {"base": "invalid_color"}
356 |
--------------------------------------------------------------------------------
/tests/test_image.py:
--------------------------------------------------------------------------------
1 | """Test the QR Generator image."""
2 | from typing import Any
3 |
4 | import pytest
5 |
6 | from homeassistant.config_entries import ConfigEntryState
7 | from homeassistant.const import CONF_NAME, CONF_VALUE_TEMPLATE
8 | from homeassistant.core import HomeAssistant
9 | from homeassistant.helpers import entity_registry as er
10 |
11 | from pytest_homeassistant_custom_component.common import MockConfigEntry
12 |
13 | from custom_components.qr_generator.const import (
14 | ATTR_BACKGROUND_COLOR,
15 | ATTR_BORDER,
16 | ATTR_COLOR,
17 | ATTR_ERROR_CORRECTION,
18 | ATTR_SCALE,
19 | ATTR_TEXT,
20 | CONF_ADVANCED,
21 | CONF_BACKGROUND_COLOR,
22 | CONF_BORDER,
23 | CONF_COLOR,
24 | CONF_ERROR_CORRECTION,
25 | CONF_SCALE,
26 | DEFAULT_BACKGROUND_COLOR,
27 | DEFAULT_BORDER,
28 | DEFAULT_COLOR,
29 | DEFAULT_ERROR_CORRECTION,
30 | DEFAULT_SCALE,
31 | DOMAIN,
32 | )
33 |
34 | DUMMY_ENTRY: dict[str, Any] = {
35 | CONF_NAME: "Test QR Code",
36 | CONF_VALUE_TEMPLATE: "Sample content",
37 | CONF_ADVANCED: True,
38 | CONF_COLOR: DEFAULT_COLOR,
39 | CONF_SCALE: DEFAULT_SCALE,
40 | CONF_BORDER: DEFAULT_BORDER,
41 | CONF_ERROR_CORRECTION: DEFAULT_ERROR_CORRECTION,
42 | CONF_BACKGROUND_COLOR: DEFAULT_BACKGROUND_COLOR,
43 | }
44 |
45 | @pytest.mark.asyncio
46 | async def test_image(hass: HomeAssistant) -> None:
47 | """Test the creation and values of the image."""
48 | config_entry: MockConfigEntry = MockConfigEntry(
49 | domain=DOMAIN, title="NINA", data=DUMMY_ENTRY
50 | )
51 |
52 | entity_registry: er = er.async_get(hass)
53 | config_entry.add_to_hass(hass)
54 |
55 | await hass.config_entries.async_setup(config_entry.entry_id)
56 | await hass.async_block_till_done()
57 |
58 | assert config_entry.state == ConfigEntryState.LOADED
59 |
60 | state = hass.states.get("image.test_qr_code")
61 | entry = entity_registry.async_get("image.test_qr_code")
62 |
63 | assert state.attributes.get(ATTR_TEXT) == "Sample content"
64 | assert state.attributes.get(ATTR_COLOR) == DEFAULT_COLOR
65 | assert state.attributes.get(ATTR_SCALE) == DEFAULT_SCALE
66 | assert state.attributes.get(ATTR_BORDER) == DEFAULT_BORDER
67 | assert state.attributes.get(ATTR_ERROR_CORRECTION) == DEFAULT_ERROR_CORRECTION
68 | assert state.attributes.get(ATTR_BACKGROUND_COLOR) == DEFAULT_BACKGROUND_COLOR
69 |
70 | assert entry.unique_id == f"{config_entry.entry_id}-qr-code"
71 |
--------------------------------------------------------------------------------