mirror of
https://github.com/home-assistant/core.git
synced 2026-06-30 02:25:31 +02:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7825792f2a | |||
| 3cb69dff34 |
@@ -1,98 +0,0 @@
|
||||
---
|
||||
name: bump-dependency
|
||||
description: Bumps a Python package dependency across Home Assistant Core integrations, regenerates core requirement files, runs verification tests and prek lint, and prepares a pull request with proper release/compare links.
|
||||
---
|
||||
|
||||
# Bump Python Package Dependency in Home Assistant Core
|
||||
|
||||
Follow these systematic steps to successfully bump a python package requirement in the repository, regenerate necessary derivative files, verify the integration, and raise a pull request.
|
||||
|
||||
## Gotchas & Non-Obvious Constraints
|
||||
|
||||
- **PR Template Integrity**: Follow Home Assistant's Pull Request template (`.github/PULL_REQUEST_TEMPLATE.md`) exactly as written, including any instructions inside the template itself. Preserve all sections, comments, and unchecked checkboxes unless the template explicitly says otherwise; the only allowed removal is the **Breaking change** section when the template instructs you to remove it if not applicable.
|
||||
- **GitHub Tag Volatility**: Release tags on GitHub are highly inconsistent (e.g., `v1.2.3` vs `1.2.3` vs `release-1.2.3`). Always use the automated resolver `resolve_dependency.py` to check HEAD status for correct tags before hardcoding comparison URLs.
|
||||
|
||||
## Step-by-Step Workflow Checklist
|
||||
|
||||
### Phase A: Research and Plan
|
||||
- [ ] **1. Identify Targets**: Note the requested target package and target version to bump.
|
||||
- [ ] **2. Discover Codebase References**: Search the codebase to find all `manifest.json` and requirements files referencing the package.
|
||||
- [ ] **3. Resolve Version/Tag Details**: Run the integrated validation helper script to resolve version details, GitHub repo, release tag format, and formatted PR links:
|
||||
```bash
|
||||
uv run python3 ./.claude/skills/bump-dependency/scripts/resolve_dependency.py <package> <old_version> [--new-version <new_version>]
|
||||
```
|
||||
- [ ] **4. Plan-Validate-Execute (Draft Plan)**: Before modifying any files, write a brief, structured plan outlining the integrations to change, old version, new version, and the resolved comparison link. Show this draft plan to the user.
|
||||
|
||||
### Phase B: Execute and Validate (Local Changes)
|
||||
- [ ] **5. Check Uncommitted Changes**: Check for any uncommitted changes in the repository. If they exist, ask the user whether to stash, commit, or discard them before proceeding.
|
||||
- [ ] **6. Git Branch Setup**: Create a clean branch starting from the latest `upstream/dev`:
|
||||
```bash
|
||||
git fetch upstream dev
|
||||
git checkout -b bump-<package>-to-<version> upstream/dev
|
||||
```
|
||||
- [ ] **7. Apply Bump to manifests**: Update the version constraint string in all identified `manifest.json` files (e.g., change `"package==1.0.0"` to `"package==1.1.0"`).
|
||||
- [ ] **8. Regenerate Core Requirements**: Run the requirements generator to update all derivative requirements and constraint files:
|
||||
```bash
|
||||
uv run python3 -m script.gen_requirements_all
|
||||
```
|
||||
- [ ] **9. Validate Requirements**: Check `git diff` to ensure that only the targeted `manifest.json` files and `requirements_all.txt` (and potentially standard constraints) were modified. No unrelated files must be affected.
|
||||
- [ ] **10. Local Venv Verification**: Install the exact targeted package version directly inside the virtual environment:
|
||||
```bash
|
||||
uv pip install "<package>==<version>"
|
||||
```
|
||||
|
||||
### Phase C: Validation Loop (Tests & Lint)
|
||||
- [ ] **11. Run Integration Tests**: Execute the pytest suite for all integrations that consume the bumped package:
|
||||
```bash
|
||||
uv run pytest tests/components/<integration_name>
|
||||
```
|
||||
- *Validation Loop*: If tests fail, analyze the error, apply appropriate fixes, and re-run pytest until all tests pass cleanly.
|
||||
- [ ] **12. Run prek Lint Checks**: Run the local prek hooks on modified files:
|
||||
```bash
|
||||
uv run prek run
|
||||
```
|
||||
- *Validation Loop*: If prek checks report any formatting or linting violations, fix them and repeat `uv run prek run` until it passes completely without errors.
|
||||
|
||||
### Phase D: User Confirmation & PR Creation
|
||||
- [ ] **13. Commit Changes**: Commit the clean changes:
|
||||
```bash
|
||||
git add <modified_files>
|
||||
git commit -m "Bump <package> to <version>"
|
||||
```
|
||||
- [ ] **14. Push Branch**: Push the local branch to your origin remote:
|
||||
```bash
|
||||
git push origin bump-<package>-to-<version>
|
||||
```
|
||||
- [ ] **15. PR Description Preparation**: Generate the pull request body from `.github/PULL_REQUEST_TEMPLATE.md`:
|
||||
- **Proposed change**: Describe the package, old version, new version, target/source branches, and insert the resolved PyPI, changelog, and comparison diff links.
|
||||
- **Type of change**: Check only 1 box in this section, and mark the `Dependency upgrade` checkbox as checked: `[x] Dependency upgrade`.
|
||||
- **Breaking change**: You may remove the "Breaking change" section entirely from the template.
|
||||
- **Validation checklists**: Mark `The code change is tested` checkbox as checked: `[x] The code change is tested`.
|
||||
- **Keep remaining template intact**: Do NOT remove any other commented-out blocks, headers, or unchecked checkboxes in the template.
|
||||
- [ ] **16. Mandatory Review Presentation**: Format the PR proposal using the **PR Presentation Template** below and display it to the user. **Stop and wait for the user to review and explicitly confirm/approve the PR template and draft details before creating the PR.**
|
||||
- [ ] **17. Raise Pull Request**: Once the user approves, create the Pull Request using the GitHub CLI:
|
||||
```bash
|
||||
gh pr create --repo home-assistant/core --base dev --head <username>:bump-<package>-to-<version> --title "Bump <package> to <version>" --body-file <pr_body_file>
|
||||
```
|
||||
|
||||
## PR Presentation Template
|
||||
|
||||
```markdown
|
||||
### 🚀 Dependency Bump Pull Request Draft Review
|
||||
|
||||
- **Package**: `<package_name>` (`<old_version>` → `<new_version>`)
|
||||
- **PR Title**: `Bump <package_name> to <new_version>`
|
||||
- **Target Branch**: `dev`
|
||||
- **Head Branch**: `<fork_username>:bump-<package_name>-to-<new_version>`
|
||||
|
||||
#### 🔗 PyPI & GitHub Links
|
||||
- **PyPI Release**: https://pypi.org/project/<package_name>/<new_version>/
|
||||
- **Changelog Link**: `<changelog_url>`
|
||||
- **Comparison Diff**: `<compare_url>`
|
||||
|
||||
#### 📁 Modified Files
|
||||
- `<list_of_modified_files>`
|
||||
|
||||
#### 📝 Proposed PR Body
|
||||
<render the complete filled PR template body here, showing all checks and modifications for user approval>
|
||||
```
|
||||
@@ -1,205 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# ruff: noqa: T201, D103, BLE001
|
||||
"""Helper script to resolve package details, GitHub repo, release tags, and diff links from PyPI."""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
from packaging.version import Version
|
||||
|
||||
_VERSION_MATCH = r"^[a-zA-Z0-9_\-\.\+\!\*]+$"
|
||||
_REPO_MATCH = r"https?://(?:www\.)?github\.com/([^/]+)/([^/]+)"
|
||||
# Project owner or repo name
|
||||
_NAME_MATCH = r"^[a-zA-Z0-9_\-\.]+$"
|
||||
|
||||
|
||||
def get_pypi_data(package_name):
|
||||
# Sanitize and URL-quote the package name to prevent URL path injection
|
||||
safe_package_name = urllib.parse.quote(package_name)
|
||||
url = f"https://pypi.org/pypi/{safe_package_name}/json"
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
headers={
|
||||
"User-Agent": "HomeAssistant-Skill-Resolver/1.0",
|
||||
"Accept": "application/json",
|
||||
},
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=10) as response:
|
||||
return json.loads(response.read().decode())
|
||||
except urllib.error.HTTPError as e:
|
||||
print(f"Error fetching PyPI data (HTTP {e.code}): {e.reason}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except urllib.error.URLError as e:
|
||||
print(f"Network error fetching PyPI data: {e.reason}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"Unexpected error fetching PyPI data: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def find_github_repo(info):
|
||||
urls = []
|
||||
if info.get("home_page"):
|
||||
urls.append(info["home_page"])
|
||||
if info.get("project_urls"):
|
||||
urls.extend(info["project_urls"].values())
|
||||
|
||||
for u in urls:
|
||||
if not u:
|
||||
continue
|
||||
cleaned_url = u.replace("git+", "")
|
||||
m = re.search(_REPO_MATCH, cleaned_url, re.IGNORECASE)
|
||||
if m:
|
||||
owner, repo = m.groups()
|
||||
# Strip query parameters, hashes, trailing slashes, and .git extension
|
||||
repo = repo.split("?")[0].split("#")[0].split("/")[0]
|
||||
repo = repo.removesuffix(".git")
|
||||
# Validate that the owner and repo name conform to valid formats,
|
||||
# preventing prompt injection payloads embedded in repository URLs.
|
||||
if re.match(_NAME_MATCH, owner) and re.match(_NAME_MATCH, repo):
|
||||
return f"https://github.com/{owner}/{repo}"
|
||||
return None
|
||||
|
||||
|
||||
def check_github_tag(repo_url, version):
|
||||
# Try with 'v' prefix, without prefix, and with 'release-' prefix
|
||||
tag_options = [f"v{version}", version, f"release-{version}"]
|
||||
for tag in tag_options:
|
||||
# Check both tree and releases paths on GitHub
|
||||
for path_template in [f"tree/{tag}", f"releases/tag/{tag}"]:
|
||||
url = f"{repo_url}/{path_template}"
|
||||
try:
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
method="HEAD",
|
||||
headers={"User-Agent": "HomeAssistant-Skill-Resolver/1.0"},
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||
if resp.status == 200:
|
||||
return tag
|
||||
except urllib.error.HTTPError as e:
|
||||
# If it's a 404, we continue checking other tag/path options
|
||||
if e.code == 404:
|
||||
continue
|
||||
# For non-404 HTTP errors (like 403 Forbidden/429 rate limit), exit with error to avoid false reports
|
||||
print(
|
||||
f"\n[ERROR] HTTP error contacting GitHub ({e.code} {e.reason}) for tag '{tag}'",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
except urllib.error.URLError as e:
|
||||
# Connection or timeout error
|
||||
print(
|
||||
f"\n[ERROR] Network error contacting GitHub: {e.reason}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
# Unexpected exceptions
|
||||
print(
|
||||
f"\n[ERROR] Unexpected error verifying tag '{tag}': {e}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Resolve PyPI package info and GitHub release diffs."
|
||||
)
|
||||
parser.add_argument("package", help="Name of the PyPI package")
|
||||
parser.add_argument(
|
||||
"old_version", help="Current version installed in Home Assistant"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--new-version", help="Target version to bump to (defaults to latest on PyPI)"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
if not re.match(_NAME_MATCH, args.package):
|
||||
print(f"[ERROR] Invalid package name format: '{args.package}'", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if not re.match(_VERSION_MATCH, args.old_version):
|
||||
print(
|
||||
f"[ERROR] Invalid old version format: '{args.old_version}'", file=sys.stderr
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
if args.new_version and not re.match(_VERSION_MATCH, args.new_version):
|
||||
print(
|
||||
f"[ERROR] Invalid target version format: '{args.new_version}'",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
data = get_pypi_data(args.package)
|
||||
info = data.get("info", {})
|
||||
|
||||
# Filter out pre-releases from the releases list to identify the latest stable version
|
||||
stable_versions = []
|
||||
for v in data.get("releases", {}):
|
||||
try:
|
||||
ver = Version(v)
|
||||
if not ver.is_prerelease:
|
||||
stable_versions.append(ver)
|
||||
except Exception:
|
||||
continue
|
||||
latest = str(max(stable_versions)) if stable_versions else info.get("version")
|
||||
|
||||
if latest and not re.match(_VERSION_MATCH, latest):
|
||||
print("[ERROR] Invalid latest version resolved from PyPI.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
target_version = args.new_version or latest
|
||||
if not target_version:
|
||||
print("[ERROR] Could not resolve a target version from PyPI.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
github_repo = find_github_repo(info)
|
||||
|
||||
print("--- RESOLVED DEPENDENCY DETAILS ---")
|
||||
print(f"Package: {args.package}")
|
||||
print(f"Current Version: {args.old_version}")
|
||||
print(f"Latest (PyPI): {latest}")
|
||||
print(f"Target Version: {target_version}")
|
||||
|
||||
if github_repo:
|
||||
print(f"GitHub Repository: {github_repo}")
|
||||
|
||||
# Verify tag formats on GitHub
|
||||
old_tag = check_github_tag(github_repo, args.old_version)
|
||||
new_tag = check_github_tag(github_repo, target_version)
|
||||
|
||||
if not old_tag or not new_tag:
|
||||
missing_tags = []
|
||||
if not old_tag:
|
||||
missing_tags.append(args.old_version)
|
||||
if not new_tag:
|
||||
missing_tags.append(target_version)
|
||||
print(
|
||||
f"\n[ERROR] Could not resolve GitHub release tag for version(s): {', '.join(missing_tags)}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
changelog_url = f"{github_repo}/releases/tag/{new_tag}"
|
||||
compare_url = f"{github_repo}/compare/{old_tag}...{new_tag}"
|
||||
|
||||
print("\n--- PR DOCUMENTATION LINKS ---")
|
||||
print(f"Changelog Link: {changelog_url}")
|
||||
print(f"Comparison Diff Link: {compare_url}")
|
||||
else:
|
||||
print("\n[WARNING] GitHub repository could not be resolved from PyPI metadata.")
|
||||
print("Please resolve the release notes and diff links manually.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -14,7 +14,7 @@ env:
|
||||
UV_HTTP_TIMEOUT: 60
|
||||
UV_SYSTEM_PYTHON: "true"
|
||||
# Base image version from https://github.com/home-assistant/docker
|
||||
BASE_IMAGE_VERSION: "2026.07.0"
|
||||
BASE_IMAGE_VERSION: "2026.05.0"
|
||||
ARCHITECTURES: '["amd64", "aarch64"]'
|
||||
|
||||
permissions: {}
|
||||
|
||||
@@ -137,7 +137,7 @@ jobs:
|
||||
sed -i "/uv/d" requirements_diff.txt
|
||||
|
||||
- name: Build wheels
|
||||
uses: home-assistant/wheels@9e17ab1ed5c4c79d8b61e29fa63de25ca2710716 # 2026.07.0
|
||||
uses: home-assistant/wheels@34957438948e0b3dcde73c77750643dadae594f5 # 2026.06.0
|
||||
with:
|
||||
abi: ${{ matrix.abi }}
|
||||
tag: musllinux_1_2
|
||||
@@ -195,7 +195,7 @@ jobs:
|
||||
sed -i "/uv/d" requirements_diff.txt
|
||||
|
||||
- name: Build wheels
|
||||
uses: home-assistant/wheels@9e17ab1ed5c4c79d8b61e29fa63de25ca2710716 # 2026.07.0
|
||||
uses: home-assistant/wheels@34957438948e0b3dcde73c77750643dadae594f5 # 2026.06.0
|
||||
with:
|
||||
abi: ${{ matrix.abi }}
|
||||
tag: musllinux_1_2
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.15.18
|
||||
rev: v0.15.17
|
||||
hooks:
|
||||
- id: ruff-check
|
||||
args:
|
||||
|
||||
@@ -17,7 +17,7 @@ from homeassistant.helpers.selector import (
|
||||
TextSelectorType,
|
||||
)
|
||||
|
||||
from .const import DEFAULT_HOST, DOMAIN, SECTION_ADDITIONAL_SETTINGS
|
||||
from .const import DEFAULT_HOST, DOMAIN, SECTION_ADVANCED_SETTINGS
|
||||
|
||||
STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
@@ -25,7 +25,7 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
vol.Required(CONF_PASSWORD): TextSelector(
|
||||
TextSelectorConfig(type=TextSelectorType.PASSWORD)
|
||||
),
|
||||
vol.Required(SECTION_ADDITIONAL_SETTINGS): section(
|
||||
vol.Required(SECTION_ADVANCED_SETTINGS): section(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_HOST, default=DEFAULT_HOST): TextSelector(
|
||||
@@ -79,7 +79,7 @@ class AutoskopeConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
username = user_input[CONF_USERNAME].lower()
|
||||
host = user_input[SECTION_ADDITIONAL_SETTINGS][CONF_HOST].lower()
|
||||
host = user_input[SECTION_ADVANCED_SETTINGS][CONF_HOST].lower()
|
||||
|
||||
try:
|
||||
cv.url(host)
|
||||
|
||||
@@ -5,5 +5,5 @@ from datetime import timedelta
|
||||
DOMAIN = "autoskope"
|
||||
|
||||
DEFAULT_HOST = "https://portal.autoskope.de"
|
||||
SECTION_ADDITIONAL_SETTINGS = "additional_settings"
|
||||
SECTION_ADVANCED_SETTINGS = "advanced_settings"
|
||||
UPDATE_INTERVAL = timedelta(seconds=60)
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
},
|
||||
"description": "Enter your Autoskope credentials.",
|
||||
"sections": {
|
||||
"additional_settings": {
|
||||
"advanced_settings": {
|
||||
"data": {
|
||||
"host": "API endpoint"
|
||||
},
|
||||
|
||||
@@ -2,23 +2,9 @@
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from duco_connectivity.models import NodeType
|
||||
|
||||
from homeassistant.const import Platform
|
||||
|
||||
DOMAIN = "duco"
|
||||
PLATFORMS = [Platform.FAN, Platform.SELECT, Platform.SENSOR]
|
||||
SCAN_INTERVAL = timedelta(seconds=10)
|
||||
BOX_NODE_ID = 1
|
||||
VENTILATION_CAPABLE_NODE_TYPES: tuple[NodeType, ...] = (
|
||||
NodeType.BOX,
|
||||
NodeType.VLV,
|
||||
NodeType.VLVRH,
|
||||
NodeType.VLVVOC,
|
||||
NodeType.VLVCO2,
|
||||
NodeType.VLVCO2RH,
|
||||
NodeType.EAV,
|
||||
NodeType.EAVRH,
|
||||
NodeType.EAVVOC,
|
||||
NodeType.EAVCO2,
|
||||
)
|
||||
|
||||
@@ -10,6 +10,7 @@ from duco_connectivity import (
|
||||
KnownActionName,
|
||||
Node,
|
||||
NodeListActionItemList,
|
||||
NodeType,
|
||||
VentilationState,
|
||||
)
|
||||
|
||||
@@ -18,7 +19,7 @@ from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN, VENTILATION_CAPABLE_NODE_TYPES
|
||||
from .const import DOMAIN
|
||||
from .coordinator import DucoConfigEntry, DucoCoordinator
|
||||
from .entity import DucoEntity
|
||||
|
||||
@@ -26,6 +27,19 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
SUPPORTED_SELECT_NODE_TYPES = {
|
||||
NodeType.BOX,
|
||||
NodeType.VLV,
|
||||
NodeType.VLVRH,
|
||||
NodeType.VLVVOC,
|
||||
NodeType.VLVCO2,
|
||||
NodeType.VLVCO2RH,
|
||||
NodeType.EAV,
|
||||
NodeType.EAVRH,
|
||||
NodeType.EAVVOC,
|
||||
NodeType.EAVCO2,
|
||||
}
|
||||
|
||||
|
||||
def _get_ventilation_options(action: ActionItem) -> tuple[str, ...] | None:
|
||||
"""Return ventilation options advertised by a node action."""
|
||||
@@ -72,7 +86,7 @@ async def async_setup_entry(
|
||||
|
||||
# Duco advertises SetVentilationState broadly, so keep the select
|
||||
# limited to the box and known valve node families.
|
||||
if node.general.node_type not in VENTILATION_CAPABLE_NODE_TYPES:
|
||||
if node.general.node_type not in SUPPORTED_SELECT_NODE_TYPES:
|
||||
continue
|
||||
|
||||
options = options_by_node.get(node.node_id)
|
||||
|
||||
@@ -25,7 +25,7 @@ from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import BOX_NODE_ID, DOMAIN, VENTILATION_CAPABLE_NODE_TYPES
|
||||
from .const import BOX_NODE_ID, DOMAIN
|
||||
from .coordinator import DucoConfigEntry, DucoCoordinator
|
||||
from .entity import DucoEntity
|
||||
|
||||
@@ -65,7 +65,7 @@ SENSOR_DESCRIPTIONS: tuple[DucoSensorEntityDescription, ...] = (
|
||||
if node.ventilation and node.ventilation.state != VentilationState.UNKNOWN
|
||||
else None
|
||||
),
|
||||
node_types=VENTILATION_CAPABLE_NODE_TYPES,
|
||||
node_types=(NodeType.BOX,),
|
||||
),
|
||||
DucoSensorEntityDescription(
|
||||
key="target_flow_level",
|
||||
@@ -76,7 +76,7 @@ SENSOR_DESCRIPTIONS: tuple[DucoSensorEntityDescription, ...] = (
|
||||
value_fn=lambda node: (
|
||||
node.ventilation.flow_lvl_tgt if node.ventilation else None
|
||||
),
|
||||
node_types=VENTILATION_CAPABLE_NODE_TYPES,
|
||||
node_types=(NodeType.BOX,),
|
||||
),
|
||||
DucoSensorEntityDescription(
|
||||
key="time_state_end",
|
||||
@@ -89,7 +89,7 @@ SENSOR_DESCRIPTIONS: tuple[DucoSensorEntityDescription, ...] = (
|
||||
if node.ventilation and node.ventilation.time_state_end != 0
|
||||
else None
|
||||
),
|
||||
node_types=VENTILATION_CAPABLE_NODE_TYPES,
|
||||
node_types=(NodeType.BOX,),
|
||||
),
|
||||
DucoSensorEntityDescription(
|
||||
key="co2",
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyenphase"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pyenphase==3.0.1"],
|
||||
"requirements": ["pyenphase==3.0.0"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_enphase-envoy._tcp.local."
|
||||
|
||||
@@ -126,8 +126,6 @@ class FreeboxRouter:
|
||||
self.raids: dict[int, dict[str, Any]] = {}
|
||||
self.sensors_temperature: dict[str, int] = {}
|
||||
self.sensors_temperature_names: dict[str, str] = {}
|
||||
self.sensors_fan: dict[str, int] = {}
|
||||
self.sensors_fan_names: dict[str, str] = {}
|
||||
self.sensors_connection: dict[str, float] = {}
|
||||
self.call_list: list[dict[str, Any]] = []
|
||||
self.home_granted = True
|
||||
@@ -192,12 +190,6 @@ class FreeboxRouter:
|
||||
self.sensors_temperature[sensor_id] = sensor.get("value")
|
||||
self.sensors_temperature_names[sensor_id] = sensor["name"]
|
||||
|
||||
# Fan speed sensors (rpm). Name and id may vary under Freebox devices.
|
||||
for fan in syst_datas.get("fans", []):
|
||||
fan_id = fan["id"]
|
||||
self.sensors_fan[fan_id] = fan.get("value")
|
||||
self.sensors_fan_names[fan_id] = fan["name"]
|
||||
|
||||
# Connection sensors
|
||||
connection_datas: dict[str, Any] = await self._api.connection.get_status()
|
||||
for sensor_key in CONNECTION_SENSORS_KEYS:
|
||||
@@ -329,11 +321,7 @@ class FreeboxRouter:
|
||||
@property
|
||||
def sensors(self) -> dict[str, Any]:
|
||||
"""Return sensors."""
|
||||
return {
|
||||
**self.sensors_temperature,
|
||||
**self.sensors_fan,
|
||||
**self.sensors_connection,
|
||||
}
|
||||
return {**self.sensors_temperature, **self.sensors_connection}
|
||||
|
||||
@property
|
||||
def call(self) -> Call:
|
||||
|
||||
@@ -11,7 +11,6 @@ from homeassistant.components.sensor import (
|
||||
)
|
||||
from homeassistant.const import (
|
||||
PERCENTAGE,
|
||||
REVOLUTIONS_PER_MINUTE,
|
||||
EntityCategory,
|
||||
UnitOfDataRate,
|
||||
UnitOfTemperature,
|
||||
@@ -94,27 +93,6 @@ async def async_setup_entry(
|
||||
for sensor_id, sensor_name in router.sensors_temperature_names.items()
|
||||
]
|
||||
|
||||
_LOGGER.debug(
|
||||
"%s - %s - %s fan sensors",
|
||||
router.name,
|
||||
router.mac,
|
||||
len(router.sensors_fan_names),
|
||||
)
|
||||
entities.extend(
|
||||
FreeboxSensor(
|
||||
router,
|
||||
SensorEntityDescription(
|
||||
key=fan_id,
|
||||
name=fan_name,
|
||||
native_unit_of_measurement=REVOLUTIONS_PER_MINUTE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
icon="mdi:fan",
|
||||
),
|
||||
)
|
||||
for fan_id, fan_name in router.sensors_fan_names.items()
|
||||
)
|
||||
|
||||
entities.extend(
|
||||
[FreeboxSensor(router, description) for description in CONNECTION_SENSORS]
|
||||
)
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["googleapiclient"],
|
||||
"requirements": ["gcal-sync==8.0.0", "oauth2client==4.1.3", "ical==13.3.0"]
|
||||
"requirements": ["gcal-sync==8.0.0", "oauth2client==4.1.3", "ical==13.2.5"]
|
||||
}
|
||||
|
||||
@@ -27,6 +27,25 @@
|
||||
"state": {
|
||||
"2d_fix": "2D Fix",
|
||||
"3d_fix": "3D Fix"
|
||||
},
|
||||
"state_attributes": {
|
||||
"climb": {
|
||||
"name": "[%key:component::gpsd::entity::sensor::climb::name%]"
|
||||
},
|
||||
"elevation": {
|
||||
"name": "[%key:common::config_flow::data::elevation%]"
|
||||
},
|
||||
"gps_time": {
|
||||
"name": "[%key:component::time_date::selector::display_options::options::time%]"
|
||||
},
|
||||
"latitude": { "name": "[%key:common::config_flow::data::latitude%]" },
|
||||
"longitude": {
|
||||
"name": "[%key:common::config_flow::data::longitude%]"
|
||||
},
|
||||
"mode": { "name": "[%key:common::config_flow::data::mode%]" },
|
||||
"speed": {
|
||||
"name": "[%key:component::sensor::entity_component::speed::name%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"time": {
|
||||
|
||||
@@ -20,7 +20,7 @@ from .const import (
|
||||
CONF_MIN_STATE_DURATION,
|
||||
CONF_START,
|
||||
PLATFORMS,
|
||||
SECTION_ADDITIONAL_SETTINGS,
|
||||
SECTION_ADVANCED_SETTINGS,
|
||||
)
|
||||
from .coordinator import HistoryStatsUpdateCoordinator
|
||||
from .data import HistoryStats
|
||||
@@ -44,8 +44,8 @@ async def async_setup_entry(
|
||||
min_state_duration: timedelta
|
||||
if duration_dict := entry.options.get(CONF_DURATION):
|
||||
duration = timedelta(**duration_dict)
|
||||
additional_settings = entry.options.get(SECTION_ADDITIONAL_SETTINGS, {})
|
||||
if min_state_duration_dict := additional_settings.get(CONF_MIN_STATE_DURATION):
|
||||
advanced_settings = entry.options.get(SECTION_ADVANCED_SETTINGS, {})
|
||||
if min_state_duration_dict := advanced_settings.get(CONF_MIN_STATE_DURATION):
|
||||
min_state_duration = timedelta(**min_state_duration_dict)
|
||||
else:
|
||||
min_state_duration = timedelta(0)
|
||||
@@ -121,13 +121,6 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
||||
hass.config_entries.async_update_entry(
|
||||
config_entry, options=options, minor_version=3
|
||||
)
|
||||
if config_entry.minor_version < 4:
|
||||
# The "advanced_settings" section was renamed to "additional_settings"
|
||||
if (additional := options.pop("advanced_settings", None)) is not None:
|
||||
options[SECTION_ADDITIONAL_SETTINGS] = additional
|
||||
hass.config_entries.async_update_entry(
|
||||
config_entry, options=options, minor_version=4
|
||||
)
|
||||
|
||||
_LOGGER.debug(
|
||||
"Migration to version %s.%s successful",
|
||||
|
||||
@@ -44,7 +44,7 @@ from .const import (
|
||||
CONF_TYPE_TIME,
|
||||
DEFAULT_NAME,
|
||||
DOMAIN,
|
||||
SECTION_ADDITIONAL_SETTINGS,
|
||||
SECTION_ADVANCED_SETTINGS,
|
||||
)
|
||||
from .coordinator import HistoryStatsUpdateCoordinator
|
||||
from .data import HistoryStats
|
||||
@@ -149,7 +149,7 @@ def _get_options_schema_with_entity_id(entity_id: str, type: str) -> vol.Schema:
|
||||
mode=SelectSelectorMode.DROPDOWN,
|
||||
),
|
||||
),
|
||||
vol.Optional(SECTION_ADDITIONAL_SETTINGS): section(
|
||||
vol.Optional(SECTION_ADVANCED_SETTINGS): section(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_MIN_STATE_DURATION): DurationSelector(
|
||||
@@ -189,7 +189,7 @@ OPTIONS_FLOW = {
|
||||
class HistoryStatsConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
||||
"""Handle a config flow for History stats."""
|
||||
|
||||
MINOR_VERSION = 4
|
||||
MINOR_VERSION = 3
|
||||
|
||||
config_flow = CONFIG_FLOW
|
||||
options_flow = OPTIONS_FLOW
|
||||
@@ -290,8 +290,8 @@ async def ws_start_preview(
|
||||
start = validated_data.get(CONF_START)
|
||||
end = validated_data.get(CONF_END)
|
||||
duration = validated_data.get(CONF_DURATION)
|
||||
additional_settings = validated_data.get(SECTION_ADDITIONAL_SETTINGS, {})
|
||||
min_state_duration = additional_settings.get(CONF_MIN_STATE_DURATION)
|
||||
advanced_settings = validated_data.get(SECTION_ADVANCED_SETTINGS, {})
|
||||
min_state_duration = advanced_settings.get(CONF_MIN_STATE_DURATION)
|
||||
state_class = validated_data.get(CONF_STATE_CLASS)
|
||||
|
||||
history_stats = HistoryStats(
|
||||
|
||||
@@ -18,4 +18,4 @@ CONF_TYPE_KEYS = [CONF_TYPE_TIME, CONF_TYPE_RATIO, CONF_TYPE_COUNT]
|
||||
|
||||
DEFAULT_NAME = "unnamed statistics"
|
||||
|
||||
SECTION_ADDITIONAL_SETTINGS = "additional_settings"
|
||||
SECTION_ADVANCED_SETTINGS = "advanced_settings"
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
},
|
||||
"description": "Read the documentation for further details on how to configure the history stats sensor using these options.",
|
||||
"sections": {
|
||||
"additional_settings": {
|
||||
"advanced_settings": {
|
||||
"data": { "min_state_duration": "Minimum state duration" },
|
||||
"data_description": {
|
||||
"min_state_duration": "The minimum state duration to account for the statistics. Default is 0 seconds."
|
||||
@@ -93,14 +93,14 @@
|
||||
},
|
||||
"description": "[%key:component::history_stats::config::step::options::description%]",
|
||||
"sections": {
|
||||
"additional_settings": {
|
||||
"advanced_settings": {
|
||||
"data": {
|
||||
"min_state_duration": "[%key:component::history_stats::config::step::options::sections::additional_settings::data::min_state_duration%]"
|
||||
"min_state_duration": "[%key:component::history_stats::config::step::options::sections::advanced_settings::data::min_state_duration%]"
|
||||
},
|
||||
"data_description": {
|
||||
"min_state_duration": "[%key:component::history_stats::config::step::options::sections::additional_settings::data_description::min_state_duration%]"
|
||||
"min_state_duration": "[%key:component::history_stats::config::step::options::sections::advanced_settings::data_description::min_state_duration%]"
|
||||
},
|
||||
"name": "[%key:component::history_stats::config::step::options::sections::additional_settings::name%]"
|
||||
"name": "[%key:component::history_stats::config::step::options::sections::advanced_settings::name%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ CODE_SCHEMA = vol.Schema(
|
||||
|
||||
SENSOR_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_NAME, default=DOMAIN): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DOMAIN): vol.Exclusive(cv.string, "sensors"),
|
||||
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
}
|
||||
@@ -57,7 +57,7 @@ SENSOR_SCHEMA = vol.Schema(
|
||||
|
||||
REMOTE_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_NAME, default=DOMAIN): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DOMAIN): vol.Exclusive(cv.string, "remotes"),
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ LIFX_SET_STATE_SCHEMA: VolDictType = {
|
||||
SERVICE_LIFX_SET_HEV_CYCLE_STATE = "set_hev_cycle_state"
|
||||
|
||||
LIFX_SET_HEV_CYCLE_STATE_SCHEMA: VolDictType = {
|
||||
vol.Required(ATTR_POWER): cv.boolean,
|
||||
ATTR_POWER: vol.Required(cv.boolean),
|
||||
ATTR_DURATION: vol.All(vol.Coerce(float), vol.Clamp(min=0, max=86400)),
|
||||
}
|
||||
|
||||
|
||||
@@ -178,7 +178,9 @@ LIFX_EFFECT_MORPH_SCHEMA = cv.make_entity_service_schema(
|
||||
{
|
||||
**LIFX_EFFECT_SCHEMA,
|
||||
ATTR_SPEED: vol.All(vol.Coerce(int), vol.Clamp(min=1, max=25)),
|
||||
vol.Exclusive(ATTR_THEME, COLOR_GROUP): vol.In(ThemeLibrary().themes),
|
||||
vol.Exclusive(ATTR_THEME, COLOR_GROUP): vol.Optional(
|
||||
vol.In(ThemeLibrary().themes)
|
||||
),
|
||||
vol.Exclusive(ATTR_PALETTE, COLOR_GROUP): vol.All(
|
||||
cv.ensure_list, [HSBK_SCHEMA]
|
||||
),
|
||||
@@ -190,7 +192,7 @@ LIFX_EFFECT_MOVE_SCHEMA = cv.make_entity_service_schema(
|
||||
**LIFX_EFFECT_SCHEMA,
|
||||
ATTR_SPEED: vol.All(vol.Coerce(float), vol.Clamp(min=0.1, max=60)),
|
||||
ATTR_DIRECTION: vol.In(EFFECT_MOVE_DIRECTIONS),
|
||||
vol.Optional(ATTR_THEME): vol.In(ThemeLibrary().themes),
|
||||
ATTR_THEME: vol.Optional(vol.In(ThemeLibrary().themes)),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -209,7 +211,9 @@ LIFX_PAINT_THEME_SCHEMA = cv.make_entity_service_schema(
|
||||
{
|
||||
**LIFX_EFFECT_SCHEMA,
|
||||
ATTR_TRANSITION: vol.All(vol.Coerce(int), vol.Clamp(min=1, max=3600)),
|
||||
vol.Exclusive(ATTR_THEME, COLOR_GROUP): vol.In(ThemeLibrary().themes),
|
||||
vol.Exclusive(ATTR_THEME, COLOR_GROUP): vol.Optional(
|
||||
vol.In(ThemeLibrary().themes)
|
||||
),
|
||||
vol.Exclusive(ATTR_PALETTE, COLOR_GROUP): vol.All(
|
||||
cv.ensure_list, [HSBK_SCHEMA]
|
||||
),
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/local_calendar",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["ical"],
|
||||
"requirements": ["ical==13.3.0"]
|
||||
"requirements": ["ical==13.2.5"]
|
||||
}
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/local_todo",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["ical==13.3.0"]
|
||||
"requirements": ["ical==13.2.5"]
|
||||
}
|
||||
|
||||
@@ -227,8 +227,8 @@ class MikrotikData:
|
||||
_LOGGER.debug("Running command %s", cmd)
|
||||
try:
|
||||
if params:
|
||||
return list(self.api(cmd, **params))
|
||||
return list(self.api(cmd))
|
||||
return list(self.api(cmd=cmd, **params))
|
||||
return list(self.api(cmd=cmd))
|
||||
except (
|
||||
librouteros.exceptions.ConnectionClosed,
|
||||
OSError,
|
||||
@@ -318,7 +318,8 @@ def get_api(entry: dict[str, Any]) -> librouteros.Api:
|
||||
"""Connect to Mikrotik hub."""
|
||||
_LOGGER.debug("Connecting to Mikrotik hub [%s]", entry[CONF_HOST])
|
||||
|
||||
kwargs = {"port": entry["port"], "encoding": "utf8"}
|
||||
_login_method = (login_plain, login_token)
|
||||
kwargs = {"login_methods": _login_method, "port": entry["port"], "encoding": "utf8"}
|
||||
|
||||
if entry[CONF_VERIFY_SSL]:
|
||||
ssl_context = ssl.create_default_context()
|
||||
@@ -327,30 +328,22 @@ def get_api(entry: dict[str, Any]) -> librouteros.Api:
|
||||
_ssl_wrapper = ssl_context.wrap_socket
|
||||
kwargs["ssl_wrapper"] = _ssl_wrapper
|
||||
|
||||
_error: Exception | None = None
|
||||
for method in (login_plain, login_token):
|
||||
try:
|
||||
kwargs["login_method"] = method
|
||||
api = librouteros.connect(
|
||||
entry[CONF_HOST],
|
||||
entry[CONF_USERNAME],
|
||||
entry[CONF_PASSWORD],
|
||||
**kwargs,
|
||||
)
|
||||
_error = None
|
||||
break
|
||||
except (
|
||||
librouteros.exceptions.LibRouterosError,
|
||||
OSError,
|
||||
TimeoutError,
|
||||
) as api_error:
|
||||
_error = api_error
|
||||
|
||||
if _error is not None:
|
||||
_LOGGER.error("Mikrotik %s error: %s", entry[CONF_HOST], _error)
|
||||
if "invalid user name or password" in str(_error):
|
||||
raise LoginError from _error
|
||||
raise CannotConnect from _error
|
||||
try:
|
||||
api = librouteros.connect(
|
||||
entry[CONF_HOST],
|
||||
entry[CONF_USERNAME],
|
||||
entry[CONF_PASSWORD],
|
||||
**kwargs,
|
||||
)
|
||||
except (
|
||||
librouteros.exceptions.LibRouterosError,
|
||||
OSError,
|
||||
TimeoutError,
|
||||
) as api_error:
|
||||
_LOGGER.error("Mikrotik %s error: %s", entry[CONF_HOST], api_error)
|
||||
if "invalid user name or password" in str(api_error):
|
||||
raise LoginError from api_error
|
||||
raise CannotConnect from api_error
|
||||
|
||||
_LOGGER.debug("Connected to %s successfully", entry[CONF_HOST])
|
||||
return api
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["librouteros"],
|
||||
"requirements": ["librouteros==4.1.1"]
|
||||
"requirements": ["librouteros==3.2.1"]
|
||||
}
|
||||
|
||||
@@ -71,6 +71,7 @@ from .const import (
|
||||
DEFAULT_QOS,
|
||||
DEFAULT_TRANSPORT,
|
||||
DEFAULT_WILL,
|
||||
DEFAULT_WS_HEADERS,
|
||||
DEFAULT_WS_PATH,
|
||||
DOMAIN,
|
||||
MQTT_CONNECTION_STATE,
|
||||
@@ -413,7 +414,7 @@ class MqttClientSetup:
|
||||
tls_insecure = config.get(CONF_TLS_INSECURE)
|
||||
if transport == TRANSPORT_WEBSOCKETS:
|
||||
ws_path: str = config.get(CONF_WS_PATH, DEFAULT_WS_PATH)
|
||||
ws_headers: dict[str, str] = config.get(CONF_WS_HEADERS, {})
|
||||
ws_headers: dict[str, str] = config.get(CONF_WS_HEADERS, DEFAULT_WS_HEADERS)
|
||||
self._client.ws_set_options(ws_path, ws_headers)
|
||||
if certificate is not None:
|
||||
self._client.tls_set(
|
||||
|
||||
@@ -373,6 +373,7 @@ from .const import (
|
||||
DEFAULT_CLIMATE_INITIAL_TEMPERATURE,
|
||||
DEFAULT_DISCOVERY,
|
||||
DEFAULT_ENCODING,
|
||||
DEFAULT_KEEPALIVE,
|
||||
DEFAULT_ON_COMMAND_TYPE,
|
||||
DEFAULT_PAYLOAD_ARM_AWAY,
|
||||
DEFAULT_PAYLOAD_ARM_CUSTOM_BYPASS,
|
||||
@@ -413,6 +414,7 @@ from .const import (
|
||||
DEFAULT_TILT_OPEN_POSITION,
|
||||
DEFAULT_TRANSPORT,
|
||||
DEFAULT_WILL,
|
||||
DEFAULT_WS_PATH,
|
||||
DOMAIN,
|
||||
REMOTE_CODE,
|
||||
REMOTE_CODE_TEXT,
|
||||
@@ -439,7 +441,7 @@ ADDON_SETUP_TIMEOUT_ROUNDS = 5
|
||||
|
||||
CONF_CLIENT_KEY_PASSWORD = "client_key_password"
|
||||
|
||||
OTHER_SETTINGS = "other_settings"
|
||||
ADVANCED_OPTIONS = "advanced_options"
|
||||
SET_CA_CERT = "set_ca_cert"
|
||||
SET_CLIENT_CERT = "set_client_cert"
|
||||
|
||||
@@ -1122,7 +1124,7 @@ def validate_light_platform_config(user_data: dict[str, Any]) -> dict[str, str]:
|
||||
if user_data.get(CONF_MIN_KELVIN, DEFAULT_MIN_KELVIN) >= user_data.get(
|
||||
CONF_MAX_KELVIN, DEFAULT_MAX_KELVIN
|
||||
):
|
||||
errors[OTHER_SETTINGS] = "max_below_min_kelvin"
|
||||
errors["other_settings"] = "max_below_min_kelvin"
|
||||
return errors
|
||||
|
||||
|
||||
@@ -1504,7 +1506,7 @@ PLATFORM_ENTITY_FIELDS: dict[Platform, dict[str, PlatformField]] = {
|
||||
selector=SUGGESTED_DISPLAY_PRECISION_SELECTOR,
|
||||
required=False,
|
||||
validator=cv.positive_int,
|
||||
section=OTHER_SETTINGS,
|
||||
section="other_settings",
|
||||
),
|
||||
CONF_OPTIONS: PlatformField(
|
||||
selector=OPTIONS_SELECTOR,
|
||||
@@ -1676,13 +1678,13 @@ PLATFORM_MQTT_FIELDS: dict[Platform, dict[str, PlatformField]] = {
|
||||
selector=TIMEOUT_SELECTOR,
|
||||
required=False,
|
||||
validator=cv.positive_int,
|
||||
section=OTHER_SETTINGS,
|
||||
section="other_settings",
|
||||
),
|
||||
CONF_OFF_DELAY: PlatformField(
|
||||
selector=TIMEOUT_SELECTOR,
|
||||
required=False,
|
||||
validator=cv.positive_int,
|
||||
section=OTHER_SETTINGS,
|
||||
section="other_settings",
|
||||
),
|
||||
},
|
||||
Platform.BUTTON: {
|
||||
@@ -3123,7 +3125,7 @@ PLATFORM_MQTT_FIELDS: dict[Platform, dict[str, PlatformField]] = {
|
||||
default=False,
|
||||
validator=cv.boolean,
|
||||
conditions=({CONF_SCHEMA: "json"},),
|
||||
section=OTHER_SETTINGS,
|
||||
section="other_settings",
|
||||
),
|
||||
CONF_FLASH_TIME_SHORT: PlatformField(
|
||||
selector=FLASH_TIME_SELECTOR,
|
||||
@@ -3131,7 +3133,7 @@ PLATFORM_MQTT_FIELDS: dict[Platform, dict[str, PlatformField]] = {
|
||||
validator=cv.positive_int,
|
||||
default=2,
|
||||
conditions=({CONF_SCHEMA: "json"},),
|
||||
section=OTHER_SETTINGS,
|
||||
section="other_settings",
|
||||
),
|
||||
CONF_FLASH_TIME_LONG: PlatformField(
|
||||
selector=FLASH_TIME_SELECTOR,
|
||||
@@ -3139,7 +3141,7 @@ PLATFORM_MQTT_FIELDS: dict[Platform, dict[str, PlatformField]] = {
|
||||
validator=cv.positive_int,
|
||||
default=10,
|
||||
conditions=({CONF_SCHEMA: "json"},),
|
||||
section=OTHER_SETTINGS,
|
||||
section="other_settings",
|
||||
),
|
||||
CONF_TRANSITION: PlatformField(
|
||||
selector=BOOLEAN_SELECTOR,
|
||||
@@ -3147,21 +3149,21 @@ PLATFORM_MQTT_FIELDS: dict[Platform, dict[str, PlatformField]] = {
|
||||
default=False,
|
||||
validator=cv.boolean,
|
||||
conditions=({CONF_SCHEMA: "json"},),
|
||||
section=OTHER_SETTINGS,
|
||||
section="other_settings",
|
||||
),
|
||||
CONF_MAX_KELVIN: PlatformField(
|
||||
selector=KELVIN_SELECTOR,
|
||||
required=False,
|
||||
validator=cv.positive_int,
|
||||
default=DEFAULT_MAX_KELVIN,
|
||||
section=OTHER_SETTINGS,
|
||||
section="other_settings",
|
||||
),
|
||||
CONF_MIN_KELVIN: PlatformField(
|
||||
selector=KELVIN_SELECTOR,
|
||||
required=False,
|
||||
validator=cv.positive_int,
|
||||
default=DEFAULT_MIN_KELVIN,
|
||||
section=OTHER_SETTINGS,
|
||||
section="other_settings",
|
||||
),
|
||||
},
|
||||
Platform.LOCK: {
|
||||
@@ -3370,7 +3372,7 @@ PLATFORM_MQTT_FIELDS: dict[Platform, dict[str, PlatformField]] = {
|
||||
selector=TIMEOUT_SELECTOR,
|
||||
required=False,
|
||||
validator=cv.positive_int,
|
||||
section=OTHER_SETTINGS,
|
||||
section="other_settings",
|
||||
),
|
||||
},
|
||||
Platform.SIREN: {
|
||||
@@ -3796,10 +3798,10 @@ PLATFORM_MQTT_FIELDS: dict[Platform, dict[str, PlatformField]] = {
|
||||
MQTT_DEVICE_PLATFORM_FIELDS = {
|
||||
CONF_NAME: PlatformField(selector=TEXT_SELECTOR, required=True),
|
||||
CONF_SW_VERSION: PlatformField(
|
||||
selector=TEXT_SELECTOR, required=False, section=OTHER_SETTINGS
|
||||
selector=TEXT_SELECTOR, required=False, section="other_settings"
|
||||
),
|
||||
CONF_HW_VERSION: PlatformField(
|
||||
selector=TEXT_SELECTOR, required=False, section=OTHER_SETTINGS
|
||||
selector=TEXT_SELECTOR, required=False, section="other_settings"
|
||||
),
|
||||
CONF_MODEL: PlatformField(selector=TEXT_SELECTOR, required=False),
|
||||
CONF_MODEL_ID: PlatformField(selector=TEXT_SELECTOR, required=False),
|
||||
@@ -4034,22 +4036,24 @@ def subentry_schema_default_data_from_fields(
|
||||
@callback
|
||||
def update_password_from_user_input(
|
||||
entry_password: str | None, user_input: dict[str, Any]
|
||||
) -> None:
|
||||
) -> dict[str, Any]:
|
||||
"""Update the password if the entry has been updated.
|
||||
|
||||
As we want to avoid reflecting the stored password in the UI,
|
||||
we replace the suggested value in the UI with a sentitel,
|
||||
and we change it back here if it was changed.
|
||||
"""
|
||||
substituted_used_data = dict(user_input)
|
||||
# Take out the password submitted
|
||||
user_password: str | None = user_input.pop(CONF_PASSWORD, None)
|
||||
user_password: str | None = substituted_used_data.pop(CONF_PASSWORD, None)
|
||||
# Only add the password if it has changed.
|
||||
# If the sentinel password is submitted, we replace that with our current
|
||||
# password from the config entry data.
|
||||
password_changed = user_password is not None and user_password != PWD_NOT_CHANGED
|
||||
password = user_password if password_changed else entry_password
|
||||
if password is not None:
|
||||
user_input[CONF_PASSWORD] = password
|
||||
substituted_used_data[CONF_PASSWORD] = password
|
||||
return substituted_used_data
|
||||
|
||||
|
||||
REAUTH_SCHEMA = vol.Schema(
|
||||
@@ -4059,35 +4063,6 @@ REAUTH_SCHEMA = vol.Schema(
|
||||
}
|
||||
)
|
||||
|
||||
OTHER_SETTINGS_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_CLIENT_ID): TEXT_SELECTOR,
|
||||
vol.Optional(CONF_KEEPALIVE): KEEPALIVE_SELECTOR,
|
||||
vol.Required(SET_CLIENT_CERT): BOOLEAN_SELECTOR,
|
||||
vol.Optional(CONF_CLIENT_CERT): CERT_UPLOAD_SELECTOR,
|
||||
vol.Optional(CONF_CLIENT_KEY): CERT_KEY_UPLOAD_SELECTOR,
|
||||
vol.Optional(CONF_CLIENT_KEY_PASSWORD): PASSWORD_SELECTOR,
|
||||
vol.Required(SET_CA_CERT): BROKER_VERIFICATION_SELECTOR,
|
||||
vol.Optional(CONF_CERTIFICATE): CA_CERT_UPLOAD_SELECTOR,
|
||||
vol.Optional(CONF_TLS_INSECURE): BOOLEAN_SELECTOR,
|
||||
vol.Required(CONF_TRANSPORT, default=DEFAULT_TRANSPORT): TRANSPORT_SELECTOR,
|
||||
vol.Optional(CONF_WS_PATH): TEXT_SELECTOR,
|
||||
vol.Optional(CONF_WS_HEADERS): WS_HEADERS_SELECTOR,
|
||||
}
|
||||
)
|
||||
CONFIG_DATAFLOW_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_BROKER): TEXT_SELECTOR,
|
||||
vol.Required(CONF_PORT, default=DEFAULT_PORT): PORT_SELECTOR,
|
||||
vol.Required(CONF_PROTOCOL, default=DEFAULT_PROTOCOL): PROTOCOL_SELECTOR,
|
||||
vol.Optional(CONF_USERNAME): TEXT_SELECTOR,
|
||||
vol.Optional(CONF_PASSWORD): PASSWORD_SELECTOR,
|
||||
vol.Required(OTHER_SETTINGS): section(
|
||||
OTHER_SETTINGS_SCHEMA, SectionConfig({"collapsed": True})
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class FlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow."""
|
||||
@@ -4097,26 +4072,24 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
_hassio_discovery: dict[str, Any] | None = None
|
||||
_addon_manager: AddonManager
|
||||
last_uploaded: dict[str, Any]
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Set up flow instance."""
|
||||
self.install_task: asyncio.Task | None = None
|
||||
self.start_task: asyncio.Task | None = None
|
||||
self.last_uploaded = {}
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
@callback
|
||||
@override
|
||||
def async_get_supported_subentry_types(
|
||||
cls, config_entry: ConfigEntry
|
||||
) -> dict[str, type[ConfigSubentryFlow]]:
|
||||
"""Return subentries supported by this handler."""
|
||||
return {CONF_DEVICE: MQTTSubentryFlowHandler}
|
||||
|
||||
@override
|
||||
@staticmethod
|
||||
@callback
|
||||
@override
|
||||
def async_get_options_flow(
|
||||
config_entry: ConfigEntry,
|
||||
) -> MQTTOptionsFlowHandler:
|
||||
@@ -4337,9 +4310,8 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
reauth_entry = self._get_reauth_entry()
|
||||
if user_input:
|
||||
substituted_used_data = deepcopy(user_input)
|
||||
update_password_from_user_input(
|
||||
reauth_entry.data.get(CONF_PASSWORD), substituted_used_data
|
||||
substituted_used_data = update_password_from_user_input(
|
||||
reauth_entry.data.get(CONF_PASSWORD), user_input
|
||||
)
|
||||
new_entry_data = {**reauth_entry.data, **substituted_used_data}
|
||||
if await self.hass.async_add_executor_job(
|
||||
@@ -4363,76 +4335,49 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
@callback
|
||||
def async_get_entry_defaults(self) -> dict[str, Any]:
|
||||
"""Load the default settings from the entry."""
|
||||
data = self._get_reconfigure_entry().data
|
||||
other_settings: dict[str, Any] = {
|
||||
key.schema: data[key.schema]
|
||||
for key in OTHER_SETTINGS_SCHEMA.schema
|
||||
if key in data
|
||||
}
|
||||
other_settings[SET_CLIENT_CERT] = (CONF_CLIENT_CERT in other_settings) and (
|
||||
CONF_CLIENT_KEY in other_settings
|
||||
)
|
||||
other_settings.pop(CONF_CLIENT_CERT, None)
|
||||
other_settings.pop(CONF_CLIENT_KEY, None)
|
||||
conf_cert = other_settings.pop(CONF_CERTIFICATE, None)
|
||||
other_settings[SET_CA_CERT] = (
|
||||
"auto"
|
||||
if conf_cert == "auto"
|
||||
else "custom"
|
||||
if conf_cert is not None
|
||||
else "off"
|
||||
)
|
||||
if CONF_WS_HEADERS in other_settings:
|
||||
other_settings[CONF_WS_HEADERS] = json_dumps(
|
||||
other_settings.pop(CONF_WS_HEADERS)
|
||||
)
|
||||
|
||||
settings: dict[str, Any] = {
|
||||
key.schema: data[key.schema]
|
||||
for key in CONFIG_DATAFLOW_SCHEMA.schema
|
||||
if key in data
|
||||
}
|
||||
settings[OTHER_SETTINGS] = other_settings
|
||||
if CONF_PASSWORD in settings:
|
||||
# Hide entry password
|
||||
settings[CONF_PASSWORD] = PWD_NOT_CHANGED
|
||||
return settings
|
||||
|
||||
async def async_step_broker(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Confirm the setup."""
|
||||
errors: dict[str, str] = {}
|
||||
schema = CONFIG_DATAFLOW_SCHEMA
|
||||
entry_config_update: dict[str, Any] = {}
|
||||
entry_defaults: dict[str, Any] | None = None
|
||||
fields: OrderedDict[Any, Any] = OrderedDict()
|
||||
validated_user_input: dict[str, Any] = {}
|
||||
if is_reconfigure := (self.source == SOURCE_RECONFIGURE):
|
||||
reconfigure_entry = self._get_reconfigure_entry()
|
||||
entry_defaults = self.async_get_entry_defaults()
|
||||
if await async_validate_broker_settings(
|
||||
if await async_get_broker_settings(
|
||||
self,
|
||||
fields,
|
||||
reconfigure_entry.data if is_reconfigure else None,
|
||||
user_input,
|
||||
entry_config_update,
|
||||
validated_user_input,
|
||||
errors,
|
||||
):
|
||||
if is_reconfigure:
|
||||
return self.async_update_and_abort(
|
||||
reconfigure_entry,
|
||||
data=entry_config_update,
|
||||
validated_user_input = update_password_from_user_input(
|
||||
reconfigure_entry.data.get(CONF_PASSWORD), validated_user_input
|
||||
)
|
||||
return self.async_create_entry(
|
||||
title=entry_config_update[CONF_BROKER],
|
||||
data=entry_config_update,
|
||||
|
||||
can_connect = await self.hass.async_add_executor_job(
|
||||
try_connection,
|
||||
validated_user_input,
|
||||
)
|
||||
|
||||
schema = self.add_suggested_values_to_schema(
|
||||
schema, (entry_defaults or {}) | (user_input or {})
|
||||
if can_connect:
|
||||
if is_reconfigure:
|
||||
return self.async_update_and_abort(
|
||||
reconfigure_entry,
|
||||
data=validated_user_input,
|
||||
)
|
||||
return self.async_create_entry(
|
||||
title=validated_user_input[CONF_BROKER],
|
||||
data=validated_user_input,
|
||||
)
|
||||
|
||||
errors["base"] = "cannot_connect"
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="broker", data_schema=vol.Schema(fields), errors=errors
|
||||
)
|
||||
return self.async_show_form(step_id="broker", data_schema=schema, errors=errors)
|
||||
|
||||
async def async_step_reconfigure(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
@@ -4743,8 +4688,8 @@ class MQTTSubentryFlowHandler(ConfigSubentryFlow):
|
||||
if user_input is not None:
|
||||
new_device_data: dict[str, Any] = user_input.copy()
|
||||
_, errors = validate_user_input(user_input, MQTT_DEVICE_PLATFORM_FIELDS)
|
||||
if OTHER_SETTINGS in new_device_data:
|
||||
new_device_data |= new_device_data.pop(OTHER_SETTINGS)
|
||||
if "other_settings" in new_device_data:
|
||||
new_device_data |= new_device_data.pop("other_settings")
|
||||
if not errors:
|
||||
self._subentry_data[CONF_DEVICE] = cast(MqttDeviceData, new_device_data)
|
||||
if self.source == SOURCE_RECONFIGURE:
|
||||
@@ -5305,162 +5250,331 @@ async def _get_uploaded_file(hass: HomeAssistant, id: str) -> bytes:
|
||||
return await hass.async_add_executor_job(_proces_uploaded_file)
|
||||
|
||||
|
||||
async def async_validate_broker_settings(
|
||||
flow: FlowHandler,
|
||||
entry_config: MappingProxyType[str, Any] | None,
|
||||
user_input: dict[str, Any] | None,
|
||||
entry_config_update: dict[str, Any],
|
||||
errors: dict[str, str],
|
||||
def _validate_pki_file(
|
||||
file_id: str | None, pem_data: str | None, errors: dict[str, str], error: str
|
||||
) -> bool:
|
||||
"""Validate the broker settings, and return the updated entry dataset."""
|
||||
|
||||
async def _async_process_file_upload(
|
||||
upload_id: str,
|
||||
field: str,
|
||||
pem_type: PEMType,
|
||||
error_code: str,
|
||||
password: str | None = None,
|
||||
) -> bool:
|
||||
"""Get uploaded file, or a preserved copy, and convert to a PEM file."""
|
||||
try:
|
||||
data_raw = await _get_uploaded_file(hass, upload_id)
|
||||
except ValueError:
|
||||
# Use preserved file if available.
|
||||
# When an uploaded file was read, but an error occurs,
|
||||
# the form will reload but the temporary file from the upload
|
||||
# will not be available any more. If it was processed correctly,
|
||||
# we can use the preserved copy.
|
||||
if upload_id in flow.last_uploaded:
|
||||
data_raw = flow.last_uploaded[upload_id]
|
||||
else:
|
||||
raise
|
||||
else:
|
||||
# Preserve a copy in case the validation fails,
|
||||
# and we need it later
|
||||
flow.last_uploaded[upload_id] = data_raw
|
||||
pem_data = async_convert_to_pem(data_raw, pem_type, password)
|
||||
if upload_id and not pem_data:
|
||||
errors["base"] = error_code
|
||||
return False
|
||||
entry_config_update[field] = pem_data
|
||||
return True
|
||||
|
||||
if user_input is None:
|
||||
return False
|
||||
|
||||
hass = flow.hass
|
||||
|
||||
# Copy basic and other entry fields
|
||||
entry_config_update |= user_input
|
||||
entry_config_update.update(entry_config_update.pop(OTHER_SETTINGS))
|
||||
# Pop incompatible fields for update
|
||||
for key in (
|
||||
SET_CA_CERT,
|
||||
SET_CLIENT_CERT,
|
||||
CONF_CERTIFICATE,
|
||||
CONF_CLIENT_CERT,
|
||||
CONF_CLIENT_KEY,
|
||||
CONF_CLIENT_KEY_PASSWORD,
|
||||
):
|
||||
entry_config_update.pop(key, None)
|
||||
|
||||
# Get current CA certificate settings from config entry
|
||||
if (set_ca_cert := user_input[OTHER_SETTINGS][SET_CA_CERT]) == "auto":
|
||||
entry_config_update[CONF_CERTIFICATE] = "auto"
|
||||
elif (
|
||||
entry_config is not None
|
||||
and set_ca_cert == "custom"
|
||||
and (current_cert := entry_config.get(CONF_CERTIFICATE))
|
||||
):
|
||||
entry_config_update[CONF_CERTIFICATE] = current_cert
|
||||
|
||||
# Prepare entry update with uploaded certificate files
|
||||
# converted to PEM format
|
||||
new_client_certificate: str | None = user_input[OTHER_SETTINGS].get(
|
||||
CONF_CLIENT_CERT
|
||||
)
|
||||
new_client_key: str | None = user_input[OTHER_SETTINGS].get(CONF_CLIENT_KEY)
|
||||
set_client_cert = user_input[OTHER_SETTINGS][SET_CLIENT_CERT]
|
||||
|
||||
if (new_client_certificate and not new_client_key) or (
|
||||
not new_client_certificate and new_client_key
|
||||
):
|
||||
errors["base"] = "invalid_inclusion"
|
||||
return False
|
||||
|
||||
if new_certificate := user_input[OTHER_SETTINGS].get(CONF_CERTIFICATE):
|
||||
if not await _async_process_file_upload(
|
||||
new_certificate, CONF_CERTIFICATE, PEMType.CERTIFICATE, "bad_certificate"
|
||||
):
|
||||
return False
|
||||
|
||||
if new_client_certificate:
|
||||
if not await _async_process_file_upload(
|
||||
new_client_certificate,
|
||||
CONF_CLIENT_CERT,
|
||||
PEMType.CERTIFICATE,
|
||||
"bad_client_cert",
|
||||
):
|
||||
return False
|
||||
elif (
|
||||
entry_config is not None
|
||||
and set_client_cert
|
||||
and (client_cert := entry_config.get(CONF_CLIENT_CERT))
|
||||
):
|
||||
entry_config_update[CONF_CLIENT_CERT] = client_cert
|
||||
|
||||
if new_client_key:
|
||||
if not await _async_process_file_upload(
|
||||
new_client_key,
|
||||
CONF_CLIENT_KEY,
|
||||
PEMType.PRIVATE_KEY,
|
||||
"client_key_error",
|
||||
password=user_input[OTHER_SETTINGS].get(CONF_CLIENT_KEY_PASSWORD),
|
||||
):
|
||||
return False
|
||||
elif (
|
||||
entry_config is not None
|
||||
and set_client_cert
|
||||
and (client_key := entry_config.get(CONF_CLIENT_KEY))
|
||||
):
|
||||
entry_config_update[CONF_CLIENT_KEY] = client_key
|
||||
|
||||
# We temporarily create the current and new uploaded certificate files
|
||||
# and we check the certificate chain.
|
||||
await async_create_certificate_temp_files(hass, entry_config_update)
|
||||
if error := await hass.async_add_executor_job(
|
||||
check_certicate_chain,
|
||||
):
|
||||
"""Return False if uploaded file could not be converted to PEM format."""
|
||||
if file_id and not pem_data:
|
||||
errors["base"] = error
|
||||
return False
|
||||
return True
|
||||
|
||||
if user_input[OTHER_SETTINGS].get(CONF_TRANSPORT, TRANSPORT_TCP) == TRANSPORT_TCP:
|
||||
entry_config_update.pop(CONF_WS_PATH, None)
|
||||
entry_config_update.pop(CONF_WS_HEADERS, None)
|
||||
else:
|
||||
# Web socket transport
|
||||
try:
|
||||
entry_config_update[CONF_WS_HEADERS] = json_loads(
|
||||
user_input[OTHER_SETTINGS].get(CONF_WS_HEADERS, "{}")
|
||||
|
||||
async def async_get_broker_settings(
|
||||
flow: ConfigFlow | OptionsFlow,
|
||||
fields: OrderedDict[Any, Any],
|
||||
entry_config: MappingProxyType[str, Any] | None,
|
||||
user_input: dict[str, Any] | None,
|
||||
validated_user_input: dict[str, Any],
|
||||
errors: dict[str, str],
|
||||
) -> bool:
|
||||
"""Build the config flow schema to collect the broker settings.
|
||||
|
||||
Shows advanced options if one or more are configured
|
||||
or when the advanced_broker_options checkbox was selected.
|
||||
Returns True when settings are collected successfully.
|
||||
"""
|
||||
hass = flow.hass
|
||||
advanced_broker_options: bool = False
|
||||
user_input_basic: dict[str, Any] = {}
|
||||
current_config: dict[str, Any] = (
|
||||
entry_config.copy() if entry_config is not None else {}
|
||||
)
|
||||
|
||||
async def _async_validate_broker_settings(
|
||||
config: dict[str, Any],
|
||||
user_input: dict[str, Any],
|
||||
validated_user_input: dict[str, Any],
|
||||
errors: dict[str, str],
|
||||
) -> bool:
|
||||
"""Additional validation on broker settings for better error messages."""
|
||||
|
||||
if CONF_PROTOCOL not in validated_user_input:
|
||||
validated_user_input[CONF_PROTOCOL] = DEFAULT_PROTOCOL
|
||||
# Get current certificate settings from config entry
|
||||
certificate: str | None = (
|
||||
"auto"
|
||||
if user_input.get(SET_CA_CERT, "off") == "auto"
|
||||
else config.get(CONF_CERTIFICATE)
|
||||
if user_input.get(SET_CA_CERT, "off") == "custom"
|
||||
else None
|
||||
)
|
||||
client_certificate: str | None = (
|
||||
config.get(CONF_CLIENT_CERT) if user_input.get(SET_CLIENT_CERT) else None
|
||||
)
|
||||
client_key: str | None = (
|
||||
config.get(CONF_CLIENT_KEY) if user_input.get(SET_CLIENT_CERT) else None
|
||||
)
|
||||
|
||||
# Prepare entry update with uploaded files
|
||||
validated_user_input.update(user_input)
|
||||
client_certificate_id: str | None = user_input.get(CONF_CLIENT_CERT)
|
||||
client_key_id: str | None = user_input.get(CONF_CLIENT_KEY)
|
||||
# We do not store the private key password in the entry data
|
||||
client_key_password: str | None = validated_user_input.pop(
|
||||
CONF_CLIENT_KEY_PASSWORD, None
|
||||
)
|
||||
if (client_certificate_id and not client_key_id) or (
|
||||
not client_certificate_id and client_key_id
|
||||
):
|
||||
errors["base"] = "invalid_inclusion"
|
||||
return False
|
||||
certificate_id: str | None = user_input.get(CONF_CERTIFICATE)
|
||||
if certificate_id:
|
||||
certificate_data_raw = await _get_uploaded_file(hass, certificate_id)
|
||||
certificate = async_convert_to_pem(
|
||||
certificate_data_raw, PEMType.CERTIFICATE
|
||||
)
|
||||
schema = vol.Schema({str: str})
|
||||
schema(entry_config_update[CONF_WS_HEADERS])
|
||||
if not _validate_pki_file(
|
||||
certificate_id, certificate, errors, "bad_certificate"
|
||||
):
|
||||
return False
|
||||
|
||||
# Return to form for file upload CA cert or client cert and key
|
||||
if (
|
||||
(
|
||||
not client_certificate
|
||||
and user_input.get(SET_CLIENT_CERT)
|
||||
and not client_certificate_id
|
||||
)
|
||||
or (
|
||||
not certificate
|
||||
and user_input.get(SET_CA_CERT, "off") == "custom"
|
||||
and not certificate_id
|
||||
)
|
||||
or (
|
||||
user_input.get(CONF_TRANSPORT) == TRANSPORT_WEBSOCKETS
|
||||
and CONF_WS_PATH not in user_input
|
||||
)
|
||||
):
|
||||
return False
|
||||
|
||||
if client_certificate_id:
|
||||
client_certificate_data = await _get_uploaded_file(
|
||||
hass, client_certificate_id
|
||||
)
|
||||
client_certificate = async_convert_to_pem(
|
||||
client_certificate_data, PEMType.CERTIFICATE
|
||||
)
|
||||
if not _validate_pki_file(
|
||||
client_certificate_id, client_certificate, errors, "bad_client_cert"
|
||||
):
|
||||
return False
|
||||
|
||||
if client_key_id:
|
||||
client_key_data = await _get_uploaded_file(hass, client_key_id)
|
||||
client_key = async_convert_to_pem(
|
||||
client_key_data, PEMType.PRIVATE_KEY, password=client_key_password
|
||||
)
|
||||
if not _validate_pki_file(
|
||||
client_key_id, client_key, errors, "client_key_error"
|
||||
):
|
||||
return False
|
||||
|
||||
certificate_data: dict[str, Any] = {}
|
||||
if certificate:
|
||||
certificate_data[CONF_CERTIFICATE] = certificate
|
||||
if client_certificate:
|
||||
certificate_data[CONF_CLIENT_CERT] = client_certificate
|
||||
certificate_data[CONF_CLIENT_KEY] = client_key
|
||||
|
||||
validated_user_input.update(certificate_data)
|
||||
await async_create_certificate_temp_files(hass, certificate_data)
|
||||
if error := await hass.async_add_executor_job(
|
||||
check_certicate_chain,
|
||||
):
|
||||
errors["base"] = error
|
||||
return False
|
||||
|
||||
validated_user_input.pop(SET_CA_CERT, None)
|
||||
validated_user_input.pop(SET_CLIENT_CERT, None)
|
||||
if validated_user_input.get(CONF_TRANSPORT, TRANSPORT_TCP) == TRANSPORT_TCP:
|
||||
validated_user_input.pop(CONF_WS_PATH, None)
|
||||
validated_user_input.pop(CONF_WS_HEADERS, None)
|
||||
return True
|
||||
try:
|
||||
validated_user_input[CONF_WS_HEADERS] = json_loads(
|
||||
validated_user_input.get(CONF_WS_HEADERS, "{}")
|
||||
)
|
||||
schema = vol.Schema({cv.string: cv.template})
|
||||
schema(validated_user_input[CONF_WS_HEADERS])
|
||||
except (*JSON_DECODE_EXCEPTIONS, vol.MultipleInvalid):
|
||||
errors["base"] = "bad_ws_headers"
|
||||
return False
|
||||
|
||||
# Test the configuration
|
||||
if entry_config is not None:
|
||||
update_password_from_user_input(
|
||||
entry_config.get(CONF_PASSWORD), entry_config_update
|
||||
)
|
||||
if await hass.async_add_executor_job(
|
||||
try_connection,
|
||||
entry_config_update,
|
||||
):
|
||||
return True
|
||||
|
||||
errors["base"] = "cannot_connect"
|
||||
if user_input:
|
||||
user_input_basic = user_input.copy()
|
||||
advanced_broker_options = user_input_basic.get(ADVANCED_OPTIONS, False)
|
||||
if ADVANCED_OPTIONS not in user_input or advanced_broker_options is False:
|
||||
if await _async_validate_broker_settings(
|
||||
current_config,
|
||||
user_input_basic,
|
||||
validated_user_input,
|
||||
errors,
|
||||
):
|
||||
return True
|
||||
# Get defaults settings from previous post
|
||||
current_broker = user_input_basic.get(CONF_BROKER)
|
||||
current_port = user_input_basic.get(CONF_PORT, DEFAULT_PORT)
|
||||
current_user = user_input_basic.get(CONF_USERNAME)
|
||||
current_pass = user_input_basic.get(CONF_PASSWORD)
|
||||
else:
|
||||
# Get default settings from entry (if any)
|
||||
current_broker = current_config.get(CONF_BROKER)
|
||||
current_port = current_config.get(CONF_PORT, DEFAULT_PORT)
|
||||
current_user = current_config.get(CONF_USERNAME)
|
||||
# Return the sentinel password to avoid exposure
|
||||
current_entry_pass = current_config.get(CONF_PASSWORD)
|
||||
current_pass = PWD_NOT_CHANGED if current_entry_pass else None
|
||||
|
||||
# Treat the previous post as an update of the current settings
|
||||
# (if there was a basic broker setup step)
|
||||
current_config.update(user_input_basic)
|
||||
|
||||
# Get default settings for advanced broker options
|
||||
current_client_id = current_config.get(CONF_CLIENT_ID)
|
||||
current_keepalive = current_config.get(CONF_KEEPALIVE, DEFAULT_KEEPALIVE)
|
||||
current_ca_certificate = current_config.get(CONF_CERTIFICATE)
|
||||
current_client_certificate = current_config.get(CONF_CLIENT_CERT)
|
||||
current_client_key = current_config.get(CONF_CLIENT_KEY)
|
||||
current_tls_insecure = current_config.get(CONF_TLS_INSECURE, False)
|
||||
current_protocol = current_config.get(CONF_PROTOCOL, DEFAULT_PROTOCOL)
|
||||
current_transport = current_config.get(CONF_TRANSPORT, DEFAULT_TRANSPORT)
|
||||
current_ws_path = current_config.get(CONF_WS_PATH, DEFAULT_WS_PATH)
|
||||
current_ws_headers = (
|
||||
json_dumps(current_config.get(CONF_WS_HEADERS))
|
||||
if CONF_WS_HEADERS in current_config
|
||||
else None
|
||||
)
|
||||
advanced_broker_options |= bool(
|
||||
current_client_id
|
||||
or current_keepalive != DEFAULT_KEEPALIVE
|
||||
or current_ca_certificate
|
||||
or current_client_certificate
|
||||
or current_client_key
|
||||
or current_tls_insecure
|
||||
or current_config.get(SET_CA_CERT, "off") != "off"
|
||||
or current_config.get(SET_CLIENT_CERT)
|
||||
or current_transport == TRANSPORT_WEBSOCKETS
|
||||
)
|
||||
|
||||
# Build form
|
||||
fields[vol.Required(CONF_BROKER, default=current_broker)] = TEXT_SELECTOR
|
||||
fields[vol.Required(CONF_PORT, default=current_port)] = PORT_SELECTOR
|
||||
fields[
|
||||
vol.Optional(
|
||||
CONF_PROTOCOL,
|
||||
description={"suggested_value": current_protocol},
|
||||
)
|
||||
] = PROTOCOL_SELECTOR
|
||||
fields[
|
||||
vol.Optional(
|
||||
CONF_USERNAME,
|
||||
description={"suggested_value": current_user},
|
||||
)
|
||||
] = TEXT_SELECTOR
|
||||
fields[
|
||||
vol.Optional(
|
||||
CONF_PASSWORD,
|
||||
description={"suggested_value": current_pass},
|
||||
)
|
||||
] = PASSWORD_SELECTOR
|
||||
# show advanced options checkbox if no defaults
|
||||
# of the advanced options are overridden
|
||||
if not advanced_broker_options:
|
||||
fields[
|
||||
vol.Optional(
|
||||
ADVANCED_OPTIONS,
|
||||
)
|
||||
] = BOOLEAN_SELECTOR
|
||||
return False
|
||||
fields[
|
||||
vol.Optional(
|
||||
CONF_CLIENT_ID,
|
||||
description={"suggested_value": current_client_id},
|
||||
)
|
||||
] = TEXT_SELECTOR
|
||||
fields[
|
||||
vol.Optional(
|
||||
CONF_KEEPALIVE,
|
||||
description={"suggested_value": current_keepalive},
|
||||
)
|
||||
] = KEEPALIVE_SELECTOR
|
||||
fields[
|
||||
vol.Optional(
|
||||
SET_CLIENT_CERT,
|
||||
default=current_client_certificate is not None
|
||||
or current_config.get(SET_CLIENT_CERT) is True,
|
||||
)
|
||||
] = BOOLEAN_SELECTOR
|
||||
if (
|
||||
current_client_certificate is not None
|
||||
or current_config.get(SET_CLIENT_CERT) is True
|
||||
):
|
||||
fields[
|
||||
vol.Optional(
|
||||
CONF_CLIENT_CERT,
|
||||
description={"suggested_value": user_input_basic.get(CONF_CLIENT_CERT)},
|
||||
)
|
||||
] = CERT_UPLOAD_SELECTOR
|
||||
fields[
|
||||
vol.Optional(
|
||||
CONF_CLIENT_KEY,
|
||||
description={"suggested_value": user_input_basic.get(CONF_CLIENT_KEY)},
|
||||
)
|
||||
] = CERT_KEY_UPLOAD_SELECTOR
|
||||
fields[
|
||||
vol.Optional(
|
||||
CONF_CLIENT_KEY_PASSWORD,
|
||||
description={
|
||||
"suggested_value": user_input_basic.get(CONF_CLIENT_KEY_PASSWORD)
|
||||
},
|
||||
)
|
||||
] = PASSWORD_SELECTOR
|
||||
verification_mode = current_config.get(SET_CA_CERT) or (
|
||||
"off"
|
||||
if current_ca_certificate is None
|
||||
else "auto"
|
||||
if current_ca_certificate == "auto"
|
||||
else "custom"
|
||||
)
|
||||
fields[
|
||||
vol.Optional(
|
||||
SET_CA_CERT,
|
||||
default=verification_mode,
|
||||
)
|
||||
] = BROKER_VERIFICATION_SELECTOR
|
||||
if current_ca_certificate is not None or verification_mode == "custom":
|
||||
fields[
|
||||
vol.Optional(
|
||||
CONF_CERTIFICATE,
|
||||
user_input_basic.get(CONF_CERTIFICATE),
|
||||
)
|
||||
] = CA_CERT_UPLOAD_SELECTOR
|
||||
fields[
|
||||
vol.Optional(
|
||||
CONF_TLS_INSECURE,
|
||||
description={"suggested_value": current_tls_insecure},
|
||||
)
|
||||
] = BOOLEAN_SELECTOR
|
||||
fields[
|
||||
vol.Optional(
|
||||
CONF_TRANSPORT,
|
||||
description={"suggested_value": current_transport},
|
||||
)
|
||||
] = TRANSPORT_SELECTOR
|
||||
if current_transport == TRANSPORT_WEBSOCKETS:
|
||||
fields[
|
||||
vol.Optional(CONF_WS_PATH, description={"suggested_value": current_ws_path})
|
||||
] = TEXT_SELECTOR
|
||||
fields[
|
||||
vol.Optional(
|
||||
CONF_WS_HEADERS, description={"suggested_value": current_ws_headers}
|
||||
)
|
||||
] = WS_HEADERS_SELECTOR
|
||||
|
||||
# Show form
|
||||
return False
|
||||
|
||||
|
||||
|
||||
@@ -315,6 +315,7 @@ DEFAULT_TILT_MAX = 100
|
||||
DEFAULT_TILT_MIN = 0
|
||||
DEFAULT_TILT_OPEN_POSITION = 100
|
||||
DEFAULT_TILT_OPTIMISTIC = False
|
||||
DEFAULT_WS_HEADERS: dict[str, str] = {}
|
||||
DEFAULT_WS_PATH = "/"
|
||||
DEFAULT_POSITION_CLOSED = 0
|
||||
DEFAULT_POSITION_OPEN = 100
|
||||
|
||||
@@ -26,53 +26,46 @@
|
||||
"step": {
|
||||
"broker": {
|
||||
"data": {
|
||||
"advanced_options": "Advanced options",
|
||||
"broker": "Broker",
|
||||
"certificate": "Upload custom CA certificate file",
|
||||
"client_cert": "Upload client certificate file",
|
||||
"client_id": "Client ID (leave empty to randomly generated one)",
|
||||
"client_key": "Upload private key file",
|
||||
"client_key_password": "[%key:common::config_flow::data::password%]",
|
||||
"keepalive": "The time between sending keep alive messages",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"port": "[%key:common::config_flow::data::port%]",
|
||||
"protocol": "MQTT protocol",
|
||||
"username": "[%key:common::config_flow::data::username%]"
|
||||
"set_ca_cert": "Broker certificate validation",
|
||||
"set_client_cert": "Use a client certificate",
|
||||
"tls_insecure": "Ignore broker certificate validation",
|
||||
"transport": "MQTT transport",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"ws_headers": "WebSocket headers in JSON format",
|
||||
"ws_path": "WebSocket path"
|
||||
},
|
||||
"data_description": {
|
||||
"advanced_options": "Enable and select **Submit** to set advanced options.",
|
||||
"broker": "The hostname or IP address of your MQTT broker.",
|
||||
"certificate": "The custom CA certificate file to validate your MQTT broker's certificate.",
|
||||
"client_cert": "The client certificate to authenticate against your MQTT broker.",
|
||||
"client_id": "The unique ID to identify the Home Assistant MQTT API as MQTT client. It is recommended to leave this option blank.",
|
||||
"client_key": "The private key file that belongs to your client certificate.",
|
||||
"client_key_password": "The password for the private key file (if set).",
|
||||
"keepalive": "A value less than 90 seconds is advised.",
|
||||
"password": "The password to log in to your MQTT broker.",
|
||||
"port": "The port your MQTT broker listens to. For example 1883.",
|
||||
"protocol": "The MQTT protocol version that is used. Note that Home Assistant will silently change to version 5 if your broker supports it.",
|
||||
"username": "The username to log in to your MQTT broker."
|
||||
"set_ca_cert": "Select **Auto** for automatic CA validation, or **Custom** and select **Next** to set a custom CA certificate, to allow validating your MQTT broker's certificate.",
|
||||
"set_client_cert": "Enable and select **Next** to set a client certificate and private key to authenticate against your MQTT broker.",
|
||||
"tls_insecure": "Option to ignore validation of your MQTT broker's certificate.",
|
||||
"transport": "The transport to be used for the connection to your MQTT broker.",
|
||||
"username": "The username to log in to your MQTT broker.",
|
||||
"ws_headers": "The WebSocket headers to pass through the WebSocket-based connection to your MQTT broker.",
|
||||
"ws_path": "The WebSocket path to be used for the connection to your MQTT broker."
|
||||
},
|
||||
"description": "Please enter the connection information of your MQTT broker.",
|
||||
"sections": {
|
||||
"other_settings": {
|
||||
"data": {
|
||||
"certificate": "Upload custom CA certificate file",
|
||||
"client_cert": "Upload client certificate file",
|
||||
"client_id": "Client ID (leave empty for a randomly generated one)",
|
||||
"client_key": "Upload private key file",
|
||||
"client_key_password": "[%key:common::config_flow::data::password%]",
|
||||
"keepalive": "The time between sending keep alive messages",
|
||||
"set_ca_cert": "Broker certificate validation",
|
||||
"set_client_cert": "Use a client certificate",
|
||||
"tls_insecure": "Ignore broker certificate validation",
|
||||
"transport": "MQTT transport",
|
||||
"ws_headers": "WebSocket headers in JSON format",
|
||||
"ws_path": "WebSocket path"
|
||||
},
|
||||
"data_description": {
|
||||
"certificate": "The custom CA certificate file to validate your MQTT broker's certificate.",
|
||||
"client_cert": "The client certificate to authenticate against your MQTT broker.",
|
||||
"client_id": "The unique ID to identify the Home Assistant MQTT API as MQTT client. It is recommended to leave this option blank.",
|
||||
"client_key": "The private key file that belongs to your client certificate.",
|
||||
"client_key_password": "The password for the private key file (if set).",
|
||||
"keepalive": "A value less than 90 seconds is advised. Defaults to 60 seconds.",
|
||||
"set_ca_cert": "When already set to **Custom**, a custom CA validation certificate is configured. Select **Auto** for automatic CA validation, or upload a custom CA certificate, to allow validating your MQTT broker's certificate.",
|
||||
"set_client_cert": "When already selected, client certificate authentication is enabled. Upload a client certificate and key to enable.",
|
||||
"tls_insecure": "Option to ignore validation of your MQTT broker's certificate.",
|
||||
"transport": "The transport to be used for the connection to your MQTT broker.",
|
||||
"ws_headers": "The WebSocket headers to pass through the WebSocket-based connection to your MQTT broker.",
|
||||
"ws_path": "The WebSocket path to be used for the connection to your MQTT broker."
|
||||
},
|
||||
"name": "Other settings"
|
||||
}
|
||||
}
|
||||
"description": "Please enter the connection information of your MQTT broker."
|
||||
},
|
||||
"hassio_confirm": {
|
||||
"description": "Do you want to configure Home Assistant to connect to the MQTT broker provided by the {addon} app?",
|
||||
@@ -1185,6 +1178,48 @@
|
||||
"invalid_inclusion": "[%key:component::mqtt::config::error::invalid_inclusion%]"
|
||||
},
|
||||
"step": {
|
||||
"broker": {
|
||||
"data": {
|
||||
"advanced_options": "[%key:component::mqtt::config::step::broker::data::advanced_options%]",
|
||||
"broker": "[%key:component::mqtt::config::step::broker::data::broker%]",
|
||||
"certificate": "[%key:component::mqtt::config::step::broker::data::certificate%]",
|
||||
"client_cert": "[%key:component::mqtt::config::step::broker::data::client_cert%]",
|
||||
"client_id": "[%key:component::mqtt::config::step::broker::data::client_id%]",
|
||||
"client_key": "[%key:component::mqtt::config::step::broker::data::client_key%]",
|
||||
"keepalive": "[%key:component::mqtt::config::step::broker::data::keepalive%]",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"port": "[%key:common::config_flow::data::port%]",
|
||||
"protocol": "[%key:component::mqtt::config::step::broker::data::protocol%]",
|
||||
"set_ca_cert": "[%key:component::mqtt::config::step::broker::data::set_ca_cert%]",
|
||||
"set_client_cert": "[%key:component::mqtt::config::step::broker::data::set_client_cert%]",
|
||||
"tls_insecure": "[%key:component::mqtt::config::step::broker::data::tls_insecure%]",
|
||||
"transport": "[%key:component::mqtt::config::step::broker::data::transport%]",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"ws_headers": "[%key:component::mqtt::config::step::broker::data::ws_headers%]",
|
||||
"ws_path": "[%key:component::mqtt::config::step::broker::data::ws_path%]"
|
||||
},
|
||||
"data_description": {
|
||||
"advanced_options": "[%key:component::mqtt::config::step::broker::data_description::advanced_options%]",
|
||||
"broker": "[%key:component::mqtt::config::step::broker::data_description::broker%]",
|
||||
"certificate": "[%key:component::mqtt::config::step::broker::data_description::certificate%]",
|
||||
"client_cert": "[%key:component::mqtt::config::step::broker::data_description::client_cert%]",
|
||||
"client_id": "[%key:component::mqtt::config::step::broker::data_description::client_id%]",
|
||||
"client_key": "[%key:component::mqtt::config::step::broker::data_description::client_key%]",
|
||||
"keepalive": "[%key:component::mqtt::config::step::broker::data_description::keepalive%]",
|
||||
"password": "[%key:component::mqtt::config::step::broker::data_description::password%]",
|
||||
"port": "[%key:component::mqtt::config::step::broker::data_description::port%]",
|
||||
"protocol": "[%key:component::mqtt::config::step::broker::data_description::protocol%]",
|
||||
"set_ca_cert": "[%key:component::mqtt::config::step::broker::data_description::set_ca_cert%]",
|
||||
"set_client_cert": "[%key:component::mqtt::config::step::broker::data_description::set_client_cert%]",
|
||||
"tls_insecure": "[%key:component::mqtt::config::step::broker::data_description::tls_insecure%]",
|
||||
"transport": "[%key:component::mqtt::config::step::broker::data_description::transport%]",
|
||||
"username": "[%key:component::mqtt::config::step::broker::data_description::username%]",
|
||||
"ws_headers": "[%key:component::mqtt::config::step::broker::data_description::ws_headers%]",
|
||||
"ws_path": "[%key:component::mqtt::config::step::broker::data_description::ws_path%]"
|
||||
},
|
||||
"description": "[%key:component::mqtt::config::step::broker::description%]",
|
||||
"title": "Broker options"
|
||||
},
|
||||
"options": {
|
||||
"data": {
|
||||
"birth_enable": "Enable birth message",
|
||||
|
||||
@@ -267,7 +267,7 @@ SWITCHES = (
|
||||
),
|
||||
NextDnsSwitchEntityDescription(
|
||||
key="block_hulu",
|
||||
translation_key="block_hulu",
|
||||
name="Block Hulu",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
entity_registry_enabled_default=False,
|
||||
state=lambda data: data.block_hulu,
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["ical"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["ical==13.3.0"]
|
||||
"requirements": ["ical==13.2.5"]
|
||||
}
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
"""SMLIGHT SLZB device integration."""
|
||||
"""SMLIGHT SLZB Zigbee device integration."""
|
||||
|
||||
from pysmlight import Api2
|
||||
|
||||
from homeassistant.const import CONF_HOST, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .bluetooth import async_connect_scanner
|
||||
from .const import DOMAIN
|
||||
from .coordinator import (
|
||||
SmConfigEntry,
|
||||
SmDataUpdateCoordinator,
|
||||
SmFirmwareUpdateCoordinator,
|
||||
SmlightData,
|
||||
base_device_info,
|
||||
)
|
||||
from .services import async_setup_services
|
||||
|
||||
@@ -39,7 +37,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: SmConfigEntry) -> bool:
|
||||
"""Set up SMLIGHT from a config entry."""
|
||||
"""Set up SMLIGHT Zigbee from a config entry."""
|
||||
client = Api2(host=entry.data[CONF_HOST], session=async_get_clientsession(hass))
|
||||
|
||||
data_coordinator = SmDataUpdateCoordinator(hass, entry, client)
|
||||
@@ -48,24 +46,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: SmConfigEntry) -> bool:
|
||||
await data_coordinator.async_config_entry_first_refresh()
|
||||
await firmware_coordinator.async_config_entry_first_refresh()
|
||||
|
||||
info = data_coordinator.data.info
|
||||
|
||||
if info.legacy_api < 2:
|
||||
if data_coordinator.data.info.legacy_api < 2:
|
||||
entry.async_create_background_task(
|
||||
hass, client.sse.client(), "smlight-sse-client"
|
||||
)
|
||||
|
||||
if info.ble is not None and info.ble.proxy_enabled:
|
||||
device_registry = dr.async_get(hass)
|
||||
device = device_registry.async_get_or_create(
|
||||
config_entry_id=entry.entry_id,
|
||||
**base_device_info(info, client.host),
|
||||
)
|
||||
entry.async_on_unload(async_connect_scanner(hass, entry, info.model, device.id))
|
||||
|
||||
entry.runtime_data = SmlightData(
|
||||
data=data_coordinator,
|
||||
firmware=firmware_coordinator,
|
||||
data=data_coordinator, firmware=firmware_coordinator
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
@@ -73,5 +60,5 @@ async def async_setup_entry(hass: HomeAssistant, entry: SmConfigEntry) -> bool:
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: SmConfigEntry) -> bool:
|
||||
"""Unload SMLIGHT config entry."""
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
"""Bluetooth proxy for SLZB devices using bleak-smlight."""
|
||||
|
||||
from functools import partial
|
||||
|
||||
from bleak_smlight import SLZB_BLE_SERVER_PORT, connect_scanner
|
||||
from pysmlight import BleProxyClient
|
||||
|
||||
from homeassistant.components.bluetooth import (
|
||||
BluetoothScanningMode,
|
||||
async_register_scanner,
|
||||
)
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import SmConfigEntry
|
||||
|
||||
|
||||
@callback
|
||||
def _async_unload(
|
||||
unload_callbacks: list[CALLBACK_TYPE],
|
||||
client: BleProxyClient,
|
||||
) -> None:
|
||||
"""Unload callbacks and stop client."""
|
||||
for callback_func in unload_callbacks:
|
||||
callback_func()
|
||||
client.stop()
|
||||
|
||||
|
||||
@callback
|
||||
def async_connect_scanner(
|
||||
hass: HomeAssistant,
|
||||
entry: SmConfigEntry,
|
||||
model: str | None,
|
||||
device_id: str,
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Connect scanner using the external bleak-smlight backend."""
|
||||
assert entry.unique_id is not None
|
||||
|
||||
client_data = connect_scanner(
|
||||
source=entry.unique_id,
|
||||
name=entry.title,
|
||||
host=entry.data[CONF_HOST],
|
||||
port=SLZB_BLE_SERVER_PORT,
|
||||
)
|
||||
|
||||
client_data.scanner.async_set_scanning_mode(BluetoothScanningMode.AUTO)
|
||||
|
||||
entry.async_create_background_task(
|
||||
hass,
|
||||
client_data.client.start(),
|
||||
f"smlight-ble-proxy-client-{entry.unique_id}",
|
||||
)
|
||||
|
||||
unload_callbacks = [
|
||||
async_register_scanner(
|
||||
hass,
|
||||
client_data.scanner,
|
||||
source_domain=DOMAIN,
|
||||
source_model=model,
|
||||
source_config_entry_id=entry.entry_id,
|
||||
source_device_id=device_id,
|
||||
),
|
||||
client_data.scanner.async_setup(),
|
||||
]
|
||||
|
||||
return partial(_async_unload, unload_callbacks, client_data.client)
|
||||
@@ -15,21 +15,11 @@ from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
from homeassistant.helpers.device_registry import (
|
||||
CONNECTION_NETWORK_MAC,
|
||||
DeviceInfo,
|
||||
format_mac,
|
||||
)
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import (
|
||||
ATTR_MANUFACTURER,
|
||||
DOMAIN,
|
||||
LOGGER,
|
||||
SCAN_FIRMWARE_INTERVAL,
|
||||
SCAN_INTERVAL,
|
||||
)
|
||||
from .const import DOMAIN, LOGGER, SCAN_FIRMWARE_INTERVAL, SCAN_INTERVAL
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
@@ -60,17 +50,6 @@ class SmFwData:
|
||||
type SmConfigEntry = ConfigEntry[SmlightData]
|
||||
|
||||
|
||||
def base_device_info(info: Info, host: str) -> DeviceInfo:
|
||||
"""Return device registry information."""
|
||||
return DeviceInfo(
|
||||
configuration_url=f"http://{host}",
|
||||
connections={(CONNECTION_NETWORK_MAC, str(info.MAC))},
|
||||
manufacturer=ATTR_MANUFACTURER,
|
||||
model=info.model,
|
||||
sw_version=f"core: {info.sw_version} / zigbee: {info.zb_version}",
|
||||
)
|
||||
|
||||
|
||||
class SmBaseDataUpdateCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
|
||||
"""Base Coordinator for SMLIGHT."""
|
||||
|
||||
@@ -114,7 +93,6 @@ class SmBaseDataUpdateCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
|
||||
info = await self.client.get_info()
|
||||
self.unique_id = format_mac(info.MAC)
|
||||
self.legacy_api = info.legacy_api
|
||||
|
||||
if info.legacy_api == 2:
|
||||
ir.async_create_issue(
|
||||
self.hass,
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
"""Base class for all SMLIGHT entities."""
|
||||
|
||||
from homeassistant.helpers.device_registry import (
|
||||
CONNECTION_NETWORK_MAC,
|
||||
DeviceInfo,
|
||||
format_mac,
|
||||
)
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .coordinator import SmBaseDataUpdateCoordinator, base_device_info
|
||||
from .const import ATTR_MANUFACTURER
|
||||
from .coordinator import SmBaseDataUpdateCoordinator
|
||||
|
||||
|
||||
class SmEntity(CoordinatorEntity[SmBaseDataUpdateCoordinator]):
|
||||
@@ -13,6 +19,14 @@ class SmEntity(CoordinatorEntity[SmBaseDataUpdateCoordinator]):
|
||||
def __init__(self, coordinator: SmBaseDataUpdateCoordinator) -> None:
|
||||
"""Initialize entity with device."""
|
||||
super().__init__(coordinator)
|
||||
self._attr_device_info = base_device_info(
|
||||
coordinator.data.info, coordinator.client.host
|
||||
mac = format_mac(coordinator.data.info.MAC)
|
||||
self._attr_device_info = DeviceInfo(
|
||||
configuration_url=f"http://{coordinator.client.host}",
|
||||
connections={(CONNECTION_NETWORK_MAC, mac)},
|
||||
manufacturer=ATTR_MANUFACTURER,
|
||||
model=coordinator.data.info.model,
|
||||
sw_version=(
|
||||
f"core: {coordinator.data.info.sw_version}"
|
||||
f" / zigbee: {coordinator.data.info.zb_version}"
|
||||
),
|
||||
)
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
"name": "SMLIGHT SLZB",
|
||||
"codeowners": ["@tl-sl"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["bluetooth"],
|
||||
"dhcp": [
|
||||
{
|
||||
"registered_devices": true
|
||||
@@ -13,7 +12,7 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["pysmlight==0.5.0", "bleak-smlight==1.1.0"],
|
||||
"requirements": ["pysmlight==0.5.0"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_slzb-06._tcp.local."
|
||||
|
||||
@@ -31,7 +31,7 @@ from homeassistant.helpers.trigger_template_entity import (
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import (
|
||||
CONF_ADVANCED_OPTIONS,
|
||||
CONF_ADDITIONAL_OPTIONS,
|
||||
CONF_COLUMN_NAME,
|
||||
CONF_QUERY,
|
||||
DOMAIN,
|
||||
@@ -120,7 +120,7 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
new_options[CONF_COLUMN_NAME] = old_options.get(CONF_COLUMN_NAME)
|
||||
new_options[CONF_QUERY] = old_options.get(CONF_QUERY)
|
||||
new_options[CONF_ADVANCED_OPTIONS] = {}
|
||||
new_options[CONF_ADDITIONAL_OPTIONS] = {}
|
||||
|
||||
for key in (
|
||||
CONF_VALUE_TEMPLATE,
|
||||
@@ -129,12 +129,19 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
CONF_STATE_CLASS,
|
||||
):
|
||||
if (value := old_options.get(key)) is not None:
|
||||
new_options[CONF_ADVANCED_OPTIONS][key] = value
|
||||
new_options[CONF_ADDITIONAL_OPTIONS][key] = value
|
||||
|
||||
hass.config_entries.async_update_entry(
|
||||
entry, data=new_data, options=new_options, version=2
|
||||
)
|
||||
|
||||
if entry.version == 2:
|
||||
new_options = {**entry.options}
|
||||
# The "advanced_options" section was renamed to "additional_options"
|
||||
if (additional := new_options.pop("advanced_options", None)) is not None:
|
||||
new_options[CONF_ADDITIONAL_OPTIONS] = additional
|
||||
hass.config_entries.async_update_entry(entry, options=new_options, version=3)
|
||||
|
||||
_LOGGER.debug(
|
||||
"Migration to version %s.%s successful",
|
||||
entry.version,
|
||||
|
||||
@@ -32,7 +32,7 @@ from homeassistant.data_entry_flow import section
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers import selector
|
||||
|
||||
from .const import CONF_ADVANCED_OPTIONS, CONF_COLUMN_NAME, CONF_QUERY, DOMAIN
|
||||
from .const import CONF_ADDITIONAL_OPTIONS, CONF_COLUMN_NAME, CONF_QUERY, DOMAIN
|
||||
from .util import (
|
||||
EmptyQueryError,
|
||||
InvalidSqlQuery,
|
||||
@@ -50,7 +50,7 @@ OPTIONS_SCHEMA: vol.Schema = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_QUERY): selector.TemplateSelector(),
|
||||
vol.Required(CONF_COLUMN_NAME): selector.TextSelector(),
|
||||
vol.Required(CONF_ADVANCED_OPTIONS): section(
|
||||
vol.Required(CONF_ADDITIONAL_OPTIONS): section(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_VALUE_TEMPLATE): selector.TemplateSelector(),
|
||||
@@ -164,7 +164,7 @@ def validate_query(db_url: str, query: str, column: str) -> bool:
|
||||
class SQLConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for SQL integration."""
|
||||
|
||||
VERSION = 2
|
||||
VERSION = 3
|
||||
|
||||
data: dict[str, Any]
|
||||
|
||||
@@ -239,12 +239,12 @@ class SQLConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
_LOGGER.debug("Invalid query: %s", err)
|
||||
errors["query"] = "query_invalid"
|
||||
|
||||
mod_advanced_options = {
|
||||
mod_additional_options = {
|
||||
k: v
|
||||
for k, v in user_input[CONF_ADVANCED_OPTIONS].items()
|
||||
for k, v in user_input[CONF_ADDITIONAL_OPTIONS].items()
|
||||
if v is not None
|
||||
}
|
||||
user_input[CONF_ADVANCED_OPTIONS] = mod_advanced_options
|
||||
user_input[CONF_ADDITIONAL_OPTIONS] = mod_additional_options
|
||||
|
||||
if not errors:
|
||||
name = self.data[CONF_NAME]
|
||||
@@ -305,12 +305,12 @@ class SQLOptionsFlowHandler(OptionsFlowWithReload):
|
||||
recorder_db,
|
||||
)
|
||||
|
||||
mod_advanced_options = {
|
||||
mod_additional_options = {
|
||||
k: v
|
||||
for k, v in user_input[CONF_ADVANCED_OPTIONS].items()
|
||||
for k, v in user_input[CONF_ADDITIONAL_OPTIONS].items()
|
||||
if v is not None
|
||||
}
|
||||
user_input[CONF_ADVANCED_OPTIONS] = mod_advanced_options
|
||||
user_input[CONF_ADDITIONAL_OPTIONS] = mod_additional_options
|
||||
|
||||
return self.async_create_entry(
|
||||
data=user_input,
|
||||
|
||||
@@ -9,5 +9,5 @@ PLATFORMS = [Platform.SENSOR]
|
||||
|
||||
CONF_COLUMN_NAME = "column"
|
||||
CONF_QUERY = "query"
|
||||
CONF_ADVANCED_OPTIONS = "advanced_options"
|
||||
CONF_ADDITIONAL_OPTIONS = "additional_options"
|
||||
DB_URL_RE = re.compile("//.*:.*@")
|
||||
|
||||
@@ -36,7 +36,7 @@ from homeassistant.helpers.trigger_template_entity import (
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from .const import CONF_ADVANCED_OPTIONS, CONF_COLUMN_NAME, CONF_QUERY, DOMAIN
|
||||
from .const import CONF_ADDITIONAL_OPTIONS, CONF_COLUMN_NAME, CONF_QUERY, DOMAIN
|
||||
from .util import (
|
||||
InvalidSqlQuery,
|
||||
async_create_sessionmaker,
|
||||
@@ -115,7 +115,9 @@ async def async_setup_entry(
|
||||
db_url: str = resolve_db_url(hass, entry.data.get(CONF_DB_URL))
|
||||
name: str = entry.title
|
||||
query_str: str = entry.options[CONF_QUERY]
|
||||
template: str | None = entry.options[CONF_ADVANCED_OPTIONS].get(CONF_VALUE_TEMPLATE)
|
||||
template: str | None = entry.options[CONF_ADDITIONAL_OPTIONS].get(
|
||||
CONF_VALUE_TEMPLATE
|
||||
)
|
||||
column_name: str = entry.options[CONF_COLUMN_NAME]
|
||||
|
||||
query_template: ValueTemplate | None = None
|
||||
@@ -136,9 +138,9 @@ async def async_setup_entry(
|
||||
name_template = Template(name, hass)
|
||||
trigger_entity_config = {CONF_NAME: name_template, CONF_UNIQUE_ID: entry.entry_id}
|
||||
for key in TRIGGER_ENTITY_OPTIONS:
|
||||
if key not in entry.options[CONF_ADVANCED_OPTIONS]:
|
||||
if key not in entry.options[CONF_ADDITIONAL_OPTIONS]:
|
||||
continue
|
||||
trigger_entity_config[key] = entry.options[CONF_ADVANCED_OPTIONS][key]
|
||||
trigger_entity_config[key] = entry.options[CONF_ADDITIONAL_OPTIONS][key]
|
||||
|
||||
await async_setup_sensor(
|
||||
hass,
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"query": "Query to run, needs to start with 'SELECT'"
|
||||
},
|
||||
"sections": {
|
||||
"advanced_options": {
|
||||
"additional_options": {
|
||||
"data": {
|
||||
"device_class": "Device class",
|
||||
"state_class": "State class",
|
||||
@@ -87,21 +87,21 @@
|
||||
"query": "[%key:component::sql::config::step::options::data_description::query%]"
|
||||
},
|
||||
"sections": {
|
||||
"advanced_options": {
|
||||
"additional_options": {
|
||||
"data": {
|
||||
"device_class": "[%key:component::sql::config::step::options::sections::advanced_options::data::device_class%]",
|
||||
"state_class": "[%key:component::sql::config::step::options::sections::advanced_options::data::state_class%]",
|
||||
"unit_of_measurement": "[%key:component::sql::config::step::options::sections::advanced_options::data::unit_of_measurement%]",
|
||||
"value_template": "[%key:component::sql::config::step::options::sections::advanced_options::data::value_template%]"
|
||||
"device_class": "[%key:component::sql::config::step::options::sections::additional_options::data::device_class%]",
|
||||
"state_class": "[%key:component::sql::config::step::options::sections::additional_options::data::state_class%]",
|
||||
"unit_of_measurement": "[%key:component::sql::config::step::options::sections::additional_options::data::unit_of_measurement%]",
|
||||
"value_template": "[%key:component::sql::config::step::options::sections::additional_options::data::value_template%]"
|
||||
},
|
||||
"data_description": {
|
||||
"device_class": "[%key:component::sql::config::step::options::sections::advanced_options::data_description::device_class%]",
|
||||
"state_class": "[%key:component::sql::config::step::options::sections::advanced_options::data_description::state_class%]",
|
||||
"unit_of_measurement": "[%key:component::sql::config::step::options::sections::advanced_options::data_description::unit_of_measurement%]",
|
||||
"value_template": "[%key:component::sql::config::step::options::sections::advanced_options::data_description::value_template%]"
|
||||
"device_class": "[%key:component::sql::config::step::options::sections::additional_options::data_description::device_class%]",
|
||||
"state_class": "[%key:component::sql::config::step::options::sections::additional_options::data_description::state_class%]",
|
||||
"unit_of_measurement": "[%key:component::sql::config::step::options::sections::additional_options::data_description::unit_of_measurement%]",
|
||||
"value_template": "[%key:component::sql::config::step::options::sections::additional_options::data_description::value_template%]"
|
||||
},
|
||||
"description": "[%key:component::sql::config::step::options::sections::advanced_options::name%]",
|
||||
"name": "[%key:component::sql::config::step::options::sections::advanced_options::name%]"
|
||||
"description": "[%key:component::sql::config::step::options::sections::additional_options::name%]",
|
||||
"name": "[%key:component::sql::config::step::options::sections::additional_options::name%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,5 +39,23 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_yaml": {
|
||||
"description": "Configuring {integration_title} using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove both the `{domain}` and the relevant Modbus configuration from your configuration.yaml file and restart Home Assistant to fix this issue.",
|
||||
"title": "The {integration_title} YAML configuration is being removed"
|
||||
},
|
||||
"deprecated_yaml_import_issue_cannot_connect": {
|
||||
"description": "Configuring {integration_title} using YAML is being removed but there was a connect error while importing your existing configuration.\nSetup will not proceed.\n\nVerify that your {integration_title} is operating correctly and restart Home Assistant to attempt the import again.\n\nAlternatively, you may remove the `{domain}` configuration from your configuration.yaml entirely, restart Home Assistant, and add the {integration_title} integration manually.",
|
||||
"title": "YAML import failed due to a connection error"
|
||||
},
|
||||
"deprecated_yaml_import_issue_missing_hub": {
|
||||
"description": "Configuring {integration_title} using YAML is being removed but the configuration was not complete, thus we could not import your configuration.\nSetup will not proceed.\n\nVerify that your {integration_title} is operating correctly and restart Home Assistant to attempt the import again.\n\nAlternatively, you may remove the `{domain}` configuration from your configuration.yaml entirely, restart Home Assistant, and add the {integration_title} integration manually.",
|
||||
"title": "YAML import failed due to incomplete config"
|
||||
},
|
||||
"deprecated_yaml_import_issue_unknown": {
|
||||
"description": "Configuring {integration_title} using YAML is being removed but there was an unknown error while importing your existing configuration.\nSetup will not proceed.\n\nVerify that your {integration_title} is operating correctly and restart Home Assistant to attempt the import again.\n\nAlternatively, you may remove the `{domain}` configuration from your configuration.yaml entirely, restart Home Assistant, and add the {integration_title} integration manually.",
|
||||
"title": "YAML import failed due to an unknown error"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,7 +192,6 @@ PLATFORMS_BY_TYPE = {
|
||||
Platform.SENSOR,
|
||||
],
|
||||
SupportedModels.WEATHER_STATION.value: [Platform.SENSOR],
|
||||
SupportedModels.CANDLE_WARMER_LAMP.value: [Platform.LIGHT, Platform.SENSOR],
|
||||
}
|
||||
CLASS_BY_DEVICE = {
|
||||
SupportedModels.CEILING_LIGHT.value: switchbot.SwitchbotCeilingLight,
|
||||
@@ -246,7 +245,6 @@ CLASS_BY_DEVICE = {
|
||||
SupportedModels.LOCK_VISION_PRO.value: switchbot.SwitchbotLock,
|
||||
SupportedModels.LOCK_VISION.value: switchbot.SwitchbotLock,
|
||||
SupportedModels.LOCK_PRO_WIFI.value: switchbot.SwitchbotLock,
|
||||
SupportedModels.CANDLE_WARMER_LAMP.value: switchbot.SwitchbotCandleWarmerLamp,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -72,7 +72,6 @@ class SupportedModels(StrEnum):
|
||||
LOCK_PRO_WIFI = "lock_pro_wifi"
|
||||
WEATHER_STATION = "weather_station"
|
||||
STANDING_FAN = "standing_fan"
|
||||
CANDLE_WARMER_LAMP = "candle_warmer_lamp"
|
||||
|
||||
|
||||
CONNECTABLE_SUPPORTED_MODEL_TYPES = {
|
||||
@@ -123,7 +122,6 @@ CONNECTABLE_SUPPORTED_MODEL_TYPES = {
|
||||
SwitchbotModel.LOCK_VISION: SupportedModels.LOCK_VISION,
|
||||
SwitchbotModel.LOCK_PRO_WIFI: SupportedModels.LOCK_PRO_WIFI,
|
||||
SwitchbotModel.STANDING_FAN: SupportedModels.STANDING_FAN,
|
||||
SwitchbotModel.CANDLE_WARMER_LAMP: SupportedModels.CANDLE_WARMER_LAMP,
|
||||
}
|
||||
|
||||
NON_CONNECTABLE_SUPPORTED_MODEL_TYPES = {
|
||||
@@ -173,7 +171,6 @@ ENCRYPTED_MODELS = {
|
||||
SwitchbotModel.LOCK_VISION_PRO,
|
||||
SwitchbotModel.LOCK_VISION,
|
||||
SwitchbotModel.LOCK_PRO_WIFI,
|
||||
SwitchbotModel.CANDLE_WARMER_LAMP,
|
||||
}
|
||||
|
||||
ENCRYPTED_SWITCHBOT_MODEL_TO_CLASS: dict[
|
||||
@@ -207,7 +204,6 @@ ENCRYPTED_SWITCHBOT_MODEL_TO_CLASS: dict[
|
||||
SwitchbotModel.LOCK_VISION_PRO: switchbot.SwitchbotLock,
|
||||
SwitchbotModel.LOCK_VISION: switchbot.SwitchbotLock,
|
||||
SwitchbotModel.LOCK_PRO_WIFI: switchbot.SwitchbotLock,
|
||||
SwitchbotModel.CANDLE_WARMER_LAMP: switchbot.SwitchbotCandleWarmerLamp,
|
||||
}
|
||||
|
||||
HASS_SENSOR_TYPE_TO_SWITCHBOT_MODEL = {
|
||||
|
||||
@@ -26,7 +26,6 @@ from .entity import SwitchbotEntity, exception_handler
|
||||
SWITCHBOT_COLOR_MODE_TO_HASS = {
|
||||
SwitchBotColorMode.RGB: ColorMode.RGB,
|
||||
SwitchBotColorMode.COLOR_TEMP: ColorMode.COLOR_TEMP,
|
||||
SwitchBotColorMode.BRIGHTNESS: ColorMode.BRIGHTNESS,
|
||||
}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -53,7 +53,7 @@ from .const import (
|
||||
PLATFORM_BROADCAST,
|
||||
PLATFORM_POLLING,
|
||||
PLATFORM_WEBHOOKS,
|
||||
SECTION_ADDITIONAL_SETTINGS,
|
||||
SECTION_ADVANCED_SETTINGS,
|
||||
SUBENTRY_TYPE_ALLOWED_CHAT_IDS,
|
||||
)
|
||||
|
||||
@@ -65,7 +65,7 @@ DESCRIPTION_PLACEHOLDERS: dict[str, str] = {
|
||||
"id_bot_username": "@id_bot",
|
||||
"id_bot_url": "https://t.me/id_bot",
|
||||
"socks_url": "socks5://username:password@proxy_ip:proxy_port",
|
||||
# used in additional settings section
|
||||
# used in advanced settings section
|
||||
"default_api_endpoint": DEFAULT_API_ENDPOINT,
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ STEP_USER_DATA_SCHEMA: vol.Schema = vol.Schema(
|
||||
autocomplete="current-password",
|
||||
)
|
||||
),
|
||||
vol.Required(SECTION_ADDITIONAL_SETTINGS): section(
|
||||
vol.Required(SECTION_ADVANCED_SETTINGS): section(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
@@ -117,7 +117,7 @@ STEP_RECONFIGURE_USER_DATA_SCHEMA: vol.Schema = vol.Schema(
|
||||
translation_key="platforms",
|
||||
)
|
||||
),
|
||||
vol.Required(SECTION_ADDITIONAL_SETTINGS): section(
|
||||
vol.Required(SECTION_ADVANCED_SETTINGS): section(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
@@ -241,10 +241,10 @@ class TelegramBotConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
# validate connection to Telegram API
|
||||
errors: dict[str, str] = {}
|
||||
user_input[CONF_API_ENDPOINT] = user_input[SECTION_ADDITIONAL_SETTINGS][
|
||||
user_input[CONF_API_ENDPOINT] = user_input[SECTION_ADVANCED_SETTINGS][
|
||||
CONF_API_ENDPOINT
|
||||
]
|
||||
user_input[CONF_PROXY_URL] = user_input[SECTION_ADDITIONAL_SETTINGS].get(
|
||||
user_input[CONF_PROXY_URL] = user_input[SECTION_ADVANCED_SETTINGS].get(
|
||||
CONF_PROXY_URL
|
||||
)
|
||||
bot_name = await self._validate_bot(
|
||||
@@ -270,7 +270,9 @@ class TelegramBotConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
CONF_PLATFORM: user_input[CONF_PLATFORM],
|
||||
CONF_API_ENDPOINT: user_input[CONF_API_ENDPOINT],
|
||||
CONF_API_KEY: user_input[CONF_API_KEY],
|
||||
CONF_PROXY_URL: user_input[CONF_PROXY_URL],
|
||||
CONF_PROXY_URL: user_input[SECTION_ADVANCED_SETTINGS].get(
|
||||
CONF_PROXY_URL
|
||||
),
|
||||
},
|
||||
options={ATTR_PARSER: PARSER_MD},
|
||||
description_placeholders=description_placeholders,
|
||||
@@ -381,10 +383,10 @@ class TelegramBotConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
data={
|
||||
CONF_PLATFORM: self._step_user_data[CONF_PLATFORM],
|
||||
CONF_API_KEY: self._step_user_data[CONF_API_KEY],
|
||||
CONF_API_ENDPOINT: self._step_user_data[SECTION_ADDITIONAL_SETTINGS][
|
||||
CONF_API_ENDPOINT: self._step_user_data[SECTION_ADVANCED_SETTINGS][
|
||||
CONF_API_ENDPOINT
|
||||
],
|
||||
CONF_PROXY_URL: self._step_user_data[SECTION_ADDITIONAL_SETTINGS].get(
|
||||
CONF_PROXY_URL: self._step_user_data[SECTION_ADVANCED_SETTINGS].get(
|
||||
CONF_PROXY_URL
|
||||
),
|
||||
CONF_URL: user_input.get(CONF_URL),
|
||||
@@ -459,7 +461,7 @@ class TelegramBotConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
STEP_RECONFIGURE_USER_DATA_SCHEMA,
|
||||
{
|
||||
**self._get_reconfigure_entry().data,
|
||||
SECTION_ADDITIONAL_SETTINGS: {
|
||||
SECTION_ADVANCED_SETTINGS: {
|
||||
CONF_API_ENDPOINT: self._get_reconfigure_entry().data[
|
||||
CONF_API_ENDPOINT
|
||||
],
|
||||
@@ -471,11 +473,11 @@ class TelegramBotConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
),
|
||||
description_placeholders=DESCRIPTION_PLACEHOLDERS,
|
||||
)
|
||||
user_input[CONF_PROXY_URL] = user_input[SECTION_ADDITIONAL_SETTINGS].get(
|
||||
user_input[CONF_PROXY_URL] = user_input[SECTION_ADVANCED_SETTINGS].get(
|
||||
CONF_PROXY_URL
|
||||
)
|
||||
|
||||
user_input[CONF_API_ENDPOINT] = user_input[SECTION_ADDITIONAL_SETTINGS][
|
||||
user_input[CONF_API_ENDPOINT] = user_input[SECTION_ADVANCED_SETTINGS][
|
||||
CONF_API_ENDPOINT
|
||||
]
|
||||
|
||||
@@ -526,7 +528,7 @@ class TelegramBotConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
STEP_RECONFIGURE_USER_DATA_SCHEMA,
|
||||
{
|
||||
**user_input,
|
||||
SECTION_ADDITIONAL_SETTINGS: {
|
||||
SECTION_ADVANCED_SETTINGS: {
|
||||
CONF_API_ENDPOINT: user_input[CONF_API_ENDPOINT],
|
||||
CONF_PROXY_URL: user_input.get(CONF_PROXY_URL),
|
||||
},
|
||||
|
||||
@@ -7,7 +7,7 @@ DOMAIN = "telegram_bot"
|
||||
PLATFORM_BROADCAST = "broadcast"
|
||||
PLATFORM_POLLING = "polling"
|
||||
PLATFORM_WEBHOOKS = "webhooks"
|
||||
SECTION_ADDITIONAL_SETTINGS = "additional_settings"
|
||||
SECTION_ADVANCED_SETTINGS = "advanced_settings"
|
||||
SUBENTRY_TYPE_ALLOWED_CHAT_IDS = "allowed_chat_ids"
|
||||
|
||||
CONF_ALLOWED_CHAT_IDS = "allowed_chat_ids"
|
||||
|
||||
@@ -33,16 +33,16 @@
|
||||
},
|
||||
"description": "Reconfigure Telegram bot",
|
||||
"sections": {
|
||||
"additional_settings": {
|
||||
"advanced_settings": {
|
||||
"data": {
|
||||
"api_endpoint": "[%key:component::telegram_bot::config::step::user::sections::additional_settings::data::api_endpoint%]",
|
||||
"proxy_url": "[%key:component::telegram_bot::config::step::user::sections::additional_settings::data::proxy_url%]"
|
||||
"api_endpoint": "[%key:component::telegram_bot::config::step::user::sections::advanced_settings::data::api_endpoint%]",
|
||||
"proxy_url": "[%key:component::telegram_bot::config::step::user::sections::advanced_settings::data::proxy_url%]"
|
||||
},
|
||||
"data_description": {
|
||||
"api_endpoint": "[%key:component::telegram_bot::config::step::user::sections::additional_settings::data_description::api_endpoint%]",
|
||||
"proxy_url": "[%key:component::telegram_bot::config::step::user::sections::additional_settings::data_description::proxy_url%]"
|
||||
"api_endpoint": "[%key:component::telegram_bot::config::step::user::sections::advanced_settings::data_description::api_endpoint%]",
|
||||
"proxy_url": "[%key:component::telegram_bot::config::step::user::sections::advanced_settings::data_description::proxy_url%]"
|
||||
},
|
||||
"name": "[%key:component::telegram_bot::config::step::user::sections::additional_settings::name%]"
|
||||
"name": "[%key:component::telegram_bot::config::step::user::sections::advanced_settings::name%]"
|
||||
}
|
||||
},
|
||||
"title": "Telegram bot setup"
|
||||
@@ -58,7 +58,7 @@
|
||||
},
|
||||
"description": "To create a Telegram bot, follow these steps:\n\n1. Open Telegram and start a chat with [{botfather_username}]({botfather_url}).\n1. Send the command `/newbot`.\n1. Follow the instructions to create your bot and get your API token.",
|
||||
"sections": {
|
||||
"additional_settings": {
|
||||
"advanced_settings": {
|
||||
"data": {
|
||||
"api_endpoint": "API endpoint",
|
||||
"proxy_url": "Proxy URL"
|
||||
|
||||
+1
-1
@@ -648,7 +648,7 @@ exclude_lines = [
|
||||
]
|
||||
|
||||
[tool.ruff]
|
||||
required-version = ">=0.15.18"
|
||||
required-version = ">=0.15.17"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = [
|
||||
|
||||
Generated
+3
-6
@@ -653,9 +653,6 @@ bleak-esphome==3.9.4
|
||||
# homeassistant.components.bluetooth
|
||||
bleak-retry-connector==4.6.1
|
||||
|
||||
# homeassistant.components.smlight
|
||||
bleak-smlight==1.1.0
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bleak==3.0.2
|
||||
|
||||
@@ -1317,7 +1314,7 @@ ibeacon-ble==1.2.0
|
||||
# homeassistant.components.local_calendar
|
||||
# homeassistant.components.local_todo
|
||||
# homeassistant.components.remote_calendar
|
||||
ical==13.3.0
|
||||
ical==13.2.5
|
||||
|
||||
# homeassistant.components.caldav
|
||||
icalendar==6.3.1
|
||||
@@ -1474,7 +1471,7 @@ libpyvivotek==0.6.1
|
||||
librehardwaremonitor-api==1.11.1
|
||||
|
||||
# homeassistant.components.mikrotik
|
||||
librouteros==4.1.1
|
||||
librouteros==3.2.1
|
||||
|
||||
# homeassistant.components.soundtouch
|
||||
libsoundtouch==0.8
|
||||
@@ -2148,7 +2145,7 @@ pyegps==0.2.5
|
||||
pyemoncms==0.1.3
|
||||
|
||||
# homeassistant.components.enphase_envoy
|
||||
pyenphase==3.0.1
|
||||
pyenphase==3.0.0
|
||||
|
||||
# homeassistant.components.envertech_evt800
|
||||
pyenvertechevt800==0.2.4
|
||||
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
# Automatically generated from .pre-commit-config.yaml by gen_requirements_all.py, do not edit
|
||||
|
||||
codespell==2.4.2
|
||||
ruff==0.15.18
|
||||
ruff==0.15.17
|
||||
yamllint==1.38.0
|
||||
zizmor==1.24.1
|
||||
|
||||
@@ -8,7 +8,7 @@ import pytest
|
||||
from homeassistant.components.autoskope.const import (
|
||||
DEFAULT_HOST,
|
||||
DOMAIN,
|
||||
SECTION_ADDITIONAL_SETTINGS,
|
||||
SECTION_ADVANCED_SETTINGS,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_USER
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||
@@ -20,7 +20,7 @@ from tests.common import MockConfigEntry
|
||||
USER_INPUT = {
|
||||
CONF_USERNAME: "test_user",
|
||||
CONF_PASSWORD: "test_password",
|
||||
SECTION_ADDITIONAL_SETTINGS: {
|
||||
SECTION_ADVANCED_SETTINGS: {
|
||||
CONF_HOST: DEFAULT_HOST,
|
||||
},
|
||||
}
|
||||
@@ -102,7 +102,7 @@ async def test_flow_invalid_url(
|
||||
{
|
||||
CONF_USERNAME: "test_user",
|
||||
CONF_PASSWORD: "test_password",
|
||||
SECTION_ADDITIONAL_SETTINGS: {
|
||||
SECTION_ADVANCED_SETTINGS: {
|
||||
CONF_HOST: "not-a-valid-url",
|
||||
},
|
||||
},
|
||||
@@ -151,7 +151,7 @@ async def test_custom_host(
|
||||
{
|
||||
CONF_USERNAME: "test_user",
|
||||
CONF_PASSWORD: "test_password",
|
||||
SECTION_ADDITIONAL_SETTINGS: {
|
||||
SECTION_ADVANCED_SETTINGS: {
|
||||
CONF_HOST: "https://custom.autoskope.server",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -217,206 +217,6 @@
|
||||
'state': '88',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.bedroom_valve_state_end_time-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.bedroom_valve_state_end_time',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'State end time',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.TIMESTAMP: 'timestamp'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'State end time',
|
||||
'platform': 'duco',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'time_state_end',
|
||||
'unique_id': 'aa:bb:cc:dd:ee:ff_60_time_state_end',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.bedroom_valve_state_end_time-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'timestamp',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Bedroom valve State end time',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.bedroom_valve_state_end_time',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.bedroom_valve_target_flow_level-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.bedroom_valve_target_flow_level',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Target flow level',
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 0,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Target flow level',
|
||||
'platform': 'duco',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'target_flow_level',
|
||||
'unique_id': 'aa:bb:cc:dd:ee:ff_60_target_flow_level',
|
||||
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.bedroom_valve_target_flow_level-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Bedroom valve Target flow level',
|
||||
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.bedroom_valve_target_flow_level',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.bedroom_valve_ventilation_state-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
|
||||
'auto',
|
||||
'aut1',
|
||||
'aut2',
|
||||
'aut3',
|
||||
'man1',
|
||||
'man2',
|
||||
'man3',
|
||||
'empt',
|
||||
'cnt1',
|
||||
'cnt2',
|
||||
'cnt3',
|
||||
'-',
|
||||
'man1x2',
|
||||
'man2x2',
|
||||
'man3x2',
|
||||
'man1x3',
|
||||
'man2x3',
|
||||
'man3x3',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.bedroom_valve_ventilation_state',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Ventilation state',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Ventilation state',
|
||||
'platform': 'duco',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'ventilation_state',
|
||||
'unique_id': 'aa:bb:cc:dd:ee:ff_60_ventilation_state',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.bedroom_valve_ventilation_state-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'enum',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Bedroom valve Ventilation state',
|
||||
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
|
||||
'auto',
|
||||
'aut1',
|
||||
'aut2',
|
||||
'aut3',
|
||||
'man1',
|
||||
'man2',
|
||||
'man3',
|
||||
'empt',
|
||||
'cnt1',
|
||||
'cnt2',
|
||||
'cnt3',
|
||||
'-',
|
||||
'man1x2',
|
||||
'man2x2',
|
||||
'man3x2',
|
||||
'man1x3',
|
||||
'man2x3',
|
||||
'man3x3',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.bedroom_valve_ventilation_state',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'auto',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.hall_valve_carbon_dioxide-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
@@ -526,206 +326,6 @@
|
||||
'state': '76',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.hall_valve_state_end_time-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.hall_valve_state_end_time',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'State end time',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.TIMESTAMP: 'timestamp'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'State end time',
|
||||
'platform': 'duco',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'time_state_end',
|
||||
'unique_id': 'aa:bb:cc:dd:ee:ff_61_time_state_end',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.hall_valve_state_end_time-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'timestamp',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Hall valve State end time',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.hall_valve_state_end_time',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.hall_valve_target_flow_level-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.hall_valve_target_flow_level',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Target flow level',
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 0,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Target flow level',
|
||||
'platform': 'duco',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'target_flow_level',
|
||||
'unique_id': 'aa:bb:cc:dd:ee:ff_61_target_flow_level',
|
||||
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.hall_valve_target_flow_level-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Hall valve Target flow level',
|
||||
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.hall_valve_target_flow_level',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.hall_valve_ventilation_state-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
|
||||
'auto',
|
||||
'aut1',
|
||||
'aut2',
|
||||
'aut3',
|
||||
'man1',
|
||||
'man2',
|
||||
'man3',
|
||||
'empt',
|
||||
'cnt1',
|
||||
'cnt2',
|
||||
'cnt3',
|
||||
'-',
|
||||
'man1x2',
|
||||
'man2x2',
|
||||
'man3x2',
|
||||
'man1x3',
|
||||
'man2x3',
|
||||
'man3x3',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.hall_valve_ventilation_state',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Ventilation state',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Ventilation state',
|
||||
'platform': 'duco',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'ventilation_state',
|
||||
'unique_id': 'aa:bb:cc:dd:ee:ff_61_ventilation_state',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.hall_valve_ventilation_state-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'enum',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Hall valve Ventilation state',
|
||||
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
|
||||
'auto',
|
||||
'aut1',
|
||||
'aut2',
|
||||
'aut3',
|
||||
'man1',
|
||||
'man2',
|
||||
'man3',
|
||||
'empt',
|
||||
'cnt1',
|
||||
'cnt2',
|
||||
'cnt3',
|
||||
'-',
|
||||
'man1x2',
|
||||
'man2x2',
|
||||
'man3x2',
|
||||
'man1x3',
|
||||
'man2x3',
|
||||
'man3x3',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.hall_valve_ventilation_state',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'auto',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.kitchen_rh_humidity-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
@@ -1472,203 +1072,3 @@
|
||||
'state': '92',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.study_valve_state_end_time-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.study_valve_state_end_time',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'State end time',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.TIMESTAMP: 'timestamp'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'State end time',
|
||||
'platform': 'duco',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'time_state_end',
|
||||
'unique_id': 'aa:bb:cc:dd:ee:ff_62_time_state_end',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.study_valve_state_end_time-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'timestamp',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Study valve State end time',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.study_valve_state_end_time',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.study_valve_target_flow_level-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.study_valve_target_flow_level',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Target flow level',
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 0,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Target flow level',
|
||||
'platform': 'duco',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'target_flow_level',
|
||||
'unique_id': 'aa:bb:cc:dd:ee:ff_62_target_flow_level',
|
||||
'unit_of_measurement': <UnitOfRatio.PERCENTAGE: '%'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.study_valve_target_flow_level-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Study valve Target flow level',
|
||||
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfRatio.PERCENTAGE: '%'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.study_valve_target_flow_level',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.study_valve_ventilation_state-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
|
||||
'auto',
|
||||
'aut1',
|
||||
'aut2',
|
||||
'aut3',
|
||||
'man1',
|
||||
'man2',
|
||||
'man3',
|
||||
'empt',
|
||||
'cnt1',
|
||||
'cnt2',
|
||||
'cnt3',
|
||||
'-',
|
||||
'man1x2',
|
||||
'man2x2',
|
||||
'man3x2',
|
||||
'man1x3',
|
||||
'man2x3',
|
||||
'man3x3',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.study_valve_ventilation_state',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Ventilation state',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Ventilation state',
|
||||
'platform': 'duco',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'ventilation_state',
|
||||
'unique_id': 'aa:bb:cc:dd:ee:ff_62_ventilation_state',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities_state[sensor.study_valve_ventilation_state-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'enum',
|
||||
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Study valve Ventilation state',
|
||||
<SensorEntityCapabilityAttribute.OPTIONS: 'options'>: list([
|
||||
'auto',
|
||||
'aut1',
|
||||
'aut2',
|
||||
'aut3',
|
||||
'man1',
|
||||
'man2',
|
||||
'man3',
|
||||
'empt',
|
||||
'cnt1',
|
||||
'cnt2',
|
||||
'cnt3',
|
||||
'-',
|
||||
'man1x2',
|
||||
'man2x2',
|
||||
'man3x2',
|
||||
'man1x3',
|
||||
'man2x3',
|
||||
'man3x3',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.study_valve_ventilation_state',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'auto',
|
||||
})
|
||||
# ---
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"""Tests for the Duco sensor platform."""
|
||||
|
||||
from dataclasses import replace
|
||||
import logging
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
@@ -30,62 +29,6 @@ from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_plat
|
||||
FILTER_REMAINING_ENTITY_ID = "sensor.living_filter_remaining"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"ventilation_node_type",
|
||||
[
|
||||
pytest.param(NodeType.BOX, id="box"),
|
||||
pytest.param(NodeType.VLV, id="vlv"),
|
||||
pytest.param(NodeType.VLVRH, id="vlvrh"),
|
||||
pytest.param(NodeType.VLVVOC, id="vlvvoc"),
|
||||
pytest.param(NodeType.VLVCO2, id="vlvco2"),
|
||||
pytest.param(NodeType.VLVCO2RH, id="vlvco2rh"),
|
||||
pytest.param(NodeType.EAV, id="eav"),
|
||||
pytest.param(NodeType.EAVRH, id="eavrh"),
|
||||
pytest.param(NodeType.EAVVOC, id="eavvoc"),
|
||||
pytest.param(NodeType.EAVCO2, id="eavco2"),
|
||||
],
|
||||
)
|
||||
async def test_ventilation_related_sensors_created_for_supported_node_types(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_duco_client: AsyncMock,
|
||||
mock_sensor_nodes: list[Node],
|
||||
ventilation_node_type: NodeType,
|
||||
) -> None:
|
||||
"""Test ventilation-related sensors are created for supported node families."""
|
||||
supported_node = replace(
|
||||
mock_sensor_nodes[0],
|
||||
general=replace(mock_sensor_nodes[0].general, node_type=ventilation_node_type),
|
||||
ventilation=replace(
|
||||
mock_sensor_nodes[0].ventilation,
|
||||
flow_lvl_tgt=42,
|
||||
time_state_end=1700000400,
|
||||
),
|
||||
)
|
||||
mock_duco_client.async_get_nodes.return_value = [
|
||||
supported_node,
|
||||
*mock_sensor_nodes[1:],
|
||||
]
|
||||
|
||||
await setup_platform_integration(hass, mock_config_entry, [Platform.SENSOR])
|
||||
|
||||
state = hass.states.get("sensor.living_ventilation_state")
|
||||
assert state is not None
|
||||
assert state.state == "auto"
|
||||
|
||||
state = hass.states.get("sensor.living_target_flow_level")
|
||||
assert state is not None
|
||||
assert state.state == "42"
|
||||
|
||||
state = hass.states.get("sensor.living_state_end_time")
|
||||
assert state is not None
|
||||
assert state.state == "2023-11-14T22:20:00+00:00"
|
||||
|
||||
assert hass.states.get("sensor.office_co2_ventilation_state") is None
|
||||
assert hass.states.get("sensor.office_co2_target_flow_level") is None
|
||||
assert hass.states.get("sensor.office_co2_state_end_time") is None
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def init_integration(
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -102,13 +102,6 @@ async def test_temperature(hass: HomeAssistant, router: Mock) -> None:
|
||||
assert hass.states.get("sensor.freebox_server_r2_temperature_cpu_b").state == "56"
|
||||
|
||||
|
||||
async def test_fan(hass: HomeAssistant, router: Mock) -> None:
|
||||
"""Test fan speed sensors expose API names and values."""
|
||||
await setup_platform(hass, SENSOR_DOMAIN)
|
||||
|
||||
assert hass.states.get("sensor.freebox_server_r2_ventilateur_1").state == "2130"
|
||||
|
||||
|
||||
async def test_battery(
|
||||
hass: HomeAssistant, freezer: FrozenDateTimeFactory, router: Mock
|
||||
) -> None:
|
||||
|
||||
@@ -10,11 +10,9 @@ from homeassistant.components.history_stats.config_flow import (
|
||||
)
|
||||
from homeassistant.components.history_stats.const import (
|
||||
CONF_END,
|
||||
CONF_MIN_STATE_DURATION,
|
||||
CONF_START,
|
||||
DEFAULT_NAME,
|
||||
DOMAIN,
|
||||
SECTION_ADDITIONAL_SETTINGS,
|
||||
)
|
||||
from homeassistant.components.sensor import CONF_STATE_CLASS, SensorStateClass
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||
@@ -478,46 +476,6 @@ async def test_migration_1_2(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("recorder_mock")
|
||||
async def test_migration_1_3(
|
||||
hass: HomeAssistant,
|
||||
sensor_entity_entry: er.RegistryEntry,
|
||||
) -> None:
|
||||
"""Test migration from v1.3 renames advanced_settings to additional_settings."""
|
||||
|
||||
history_stats_config_entry = MockConfigEntry(
|
||||
data={},
|
||||
domain=DOMAIN,
|
||||
options={
|
||||
CONF_NAME: DEFAULT_NAME,
|
||||
CONF_ENTITY_ID: sensor_entity_entry.entity_id,
|
||||
CONF_STATE: ["on"],
|
||||
CONF_TYPE: "count",
|
||||
CONF_START: "{{ as_timestamp(utcnow()) - 3600 }}",
|
||||
CONF_END: "{{ utcnow() }}",
|
||||
"advanced_settings": {CONF_MIN_STATE_DURATION: {"seconds": 30}},
|
||||
},
|
||||
title="My history stats",
|
||||
version=1,
|
||||
minor_version=3,
|
||||
)
|
||||
history_stats_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(history_stats_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert history_stats_config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
assert "advanced_settings" not in history_stats_config_entry.options
|
||||
assert history_stats_config_entry.options[SECTION_ADDITIONAL_SETTINGS] == {
|
||||
CONF_MIN_STATE_DURATION: {"seconds": 30}
|
||||
}
|
||||
assert history_stats_config_entry.version == 1
|
||||
assert (
|
||||
history_stats_config_entry.minor_version
|
||||
== HistoryStatsConfigFlowHandler.MINOR_VERSION
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("recorder_mock")
|
||||
async def test_migration_from_future_version(
|
||||
hass: HomeAssistant,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1131,7 +1131,7 @@
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'block_hulu',
|
||||
'translation_key': None,
|
||||
'unique_id': 'xyz12_block_hulu',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
|
||||
@@ -4,7 +4,6 @@ from collections.abc import AsyncGenerator, Generator
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from pysmlight.exceptions import SmlightAuthError
|
||||
from pysmlight.models import BleFeatures
|
||||
from pysmlight.sse import sseClient
|
||||
from pysmlight.web import ActionWrapper, CmdWrapper, Firmware, Info, Sensors
|
||||
import pytest
|
||||
@@ -27,29 +26,6 @@ MOCK_USERNAME = "test-user"
|
||||
MOCK_PASSWORD = "test-pass"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_bluetooth_scanner() -> Generator[MagicMock]:
|
||||
"""Mock bluetooth scanner."""
|
||||
with patch(
|
||||
"homeassistant.components.smlight.bluetooth.async_register_scanner"
|
||||
) as mock_register:
|
||||
yield mock_register
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_connect_scanner() -> Generator[MagicMock]:
|
||||
"""Mock bleak_smlight connect_scanner."""
|
||||
with patch(
|
||||
"homeassistant.components.smlight.bluetooth.connect_scanner"
|
||||
) as mock_connect:
|
||||
client_data = MagicMock()
|
||||
client_data.scanner = MagicMock()
|
||||
client_data.client = MagicMock()
|
||||
client_data.client.start = AsyncMock()
|
||||
mock_connect.return_value = client_data
|
||||
yield mock_connect
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config_entry() -> MockConfigEntry:
|
||||
"""Return the default mocked config entry."""
|
||||
@@ -151,7 +127,6 @@ def mock_smlight_client(request: pytest.FixtureRequest) -> Generator[MagicMock]:
|
||||
MOCK_ULTIMA = Info(
|
||||
MAC="AA:BB:CC:DD:EE:FF",
|
||||
model="SLZB-Ultima3",
|
||||
ble=BleFeatures(ble_enabled=True, proxy_enabled=True),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
"""Tests for the SMLIGHT Bluetooth platform."""
|
||||
|
||||
from unittest.mock import ANY, MagicMock
|
||||
|
||||
from pysmlight import Info
|
||||
from pysmlight.models import BleFeatures
|
||||
import pytest
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_ultima_client")
|
||||
async def test_bluetooth_scanner_lifecycle(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_connect_scanner: MagicMock,
|
||||
mock_bluetooth_scanner: MagicMock,
|
||||
) -> None:
|
||||
"""Test setting up and unloading SMLIGHT Bluetooth scanner (lifecycle)."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
mock_connect_scanner.assert_called_once_with(
|
||||
source=mock_config_entry.unique_id,
|
||||
name=mock_config_entry.title,
|
||||
host=mock_config_entry.data[CONF_HOST],
|
||||
port=5050,
|
||||
)
|
||||
|
||||
client_data = mock_connect_scanner.return_value
|
||||
client_data.client.start.assert_called_once()
|
||||
mock_bluetooth_scanner.assert_called_once_with(
|
||||
hass,
|
||||
client_data.scanner,
|
||||
source_domain="smlight",
|
||||
source_model="SLZB-Ultima3",
|
||||
source_config_entry_id=mock_config_entry.entry_id,
|
||||
source_device_id=ANY,
|
||||
)
|
||||
|
||||
await hass.config_entries.async_unload(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
client_data.client.stop.assert_called_once()
|
||||
|
||||
|
||||
async def test_bluetooth_not_started_for_disabled_settings(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_connect_scanner: MagicMock,
|
||||
mock_smlight_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test that bluetooth scanner is not started for SLZB device with disabled settings."""
|
||||
mock_smlight_client.get_info.side_effect = None
|
||||
mock_smlight_client.get_info.return_value = Info(
|
||||
MAC="AA:BB:CC:DD:EE:FF",
|
||||
model="SLZB-MR3U",
|
||||
u_device=True,
|
||||
ble=BleFeatures(proxy_enabled=False),
|
||||
)
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
mock_connect_scanner.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_smlight_client")
|
||||
async def test_bluetooth_not_started_for_classic_device(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_connect_scanner: MagicMock,
|
||||
) -> None:
|
||||
"""Test that bluetooth scanner is not started for classic (non-U) devices."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
mock_connect_scanner.assert_not_called()
|
||||
@@ -132,11 +132,7 @@ async def test_zeroconf_flow(
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "confirm_discovery"
|
||||
|
||||
progress = [
|
||||
flow
|
||||
for flow in hass.config_entries.flow.async_progress()
|
||||
if flow["handler"] == DOMAIN
|
||||
]
|
||||
progress = hass.config_entries.flow.async_progress()
|
||||
assert len(progress) == 1
|
||||
assert progress[0]["flow_id"] == result["flow_id"]
|
||||
assert progress[0]["context"]["confirm_only"] is True
|
||||
@@ -173,11 +169,7 @@ async def test_zeroconf_flow_auth(
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "confirm_discovery"
|
||||
|
||||
progress = [
|
||||
flow
|
||||
for flow in hass.config_entries.flow.async_progress()
|
||||
if flow["handler"] == DOMAIN
|
||||
]
|
||||
progress = hass.config_entries.flow.async_progress()
|
||||
assert len(progress) == 1
|
||||
assert progress[0]["flow_id"] == result["flow_id"]
|
||||
assert progress[0]["context"]["confirm_only"] is True
|
||||
@@ -189,11 +181,7 @@ async def test_zeroconf_flow_auth(
|
||||
assert result2["type"] is FlowResultType.FORM
|
||||
assert result2["step_id"] == "auth"
|
||||
|
||||
progress2 = [
|
||||
flow
|
||||
for flow in hass.config_entries.flow.async_progress()
|
||||
if flow["handler"] == DOMAIN
|
||||
]
|
||||
progress2 = hass.config_entries.flow.async_progress()
|
||||
assert len(progress2) == 1
|
||||
assert progress2[0]["flow_id"] == result["flow_id"]
|
||||
|
||||
|
||||
@@ -72,11 +72,7 @@ async def test_async_setup_missing_credentials(
|
||||
|
||||
await setup_integration(hass, mock_config_entry_host)
|
||||
|
||||
progress = [
|
||||
flow
|
||||
for flow in hass.config_entries.flow.async_progress()
|
||||
if flow["handler"] == DOMAIN and flow["context"].get("source") == "reauth"
|
||||
]
|
||||
progress = hass.config_entries.flow.async_progress()
|
||||
assert len(progress) == 1
|
||||
assert progress[0]["step_id"] == "reauth_confirm"
|
||||
assert progress[0]["context"]["unique_id"] == "aa:bb:cc:dd:ee:ff"
|
||||
|
||||
@@ -9,7 +9,7 @@ from homeassistant.components.sensor import (
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.components.sql.const import (
|
||||
CONF_ADVANCED_OPTIONS,
|
||||
CONF_ADDITIONAL_OPTIONS,
|
||||
CONF_COLUMN_NAME,
|
||||
CONF_QUERY,
|
||||
DOMAIN,
|
||||
@@ -35,7 +35,7 @@ from tests.common import MockConfigEntry
|
||||
ENTRY_CONFIG = {
|
||||
CONF_QUERY: "SELECT 5 as value",
|
||||
CONF_COLUMN_NAME: "value",
|
||||
CONF_ADVANCED_OPTIONS: {
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_UNIT_OF_MEASUREMENT: "MiB",
|
||||
CONF_DEVICE_CLASS: SensorDeviceClass.DATA_SIZE,
|
||||
CONF_STATE_CLASS: SensorStateClass.TOTAL,
|
||||
@@ -46,7 +46,7 @@ ENTRY_CONFIG_BLANK_QUERY = {
|
||||
CONF_NAME: "Get Value",
|
||||
CONF_QUERY: " ",
|
||||
CONF_COLUMN_NAME: "value",
|
||||
CONF_ADVANCED_OPTIONS: {
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_UNIT_OF_MEASUREMENT: "MiB",
|
||||
CONF_DEVICE_CLASS: SensorDeviceClass.DATA_SIZE,
|
||||
CONF_STATE_CLASS: SensorStateClass.TOTAL,
|
||||
@@ -56,7 +56,7 @@ ENTRY_CONFIG_BLANK_QUERY = {
|
||||
ENTRY_CONFIG_WITH_VALUE_TEMPLATE = {
|
||||
CONF_QUERY: "SELECT 5 as value",
|
||||
CONF_COLUMN_NAME: "value",
|
||||
CONF_ADVANCED_OPTIONS: {
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_UNIT_OF_MEASUREMENT: "MiB",
|
||||
CONF_VALUE_TEMPLATE: "{{ value }}",
|
||||
},
|
||||
@@ -68,7 +68,7 @@ ENTRY_CONFIG_WITH_QUERY_TEMPLATE = {
|
||||
" 5 {% else %} 6 {% endif %} as value"
|
||||
),
|
||||
CONF_COLUMN_NAME: "value",
|
||||
CONF_ADVANCED_OPTIONS: {
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_UNIT_OF_MEASUREMENT: "MiB",
|
||||
CONF_VALUE_TEMPLATE: "{{ value }}",
|
||||
},
|
||||
@@ -77,7 +77,7 @@ ENTRY_CONFIG_WITH_QUERY_TEMPLATE = {
|
||||
ENTRY_CONFIG_WITH_BROKEN_QUERY_TEMPLATE = {
|
||||
CONF_QUERY: "SELECT {{ 5 as value",
|
||||
CONF_COLUMN_NAME: "value",
|
||||
CONF_ADVANCED_OPTIONS: {
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_UNIT_OF_MEASUREMENT: "MiB",
|
||||
CONF_VALUE_TEMPLATE: "{{ value }}",
|
||||
},
|
||||
@@ -86,7 +86,7 @@ ENTRY_CONFIG_WITH_BROKEN_QUERY_TEMPLATE = {
|
||||
ENTRY_CONFIG_WITH_BROKEN_QUERY_TEMPLATE_OPT = {
|
||||
CONF_QUERY: "SELECT {{ 5 as value",
|
||||
CONF_COLUMN_NAME: "value",
|
||||
CONF_ADVANCED_OPTIONS: {
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_UNIT_OF_MEASUREMENT: "MiB",
|
||||
CONF_VALUE_TEMPLATE: "{{ value }}",
|
||||
},
|
||||
@@ -95,7 +95,7 @@ ENTRY_CONFIG_WITH_BROKEN_QUERY_TEMPLATE_OPT = {
|
||||
ENTRY_CONFIG_INVALID_QUERY = {
|
||||
CONF_QUERY: "SELECT 5 FROM as value",
|
||||
CONF_COLUMN_NAME: "size",
|
||||
CONF_ADVANCED_OPTIONS: {
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_UNIT_OF_MEASUREMENT: "MiB",
|
||||
},
|
||||
}
|
||||
@@ -104,7 +104,7 @@ ENTRY_CONFIG_INVALID_QUERY = {
|
||||
ENTRY_CONFIG_INVALID_QUERY_2 = {
|
||||
CONF_QUERY: "SELECT5 FROM as value",
|
||||
CONF_COLUMN_NAME: "size",
|
||||
CONF_ADVANCED_OPTIONS: {
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_UNIT_OF_MEASUREMENT: "MiB",
|
||||
},
|
||||
}
|
||||
@@ -113,7 +113,7 @@ ENTRY_CONFIG_INVALID_QUERY_2 = {
|
||||
ENTRY_CONFIG_INVALID_QUERY_3 = {
|
||||
CONF_QUERY: ";;",
|
||||
CONF_COLUMN_NAME: "size",
|
||||
CONF_ADVANCED_OPTIONS: {
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_UNIT_OF_MEASUREMENT: "MiB",
|
||||
},
|
||||
}
|
||||
@@ -122,7 +122,7 @@ ENTRY_CONFIG_INVALID_QUERY_3 = {
|
||||
ENTRY_CONFIG_INVALID_QUERY_OPT = {
|
||||
CONF_QUERY: "SELECT 5 FROM as value",
|
||||
CONF_COLUMN_NAME: "size",
|
||||
CONF_ADVANCED_OPTIONS: {
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_UNIT_OF_MEASUREMENT: "MiB",
|
||||
},
|
||||
}
|
||||
@@ -131,7 +131,7 @@ ENTRY_CONFIG_INVALID_QUERY_OPT = {
|
||||
ENTRY_CONFIG_INVALID_QUERY_2_OPT = {
|
||||
CONF_QUERY: "SELECT5 FROM as value",
|
||||
CONF_COLUMN_NAME: "size",
|
||||
CONF_ADVANCED_OPTIONS: {
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_UNIT_OF_MEASUREMENT: "MiB",
|
||||
},
|
||||
}
|
||||
@@ -140,7 +140,7 @@ ENTRY_CONFIG_INVALID_QUERY_2_OPT = {
|
||||
ENTRY_CONFIG_INVALID_QUERY_3_OPT = {
|
||||
CONF_QUERY: ";;",
|
||||
CONF_COLUMN_NAME: "size",
|
||||
CONF_ADVANCED_OPTIONS: {
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_UNIT_OF_MEASUREMENT: "MiB",
|
||||
},
|
||||
}
|
||||
@@ -152,7 +152,7 @@ ENTRY_CONFIG_QUERY_READ_ONLY_CTE = {
|
||||
" SELECT state FROM test WHERE row_num = 1 LIMIT 1;"
|
||||
),
|
||||
CONF_COLUMN_NAME: "state",
|
||||
CONF_ADVANCED_OPTIONS: {
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_UNIT_OF_MEASUREMENT: "MiB",
|
||||
},
|
||||
}
|
||||
@@ -160,7 +160,7 @@ ENTRY_CONFIG_QUERY_READ_ONLY_CTE = {
|
||||
ENTRY_CONFIG_QUERY_NO_READ_ONLY = {
|
||||
CONF_QUERY: "UPDATE states SET state = 999999 WHERE state_id = 11125",
|
||||
CONF_COLUMN_NAME: "state",
|
||||
CONF_ADVANCED_OPTIONS: {
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_UNIT_OF_MEASUREMENT: "MiB",
|
||||
},
|
||||
}
|
||||
@@ -171,7 +171,7 @@ ENTRY_CONFIG_QUERY_NO_READ_ONLY_CTE = {
|
||||
" UPDATE states SET states.state = test.state;"
|
||||
),
|
||||
CONF_COLUMN_NAME: "size",
|
||||
CONF_ADVANCED_OPTIONS: {
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_UNIT_OF_MEASUREMENT: "MiB",
|
||||
},
|
||||
}
|
||||
@@ -182,7 +182,7 @@ ENTRY_CONFIG_QUERY_READ_ONLY_CTE_OPT = {
|
||||
" SELECT state FROM test WHERE row_num = 1 LIMIT 1;"
|
||||
),
|
||||
CONF_COLUMN_NAME: "state",
|
||||
CONF_ADVANCED_OPTIONS: {
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_UNIT_OF_MEASUREMENT: "MiB",
|
||||
},
|
||||
}
|
||||
@@ -190,7 +190,7 @@ ENTRY_CONFIG_QUERY_READ_ONLY_CTE_OPT = {
|
||||
ENTRY_CONFIG_QUERY_NO_READ_ONLY_OPT = {
|
||||
CONF_QUERY: "UPDATE 5 as value",
|
||||
CONF_COLUMN_NAME: "size",
|
||||
CONF_ADVANCED_OPTIONS: {
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_UNIT_OF_MEASUREMENT: "MiB",
|
||||
},
|
||||
}
|
||||
@@ -201,7 +201,7 @@ ENTRY_CONFIG_QUERY_NO_READ_ONLY_CTE_OPT = {
|
||||
" UPDATE states SET states.state = test.state;"
|
||||
),
|
||||
CONF_COLUMN_NAME: "size",
|
||||
CONF_ADVANCED_OPTIONS: {
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_UNIT_OF_MEASUREMENT: "MiB",
|
||||
},
|
||||
}
|
||||
@@ -210,7 +210,7 @@ ENTRY_CONFIG_QUERY_NO_READ_ONLY_CTE_OPT = {
|
||||
ENTRY_CONFIG_MULTIPLE_QUERIES = {
|
||||
CONF_QUERY: "SELECT 5 as state; UPDATE states SET state = 10;",
|
||||
CONF_COLUMN_NAME: "state",
|
||||
CONF_ADVANCED_OPTIONS: {
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_UNIT_OF_MEASUREMENT: "MiB",
|
||||
},
|
||||
}
|
||||
@@ -219,7 +219,7 @@ ENTRY_CONFIG_MULTIPLE_QUERIES = {
|
||||
ENTRY_CONFIG_MULTIPLE_QUERIES_OPT = {
|
||||
CONF_QUERY: "SELECT 5 as state; UPDATE states SET state = 10;",
|
||||
CONF_COLUMN_NAME: "state",
|
||||
CONF_ADVANCED_OPTIONS: {
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_UNIT_OF_MEASUREMENT: "MiB",
|
||||
},
|
||||
}
|
||||
@@ -228,7 +228,7 @@ ENTRY_CONFIG_MULTIPLE_QUERIES_OPT = {
|
||||
ENTRY_CONFIG_INVALID_COLUMN_NAME = {
|
||||
CONF_QUERY: "SELECT 5 as value",
|
||||
CONF_COLUMN_NAME: "size",
|
||||
CONF_ADVANCED_OPTIONS: {
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_UNIT_OF_MEASUREMENT: "MiB",
|
||||
},
|
||||
}
|
||||
@@ -236,7 +236,7 @@ ENTRY_CONFIG_INVALID_COLUMN_NAME = {
|
||||
ENTRY_CONFIG_INVALID_COLUMN_NAME_OPT = {
|
||||
CONF_QUERY: "SELECT 5 as value",
|
||||
CONF_COLUMN_NAME: "size",
|
||||
CONF_ADVANCED_OPTIONS: {
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_UNIT_OF_MEASUREMENT: "MiB",
|
||||
},
|
||||
}
|
||||
@@ -244,7 +244,7 @@ ENTRY_CONFIG_INVALID_COLUMN_NAME_OPT = {
|
||||
ENTRY_CONFIG_NO_RESULTS = {
|
||||
CONF_QUERY: "SELECT kalle as value from no_table;",
|
||||
CONF_COLUMN_NAME: "value",
|
||||
CONF_ADVANCED_OPTIONS: {
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_UNIT_OF_MEASUREMENT: "MiB",
|
||||
},
|
||||
}
|
||||
@@ -365,8 +365,8 @@ async def init_integration(
|
||||
"""Set up the SQL integration in Home Assistant."""
|
||||
if not options:
|
||||
options = ENTRY_CONFIG
|
||||
if CONF_ADVANCED_OPTIONS not in options:
|
||||
options[CONF_ADVANCED_OPTIONS] = {}
|
||||
if CONF_ADDITIONAL_OPTIONS not in options:
|
||||
options[CONF_ADDITIONAL_OPTIONS] = {}
|
||||
|
||||
config_entry = MockConfigEntry(
|
||||
title=title,
|
||||
|
||||
@@ -16,7 +16,7 @@ from homeassistant.components.sensor import (
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.components.sql.const import (
|
||||
CONF_ADVANCED_OPTIONS,
|
||||
CONF_ADDITIONAL_OPTIONS,
|
||||
CONF_COLUMN_NAME,
|
||||
CONF_QUERY,
|
||||
DOMAIN,
|
||||
@@ -100,7 +100,7 @@ async def test_form_simple(
|
||||
assert result["options"] == {
|
||||
CONF_QUERY: "SELECT 5 as value",
|
||||
CONF_COLUMN_NAME: "value",
|
||||
CONF_ADVANCED_OPTIONS: {
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_UNIT_OF_MEASUREMENT: "MiB",
|
||||
CONF_DEVICE_CLASS: SensorDeviceClass.DATA_SIZE,
|
||||
CONF_STATE_CLASS: SensorStateClass.TOTAL,
|
||||
@@ -141,7 +141,7 @@ async def test_form_with_query_template(
|
||||
" 5 {% else %} 6 {% endif %} as value"
|
||||
),
|
||||
CONF_COLUMN_NAME: "value",
|
||||
CONF_ADVANCED_OPTIONS: {
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_UNIT_OF_MEASUREMENT: "MiB",
|
||||
CONF_VALUE_TEMPLATE: "{{ value }}",
|
||||
},
|
||||
@@ -188,7 +188,7 @@ async def test_form_with_broken_query_template(
|
||||
" 5 {% else %} 6 {% endif %} as value"
|
||||
),
|
||||
CONF_COLUMN_NAME: "value",
|
||||
CONF_ADVANCED_OPTIONS: {
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_UNIT_OF_MEASUREMENT: "MiB",
|
||||
CONF_VALUE_TEMPLATE: "{{ value }}",
|
||||
},
|
||||
@@ -225,7 +225,7 @@ async def test_form_with_value_template(
|
||||
assert result["options"] == {
|
||||
CONF_QUERY: "SELECT 5 as value",
|
||||
CONF_COLUMN_NAME: "value",
|
||||
CONF_ADVANCED_OPTIONS: {
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_UNIT_OF_MEASUREMENT: "MiB",
|
||||
CONF_VALUE_TEMPLATE: "{{ value }}",
|
||||
},
|
||||
@@ -348,7 +348,7 @@ async def test_flow_fails_invalid_query(hass: HomeAssistant) -> None:
|
||||
assert result["options"] == {
|
||||
CONF_QUERY: "SELECT 5 as value",
|
||||
CONF_COLUMN_NAME: "value",
|
||||
CONF_ADVANCED_OPTIONS: {
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_UNIT_OF_MEASUREMENT: "MiB",
|
||||
CONF_DEVICE_CLASS: SensorDeviceClass.DATA_SIZE,
|
||||
CONF_STATE_CLASS: SensorStateClass.TOTAL,
|
||||
@@ -391,7 +391,7 @@ async def test_flow_fails_invalid_column_name(hass: HomeAssistant) -> None:
|
||||
assert result["options"] == {
|
||||
CONF_QUERY: "SELECT 5 as value",
|
||||
CONF_COLUMN_NAME: "value",
|
||||
CONF_ADVANCED_OPTIONS: {
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_UNIT_OF_MEASUREMENT: "MiB",
|
||||
CONF_DEVICE_CLASS: SensorDeviceClass.DATA_SIZE,
|
||||
CONF_STATE_CLASS: SensorStateClass.TOTAL,
|
||||
@@ -407,7 +407,7 @@ async def test_options_flow(hass: HomeAssistant) -> None:
|
||||
options={
|
||||
CONF_QUERY: "SELECT 5 as value",
|
||||
CONF_COLUMN_NAME: "value",
|
||||
CONF_ADVANCED_OPTIONS: {
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_UNIT_OF_MEASUREMENT: "MiB",
|
||||
CONF_DEVICE_CLASS: SensorDeviceClass.DATA_SIZE,
|
||||
CONF_STATE_CLASS: SensorStateClass.TOTAL,
|
||||
@@ -430,7 +430,7 @@ async def test_options_flow(hass: HomeAssistant) -> None:
|
||||
user_input={
|
||||
CONF_QUERY: "SELECT 5 as size",
|
||||
CONF_COLUMN_NAME: "size",
|
||||
CONF_ADVANCED_OPTIONS: {
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_UNIT_OF_MEASUREMENT: "MiB",
|
||||
CONF_VALUE_TEMPLATE: "{{ value }}",
|
||||
CONF_DEVICE_CLASS: SensorDeviceClass.DATA_SIZE,
|
||||
@@ -443,7 +443,7 @@ async def test_options_flow(hass: HomeAssistant) -> None:
|
||||
assert result["data"] == {
|
||||
CONF_QUERY: "SELECT 5 as size",
|
||||
CONF_COLUMN_NAME: "size",
|
||||
CONF_ADVANCED_OPTIONS: {
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_UNIT_OF_MEASUREMENT: "MiB",
|
||||
CONF_VALUE_TEMPLATE: "{{ value }}",
|
||||
CONF_DEVICE_CLASS: SensorDeviceClass.DATA_SIZE,
|
||||
@@ -460,7 +460,7 @@ async def test_options_flow_name_previously_removed(hass: HomeAssistant) -> None
|
||||
options={
|
||||
CONF_QUERY: "SELECT 5 as value",
|
||||
CONF_COLUMN_NAME: "value",
|
||||
CONF_ADVANCED_OPTIONS: {
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_UNIT_OF_MEASUREMENT: "MiB",
|
||||
},
|
||||
},
|
||||
@@ -482,7 +482,7 @@ async def test_options_flow_name_previously_removed(hass: HomeAssistant) -> None
|
||||
user_input={
|
||||
CONF_QUERY: "SELECT 5 as size",
|
||||
CONF_COLUMN_NAME: "size",
|
||||
CONF_ADVANCED_OPTIONS: {
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_UNIT_OF_MEASUREMENT: "MiB",
|
||||
},
|
||||
},
|
||||
@@ -493,7 +493,7 @@ async def test_options_flow_name_previously_removed(hass: HomeAssistant) -> None
|
||||
assert result["data"] == {
|
||||
CONF_QUERY: "SELECT 5 as size",
|
||||
CONF_COLUMN_NAME: "size",
|
||||
CONF_ADVANCED_OPTIONS: {
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_UNIT_OF_MEASUREMENT: "MiB",
|
||||
},
|
||||
}
|
||||
@@ -507,7 +507,7 @@ async def test_options_flow_fails_db_url(hass: HomeAssistant) -> None:
|
||||
options={
|
||||
CONF_QUERY: "SELECT 5 as value",
|
||||
CONF_COLUMN_NAME: "value",
|
||||
CONF_ADVANCED_OPTIONS: {
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_UNIT_OF_MEASUREMENT: "MiB",
|
||||
},
|
||||
},
|
||||
@@ -529,7 +529,7 @@ async def test_options_flow_fails_db_url(hass: HomeAssistant) -> None:
|
||||
user_input={
|
||||
CONF_QUERY: "SELECT 5 as size",
|
||||
CONF_COLUMN_NAME: "size",
|
||||
CONF_ADVANCED_OPTIONS: {
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_UNIT_OF_MEASUREMENT: "MiB",
|
||||
},
|
||||
},
|
||||
@@ -546,7 +546,7 @@ async def test_options_flow_fails_invalid_query(hass: HomeAssistant) -> None:
|
||||
options={
|
||||
CONF_QUERY: "SELECT 5 as value",
|
||||
CONF_COLUMN_NAME: "value",
|
||||
CONF_ADVANCED_OPTIONS: {
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_UNIT_OF_MEASUREMENT: "MiB",
|
||||
},
|
||||
},
|
||||
@@ -630,7 +630,7 @@ async def test_options_flow_fails_invalid_query(hass: HomeAssistant) -> None:
|
||||
user_input={
|
||||
CONF_QUERY: "SELECT 5 as size",
|
||||
CONF_COLUMN_NAME: "size",
|
||||
CONF_ADVANCED_OPTIONS: {
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_UNIT_OF_MEASUREMENT: "MiB",
|
||||
},
|
||||
},
|
||||
@@ -640,7 +640,7 @@ async def test_options_flow_fails_invalid_query(hass: HomeAssistant) -> None:
|
||||
assert result["data"] == {
|
||||
CONF_QUERY: "SELECT 5 as size",
|
||||
CONF_COLUMN_NAME: "size",
|
||||
CONF_ADVANCED_OPTIONS: {
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_UNIT_OF_MEASUREMENT: "MiB",
|
||||
},
|
||||
}
|
||||
@@ -654,7 +654,7 @@ async def test_options_flow_fails_invalid_column_name(hass: HomeAssistant) -> No
|
||||
options={
|
||||
CONF_QUERY: "SELECT 5 as value",
|
||||
CONF_COLUMN_NAME: "value",
|
||||
CONF_ADVANCED_OPTIONS: {
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_UNIT_OF_MEASUREMENT: "MiB",
|
||||
},
|
||||
},
|
||||
@@ -682,7 +682,7 @@ async def test_options_flow_fails_invalid_column_name(hass: HomeAssistant) -> No
|
||||
user_input={
|
||||
CONF_QUERY: "SELECT 5 as value",
|
||||
CONF_COLUMN_NAME: "value",
|
||||
CONF_ADVANCED_OPTIONS: {
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_UNIT_OF_MEASUREMENT: "MiB",
|
||||
},
|
||||
},
|
||||
@@ -692,7 +692,7 @@ async def test_options_flow_fails_invalid_column_name(hass: HomeAssistant) -> No
|
||||
assert result["data"] == {
|
||||
CONF_QUERY: "SELECT 5 as value",
|
||||
CONF_COLUMN_NAME: "value",
|
||||
CONF_ADVANCED_OPTIONS: {
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_UNIT_OF_MEASUREMENT: "MiB",
|
||||
},
|
||||
}
|
||||
@@ -706,7 +706,7 @@ async def test_options_flow_db_url_empty(hass: HomeAssistant) -> None:
|
||||
options={
|
||||
CONF_QUERY: "SELECT 5 as value",
|
||||
CONF_COLUMN_NAME: "value",
|
||||
CONF_ADVANCED_OPTIONS: {
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_UNIT_OF_MEASUREMENT: "MiB",
|
||||
},
|
||||
},
|
||||
@@ -727,7 +727,7 @@ async def test_options_flow_db_url_empty(hass: HomeAssistant) -> None:
|
||||
user_input={
|
||||
CONF_QUERY: "SELECT 5 as size",
|
||||
CONF_COLUMN_NAME: "size",
|
||||
CONF_ADVANCED_OPTIONS: {
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_UNIT_OF_MEASUREMENT: "MiB",
|
||||
},
|
||||
},
|
||||
@@ -738,7 +738,7 @@ async def test_options_flow_db_url_empty(hass: HomeAssistant) -> None:
|
||||
assert result["data"] == {
|
||||
CONF_QUERY: "SELECT 5 as size",
|
||||
CONF_COLUMN_NAME: "size",
|
||||
CONF_ADVANCED_OPTIONS: {
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_UNIT_OF_MEASUREMENT: "MiB",
|
||||
},
|
||||
}
|
||||
@@ -770,7 +770,7 @@ async def test_full_flow_not_recorder_db(
|
||||
{
|
||||
CONF_QUERY: "SELECT 5 as value",
|
||||
CONF_COLUMN_NAME: "value",
|
||||
CONF_ADVANCED_OPTIONS: {},
|
||||
CONF_ADDITIONAL_OPTIONS: {},
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
@@ -781,7 +781,7 @@ async def test_full_flow_not_recorder_db(
|
||||
assert result["options"] == {
|
||||
CONF_QUERY: "SELECT 5 as value",
|
||||
CONF_COLUMN_NAME: "value",
|
||||
CONF_ADVANCED_OPTIONS: {},
|
||||
CONF_ADDITIONAL_OPTIONS: {},
|
||||
}
|
||||
|
||||
entry = hass.config_entries.async_entries(DOMAIN)[0]
|
||||
@@ -796,7 +796,7 @@ async def test_full_flow_not_recorder_db(
|
||||
user_input={
|
||||
CONF_QUERY: "SELECT 5 as value",
|
||||
CONF_COLUMN_NAME: "value",
|
||||
CONF_ADVANCED_OPTIONS: {
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_UNIT_OF_MEASUREMENT: "MiB",
|
||||
},
|
||||
},
|
||||
@@ -807,7 +807,7 @@ async def test_full_flow_not_recorder_db(
|
||||
assert result["data"] == {
|
||||
CONF_QUERY: "SELECT 5 as value",
|
||||
CONF_COLUMN_NAME: "value",
|
||||
CONF_ADVANCED_OPTIONS: {
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_UNIT_OF_MEASUREMENT: "MiB",
|
||||
},
|
||||
}
|
||||
@@ -822,7 +822,7 @@ async def test_device_state_class(hass: HomeAssistant) -> None:
|
||||
options={
|
||||
CONF_QUERY: "SELECT 5 as value",
|
||||
CONF_COLUMN_NAME: "value",
|
||||
CONF_ADVANCED_OPTIONS: {
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_UNIT_OF_MEASUREMENT: "MiB",
|
||||
},
|
||||
},
|
||||
@@ -839,7 +839,7 @@ async def test_device_state_class(hass: HomeAssistant) -> None:
|
||||
user_input={
|
||||
CONF_QUERY: "SELECT 5 as value",
|
||||
CONF_COLUMN_NAME: "value",
|
||||
CONF_ADVANCED_OPTIONS: {
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_UNIT_OF_MEASUREMENT: "MiB",
|
||||
CONF_DEVICE_CLASS: SensorDeviceClass.DATA_SIZE,
|
||||
CONF_STATE_CLASS: SensorStateClass.TOTAL,
|
||||
@@ -852,7 +852,7 @@ async def test_device_state_class(hass: HomeAssistant) -> None:
|
||||
assert result["data"] == {
|
||||
CONF_QUERY: "SELECT 5 as value",
|
||||
CONF_COLUMN_NAME: "value",
|
||||
CONF_ADVANCED_OPTIONS: {
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_UNIT_OF_MEASUREMENT: "MiB",
|
||||
CONF_DEVICE_CLASS: SensorDeviceClass.DATA_SIZE,
|
||||
CONF_STATE_CLASS: SensorStateClass.TOTAL,
|
||||
@@ -868,7 +868,7 @@ async def test_device_state_class(hass: HomeAssistant) -> None:
|
||||
user_input={
|
||||
CONF_QUERY: "SELECT 5 as value",
|
||||
CONF_COLUMN_NAME: "value",
|
||||
CONF_ADVANCED_OPTIONS: {
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_UNIT_OF_MEASUREMENT: "MiB",
|
||||
},
|
||||
},
|
||||
@@ -881,7 +881,7 @@ async def test_device_state_class(hass: HomeAssistant) -> None:
|
||||
assert result["data"] == {
|
||||
CONF_QUERY: "SELECT 5 as value",
|
||||
CONF_COLUMN_NAME: "value",
|
||||
CONF_ADVANCED_OPTIONS: {
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_UNIT_OF_MEASUREMENT: "MiB",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ from homeassistant.components.sensor import (
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.components.sql.const import (
|
||||
CONF_ADVANCED_OPTIONS,
|
||||
CONF_ADDITIONAL_OPTIONS,
|
||||
CONF_COLUMN_NAME,
|
||||
CONF_QUERY,
|
||||
DOMAIN,
|
||||
@@ -122,10 +122,10 @@ async def test_migration_from_future(
|
||||
options={
|
||||
CONF_QUERY: "SELECT 5.01 as value",
|
||||
CONF_COLUMN_NAME: "value",
|
||||
CONF_ADVANCED_OPTIONS: {},
|
||||
CONF_ADDITIONAL_OPTIONS: {},
|
||||
},
|
||||
entry_id="1",
|
||||
version=3,
|
||||
version=4,
|
||||
)
|
||||
|
||||
config_entry.add_to_hass(hass)
|
||||
@@ -135,10 +135,10 @@ async def test_migration_from_future(
|
||||
assert config_entry.state is ConfigEntryState.MIGRATION_ERROR
|
||||
|
||||
|
||||
async def test_migration_from_v1_to_v2(
|
||||
async def test_migration_from_v1_to_v3(
|
||||
recorder_mock: Recorder, hass: HomeAssistant
|
||||
) -> None:
|
||||
"""Test migration from version 1 to 2."""
|
||||
"""Test migration from version 1 to 3."""
|
||||
config_entry = MockConfigEntry(
|
||||
title="Test migration",
|
||||
domain=DOMAIN,
|
||||
@@ -163,12 +163,60 @@ async def test_migration_from_v1_to_v2(
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
assert config_entry.version == 3
|
||||
|
||||
assert config_entry.data == {}
|
||||
assert config_entry.options == {
|
||||
CONF_QUERY: "SELECT 5.01 as value",
|
||||
CONF_COLUMN_NAME: "value",
|
||||
CONF_ADVANCED_OPTIONS: {
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_VALUE_TEMPLATE: "{{ value | int }}",
|
||||
CONF_UNIT_OF_MEASUREMENT: "MiB",
|
||||
CONF_DEVICE_CLASS: SensorDeviceClass.DATA_SIZE,
|
||||
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
|
||||
},
|
||||
}
|
||||
|
||||
state = hass.states.get("sensor.test_migration")
|
||||
assert state.state == "5"
|
||||
|
||||
|
||||
async def test_migration_from_v2_to_v3(
|
||||
recorder_mock: Recorder, hass: HomeAssistant
|
||||
) -> None:
|
||||
"""Test migration from version 2 to 3 renames the options section."""
|
||||
config_entry = MockConfigEntry(
|
||||
title="Test migration",
|
||||
domain=DOMAIN,
|
||||
source=SOURCE_USER,
|
||||
data={},
|
||||
options={
|
||||
CONF_QUERY: "SELECT 5.01 as value",
|
||||
CONF_COLUMN_NAME: "value",
|
||||
"advanced_options": {
|
||||
CONF_VALUE_TEMPLATE: "{{ value | int }}",
|
||||
CONF_UNIT_OF_MEASUREMENT: "MiB",
|
||||
CONF_DEVICE_CLASS: SensorDeviceClass.DATA_SIZE,
|
||||
CONF_STATE_CLASS: SensorStateClass.MEASUREMENT,
|
||||
},
|
||||
},
|
||||
entry_id="1",
|
||||
version=2,
|
||||
minor_version=1,
|
||||
)
|
||||
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
assert config_entry.version == 3
|
||||
|
||||
assert "advanced_options" not in config_entry.options
|
||||
assert config_entry.options == {
|
||||
CONF_QUERY: "SELECT 5.01 as value",
|
||||
CONF_COLUMN_NAME: "value",
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_VALUE_TEMPLATE: "{{ value | int }}",
|
||||
CONF_UNIT_OF_MEASUREMENT: "MiB",
|
||||
CONF_DEVICE_CLASS: SensorDeviceClass.DATA_SIZE,
|
||||
|
||||
@@ -18,7 +18,7 @@ from homeassistant.components.sensor import (
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.components.sql.const import (
|
||||
CONF_ADVANCED_OPTIONS,
|
||||
CONF_ADDITIONAL_OPTIONS,
|
||||
CONF_COLUMN_NAME,
|
||||
CONF_QUERY,
|
||||
DOMAIN,
|
||||
@@ -94,7 +94,7 @@ async def test_query_value_template(
|
||||
options = {
|
||||
CONF_QUERY: "SELECT 5.01 as value",
|
||||
CONF_COLUMN_NAME: "value",
|
||||
CONF_ADVANCED_OPTIONS: {
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_VALUE_TEMPLATE: "{{ value | int }}",
|
||||
CONF_UNIT_OF_MEASUREMENT: "MiB",
|
||||
CONF_DEVICE_CLASS: SensorDeviceClass.DATA_SIZE,
|
||||
@@ -126,7 +126,7 @@ async def test_template_query(
|
||||
" 5 {% else %} 6 {% endif %} as value"
|
||||
),
|
||||
CONF_COLUMN_NAME: "value",
|
||||
CONF_ADVANCED_OPTIONS: {
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_VALUE_TEMPLATE: "{{ value | int }}",
|
||||
},
|
||||
}
|
||||
@@ -168,7 +168,7 @@ async def test_broken_template_query(
|
||||
options = {
|
||||
CONF_QUERY: "SELECT {{ 5 as value",
|
||||
CONF_COLUMN_NAME: "value",
|
||||
CONF_ADVANCED_OPTIONS: {
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_VALUE_TEMPLATE: "{{ value | int }}",
|
||||
},
|
||||
}
|
||||
@@ -672,7 +672,7 @@ async def test_attributes_from_entry_config(
|
||||
options={
|
||||
CONF_QUERY: "SELECT 5 as value",
|
||||
CONF_COLUMN_NAME: "value",
|
||||
CONF_ADVANCED_OPTIONS: {
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_UNIT_OF_MEASUREMENT: "MiB",
|
||||
CONF_DEVICE_CLASS: SensorDeviceClass.DATA_SIZE,
|
||||
CONF_STATE_CLASS: SensorStateClass.TOTAL,
|
||||
@@ -694,7 +694,7 @@ async def test_attributes_from_entry_config(
|
||||
options={
|
||||
CONF_QUERY: "SELECT 6 as value",
|
||||
CONF_COLUMN_NAME: "value",
|
||||
CONF_ADVANCED_OPTIONS: {
|
||||
CONF_ADDITIONAL_OPTIONS: {
|
||||
CONF_UNIT_OF_MEASUREMENT: "MiB",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1099,32 +1099,6 @@ FLOOR_LAMP_SERVICE_INFO = BluetoothServiceInfoBleak(
|
||||
tx_power=-127,
|
||||
)
|
||||
|
||||
CANDLE_WARMER_LAMP_SERVICE_INFO = BluetoothServiceInfoBleak(
|
||||
name="Candle Warmer Lamp",
|
||||
manufacturer_data={
|
||||
2409: b"\x90\xe5\xb1h\xda\xaa\n\xb0 \x00",
|
||||
},
|
||||
service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b'\x00\x00\x00\x00\x11"\xb8'},
|
||||
service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
|
||||
address="AA:BB:CC:DD:EE:FF",
|
||||
rssi=-60,
|
||||
source="local",
|
||||
advertisement=generate_advertisement_data(
|
||||
local_name="Candle Warmer Lamp",
|
||||
manufacturer_data={
|
||||
2409: b"\x90\xe5\xb1h\xda\xaa\n\xb0 \x00",
|
||||
},
|
||||
service_data={
|
||||
"0000fd3d-0000-1000-8000-00805f9b34fb": b'\x00\x00\x00\x00\x11"\xb8'
|
||||
},
|
||||
service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
|
||||
),
|
||||
device=generate_ble_device("AA:BB:CC:DD:EE:FF", "Candle Warmer Lamp"),
|
||||
time=0,
|
||||
connectable=True,
|
||||
tx_power=-127,
|
||||
)
|
||||
|
||||
RGBICWW_STRIP_LIGHT_SERVICE_INFO = BluetoothServiceInfoBleak(
|
||||
name="RGBICWW Strip Light",
|
||||
manufacturer_data={
|
||||
|
||||
@@ -27,7 +27,6 @@ from . import (
|
||||
AIR_PURIFIER_TABLE_US_SERVICE_INFO,
|
||||
AIR_PURIFIER_US_SERVICE_INFO,
|
||||
BULB_SERVICE_INFO,
|
||||
CANDLE_WARMER_LAMP_SERVICE_INFO,
|
||||
CEILING_LIGHT_SERVICE_INFO,
|
||||
FLOOR_LAMP_SERVICE_INFO,
|
||||
PERMANENT_OUTDOOR_LIGHT_SERVICE_INFO,
|
||||
@@ -133,15 +132,6 @@ FLOOR_LAMP_PARAMETERS = (
|
||||
],
|
||||
)
|
||||
|
||||
CANDLE_WARMER_LAMP_PARAMETERS = (
|
||||
COMMON_PARAMETERS,
|
||||
[
|
||||
TURN_ON_PARAMETERS,
|
||||
TURN_OFF_PARAMETERS,
|
||||
SET_BRIGHTNESS_PARAMETERS,
|
||||
],
|
||||
)
|
||||
|
||||
AIR_PURIFIER_LIGHT_PARAMETERS = (
|
||||
COMMON_PARAMETERS,
|
||||
[
|
||||
@@ -479,76 +469,6 @@ async def test_floor_lamp_services_exception(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(*CANDLE_WARMER_LAMP_PARAMETERS)
|
||||
async def test_candle_warmer_lamp_services(
|
||||
hass: HomeAssistant,
|
||||
mock_entry_encrypted_factory: Callable[[str], MockConfigEntry],
|
||||
service: str,
|
||||
service_data: dict,
|
||||
mock_method: str,
|
||||
expected_args: Any,
|
||||
) -> None:
|
||||
"""Test all SwitchBot candle warmer lamp services."""
|
||||
inject_bluetooth_service_info(hass, CANDLE_WARMER_LAMP_SERVICE_INFO)
|
||||
|
||||
entry = mock_entry_encrypted_factory(sensor_type="candle_warmer_lamp")
|
||||
entry.add_to_hass(hass)
|
||||
entity_id = "light.test_name"
|
||||
|
||||
mocked_instance = AsyncMock(return_value=True)
|
||||
|
||||
with patch.multiple(
|
||||
"homeassistant.components.switchbot.light.switchbot.SwitchbotCandleWarmerLamp",
|
||||
**{mock_method: mocked_instance},
|
||||
update=AsyncMock(return_value=None),
|
||||
):
|
||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
service,
|
||||
{**service_data, ATTR_ENTITY_ID: entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mocked_instance.assert_awaited_once_with(*expected_args)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(*CANDLE_WARMER_LAMP_PARAMETERS)
|
||||
async def test_candle_warmer_lamp_services_exception(
|
||||
hass: HomeAssistant,
|
||||
mock_entry_encrypted_factory: Callable[[str], MockConfigEntry],
|
||||
service: str,
|
||||
service_data: dict,
|
||||
mock_method: str,
|
||||
expected_args: Any,
|
||||
) -> None:
|
||||
"""Test all SwitchBot candle warmer lamp services with exception."""
|
||||
inject_bluetooth_service_info(hass, CANDLE_WARMER_LAMP_SERVICE_INFO)
|
||||
|
||||
entry = mock_entry_encrypted_factory(sensor_type="candle_warmer_lamp")
|
||||
entry.add_to_hass(hass)
|
||||
entity_id = "light.test_name"
|
||||
exception = SwitchbotOperationError("Operation failed")
|
||||
error_message = "An error occurred while performing the action: Operation failed"
|
||||
with patch.multiple(
|
||||
"homeassistant.components.switchbot.light.switchbot.SwitchbotCandleWarmerLamp",
|
||||
**{mock_method: AsyncMock(side_effect=exception)},
|
||||
update=AsyncMock(return_value=None),
|
||||
):
|
||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with pytest.raises(HomeAssistantError, match=error_message):
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
service,
|
||||
{**service_data, ATTR_ENTITY_ID: entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("service_info", "sensor_type"),
|
||||
[
|
||||
|
||||
@@ -20,7 +20,7 @@ from homeassistant.components.telegram_bot.const import (
|
||||
PARSER_PLAIN_TEXT,
|
||||
PLATFORM_BROADCAST,
|
||||
PLATFORM_WEBHOOKS,
|
||||
SECTION_ADDITIONAL_SETTINGS,
|
||||
SECTION_ADVANCED_SETTINGS,
|
||||
SUBENTRY_TYPE_ALLOWED_CHAT_IDS,
|
||||
)
|
||||
from homeassistant.components.telegram_bot.webhooks import TELEGRAM_WEBHOOK_URL
|
||||
@@ -100,7 +100,7 @@ async def test_reconfigure_flow_broadcast(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_PLATFORM: PLATFORM_BROADCAST,
|
||||
SECTION_ADDITIONAL_SETTINGS: {
|
||||
SECTION_ADVANCED_SETTINGS: {
|
||||
CONF_PROXY_URL: "invalid",
|
||||
},
|
||||
},
|
||||
@@ -117,7 +117,7 @@ async def test_reconfigure_flow_broadcast(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_PLATFORM: PLATFORM_BROADCAST,
|
||||
SECTION_ADDITIONAL_SETTINGS: {
|
||||
SECTION_ADVANCED_SETTINGS: {
|
||||
CONF_PROXY_URL: "https://test",
|
||||
},
|
||||
},
|
||||
@@ -155,7 +155,7 @@ async def test_reconfigure_flow_webhooks(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_PLATFORM: PLATFORM_WEBHOOKS,
|
||||
SECTION_ADDITIONAL_SETTINGS: {
|
||||
SECTION_ADVANCED_SETTINGS: {
|
||||
CONF_API_ENDPOINT: DEFAULT_API_ENDPOINT,
|
||||
CONF_PROXY_URL: "https://test",
|
||||
},
|
||||
@@ -271,7 +271,7 @@ async def test_reconfigure_flow_logout_failed(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_PLATFORM: PLATFORM_BROADCAST,
|
||||
SECTION_ADDITIONAL_SETTINGS: {
|
||||
SECTION_ADVANCED_SETTINGS: {
|
||||
CONF_API_ENDPOINT: "http://mock1",
|
||||
},
|
||||
},
|
||||
@@ -289,7 +289,7 @@ async def test_reconfigure_flow_logout_failed(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_PLATFORM: PLATFORM_BROADCAST,
|
||||
SECTION_ADDITIONAL_SETTINGS: {
|
||||
SECTION_ADVANCED_SETTINGS: {
|
||||
CONF_API_ENDPOINT: "http://mock2",
|
||||
},
|
||||
},
|
||||
@@ -327,7 +327,7 @@ async def test_create_entry(
|
||||
{
|
||||
CONF_PLATFORM: PLATFORM_WEBHOOKS,
|
||||
CONF_API_KEY: "mock api key",
|
||||
SECTION_ADDITIONAL_SETTINGS: {
|
||||
SECTION_ADVANCED_SETTINGS: {
|
||||
CONF_PROXY_URL: "invalid",
|
||||
},
|
||||
},
|
||||
@@ -350,7 +350,7 @@ async def test_create_entry(
|
||||
{
|
||||
CONF_PLATFORM: PLATFORM_WEBHOOKS,
|
||||
CONF_API_KEY: "mock api key",
|
||||
SECTION_ADDITIONAL_SETTINGS: {
|
||||
SECTION_ADVANCED_SETTINGS: {
|
||||
CONF_PROXY_URL: "https://proxy",
|
||||
},
|
||||
},
|
||||
@@ -374,7 +374,7 @@ async def test_create_entry(
|
||||
{
|
||||
CONF_PLATFORM: PLATFORM_WEBHOOKS,
|
||||
CONF_API_KEY: "mock api key",
|
||||
SECTION_ADDITIONAL_SETTINGS: {
|
||||
SECTION_ADVANCED_SETTINGS: {
|
||||
CONF_PROXY_URL: "https://proxy",
|
||||
},
|
||||
},
|
||||
@@ -446,7 +446,7 @@ async def test_create_webhook_entry(
|
||||
{
|
||||
CONF_PLATFORM: PLATFORM_WEBHOOKS,
|
||||
CONF_API_KEY: "mock api key",
|
||||
SECTION_ADDITIONAL_SETTINGS: {
|
||||
SECTION_ADVANCED_SETTINGS: {
|
||||
CONF_API_ENDPOINT: api_endpoint,
|
||||
},
|
||||
},
|
||||
@@ -774,7 +774,7 @@ async def test_duplicate_entry(hass: HomeAssistant) -> None:
|
||||
data = {
|
||||
CONF_PLATFORM: PLATFORM_BROADCAST,
|
||||
CONF_API_KEY: "mock api key",
|
||||
SECTION_ADDITIONAL_SETTINGS: {
|
||||
SECTION_ADVANCED_SETTINGS: {
|
||||
CONF_API_ENDPOINT: "http://mock_api_endpoint",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ from homeassistant.components.telegram_bot.const import (
|
||||
PARSER_MD2,
|
||||
PARSER_PLAIN_TEXT,
|
||||
PLATFORM_BROADCAST,
|
||||
SECTION_ADDITIONAL_SETTINGS,
|
||||
SECTION_ADVANCED_SETTINGS,
|
||||
SERVICE_ANSWER_CALLBACK_QUERY,
|
||||
SERVICE_DELETE_MESSAGE,
|
||||
SERVICE_EDIT_CAPTION,
|
||||
@@ -1093,7 +1093,7 @@ async def test_send_message_no_chat_id_error(
|
||||
data={
|
||||
CONF_PLATFORM: PLATFORM_BROADCAST,
|
||||
CONF_API_KEY: "mock api key",
|
||||
SECTION_ADDITIONAL_SETTINGS: {},
|
||||
SECTION_ADVANCED_SETTINGS: {},
|
||||
},
|
||||
options={ATTR_PARSER: PARSER_PLAIN_TEXT},
|
||||
)
|
||||
|
||||
@@ -2,19 +2,7 @@
|
||||
|
||||
import contextlib
|
||||
|
||||
from astroid import nodes
|
||||
from pylint.checkers import BaseChecker
|
||||
from pylint.testutils.unittest_linter import UnittestLinter
|
||||
from pylint.utils.ast_walker import ASTWalker
|
||||
|
||||
|
||||
def walk_checker(
|
||||
linter: UnittestLinter, checker: BaseChecker, node: nodes.NodeNG
|
||||
) -> None:
|
||||
"""Run the given checker over the parsed node."""
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(checker)
|
||||
walker.walk(node)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
|
||||
import astroid
|
||||
from pylint.testutils import UnittestLinter
|
||||
from pylint.utils.ast_walker import ASTWalker
|
||||
from pylint_home_assistant.checkers.actions.service_registration import (
|
||||
ServiceRegistrationChecker,
|
||||
)
|
||||
import pytest
|
||||
|
||||
from tests.pylint import assert_no_messages, walk_checker
|
||||
from tests.pylint import assert_no_messages
|
||||
|
||||
|
||||
@pytest.fixture(name="registration_checker")
|
||||
@@ -67,9 +68,11 @@ def test_no_warning(
|
||||
) -> None:
|
||||
"""Test cases that should not trigger a warning."""
|
||||
root_node = astroid.parse(code, module_name)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(registration_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, registration_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_hass_services_register_flagged(
|
||||
@@ -84,7 +87,9 @@ async def async_setup_entry(hass, entry):
|
||||
""",
|
||||
"homeassistant.components.test_integration",
|
||||
)
|
||||
walk_checker(linter, registration_checker, root_node)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(registration_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
@@ -103,7 +108,9 @@ async def async_setup_entry(hass, entry):
|
||||
""",
|
||||
"homeassistant.components.test_integration",
|
||||
)
|
||||
walk_checker(linter, registration_checker, root_node)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(registration_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
@@ -121,7 +128,9 @@ async def async_setup_entry(hass, entry):
|
||||
""",
|
||||
"homeassistant.components.test_integration",
|
||||
)
|
||||
walk_checker(linter, registration_checker, root_node)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(registration_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
@@ -139,7 +148,9 @@ async def async_setup_entry(hass, entry):
|
||||
""",
|
||||
"homeassistant.components.test_integration",
|
||||
)
|
||||
walk_checker(linter, registration_checker, root_node)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(registration_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
@@ -159,7 +170,9 @@ async def async_setup_entry(hass, entry):
|
||||
""",
|
||||
"homeassistant.components.test_integration",
|
||||
)
|
||||
walk_checker(linter, registration_checker, root_node)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(registration_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 3
|
||||
@@ -180,7 +193,9 @@ async def async_setup_entry(hass, entry):
|
||||
""",
|
||||
"homeassistant.components.test_integration",
|
||||
)
|
||||
walk_checker(linter, registration_checker, root_node)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(registration_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
@@ -204,6 +219,8 @@ async def async_setup_entry(hass, entry):
|
||||
""",
|
||||
"homeassistant.components.test_integration",
|
||||
)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(registration_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, registration_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
|
||||
import astroid
|
||||
from pylint.testutils import UnittestLinter
|
||||
from pylint.utils.ast_walker import ASTWalker
|
||||
from pylint_home_assistant.checkers.actions.swallowed_exceptions import (
|
||||
SwallowedActionExceptionsChecker,
|
||||
)
|
||||
import pytest
|
||||
|
||||
from tests.pylint import assert_no_messages, walk_checker
|
||||
from tests.pylint import assert_no_messages
|
||||
|
||||
|
||||
@pytest.fixture(name="error_propagation_checker")
|
||||
@@ -128,9 +129,11 @@ def test_no_warning(
|
||||
) -> None:
|
||||
"""Test cases that should not trigger a warning."""
|
||||
root_node = astroid.parse(code, "homeassistant.components.test_integration.switch")
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(error_propagation_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, error_propagation_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_log_only_flagged(
|
||||
@@ -149,7 +152,9 @@ class MySwitch(SwitchEntity):
|
||||
""",
|
||||
"homeassistant.components.test_integration.switch",
|
||||
)
|
||||
walk_checker(linter, error_propagation_checker, root_node)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(error_propagation_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
@@ -174,7 +179,9 @@ class MySwitch(SwitchEntity):
|
||||
""",
|
||||
"homeassistant.components.test_integration.switch",
|
||||
)
|
||||
walk_checker(linter, error_propagation_checker, root_node)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(error_propagation_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
@@ -196,7 +203,9 @@ class MySwitch(SwitchEntity):
|
||||
""",
|
||||
"homeassistant.components.test_integration.switch",
|
||||
)
|
||||
walk_checker(linter, error_propagation_checker, root_node)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(error_propagation_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
@@ -220,7 +229,9 @@ class MySwitch(SwitchEntity):
|
||||
""",
|
||||
"homeassistant.components.test_integration.switch",
|
||||
)
|
||||
walk_checker(linter, error_propagation_checker, root_node)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(error_propagation_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
@@ -244,7 +255,9 @@ class MySwitch(SwitchEntity):
|
||||
""",
|
||||
"homeassistant.components.test_integration.switch",
|
||||
)
|
||||
walk_checker(linter, error_propagation_checker, root_node)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(error_propagation_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
@@ -267,7 +280,9 @@ class MySwitch(SwitchEntity):
|
||||
""",
|
||||
"homeassistant.components.test_integration.switch",
|
||||
)
|
||||
walk_checker(linter, error_propagation_checker, root_node)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(error_propagation_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
@@ -289,7 +304,9 @@ class MySwitch(SwitchEntity):
|
||||
""",
|
||||
"homeassistant.components.test_integration.switch",
|
||||
)
|
||||
walk_checker(linter, error_propagation_checker, root_node)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(error_propagation_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
@@ -317,7 +334,9 @@ class MySwitch(SwitchEntity):
|
||||
""",
|
||||
"homeassistant.components.test_integration.switch",
|
||||
)
|
||||
walk_checker(linter, error_propagation_checker, root_node)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(error_propagation_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 2
|
||||
@@ -337,7 +356,9 @@ class MySwitch(SwitchEntity):
|
||||
""",
|
||||
"homeassistant.components.test_integration.switch",
|
||||
)
|
||||
walk_checker(linter, error_propagation_checker, root_node)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(error_propagation_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
@@ -358,9 +379,11 @@ class MySwitch(SwitchEntity):
|
||||
""",
|
||||
"homeassistant.components.test_integration.switch",
|
||||
)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(error_propagation_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, error_propagation_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_decorator_swallows_flagged(
|
||||
@@ -385,7 +408,9 @@ class MySwitch(SwitchEntity):
|
||||
""",
|
||||
"homeassistant.components.test_integration.switch",
|
||||
)
|
||||
walk_checker(linter, error_propagation_checker, root_node)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(error_propagation_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
@@ -416,7 +441,9 @@ class MySwitch(SwitchEntity):
|
||||
""",
|
||||
"homeassistant.components.test_integration.switch",
|
||||
)
|
||||
walk_checker(linter, error_propagation_checker, root_node)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(error_propagation_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
@@ -444,9 +471,11 @@ class MySwitch(SwitchEntity):
|
||||
""",
|
||||
"homeassistant.components.test_integration.switch",
|
||||
)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(error_propagation_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, error_propagation_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_custom_service_method_flagged(
|
||||
@@ -473,7 +502,9 @@ class MyFan(FanEntity):
|
||||
""",
|
||||
"homeassistant.components.test_integration.fan",
|
||||
)
|
||||
walk_checker(linter, error_propagation_checker, root_node)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(error_propagation_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
@@ -504,9 +535,11 @@ class MyFan(FanEntity):
|
||||
""",
|
||||
"homeassistant.components.test_integration.fan",
|
||||
)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(error_propagation_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, error_propagation_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_unregistered_custom_method_ignored(
|
||||
@@ -525,9 +558,11 @@ class MyFan(FanEntity):
|
||||
""",
|
||||
"homeassistant.components.test_integration.fan",
|
||||
)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(error_propagation_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, error_propagation_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_standalone_service_handler_flagged(
|
||||
@@ -548,7 +583,9 @@ async def _handle_do_thing(call):
|
||||
""",
|
||||
"homeassistant.components.test_integration.services",
|
||||
)
|
||||
walk_checker(linter, error_propagation_checker, root_node)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(error_propagation_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
@@ -573,7 +610,9 @@ async def _handle_reset(call):
|
||||
""",
|
||||
"homeassistant.components.test_integration.services",
|
||||
)
|
||||
walk_checker(linter, error_propagation_checker, root_node)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(error_propagation_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
@@ -598,9 +637,11 @@ async def _handle_do_thing(call):
|
||||
""",
|
||||
"homeassistant.components.test_integration.services",
|
||||
)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(error_propagation_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, error_propagation_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_not_integration_module_ignored(
|
||||
@@ -619,6 +660,8 @@ class MySwitch(SwitchEntity):
|
||||
""",
|
||||
"tests.components.test_integration.test_switch",
|
||||
)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(error_propagation_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, error_propagation_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
@@ -8,9 +8,10 @@ from pathlib import Path
|
||||
import astroid
|
||||
from pylint.checkers import BaseChecker
|
||||
from pylint.testutils.unittest_linter import UnittestLinter
|
||||
from pylint.utils.ast_walker import ASTWalker
|
||||
import pytest
|
||||
|
||||
from tests.pylint import assert_no_messages, walk_checker
|
||||
from tests.pylint import assert_no_messages
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -61,9 +62,11 @@ def test_enforce_config_flow_no_name(
|
||||
) -> None:
|
||||
"""Good test cases."""
|
||||
root_node = astroid.parse(code, module_name)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(enforce_config_flow_no_name_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, enforce_config_flow_no_name_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -107,8 +110,10 @@ def test_enforce_config_flow_no_name_bad(
|
||||
) -> None:
|
||||
"""Bad test cases."""
|
||||
root_node = astroid.parse(code, module_name)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(enforce_config_flow_no_name_checker)
|
||||
|
||||
walk_checker(linter, enforce_config_flow_no_name_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
assert messages[0].msg_id == "home-assistant-config-flow-name-field"
|
||||
@@ -129,9 +134,11 @@ def test_enforce_config_flow_no_name_subentry_flow(
|
||||
)
|
||||
"""
|
||||
root_node = astroid.parse(code, "homeassistant.components.test.config_flow")
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(enforce_config_flow_no_name_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, enforce_config_flow_no_name_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_enforce_config_flow_no_name_helper_integration(
|
||||
@@ -153,8 +160,11 @@ def test_enforce_config_flow_no_name_helper_integration(
|
||||
root_node = astroid.parse(code, "homeassistant.components.my_helper.config_flow")
|
||||
root_node.file = str(integration_dir / "config_flow.py")
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(enforce_config_flow_no_name_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, enforce_config_flow_no_name_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_enforce_config_flow_no_name_non_helper_integration(
|
||||
@@ -174,7 +184,10 @@ def test_enforce_config_flow_no_name_non_helper_integration(
|
||||
root_node = astroid.parse(code, "homeassistant.components.my_device.config_flow")
|
||||
root_node.file = str(integration_dir / "config_flow.py")
|
||||
|
||||
walk_checker(linter, enforce_config_flow_no_name_checker, root_node)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(enforce_config_flow_no_name_checker)
|
||||
|
||||
walker.walk(root_node)
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
assert messages[0].msg_id == "home-assistant-config-flow-name-field"
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
import astroid
|
||||
from pylint.checkers import BaseChecker
|
||||
from pylint.testutils.unittest_linter import UnittestLinter
|
||||
from pylint.utils.ast_walker import ASTWalker
|
||||
import pytest
|
||||
|
||||
from tests.pylint import assert_no_messages, walk_checker
|
||||
from tests.pylint import assert_no_messages
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -70,9 +71,11 @@ def test_enforce_config_flow_no_polling(
|
||||
) -> None:
|
||||
"""Good test cases."""
|
||||
root_node = astroid.parse(code, module_name)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(enforce_config_flow_no_polling_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, enforce_config_flow_no_polling_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -130,8 +133,10 @@ def test_enforce_config_flow_no_polling_bad(
|
||||
) -> None:
|
||||
"""Bad test cases."""
|
||||
root_node = astroid.parse(code, module_name)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(enforce_config_flow_no_polling_checker)
|
||||
|
||||
walk_checker(linter, enforce_config_flow_no_polling_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
assert messages[0].msg_id == "home-assistant-config-flow-polling-field"
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
import astroid
|
||||
from pylint.checkers import BaseChecker
|
||||
from pylint.testutils.unittest_linter import UnittestLinter
|
||||
from pylint.utils.ast_walker import ASTWalker
|
||||
import pytest
|
||||
|
||||
from tests.pylint import assert_no_messages, walk_checker
|
||||
from tests.pylint import assert_no_messages
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -63,9 +64,11 @@ def test_enforce_unique_id_no_ip(
|
||||
) -> None:
|
||||
"""Good test cases."""
|
||||
root_node = astroid.parse(code, module_name)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(enforce_config_entry_unique_id_no_ip_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, enforce_config_entry_unique_id_no_ip_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -118,8 +121,10 @@ def test_enforce_unique_id_no_ip_bad_call(
|
||||
) -> None:
|
||||
"""Bad async_set_unique_id call test cases."""
|
||||
root_node = astroid.parse(code, module_name)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(enforce_config_entry_unique_id_no_ip_checker)
|
||||
|
||||
walk_checker(linter, enforce_config_entry_unique_id_no_ip_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
assert messages[0].msg_id == "home-assistant-unique-id-ip-based"
|
||||
@@ -177,8 +182,10 @@ def test_enforce_unique_id_no_ip_bad_call_variable(
|
||||
) -> None:
|
||||
"""Bad async_set_unique_id call test cases."""
|
||||
root_node = astroid.parse(code, module_name)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(enforce_config_entry_unique_id_no_ip_checker)
|
||||
|
||||
walk_checker(linter, enforce_config_entry_unique_id_no_ip_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
assert messages[0].msg_id == "home-assistant-unique-id-ip-based"
|
||||
|
||||
@@ -4,6 +4,7 @@ from pathlib import Path
|
||||
|
||||
import astroid
|
||||
from pylint.testutils import MessageTest, UnittestLinter
|
||||
from pylint.utils.ast_walker import ASTWalker
|
||||
from pylint_home_assistant.checkers.quality_scale.config_entry_unloading import (
|
||||
ConfigEntryUnloadingChecker,
|
||||
)
|
||||
@@ -11,7 +12,7 @@ from pylint_home_assistant.helpers.quality_scale import clear_quality_scale_cach
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
from tests.pylint import assert_adds_messages, assert_no_messages, walk_checker
|
||||
from tests.pylint import assert_adds_messages, assert_no_messages
|
||||
|
||||
|
||||
@pytest.fixture(name="unloading_checker")
|
||||
@@ -55,8 +56,11 @@ async def async_unload_entry(hass, entry):
|
||||
)
|
||||
root_node.file = str(integration_dir / "__init__.py")
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(unloading_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, unloading_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_unload_entry_missing_fires(
|
||||
@@ -77,6 +81,9 @@ async def async_setup_entry(hass, entry):
|
||||
)
|
||||
root_node.file = str(integration_dir / "__init__.py")
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(unloading_checker)
|
||||
|
||||
with assert_adds_messages(
|
||||
linter,
|
||||
MessageTest(
|
||||
@@ -86,7 +93,7 @@ async def async_setup_entry(hass, entry):
|
||||
col_offset=0,
|
||||
),
|
||||
):
|
||||
walk_checker(linter, unloading_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -139,5 +146,8 @@ async def async_setup_entry(hass, entry):
|
||||
)
|
||||
root_node.file = str(integration_dir / "__init__.py")
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(unloading_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, unloading_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
@@ -4,12 +4,13 @@ from pathlib import Path
|
||||
|
||||
import astroid
|
||||
from pylint.testutils import MessageTest, UnittestLinter
|
||||
from pylint.utils.ast_walker import ASTWalker
|
||||
from pylint_home_assistant.checkers.quality_scale.diagnostics import DiagnosticsChecker
|
||||
from pylint_home_assistant.helpers.quality_scale import clear_quality_scale_cache
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
from tests.pylint import assert_adds_messages, assert_no_messages, walk_checker
|
||||
from tests.pylint import assert_adds_messages, assert_no_messages
|
||||
|
||||
|
||||
@pytest.fixture(name="diagnostics_checker")
|
||||
@@ -74,8 +75,11 @@ def test_diagnostics_present(
|
||||
root_node = astroid.parse(code, "homeassistant.components.test_int.diagnostics")
|
||||
root_node.file = str(integration_dir / "diagnostics.py")
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(diagnostics_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, diagnostics_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_diagnostics_missing_fires(
|
||||
@@ -96,6 +100,9 @@ async def async_setup(hass, config):
|
||||
)
|
||||
root_node.file = str(integration_dir / "diagnostics.py")
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(diagnostics_checker)
|
||||
|
||||
with assert_adds_messages(
|
||||
linter,
|
||||
MessageTest(
|
||||
@@ -105,7 +112,7 @@ async def async_setup(hass, config):
|
||||
col_offset=0,
|
||||
),
|
||||
):
|
||||
walk_checker(linter, diagnostics_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -158,5 +165,8 @@ async def async_setup(hass, config):
|
||||
)
|
||||
root_node.file = str(integration_dir / "diagnostics.py")
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(diagnostics_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, diagnostics_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
@@ -6,6 +6,7 @@ from pathlib import Path
|
||||
import astroid
|
||||
from astroid import nodes
|
||||
from pylint.testutils import MessageTest, UnittestLinter
|
||||
from pylint.utils.ast_walker import ASTWalker
|
||||
from pylint_home_assistant.checkers.quality_scale.entity_unique_id import (
|
||||
EntityUniqueIdChecker,
|
||||
)
|
||||
@@ -14,7 +15,7 @@ from pylint_home_assistant.helpers.quality_scale import clear_quality_scale_cach
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
from tests.pylint import assert_adds_messages, assert_no_messages, walk_checker
|
||||
from tests.pylint import assert_adds_messages, assert_no_messages
|
||||
|
||||
|
||||
@pytest.fixture(name="entity_unique_id_checker")
|
||||
@@ -257,8 +258,10 @@ def test_handled(
|
||||
_create_quality_scale(integration_dir, {"entity-unique-id": "done"})
|
||||
|
||||
root_node = _parse(code, integration_dir)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(entity_unique_id_checker)
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, entity_unique_id_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -348,8 +351,10 @@ def test_ancestor_satisfies_rule(
|
||||
astroid.parse(ancestor_code, "homeassistant.components.test_integration.eui_entity")
|
||||
root_node = _parse(sensor_code, integration_dir)
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(entity_unique_id_checker)
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, entity_unique_id_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_missing_fires(
|
||||
@@ -372,8 +377,10 @@ class MySensor(Entity):
|
||||
)
|
||||
|
||||
class_node = next(root_node.nodes_of_class(nodes.ClassDef))
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(entity_unique_id_checker)
|
||||
with assert_adds_messages(linter, _expect_missing(class_node)):
|
||||
walk_checker(linter, entity_unique_id_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -437,8 +444,10 @@ def test_conditional_self_assignment_fires(
|
||||
for cls in root_node.nodes_of_class(nodes.ClassDef)
|
||||
if cls.name == class_name
|
||||
)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(entity_unique_id_checker)
|
||||
with assert_adds_messages(linter, _expect_missing(class_node)):
|
||||
walk_checker(linter, entity_unique_id_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_explicit_none_class_body_fires(
|
||||
@@ -461,8 +470,10 @@ class MySensor(Entity):
|
||||
)
|
||||
|
||||
class_node = next(root_node.nodes_of_class(nodes.ClassDef))
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(entity_unique_id_checker)
|
||||
with assert_adds_messages(linter, _expect_missing(class_node)):
|
||||
walk_checker(linter, entity_unique_id_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_subclass_nullifies_ancestor_value(
|
||||
@@ -499,8 +510,10 @@ class MySensor(TestIntegrationBaseEntity):
|
||||
for cls in root_node.nodes_of_class(nodes.ClassDef)
|
||||
if cls.name == "MySensor"
|
||||
)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(entity_unique_id_checker)
|
||||
with assert_adds_messages(linter, _expect_missing(class_node)):
|
||||
walk_checker(linter, entity_unique_id_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_explicit_none_self_assign_fires(
|
||||
@@ -524,8 +537,10 @@ class MySensor(Entity):
|
||||
)
|
||||
|
||||
class_node = next(root_node.nodes_of_class(nodes.ClassDef))
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(entity_unique_id_checker)
|
||||
with assert_adds_messages(linter, _expect_missing(class_node)):
|
||||
walk_checker(linter, entity_unique_id_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_bare_annotation_only_fires(
|
||||
@@ -548,8 +563,10 @@ class MySensor(Entity):
|
||||
)
|
||||
|
||||
class_node = next(root_node.nodes_of_class(nodes.ClassDef))
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(entity_unique_id_checker)
|
||||
with assert_adds_messages(linter, _expect_missing(class_node)):
|
||||
walk_checker(linter, entity_unique_id_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_entity_default_does_not_satisfy(
|
||||
@@ -579,8 +596,10 @@ class MySensor(Entity):
|
||||
)
|
||||
|
||||
class_node = next(root_node.nodes_of_class(nodes.ClassDef))
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(entity_unique_id_checker)
|
||||
with assert_adds_messages(linter, _expect_missing(class_node)):
|
||||
walk_checker(linter, entity_unique_id_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -619,8 +638,10 @@ def test_class_not_subject_to_rule(
|
||||
|
||||
root_node = _parse(code, integration_dir)
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(entity_unique_id_checker)
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, entity_unique_id_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_leaf_class_still_fires(
|
||||
@@ -650,8 +671,10 @@ class SomethingUnrelated:
|
||||
for cls in root_node.nodes_of_class(nodes.ClassDef)
|
||||
if cls.name == "LonelySensor"
|
||||
)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(entity_unique_id_checker)
|
||||
with assert_adds_messages(linter, _expect_missing(class_node)):
|
||||
walk_checker(linter, entity_unique_id_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_dict_status_done_fires(
|
||||
@@ -677,8 +700,10 @@ class MySensor(Entity):
|
||||
)
|
||||
|
||||
class_node = next(root_node.nodes_of_class(nodes.ClassDef))
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(entity_unique_id_checker)
|
||||
with assert_adds_messages(linter, _expect_missing(class_node)):
|
||||
walk_checker(linter, entity_unique_id_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -748,8 +773,10 @@ class MySensor(Entity):
|
||||
"""
|
||||
root_node = _parse(code, integration_dir, module_name, file_name)
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(entity_unique_id_checker)
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, entity_unique_id_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def _find_attr_value_node(
|
||||
@@ -821,8 +848,10 @@ def test_static_class_body_string_in_multi_entry_fires(
|
||||
|
||||
root_node = _parse(code, integration_dir)
|
||||
value_node = _find_attr_value_node(root_node)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(entity_unique_id_checker)
|
||||
with assert_adds_messages(linter, _expect_static(value_node, "MySensor")):
|
||||
walk_checker(linter, entity_unique_id_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_static_class_body_string_no_manifest_fires(
|
||||
@@ -846,8 +875,10 @@ class MySensor(Entity):
|
||||
)
|
||||
|
||||
value_node = _find_attr_value_node(root_node)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(entity_unique_id_checker)
|
||||
with assert_adds_messages(linter, _expect_static(value_node, "MySensor")):
|
||||
walk_checker(linter, entity_unique_id_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
_STATIC_CLASS_BODY_STRING = """
|
||||
@@ -915,5 +946,7 @@ def test_static_rule_does_not_warn(
|
||||
_create_quality_scale(integration_dir, {"entity-unique-id": rule_status})
|
||||
|
||||
root_node = _parse(code, integration_dir)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(entity_unique_id_checker)
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, entity_unique_id_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
@@ -5,6 +5,7 @@ from pathlib import Path
|
||||
import astroid
|
||||
from astroid import nodes
|
||||
from pylint.testutils import MessageTest, UnittestLinter
|
||||
from pylint.utils.ast_walker import ASTWalker
|
||||
from pylint_home_assistant.checkers.quality_scale.has_entity_name import (
|
||||
HasEntityNameChecker,
|
||||
)
|
||||
@@ -12,7 +13,7 @@ from pylint_home_assistant.helpers.quality_scale import clear_quality_scale_cach
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
from tests.pylint import assert_adds_messages, assert_no_messages, walk_checker
|
||||
from tests.pylint import assert_adds_messages, assert_no_messages
|
||||
|
||||
|
||||
@pytest.fixture(name="has_entity_name_checker")
|
||||
@@ -144,8 +145,10 @@ def test_handled(
|
||||
_create_quality_scale(integration_dir, {"has-entity-name": "done"})
|
||||
|
||||
root_node = _parse(code, integration_dir)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(has_entity_name_checker)
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, has_entity_name_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_ancestor_class_level(
|
||||
@@ -177,8 +180,10 @@ class MySensor(TestIntegrationBaseEntity):
|
||||
integration_dir,
|
||||
)
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(has_entity_name_checker)
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, has_entity_name_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_ancestor_self_assign(
|
||||
@@ -211,8 +216,10 @@ class MySensor(TestIntegrationBaseEntity):
|
||||
integration_dir,
|
||||
)
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(has_entity_name_checker)
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, has_entity_name_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_missing_fires(
|
||||
@@ -235,8 +242,10 @@ class MySensor(Entity):
|
||||
)
|
||||
|
||||
class_node = next(root_node.nodes_of_class(nodes.ClassDef))
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(has_entity_name_checker)
|
||||
with assert_adds_messages(linter, _expect_missing(class_node)):
|
||||
walk_checker(linter, has_entity_name_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -324,8 +333,10 @@ def test_conditional_self_assignment_fires(
|
||||
for cls in root_node.nodes_of_class(nodes.ClassDef)
|
||||
if cls.name == class_name
|
||||
)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(has_entity_name_checker)
|
||||
with assert_adds_messages(linter, _expect_missing(class_node)):
|
||||
walk_checker(linter, has_entity_name_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_explicit_false_fires(
|
||||
@@ -348,8 +359,10 @@ class MySensor(Entity):
|
||||
)
|
||||
|
||||
class_node = next(root_node.nodes_of_class(nodes.ClassDef))
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(has_entity_name_checker)
|
||||
with assert_adds_messages(linter, _expect_missing(class_node)):
|
||||
walk_checker(linter, has_entity_name_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_generic_subscript_base_sets_flag(
|
||||
@@ -381,8 +394,10 @@ class MySensor(TestIntegrationGenericBase[int]):
|
||||
integration_dir,
|
||||
)
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(has_entity_name_checker)
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, has_entity_name_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_two_level_subscript_chain(
|
||||
@@ -423,8 +438,10 @@ class MyLight(TestIntegrationLightBase):
|
||||
file_name="light.py",
|
||||
)
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(has_entity_name_checker)
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, has_entity_name_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_entity_description_fallback(
|
||||
@@ -454,8 +471,10 @@ class MyEntity(Entity):
|
||||
integration_dir,
|
||||
)
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(has_entity_name_checker)
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, has_entity_name_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_entity_description_subscripted_annotation(
|
||||
@@ -485,8 +504,10 @@ class MyEntity[T](Entity):
|
||||
integration_dir,
|
||||
)
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(has_entity_name_checker)
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, has_entity_name_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_entity_description_without_flag_still_fires(
|
||||
@@ -521,8 +542,10 @@ class MyEntity(Entity):
|
||||
for cls in root_node.nodes_of_class(nodes.ClassDef)
|
||||
if cls.name == "MyEntity"
|
||||
)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(has_entity_name_checker)
|
||||
with assert_adds_messages(linter, _expect_missing(class_node)):
|
||||
walk_checker(linter, has_entity_name_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_entity_description_set_in_ancestor(
|
||||
@@ -562,8 +585,10 @@ class MySensor(TestIntegrationBaseEntity):
|
||||
integration_dir,
|
||||
)
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(has_entity_name_checker)
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, has_entity_name_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_mixin_subclassed_in_same_module_ignored(
|
||||
@@ -588,8 +613,10 @@ class ActualEntity(MyClimateMixin):
|
||||
integration_dir,
|
||||
)
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(has_entity_name_checker)
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, has_entity_name_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_subclassed_via_subscript_ignored(
|
||||
@@ -614,8 +641,10 @@ class ConcreteEntity(GenericBase[int]):
|
||||
integration_dir,
|
||||
)
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(has_entity_name_checker)
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, has_entity_name_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_leaf_class_still_fires(
|
||||
@@ -645,8 +674,10 @@ class SomethingUnrelated:
|
||||
for cls in root_node.nodes_of_class(nodes.ClassDef)
|
||||
if cls.name == "LonelySensor"
|
||||
)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(has_entity_name_checker)
|
||||
with assert_adds_messages(linter, _expect_missing(class_node)):
|
||||
walk_checker(linter, has_entity_name_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_non_entity_class_ignored(
|
||||
@@ -666,8 +697,10 @@ class NotAnEntity:
|
||||
integration_dir,
|
||||
)
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(has_entity_name_checker)
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, has_entity_name_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_dict_status_done_fires(
|
||||
@@ -693,8 +726,10 @@ class MySensor(Entity):
|
||||
)
|
||||
|
||||
class_node = next(root_node.nodes_of_class(nodes.ClassDef))
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(has_entity_name_checker)
|
||||
with assert_adds_messages(linter, _expect_missing(class_node)):
|
||||
walk_checker(linter, has_entity_name_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -764,5 +799,7 @@ class MySensor(Entity):
|
||||
"""
|
||||
root_node = _parse(code, integration_dir, module_name, file_name)
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(has_entity_name_checker)
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, has_entity_name_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
@@ -4,6 +4,7 @@ from pathlib import Path
|
||||
|
||||
import astroid
|
||||
from pylint.testutils import MessageTest, UnittestLinter
|
||||
from pylint.utils.ast_walker import ASTWalker
|
||||
from pylint_home_assistant.checkers.quality_scale.parallel_updates import (
|
||||
ParallelUpdatesChecker,
|
||||
)
|
||||
@@ -11,7 +12,7 @@ from pylint_home_assistant.helpers.quality_scale import clear_quality_scale_cach
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
from tests.pylint import assert_adds_messages, assert_no_messages, walk_checker
|
||||
from tests.pylint import assert_adds_messages, assert_no_messages
|
||||
|
||||
|
||||
@pytest.fixture(name="parallel_updates_checker")
|
||||
@@ -60,8 +61,11 @@ def test_parallel_updates_present(
|
||||
root_node = astroid.parse("PARALLEL_UPDATES = 1\n", module_name)
|
||||
root_node.file = str(integration_dir / "sensor.py")
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(parallel_updates_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, parallel_updates_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_parallel_updates_zero(
|
||||
@@ -78,8 +82,11 @@ def test_parallel_updates_zero(
|
||||
)
|
||||
root_node.file = str(integration_dir / "sensor.py")
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(parallel_updates_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, parallel_updates_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_parallel_updates_annotated_assignment(
|
||||
@@ -97,8 +104,11 @@ def test_parallel_updates_annotated_assignment(
|
||||
)
|
||||
root_node.file = str(integration_dir / "sensor.py")
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(parallel_updates_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, parallel_updates_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_parallel_updates_missing_fires(
|
||||
@@ -116,6 +126,9 @@ def test_parallel_updates_missing_fires(
|
||||
)
|
||||
root_node.file = str(integration_dir / "sensor.py")
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(parallel_updates_checker)
|
||||
|
||||
with assert_adds_messages(
|
||||
linter,
|
||||
MessageTest(
|
||||
@@ -125,7 +138,7 @@ def test_parallel_updates_missing_fires(
|
||||
col_offset=0,
|
||||
),
|
||||
):
|
||||
walk_checker(linter, parallel_updates_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_parallel_updates_missing_status_done_dict(
|
||||
@@ -146,6 +159,9 @@ def test_parallel_updates_missing_status_done_dict(
|
||||
)
|
||||
root_node.file = str(integration_dir / "sensor.py")
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(parallel_updates_checker)
|
||||
|
||||
with assert_adds_messages(
|
||||
linter,
|
||||
MessageTest(
|
||||
@@ -155,7 +171,7 @@ def test_parallel_updates_missing_status_done_dict(
|
||||
col_offset=0,
|
||||
),
|
||||
):
|
||||
walk_checker(linter, parallel_updates_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -215,5 +231,8 @@ def test_parallel_updates_not_fired(
|
||||
)
|
||||
root_node.file = str(integration_dir / "sensor.py")
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(parallel_updates_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, parallel_updates_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
@@ -4,6 +4,7 @@ from pathlib import Path
|
||||
|
||||
import astroid
|
||||
from pylint.testutils import MessageTest, UnittestLinter
|
||||
from pylint.utils.ast_walker import ASTWalker
|
||||
from pylint_home_assistant.checkers.quality_scale.reauthentication_flow import (
|
||||
ReauthenticationFlowChecker,
|
||||
)
|
||||
@@ -11,7 +12,7 @@ from pylint_home_assistant.helpers.quality_scale import clear_quality_scale_cach
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
from tests.pylint import assert_adds_messages, assert_no_messages, walk_checker
|
||||
from tests.pylint import assert_adds_messages, assert_no_messages
|
||||
|
||||
|
||||
@pytest.fixture(name="reauth_checker")
|
||||
@@ -53,8 +54,11 @@ class MyConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
root_node.file = str(integration_dir / "config_flow.py")
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(reauth_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, reauth_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_reauth_missing_fires(
|
||||
@@ -76,6 +80,9 @@ class MyConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
root_node.file = str(integration_dir / "config_flow.py")
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(reauth_checker)
|
||||
|
||||
with assert_adds_messages(
|
||||
linter,
|
||||
MessageTest(
|
||||
@@ -85,7 +92,7 @@ class MyConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
col_offset=0,
|
||||
),
|
||||
):
|
||||
walk_checker(linter, reauth_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -139,5 +146,8 @@ class MyConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
root_node.file = str(integration_dir / "config_flow.py")
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(reauth_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, reauth_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
@@ -5,9 +5,10 @@ from pylint.checkers import BaseChecker
|
||||
from pylint.interfaces import UNDEFINED
|
||||
from pylint.testutils import MessageTest
|
||||
from pylint.testutils.unittest_linter import UnittestLinter
|
||||
from pylint.utils.ast_walker import ASTWalker
|
||||
import pytest
|
||||
|
||||
from . import assert_adds_messages, assert_no_messages, walk_checker
|
||||
from . import assert_adds_messages, assert_no_messages
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -53,9 +54,11 @@ def test_enforce_class_module_good(
|
||||
) -> None:
|
||||
"""Good test cases."""
|
||||
root_node = astroid.parse(code, path)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(enforce_class_module_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, enforce_class_module_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -87,9 +90,11 @@ def test_enforce_class_platform_good(
|
||||
pass
|
||||
"""
|
||||
root_node = astroid.parse(code, path)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(enforce_class_module_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, enforce_class_module_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -123,6 +128,8 @@ def test_enforce_class_module_bad_simple(
|
||||
""",
|
||||
path,
|
||||
)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(enforce_class_module_checker)
|
||||
|
||||
with assert_adds_messages(
|
||||
linter,
|
||||
@@ -147,7 +154,7 @@ def test_enforce_class_module_bad_simple(
|
||||
end_col_offset=35,
|
||||
),
|
||||
):
|
||||
walk_checker(linter, enforce_class_module_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -178,6 +185,8 @@ def test_enforce_class_module_bad_nested(
|
||||
""",
|
||||
path,
|
||||
)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(enforce_class_module_checker)
|
||||
|
||||
with assert_adds_messages(
|
||||
linter,
|
||||
@@ -202,7 +211,7 @@ def test_enforce_class_module_bad_nested(
|
||||
end_col_offset=21,
|
||||
),
|
||||
):
|
||||
walk_checker(linter, enforce_class_module_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -227,9 +236,11 @@ def test_enforce_entity_good(
|
||||
pass
|
||||
"""
|
||||
root_node = astroid.parse(code, path)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(enforce_class_module_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, enforce_class_module_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -254,6 +265,8 @@ def test_enforce_entity_bad(
|
||||
pass
|
||||
"""
|
||||
root_node = astroid.parse(code, path)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(enforce_class_module_checker)
|
||||
|
||||
with assert_adds_messages(
|
||||
linter,
|
||||
@@ -268,4 +281,4 @@ def test_enforce_entity_bad(
|
||||
end_col_offset=18,
|
||||
),
|
||||
):
|
||||
walk_checker(linter, enforce_class_module_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
@@ -5,9 +5,10 @@ from pylint.checkers import BaseChecker
|
||||
from pylint.interfaces import UNDEFINED
|
||||
from pylint.testutils import MessageTest
|
||||
from pylint.testutils.unittest_linter import UnittestLinter
|
||||
from pylint.utils.ast_walker import ASTWalker
|
||||
import pytest
|
||||
|
||||
from . import assert_adds_messages, assert_no_messages, walk_checker
|
||||
from . import assert_adds_messages, assert_no_messages
|
||||
|
||||
|
||||
def test_good_callback(linter: UnittestLinter, decorator_checker: BaseChecker) -> None:
|
||||
@@ -23,9 +24,11 @@ def test_good_callback(linter: UnittestLinter, decorator_checker: BaseChecker) -
|
||||
"""
|
||||
|
||||
root_node = astroid.parse(code)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(decorator_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, decorator_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_bad_callback(linter: UnittestLinter, decorator_checker: BaseChecker) -> None:
|
||||
@@ -41,6 +44,8 @@ def test_bad_callback(linter: UnittestLinter, decorator_checker: BaseChecker) ->
|
||||
"""
|
||||
|
||||
root_node = astroid.parse(code)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(decorator_checker)
|
||||
|
||||
with assert_adds_messages(
|
||||
linter,
|
||||
@@ -55,7 +60,7 @@ def test_bad_callback(linter: UnittestLinter, decorator_checker: BaseChecker) ->
|
||||
end_col_offset=15,
|
||||
),
|
||||
):
|
||||
walk_checker(linter, decorator_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -105,9 +110,11 @@ def test_good_fixture(
|
||||
"""
|
||||
|
||||
root_node = astroid.parse(code, path)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(decorator_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, decorator_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -139,6 +146,8 @@ def test_bad_fixture_session_scope(
|
||||
"""
|
||||
|
||||
root_node = astroid.parse(code, path)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(decorator_checker)
|
||||
|
||||
with assert_adds_messages(
|
||||
linter,
|
||||
@@ -153,7 +162,7 @@ def test_bad_fixture_session_scope(
|
||||
end_col_offset=32,
|
||||
),
|
||||
):
|
||||
walk_checker(linter, decorator_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -184,6 +193,8 @@ def test_bad_fixture_package_scope(
|
||||
"""
|
||||
|
||||
root_node = astroid.parse(code, path)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(decorator_checker)
|
||||
|
||||
with assert_adds_messages(
|
||||
linter,
|
||||
@@ -198,7 +209,7 @@ def test_bad_fixture_package_scope(
|
||||
end_col_offset=32,
|
||||
),
|
||||
):
|
||||
walk_checker(linter, decorator_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -236,6 +247,8 @@ def test_bad_fixture_autouse(
|
||||
"""
|
||||
|
||||
root_node = astroid.parse(code, path)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(decorator_checker)
|
||||
|
||||
with assert_adds_messages(
|
||||
linter,
|
||||
@@ -250,4 +263,4 @@ def test_bad_fixture_autouse(
|
||||
end_col_offset=17 + len(keywords),
|
||||
),
|
||||
):
|
||||
walk_checker(linter, decorator_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
|
||||
import astroid
|
||||
from pylint.testutils import UnittestLinter
|
||||
from pylint.utils.ast_walker import ASTWalker
|
||||
from pylint_home_assistant.checkers.test_determinism import HassTestDeterminismChecker
|
||||
import pytest
|
||||
|
||||
from . import assert_no_messages, walk_checker
|
||||
from . import assert_no_messages
|
||||
|
||||
|
||||
@pytest.fixture(name="determinism_checker")
|
||||
@@ -143,9 +144,11 @@ def test_no_warning(
|
||||
) -> None:
|
||||
"""Test cases that should not trigger a warning."""
|
||||
root_node = astroid.parse(code, module_name)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(determinism_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, determinism_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_if_statement_flagged(
|
||||
@@ -164,7 +167,9 @@ def test_sensor_value(hass) -> None:
|
||||
""",
|
||||
"tests.components.test_integration.test_sensor",
|
||||
)
|
||||
walk_checker(linter, determinism_checker, root_node)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(determinism_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
@@ -188,7 +193,9 @@ def test_something(hass) -> None:
|
||||
""",
|
||||
"tests.components.test_integration.test_init",
|
||||
)
|
||||
walk_checker(linter, determinism_checker, root_node)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(determinism_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 2
|
||||
@@ -209,7 +216,9 @@ async def test_something(hass) -> None:
|
||||
""",
|
||||
"tests.components.test_integration.test_init",
|
||||
)
|
||||
walk_checker(linter, determinism_checker, root_node)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(determinism_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
@@ -232,7 +241,9 @@ def test_something(state) -> None:
|
||||
""",
|
||||
"tests.components.test_integration.test_init",
|
||||
)
|
||||
walk_checker(linter, determinism_checker, root_node)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(determinism_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
|
||||
import astroid
|
||||
from pylint.testutils import UnittestLinter
|
||||
from pylint.utils.ast_walker import ASTWalker
|
||||
from pylint_home_assistant.checkers.domain_constant import DomainConstantChecker
|
||||
import pytest
|
||||
|
||||
from . import assert_no_messages, walk_checker
|
||||
from . import assert_no_messages
|
||||
|
||||
|
||||
@pytest.fixture(name="domain_constant_checker")
|
||||
@@ -220,9 +221,11 @@ def test_no_warning(
|
||||
) -> None:
|
||||
"""Test cases that should not trigger a warning."""
|
||||
root_node = astroid.parse(code, "tests.components.test_integration.test_init")
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(domain_constant_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, domain_constant_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -252,7 +255,9 @@ def test_domain_argument_flagged(
|
||||
) -> None:
|
||||
"""Test that non-domain arguments are flagged."""
|
||||
root_node = astroid.parse(code, "tests.components.test_integration.test_init")
|
||||
walk_checker(linter, domain_constant_checker, root_node)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(domain_constant_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
@@ -271,6 +276,8 @@ async_setup_component(hass, OTHER, {})
|
||||
""",
|
||||
"homeassistant.components.test_integration",
|
||||
)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(domain_constant_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, domain_constant_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
|
||||
import astroid
|
||||
from pylint.testutils import UnittestLinter
|
||||
from pylint.utils.ast_walker import ASTWalker
|
||||
from pylint_home_assistant.checkers.duplicate_const import DuplicateConstChecker
|
||||
import pytest
|
||||
|
||||
from . import assert_no_messages, walk_checker
|
||||
from . import assert_no_messages
|
||||
|
||||
# Pre-load homeassistant.const so astroid can resolve it.
|
||||
astroid.MANAGER.ast_from_module_name("homeassistant.const")
|
||||
@@ -63,9 +64,11 @@ def test_no_warning(
|
||||
) -> None:
|
||||
"""Test cases that should not trigger a warning."""
|
||||
root_node = astroid.parse(code, "homeassistant.components.test_integration.const")
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(duplicate_const_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, duplicate_const_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -118,7 +121,9 @@ def test_duplicate_const_flagged(
|
||||
) -> None:
|
||||
"""Test that duplicate constants are flagged."""
|
||||
root_node = astroid.parse(code, "homeassistant.components.test_integration.const")
|
||||
walk_checker(linter, duplicate_const_checker, root_node)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(duplicate_const_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
@@ -137,6 +142,8 @@ CONF_HOST = "host"
|
||||
""",
|
||||
"tests.components.test_integration.test_const",
|
||||
)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(duplicate_const_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, duplicate_const_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
|
||||
import astroid
|
||||
from pylint.testutils import UnittestLinter
|
||||
from pylint.utils.ast_walker import ASTWalker
|
||||
from pylint_home_assistant.checkers.entity_description_defaults import (
|
||||
EntityDescriptionDefaultsChecker,
|
||||
)
|
||||
import pytest
|
||||
|
||||
from . import assert_no_messages, walk_checker
|
||||
from . import assert_no_messages
|
||||
|
||||
# Pre-load EntityDescription so astroid can resolve it in parsed snippets.
|
||||
# This avoids depending on component-level imports which may not be
|
||||
@@ -74,9 +75,11 @@ def test_no_warning(
|
||||
) -> None:
|
||||
"""Test cases that should not trigger a warning."""
|
||||
root_node = astroid.parse(code, "homeassistant.components.test_integration.sensor")
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(defaults_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, defaults_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -158,7 +161,9 @@ def test_redundant_default_flagged(
|
||||
) -> None:
|
||||
"""Test that redundant defaults are flagged."""
|
||||
root_node = astroid.parse(code, "homeassistant.components.test_integration.sensor")
|
||||
walk_checker(linter, defaults_checker, root_node)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(defaults_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
@@ -180,9 +185,11 @@ JobDescription(icon=None)
|
||||
""",
|
||||
"homeassistant.components.test_integration.sensor",
|
||||
)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(defaults_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, defaults_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_local_entity_description_name_ignored(
|
||||
@@ -202,9 +209,11 @@ MyDescription(entity_registry_enabled_default=True)
|
||||
""",
|
||||
"homeassistant.components.test_integration.sensor",
|
||||
)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(defaults_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, defaults_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_aliased_description_flagged(
|
||||
@@ -221,7 +230,9 @@ Alias(key="temperature", icon=None)
|
||||
""",
|
||||
"homeassistant.components.test_integration.sensor",
|
||||
)
|
||||
walk_checker(linter, defaults_checker, root_node)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(defaults_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
@@ -245,6 +256,8 @@ EntityDescription(
|
||||
""",
|
||||
"tests.components.test_integration.test_sensor",
|
||||
)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(defaults_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, defaults_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
@@ -6,13 +6,14 @@ from pathlib import Path
|
||||
import astroid
|
||||
from astroid import nodes
|
||||
from pylint.testutils import MessageTest, UnittestLinter
|
||||
from pylint.utils.ast_walker import ASTWalker
|
||||
from pylint_home_assistant.checkers.entity_unique_id_format import (
|
||||
EntityUniqueIdFormatChecker,
|
||||
)
|
||||
from pylint_home_assistant.helpers.integration import clear_caches
|
||||
import pytest
|
||||
|
||||
from . import assert_adds_messages, assert_no_messages, walk_checker
|
||||
from . import assert_adds_messages, assert_no_messages
|
||||
|
||||
|
||||
@pytest.fixture(name="checker")
|
||||
@@ -180,8 +181,10 @@ def test_redundant_domain_fires(
|
||||
|
||||
root_node = _parse(code, integration_dir)
|
||||
value_node = _find_attr_value_node(root_node)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(checker)
|
||||
with assert_adds_messages(linter, _expect_redundant_domain(value_node, "MySensor")):
|
||||
walk_checker(linter, checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_redundant_domain_fires_in_both_branches(
|
||||
@@ -220,12 +223,14 @@ class MySensor(Entity):
|
||||
)
|
||||
]
|
||||
assert len(value_nodes) == 2
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(checker)
|
||||
with assert_adds_messages(
|
||||
linter,
|
||||
_expect_redundant_domain(value_nodes[0], "MySensor"),
|
||||
_expect_redundant_domain(value_nodes[1], "MySensor"),
|
||||
):
|
||||
walk_checker(linter, checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -275,8 +280,10 @@ def test_redundant_domain_does_not_fire(
|
||||
integration_dir = _make_integration(tmp_path)
|
||||
|
||||
root_node = _parse(code, integration_dir)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(checker)
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -363,7 +370,9 @@ def test_redundant_domain_literal_fires(
|
||||
integration_dir = _make_integration(tmp_path, domain="myhub")
|
||||
|
||||
root_node = _parse(code, integration_dir)
|
||||
walk_checker(linter, checker, root_node)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(checker)
|
||||
walker.walk(root_node)
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
assert messages[0].msg_id == "home-assistant-entity-unique-id-redundant-domain"
|
||||
@@ -401,7 +410,9 @@ class MySensor(Entity):
|
||||
""",
|
||||
integration_dir,
|
||||
)
|
||||
walk_checker(linter, checker, root_node)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(checker)
|
||||
walker.walk(root_node)
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == (1 if fires else 0)
|
||||
|
||||
@@ -481,8 +492,10 @@ def test_redundant_domain_literal_does_not_fire_on_word_substrings(
|
||||
integration_dir = _make_integration(tmp_path, domain="myhub")
|
||||
|
||||
root_node = _parse(code, integration_dir)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(checker)
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -526,10 +539,12 @@ def test_redundant_domain_fires_in_unique_id_property(
|
||||
|
||||
root_node = _parse(code, integration_dir)
|
||||
return_node = next(root_node.nodes_of_class(nodes.Return))
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(checker)
|
||||
with assert_adds_messages(
|
||||
linter, _expect_redundant_domain(return_node.value, "MySensor")
|
||||
):
|
||||
walk_checker(linter, checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -576,8 +591,10 @@ def test_out_of_scope_ignored(
|
||||
root_node = _parse(
|
||||
code, integration_dir, module_name=module_name, file_name=file_name
|
||||
)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(checker)
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -623,8 +640,10 @@ class MyEntity(Entity):
|
||||
file_name=file_name,
|
||||
)
|
||||
value_node = _find_attr_value_node(root_node)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(checker)
|
||||
with assert_adds_messages(linter, _expect_redundant_domain(value_node, "MyEntity")):
|
||||
walk_checker(linter, checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_same_module_mixin_base_fires(
|
||||
@@ -655,7 +674,9 @@ class MyConcreteSensor(MyBaseSensor):
|
||||
integration_dir,
|
||||
)
|
||||
value_node = _find_attr_value_node(root_node)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(checker)
|
||||
with assert_adds_messages(
|
||||
linter, _expect_redundant_domain(value_node, "MyBaseSensor")
|
||||
):
|
||||
walk_checker(linter, checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
@@ -5,6 +5,7 @@ from pathlib import Path
|
||||
|
||||
import astroid
|
||||
from pylint.testutils import UnittestLinter
|
||||
from pylint.utils.ast_walker import ASTWalker
|
||||
from pylint_home_assistant.checkers.exception_translations import (
|
||||
ExceptionTranslationsChecker,
|
||||
)
|
||||
@@ -13,7 +14,7 @@ from pylint_home_assistant.helpers.translations import clear_translations_cache
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
from . import assert_no_messages, walk_checker
|
||||
from . import assert_no_messages
|
||||
|
||||
# Pre-load so astroid can resolve exception classes in parsed snippets.
|
||||
astroid.MANAGER.ast_from_module_name("homeassistant.exceptions")
|
||||
@@ -124,8 +125,11 @@ def test_no_warning(
|
||||
root_node = astroid.parse(code, "homeassistant.components.test_int.coordinator")
|
||||
root_node.file = str(integration_dir / "coordinator.py")
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(translations_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, translations_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -160,7 +164,9 @@ def test_hardcoded_string_flagged(
|
||||
root_node = astroid.parse(code, "homeassistant.components.test_int.coordinator")
|
||||
root_node.file = str(integration_dir / "coordinator.py")
|
||||
|
||||
walk_checker(linter, translations_checker, root_node)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(translations_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
@@ -212,7 +218,9 @@ def test_translation_key_domain_mismatch_flagged(
|
||||
)
|
||||
root_node.file = str(integration_dir / "coordinator.py")
|
||||
|
||||
walk_checker(linter, translations_checker, root_node)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(translations_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
@@ -241,7 +249,9 @@ raise HomeAssistantError(
|
||||
)
|
||||
root_node.file = str(integration_dir / "coordinator.py")
|
||||
|
||||
walk_checker(linter, translations_checker, root_node)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(translations_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
@@ -270,7 +280,9 @@ raise HomeAssistantError(
|
||||
)
|
||||
root_node.file = str(integration_dir / "coordinator.py")
|
||||
|
||||
walk_checker(linter, translations_checker, root_node)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(translations_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
@@ -300,8 +312,11 @@ raise HomeAssistantError(
|
||||
)
|
||||
root_node.file = str(integration_dir / "coordinator.py")
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(translations_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, translations_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_extra_placeholders_flagged(
|
||||
@@ -329,7 +344,9 @@ raise HomeAssistantError(
|
||||
)
|
||||
root_node.file = str(integration_dir / "coordinator.py")
|
||||
|
||||
walk_checker(linter, translations_checker, root_node)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(translations_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
@@ -361,7 +378,9 @@ raise HomeAssistantError(
|
||||
)
|
||||
root_node.file = str(integration_dir / "coordinator.py")
|
||||
|
||||
walk_checker(linter, translations_checker, root_node)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(translations_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
@@ -393,8 +412,11 @@ raise HomeAssistantError(
|
||||
)
|
||||
root_node.file = str(integration_dir / "coordinator.py")
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(translations_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, translations_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_placeholder_variable_resolved(
|
||||
@@ -423,8 +445,11 @@ raise HomeAssistantError(
|
||||
)
|
||||
root_node.file = str(integration_dir / "coordinator.py")
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(translations_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, translations_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_placeholder_variable_mismatch_flagged(
|
||||
@@ -453,7 +478,9 @@ raise HomeAssistantError(
|
||||
)
|
||||
root_node.file = str(integration_dir / "coordinator.py")
|
||||
|
||||
walk_checker(linter, translations_checker, root_node)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(translations_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
@@ -486,8 +513,11 @@ raise HomeAssistantError(
|
||||
)
|
||||
root_node.file = str(integration_dir / "coordinator.py")
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(translations_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, translations_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_constant_placeholder_keys_ok(
|
||||
@@ -516,8 +546,11 @@ raise HomeAssistantError(
|
||||
)
|
||||
root_node.file = str(integration_dir / "coordinator.py")
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(translations_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, translations_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_key_reference_resolution(
|
||||
@@ -555,8 +588,11 @@ raise HomeAssistantError(
|
||||
)
|
||||
root_node.file = str(integration_dir / "coordinator.py")
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(translations_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, translations_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_no_strings_json_flags_missing_key(
|
||||
@@ -578,7 +614,9 @@ raise HomeAssistantError(
|
||||
)
|
||||
root_node.file = str(integration_dir / "coordinator.py")
|
||||
|
||||
walk_checker(linter, translations_checker, root_node)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(translations_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
@@ -609,7 +647,9 @@ raise HomeAssistantError(
|
||||
)
|
||||
root_node.file = str(integration_dir / "coordinator.py")
|
||||
|
||||
walk_checker(linter, translations_checker, root_node)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(translations_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
@@ -642,7 +682,9 @@ raise HomeAssistantError(
|
||||
)
|
||||
root_node.file = str(integration_dir / "coordinator.py")
|
||||
|
||||
walk_checker(linter, translations_checker, root_node)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(translations_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
@@ -658,6 +700,8 @@ def test_not_integration_ignored(
|
||||
f'{_HA_IMPORTS}\nraise HomeAssistantError("hardcoded")',
|
||||
"tests.components.test_integration",
|
||||
)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(translations_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, translations_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
import astroid
|
||||
from pylint.checkers import BaseChecker
|
||||
from pylint.testutils.unittest_linter import UnittestLinter
|
||||
from pylint.utils.ast_walker import ASTWalker
|
||||
import pytest
|
||||
|
||||
from . import assert_no_messages, walk_checker
|
||||
from . import assert_no_messages
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -76,9 +77,11 @@ def test_enforce_greek_micro_char(
|
||||
) -> None:
|
||||
"""Good test cases."""
|
||||
root_node = astroid.parse(code, "homeassistant.components.pylint_test")
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(enforce_greek_micro_char_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, enforce_greek_micro_char_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -150,8 +153,10 @@ def test_enforce_greek_micro_char_assign_bad(
|
||||
) -> None:
|
||||
"""Bad assignment test cases."""
|
||||
root_node = astroid.parse(code, "homeassistant.components.pylint_test")
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(enforce_greek_micro_char_checker)
|
||||
|
||||
walk_checker(linter, enforce_greek_micro_char_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
message = next(iter(messages))
|
||||
|
||||
@@ -5,11 +5,12 @@ from pathlib import Path
|
||||
|
||||
import astroid
|
||||
from pylint.testutils import UnittestLinter
|
||||
from pylint.utils.ast_walker import ASTWalker
|
||||
from pylint_home_assistant.checkers.mdi_icons import MdiIconsChecker
|
||||
from pylint_home_assistant.helpers.icons import clear_icons_cache
|
||||
import pytest
|
||||
|
||||
from . import assert_no_messages, walk_checker
|
||||
from . import assert_no_messages
|
||||
|
||||
|
||||
@pytest.fixture(name="mdi_checker")
|
||||
@@ -77,9 +78,11 @@ def test_python_no_warning(
|
||||
) -> None:
|
||||
"""Test that valid MDI icons in Python code pass."""
|
||||
root_node = astroid.parse(code, "homeassistant.components.test_integration.sensor")
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(mdi_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, mdi_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -115,7 +118,9 @@ def test_python_invalid_icon_flagged(
|
||||
) -> None:
|
||||
"""Test that invalid MDI icons in Python code are flagged."""
|
||||
root_node = astroid.parse(code, "homeassistant.components.test_integration.sensor")
|
||||
walk_checker(linter, mdi_checker, root_node)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(mdi_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
@@ -132,9 +137,11 @@ def test_python_not_integration_ignored(
|
||||
'ICON = "mdi:nonexistent-icon"',
|
||||
"tests.components.test_integration",
|
||||
)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(mdi_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, mdi_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
# --- icons.json tests ---
|
||||
@@ -166,8 +173,11 @@ def test_icons_json_valid(
|
||||
)
|
||||
root_node.file = str(integration_dir / "__init__.py")
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(mdi_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, mdi_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_icons_json_invalid_flagged(
|
||||
@@ -193,7 +203,9 @@ def test_icons_json_invalid_flagged(
|
||||
)
|
||||
root_node.file = str(integration_dir / "__init__.py")
|
||||
|
||||
walk_checker(linter, mdi_checker, root_node)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(mdi_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
@@ -215,8 +227,11 @@ def test_icons_json_no_file_no_warning(
|
||||
)
|
||||
root_node.file = str(integration_dir / "__init__.py")
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(mdi_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, mdi_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_icons_json_nested_invalid_flagged(
|
||||
@@ -250,7 +265,9 @@ def test_icons_json_nested_invalid_flagged(
|
||||
)
|
||||
root_node.file = str(integration_dir / "__init__.py")
|
||||
|
||||
walk_checker(linter, mdi_checker, root_node)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(mdi_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
import astroid
|
||||
from pylint.checkers import BaseChecker
|
||||
from pylint.testutils.unittest_linter import UnittestLinter
|
||||
from pylint.utils.ast_walker import ASTWalker
|
||||
import pytest
|
||||
|
||||
from . import assert_no_messages, walk_checker
|
||||
from . import assert_no_messages
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -70,9 +71,11 @@ def test_enforce_naive_now_good(
|
||||
) -> None:
|
||||
"""Good test cases -- no message expected."""
|
||||
root_node = astroid.parse(code, "homeassistant.components.pylint_test")
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(enforce_naive_now_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, enforce_naive_now_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -155,8 +158,10 @@ def test_enforce_naive_now_bad(
|
||||
) -> None:
|
||||
"""Bad test cases -- one message expected per call."""
|
||||
root_node = astroid.parse(code, "homeassistant.components.pylint_test")
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(enforce_naive_now_checker)
|
||||
|
||||
walk_checker(linter, enforce_naive_now_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
assert messages[0].msg_id == "home-assistant-enforce-naive-now"
|
||||
@@ -192,6 +197,8 @@ def test_enforce_naive_now_skips_util_dt(
|
||||
) -> None:
|
||||
"""``homeassistant.util.dt`` defines ``naive_now`` itself, so it is skipped."""
|
||||
root_node = astroid.parse(code, "homeassistant.util.dt")
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(enforce_naive_now_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, enforce_naive_now_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
import astroid
|
||||
from pylint.checkers import BaseChecker
|
||||
from pylint.testutils.unittest_linter import UnittestLinter
|
||||
from pylint.utils.ast_walker import ASTWalker
|
||||
import pytest
|
||||
|
||||
from . import assert_no_messages, walk_checker
|
||||
from . import assert_no_messages
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -135,9 +136,11 @@ def test_enforce_now_good(
|
||||
) -> None:
|
||||
"""Good test cases -- no message expected."""
|
||||
root_node = astroid.parse(code, "homeassistant.components.pylint_test")
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(enforce_now_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, enforce_now_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -227,8 +230,10 @@ def test_enforce_now_bad(
|
||||
) -> None:
|
||||
"""Bad test cases -- one message expected per call."""
|
||||
root_node = astroid.parse(code, "homeassistant.components.pylint_test")
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(enforce_now_checker)
|
||||
|
||||
walk_checker(linter, enforce_now_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
assert messages[0].msg_id == "home-assistant-enforce-now"
|
||||
@@ -265,6 +270,8 @@ def test_enforce_now_skips_util_dt(
|
||||
) -> None:
|
||||
"""``homeassistant.util.dt`` defines ``now`` itself, so it is skipped."""
|
||||
root_node = astroid.parse(code, "homeassistant.util.dt")
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(enforce_now_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, enforce_now_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
@@ -5,9 +5,10 @@ from pathlib import Path
|
||||
import astroid
|
||||
from pylint.checkers import BaseChecker
|
||||
from pylint.testutils.unittest_linter import UnittestLinter
|
||||
from pylint.utils.ast_walker import ASTWalker
|
||||
import pytest
|
||||
|
||||
from . import assert_no_messages, walk_checker
|
||||
from . import assert_no_messages
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -103,9 +104,11 @@ def test_enforce_runtime_data(
|
||||
) -> None:
|
||||
"""Good test cases."""
|
||||
root_node = astroid.parse(code, module_name)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(enforce_runtime_data_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, enforce_runtime_data_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -157,8 +160,10 @@ def test_enforce_runtime_data_bad(
|
||||
) -> None:
|
||||
"""Bad test cases."""
|
||||
root_node = astroid.parse(code, module_name)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(enforce_runtime_data_checker)
|
||||
|
||||
walk_checker(linter, enforce_runtime_data_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
assert messages[0].msg_id == "home-assistant-use-runtime-data"
|
||||
@@ -182,8 +187,11 @@ def test_enforce_runtime_data_no_config_flow(
|
||||
root_node = astroid.parse(code, "homeassistant.components.yaml_only")
|
||||
root_node.file = str(init_file)
|
||||
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(enforce_runtime_data_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, enforce_runtime_data_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_enforce_runtime_data_with_config_flow(
|
||||
@@ -205,7 +213,10 @@ def test_enforce_runtime_data_with_config_flow(
|
||||
root_node = astroid.parse(code, "homeassistant.components.modern")
|
||||
root_node.file = str(init_file)
|
||||
|
||||
walk_checker(linter, enforce_runtime_data_checker, root_node)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(enforce_runtime_data_checker)
|
||||
|
||||
walker.walk(root_node)
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
assert messages[0].msg_id == "home-assistant-use-runtime-data"
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
|
||||
import astroid
|
||||
from pylint.testutils import UnittestLinter
|
||||
from pylint.utils.ast_walker import ASTWalker
|
||||
from pylint_home_assistant.checkers.sequential_executor_jobs import (
|
||||
SequentialExecutorJobsChecker,
|
||||
)
|
||||
import pytest
|
||||
|
||||
from . import assert_no_messages, walk_checker
|
||||
from . import assert_no_messages
|
||||
|
||||
|
||||
@pytest.fixture(name="executor_checker")
|
||||
@@ -55,9 +56,11 @@ def test_no_warning(
|
||||
) -> None:
|
||||
"""Test cases that should not trigger a warning."""
|
||||
root_node = astroid.parse(code, "homeassistant.components.test_integration")
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(executor_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, executor_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_two_sequential_flagged(
|
||||
@@ -73,7 +76,9 @@ async def async_setup(hass, config):
|
||||
""",
|
||||
"homeassistant.components.test_integration",
|
||||
)
|
||||
walk_checker(linter, executor_checker, root_node)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(executor_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
@@ -94,7 +99,9 @@ async def async_setup(hass, config):
|
||||
""",
|
||||
"homeassistant.components.test_integration",
|
||||
)
|
||||
walk_checker(linter, executor_checker, root_node)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(executor_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 2
|
||||
@@ -113,7 +120,9 @@ async def async_setup(hass, config):
|
||||
""",
|
||||
"homeassistant.components.test_integration",
|
||||
)
|
||||
walk_checker(linter, executor_checker, root_node)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(executor_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
@@ -135,7 +144,9 @@ async def async_setup(hass, config):
|
||||
""",
|
||||
"homeassistant.components.test_integration",
|
||||
)
|
||||
walk_checker(linter, executor_checker, root_node)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(executor_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
@@ -157,7 +168,9 @@ async def async_setup(hass, config):
|
||||
""",
|
||||
"homeassistant.components.test_integration",
|
||||
)
|
||||
walk_checker(linter, executor_checker, root_node)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(executor_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
@@ -180,7 +193,9 @@ async def async_setup(hass, config):
|
||||
""",
|
||||
"homeassistant.components.test_integration",
|
||||
)
|
||||
walk_checker(linter, executor_checker, root_node)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(executor_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
@@ -203,7 +218,9 @@ async def async_setup(hass, config):
|
||||
""",
|
||||
"homeassistant.components.test_integration",
|
||||
)
|
||||
walk_checker(linter, executor_checker, root_node)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(executor_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
@@ -223,7 +240,9 @@ async def async_setup(hass, config):
|
||||
""",
|
||||
"homeassistant.components.test_integration",
|
||||
)
|
||||
walk_checker(linter, executor_checker, root_node)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(executor_checker)
|
||||
walker.walk(root_node)
|
||||
|
||||
messages = linter.release_messages()
|
||||
assert len(messages) == 1
|
||||
@@ -243,6 +262,8 @@ async def async_setup(hass, config):
|
||||
""",
|
||||
"tests.components.test_integration",
|
||||
)
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(executor_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, executor_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
@@ -5,9 +5,10 @@ from pylint.checkers import BaseChecker
|
||||
from pylint.interfaces import UNDEFINED
|
||||
from pylint.testutils import MessageTest
|
||||
from pylint.testutils.unittest_linter import UnittestLinter
|
||||
from pylint.utils.ast_walker import ASTWalker
|
||||
import pytest
|
||||
|
||||
from . import assert_adds_messages, assert_no_messages, walk_checker
|
||||
from . import assert_adds_messages, assert_no_messages
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -74,9 +75,11 @@ def test_enforce_sorted_platforms(
|
||||
) -> None:
|
||||
"""Good test cases."""
|
||||
root_node = astroid.parse(code, "homeassistant.components.pylint_test")
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(enforce_sorted_platforms_checker)
|
||||
|
||||
with assert_no_messages(linter):
|
||||
walk_checker(linter, enforce_sorted_platforms_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
def test_enforce_sorted_platforms_bad(
|
||||
|
||||
@@ -7,9 +7,10 @@ from pylint.checkers import BaseChecker
|
||||
from pylint.interfaces import INFERENCE
|
||||
from pylint.testutils import MessageTest
|
||||
from pylint.testutils.unittest_linter import UnittestLinter
|
||||
from pylint.utils.ast_walker import ASTWalker
|
||||
import pytest
|
||||
|
||||
from . import assert_adds_messages, assert_no_messages, walk_checker
|
||||
from . import assert_adds_messages, assert_no_messages
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -116,6 +117,8 @@ def test_enforce_super_call(
|
||||
) -> None:
|
||||
"""Good test cases."""
|
||||
root_node = astroid.parse(code, "homeassistant.components.pylint_test")
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(super_call_checker)
|
||||
|
||||
with (
|
||||
patch(
|
||||
@@ -124,7 +127,7 @@ def test_enforce_super_call(
|
||||
),
|
||||
assert_no_messages(linter),
|
||||
):
|
||||
walk_checker(linter, super_call_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -196,6 +199,8 @@ def test_enforce_super_call_bad(
|
||||
) -> None:
|
||||
"""Bad test cases."""
|
||||
root_node = astroid.parse(code, "homeassistant.components.pylint_test")
|
||||
walker = ASTWalker(linter)
|
||||
walker.add_checker(super_call_checker)
|
||||
node = root_node.body[node_idx].body[0]
|
||||
|
||||
with (
|
||||
@@ -217,4 +222,4 @@ def test_enforce_super_call_bad(
|
||||
),
|
||||
),
|
||||
):
|
||||
walk_checker(linter, super_call_checker, root_node)
|
||||
walker.walk(root_node)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user