Compare commits

..

2 Commits

Author SHA1 Message Date
Ariel Ebersberger 7825792f2a Use a major version bump for the section rename migration 2026-06-29 17:49:48 +02:00
Ariel Ebersberger 3cb69dff34 Rename "advanced_options" section to "additional_options" in SQL 2026-06-29 13:36:16 +02:00
108 changed files with 1877 additions and 2481 deletions
-98
View File
@@ -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()
+1 -1
View File
@@ -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: {}
+2 -2
View File
@@ -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 -1
View File
@@ -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)
+1 -1
View File
@@ -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"
},
-14
View File
@@ -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,
)
+16 -2
View File
@@ -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)
+4 -4
View File
@@ -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."
+1 -13
View File
@@ -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%]"
}
}
}
+2 -2
View File
@@ -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,
}
+1 -1
View File
@@ -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)),
}
+7 -3
View File
@@ -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"]
}
+2 -1
View File
@@ -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(
+368 -254
View File
@@ -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
+1
View File
@@ -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
+71 -36
View File
@@ -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",
+1 -1
View File
@@ -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"]
}
+6 -19
View File
@@ -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,
+17 -3
View File
@@ -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."
+10 -3
View File
@@ -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,
+9 -9
View File
@@ -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,
+1 -1
View File
@@ -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("//.*:.*@")
+6 -4
View File
@@ -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,
+12 -12
View File
@@ -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
View File
@@ -648,7 +648,7 @@ exclude_lines = [
]
[tool.ruff]
required-version = ">=0.15.18"
required-version = ">=0.15.17"
[tool.ruff.lint]
select = [
+3 -6
View File
@@ -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
+1 -1
View File
@@ -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',
})
# ---
-57
View File
@@ -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,
-7
View File
@@ -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,
})
-25
View File
@@ -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()
+3 -15
View File
@@ -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"]
+1 -5
View File
@@ -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"
+26 -26
View File
@@ -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,
+33 -33
View File
@@ -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",
},
}
+54 -6
View File
@@ -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,
+6 -6
View File
@@ -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",
},
},
-26
View File
@@ -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={
-80
View File
@@ -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},
)
-12
View File
@@ -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)
+19 -6
View File
@@ -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"
+8 -3
View File
@@ -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)
+14 -4
View File
@@ -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)
+20 -7
View File
@@ -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)
+20 -7
View File
@@ -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)
+17 -6
View File
@@ -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
+11 -4
View File
@@ -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)
+11 -4
View File
@@ -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)
+32 -11
View File
@@ -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)
+63 -19
View File
@@ -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)
+8 -3
View File
@@ -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))
+25 -8
View File
@@ -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
+11 -4
View File
@@ -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)
+11 -4
View File
@@ -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)
+16 -5
View File
@@ -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"
+32 -11
View File
@@ -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 -2
View File
@@ -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(
+8 -3
View File
@@ -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