Compare commits

...

23 Commits

Author SHA1 Message Date
dependabot[bot] a2e00eb0b5 Bump actions/cache/save from 5.0.5 to 6.1.0 (#175168)
Signed-off-by: dependabot[bot] <support@github.com>
2026-06-30 10:34:47 +02:00
dependabot[bot] 0ea1ec6fc7 Bump actions/cache/restore from 5.0.5 to 6.1.0 (#175172)
Signed-off-by: dependabot[bot] <support@github.com>
2026-06-30 10:30:31 +02:00
Erik Montnemery de6b679e6e Rewrite homeassistant started trigger (#175160)
Co-authored-by: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-30 10:27:31 +02:00
epenet 3bcdb621ec Use UnitOfDensity/UnitOfRatio in tests (#175187) 2026-06-30 10:27:18 +02:00
epenet 003aed3d44 Set override decorator in switchbot (#175190) 2026-06-30 10:25:37 +02:00
Onero-testdev 1d0beeeef9 Add SwitchBot Standing Fan select platform (#173580)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 10:01:34 +02:00
TimL f0850b67f2 Bump pysmlight to 0.5.2 (#175179) 2026-06-30 11:00:28 +03:00
Oscar Calvo fbd83f8a52 Add quality scale (bronze) to ccm15 (#173801) 2026-06-30 10:21:07 +03:00
epenet bc18d0678a Use UnitOfDensity/UnitOfRatio in entity components (#175169) 2026-06-30 09:10:02 +02:00
TimL c0971e99e9 add smlight to bluetooth_adapters so its loaded before integrations (#175176) 2026-06-30 09:07:41 +02:00
epenet 771da03707 Use UnitOfDensity/UnitOfRatio in screenlogic (#175175) 2026-06-30 09:06:53 +02:00
Erik Montnemery 80a04d2820 Make did_not_trigger of async_initialize_triggers kwarg only (#175178) 2026-06-30 08:55:59 +02:00
Greg Haines feadcc057d Add diagnostics for CentriConnect (#175046) 2026-06-30 08:36:10 +02:00
epenet cf1e0f0aa5 Use UnitOfDensity/UnitOfRatio in kaiterra (#175174) 2026-06-30 08:35:13 +02:00
epenet cfa49bb0dd Use UnitOfDensity/UnitOfRatio in isy994 (#175173) 2026-06-30 08:32:05 +02:00
Erik Montnemery a0d96a2c62 Remove no longer needed sleep from HomeAssistant.async_start (#175152) 2026-06-30 06:36:25 +02:00
renovate[bot] eb9ebd17b6 Update coverage to 7.14.3 (#175155) 2026-06-30 06:32:50 +02:00
Robert Resch 5ea38fcc07 Bump uv to 0.11.25 (#175153) 2026-06-30 06:27:59 +02:00
Robert Resch 15ef228cfa Bump wheels and base image to 2026.07.0 to use alpine 3.24 (#175133) 2026-06-29 23:41:01 +02:00
Arie Catsman e815c9f0cc bump pyenphase to 3.0.1 (#175141) 2026-06-29 23:31:43 +03:00
Simone Chemelli 434b3ca309 Bump librouteros to 4.1.1 (#175116) 2026-06-29 20:04:32 +01:00
Markus Tuominen a557e96c53 Add walk_checker helper to deduplicate pylint rule tests (#175113) 2026-06-29 20:41:07 +02:00
Jan Bouwhuis 324c95140b Refactor MQTT config entry (#173929)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Ariel Ebersberger <31776703+justanotherariel@users.noreply.github.com>
2026-06-29 20:07:26 +02:00
98 changed files with 1842 additions and 1942 deletions
+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.05.0"
BASE_IMAGE_VERSION: "2026.07.0"
ARCHITECTURES: '["amd64", "aarch64"]'
permissions: {}
+15 -15
View File
@@ -352,7 +352,7 @@ jobs:
check-latest: true
- name: Restore base Python virtual environment
id: cache-venv
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: venv
key: >-
@@ -372,7 +372,7 @@ jobs:
echo "full_key=${partial_key}${HASH_FILES}" >> $GITHUB_OUTPUT
- name: Restore uv wheel cache
if: steps.cache-venv.outputs.cache-hit != 'true'
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ env.UV_CACHE_DIR }}
key: ${{ steps.generate-uv-key.outputs.full_key }}
@@ -442,13 +442,13 @@ jobs:
uv cache prune --ci
- name: Save uv wheel cache
if: steps.cache-venv.outputs.cache-hit != 'true'
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ env.UV_CACHE_DIR }}
key: ${{ steps.generate-uv-key.outputs.full_key }}
- name: Save base Python virtual environment
if: always() && steps.create-venv.outcome == 'success'
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: venv
key: >-
@@ -486,7 +486,7 @@ jobs:
check-latest: true
- name: Restore full Python virtual environment
id: cache-venv
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: venv
fail-on-cache-miss: true
@@ -523,7 +523,7 @@ jobs:
check-latest: true
- name: Restore full Python virtual environment
id: cache-venv
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: venv
fail-on-cache-miss: true
@@ -614,7 +614,7 @@ jobs:
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: venv
fail-on-cache-miss: true
@@ -665,7 +665,7 @@ jobs:
check-latest: true
- name: Restore full Python virtual environment
id: cache-venv
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: venv
fail-on-cache-miss: true
@@ -718,7 +718,7 @@ jobs:
check-latest: true
- name: Restore full Python virtual environment
id: cache-venv
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: venv
fail-on-cache-miss: true
@@ -775,7 +775,7 @@ jobs:
echo "key=mypy-${MYPY_CACHE_VERSION}-${mypy_version}-${HA_SHORT_VERSION}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
- name: Restore full Python virtual environment
id: cache-venv
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: venv
fail-on-cache-miss: true
@@ -846,7 +846,7 @@ jobs:
check-latest: true
- name: Restore full Python virtual environment
id: cache-venv
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: venv
fail-on-cache-miss: true
@@ -911,7 +911,7 @@ jobs:
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: venv
fail-on-cache-miss: true
@@ -1053,7 +1053,7 @@ jobs:
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: venv
fail-on-cache-miss: true
@@ -1209,7 +1209,7 @@ jobs:
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: venv
fail-on-cache-miss: true
@@ -1377,7 +1377,7 @@ jobs:
check-latest: true
- name: Restore full Python ${{ matrix.python-version }} virtual environment
id: cache-venv
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: venv
fail-on-cache-miss: true
+2 -2
View File
@@ -137,7 +137,7 @@ jobs:
sed -i "/uv/d" requirements_diff.txt
- name: Build wheels
uses: home-assistant/wheels@34957438948e0b3dcde73c77750643dadae594f5 # 2026.06.0
uses: home-assistant/wheels@9e17ab1ed5c4c79d8b61e29fa63de25ca2710716 # 2026.07.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@34957438948e0b3dcde73c77750643dadae594f5 # 2026.06.0
uses: home-assistant/wheels@9e17ab1ed5c4c79d8b61e29fa63de25ca2710716 # 2026.07.0
with:
abi: ${{ matrix.abi }}
tag: musllinux_1_2
@@ -5,7 +5,7 @@ import logging
from typing import Final, final, override
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
from homeassistant.const import UnitOfDensity
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity import Entity
@@ -152,4 +152,4 @@ class AirQualityEntity(Entity):
@override
def unit_of_measurement(self) -> str:
"""Return the unit of measurement of this entity."""
return CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
return UnitOfDensity.MICROGRAMS_PER_CUBIC_METER
@@ -5,13 +5,7 @@ from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
)
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass
from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION,
STATE_OFF,
STATE_ON,
)
from homeassistant.const import STATE_OFF, STATE_ON, UnitOfDensity, UnitOfRatio
from homeassistant.core import HomeAssistant
from homeassistant.helpers.automation import DomainSpec
from homeassistant.helpers.condition import (
@@ -60,12 +54,12 @@ CONDITIONS: dict[str, type[Condition]] = {
# Numerical sensor conditions with unit conversion
"is_co_value": make_entity_numerical_condition_with_unit(
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.CO)},
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
CarbonMonoxideConcentrationConverter,
),
"is_ozone_value": make_entity_numerical_condition_with_unit(
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.OZONE)},
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
OzoneConcentrationConverter,
),
"is_voc_value": make_entity_numerical_condition_with_unit(
@@ -74,7 +68,7 @@ CONDITIONS: dict[str, type[Condition]] = {
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS
)
},
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
MassVolumeConcentrationConverter,
),
"is_voc_ratio_value": make_entity_numerical_condition_with_unit(
@@ -83,48 +77,48 @@ CONDITIONS: dict[str, type[Condition]] = {
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS
)
},
CONCENTRATION_PARTS_PER_BILLION,
UnitOfRatio.PARTS_PER_BILLION,
UnitlessRatioConverter,
),
"is_no_value": make_entity_numerical_condition_with_unit(
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.NITROGEN_MONOXIDE)},
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
NitrogenMonoxideConcentrationConverter,
),
"is_no2_value": make_entity_numerical_condition_with_unit(
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.NITROGEN_DIOXIDE)},
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
NitrogenDioxideConcentrationConverter,
),
"is_so2_value": make_entity_numerical_condition_with_unit(
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.SULPHUR_DIOXIDE)},
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
SulphurDioxideConcentrationConverter,
),
# Numerical sensor conditions without unit conversion (single-unit device classes)
"is_co2_value": make_entity_numerical_condition(
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.CO2)},
valid_unit=CONCENTRATION_PARTS_PER_MILLION,
valid_unit=UnitOfRatio.PARTS_PER_MILLION,
),
"is_pm1_value": make_entity_numerical_condition(
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM1)},
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
valid_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
),
"is_pm25_value": make_entity_numerical_condition(
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM25)},
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
valid_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
),
"is_pm4_value": make_entity_numerical_condition(
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM4)},
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
valid_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
),
"is_pm10_value": make_entity_numerical_condition(
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM10)},
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
valid_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
),
"is_n2o_value": make_entity_numerical_condition(
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.NITROUS_OXIDE)},
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
valid_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
),
}
+27 -33
View File
@@ -5,13 +5,7 @@ from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
)
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass
from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION,
STATE_OFF,
STATE_ON,
)
from homeassistant.const import STATE_OFF, STATE_ON, UnitOfDensity, UnitOfRatio
from homeassistant.core import HomeAssistant
from homeassistant.helpers.automation import DomainSpec
from homeassistant.helpers.trigger import (
@@ -65,25 +59,25 @@ TRIGGERS: dict[str, type[Trigger]] = {
# Numerical sensor triggers with unit conversion
"co_changed": make_entity_numerical_state_changed_with_unit_trigger(
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.CO)},
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
CarbonMonoxideConcentrationConverter,
),
"co_crossed_threshold": (
make_entity_numerical_state_crossed_threshold_with_unit_trigger(
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.CO)},
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
CarbonMonoxideConcentrationConverter,
)
),
"ozone_changed": make_entity_numerical_state_changed_with_unit_trigger(
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.OZONE)},
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
OzoneConcentrationConverter,
),
"ozone_crossed_threshold": (
make_entity_numerical_state_crossed_threshold_with_unit_trigger(
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.OZONE)},
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
OzoneConcentrationConverter,
)
),
@@ -93,7 +87,7 @@ TRIGGERS: dict[str, type[Trigger]] = {
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS
)
},
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
MassVolumeConcentrationConverter,
),
"voc_crossed_threshold": (
@@ -103,7 +97,7 @@ TRIGGERS: dict[str, type[Trigger]] = {
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS
)
},
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
MassVolumeConcentrationConverter,
)
),
@@ -113,7 +107,7 @@ TRIGGERS: dict[str, type[Trigger]] = {
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS
)
},
CONCENTRATION_PARTS_PER_BILLION,
UnitOfRatio.PARTS_PER_BILLION,
UnitlessRatioConverter,
),
"voc_ratio_crossed_threshold": (
@@ -123,13 +117,13 @@ TRIGGERS: dict[str, type[Trigger]] = {
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS
)
},
CONCENTRATION_PARTS_PER_BILLION,
UnitOfRatio.PARTS_PER_BILLION,
UnitlessRatioConverter,
)
),
"no_changed": make_entity_numerical_state_changed_with_unit_trigger(
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.NITROGEN_MONOXIDE)},
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
NitrogenMonoxideConcentrationConverter,
),
"no_crossed_threshold": (
@@ -139,13 +133,13 @@ TRIGGERS: dict[str, type[Trigger]] = {
device_class=SensorDeviceClass.NITROGEN_MONOXIDE
)
},
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
NitrogenMonoxideConcentrationConverter,
)
),
"no2_changed": make_entity_numerical_state_changed_with_unit_trigger(
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.NITROGEN_DIOXIDE)},
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
NitrogenDioxideConcentrationConverter,
),
"no2_crossed_threshold": (
@@ -155,70 +149,70 @@ TRIGGERS: dict[str, type[Trigger]] = {
device_class=SensorDeviceClass.NITROGEN_DIOXIDE
)
},
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
NitrogenDioxideConcentrationConverter,
)
),
"so2_changed": make_entity_numerical_state_changed_with_unit_trigger(
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.SULPHUR_DIOXIDE)},
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
SulphurDioxideConcentrationConverter,
),
"so2_crossed_threshold": (
make_entity_numerical_state_crossed_threshold_with_unit_trigger(
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.SULPHUR_DIOXIDE)},
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
SulphurDioxideConcentrationConverter,
)
),
# Numerical sensor triggers without unit conversion (single-unit device classes)
"co2_changed": make_entity_numerical_state_changed_trigger(
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.CO2)},
valid_unit=CONCENTRATION_PARTS_PER_MILLION,
valid_unit=UnitOfRatio.PARTS_PER_MILLION,
),
"co2_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.CO2)},
valid_unit=CONCENTRATION_PARTS_PER_MILLION,
valid_unit=UnitOfRatio.PARTS_PER_MILLION,
),
"pm1_changed": make_entity_numerical_state_changed_trigger(
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM1)},
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
valid_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
),
"pm1_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM1)},
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
valid_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
),
"pm25_changed": make_entity_numerical_state_changed_trigger(
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM25)},
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
valid_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
),
"pm25_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM25)},
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
valid_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
),
"pm4_changed": make_entity_numerical_state_changed_trigger(
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM4)},
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
valid_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
),
"pm4_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM4)},
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
valid_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
),
"pm10_changed": make_entity_numerical_state_changed_trigger(
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM10)},
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
valid_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
),
"pm10_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.PM10)},
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
valid_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
),
"n2o_changed": make_entity_numerical_state_changed_trigger(
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.NITROUS_OXIDE)},
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
valid_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
),
"n2o_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
{SENSOR_DOMAIN: DomainSpec(device_class=SensorDeviceClass.NITROUS_OXIDE)},
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
valid_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
),
}
+12 -15
View File
@@ -27,7 +27,6 @@ from homeassistant.const import ( # noqa: F401
CONF_PATH,
CONF_TRIGGERS,
CONF_VARIABLES,
EVENT_HOMEASSISTANT_STARTED,
SERVICE_RELOAD,
SERVICE_TOGGLE,
SERVICE_TURN_OFF,
@@ -38,7 +37,7 @@ from homeassistant.core import (
CALLBACK_TYPE,
Context,
CoreState,
Event,
HassJob,
HomeAssistant,
ServiceCall,
callback,
@@ -830,13 +829,13 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity):
if self._condition is not None:
self._condition.async_unload()
async def _async_enable_automation(self, event: Event) -> None:
"""Start automation on startup."""
async def _async_enable_automation(self) -> None:
"""Arm the automation's triggers on startup."""
# Don't do anything if no longer enabled or already attached
if not self._is_enabled or self._async_detach_triggers is not None:
return
self._async_detach_triggers = await self._async_attach_triggers(True)
self._async_detach_triggers = await self._async_attach_triggers()
self.async_write_ha_state()
async def _async_enable(self) -> None:
@@ -851,13 +850,14 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity):
self._is_enabled = True
# HomeAssistant is starting up
if self.hass.state is not CoreState.not_running:
self._async_detach_triggers = await self._async_attach_triggers(False)
self._async_detach_triggers = await self._async_attach_triggers()
return
self.hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STARTED,
self._async_enable_automation,
)
# Arm the triggers in a startup job, which runs after all listeners to
# EVENT_HOMEASSISTANT_START have run but before EVENT_HOMEASSISTANT_STARTED
# has fired. This ensures automations do not fire during startup, but
# triggers listening for the started event are armed in time to catch it.
self.hass.async_add_startup_job(HassJob(self._async_enable_automation))
async def _async_disable(self, stop_actions: bool = DEFAULT_STOP_ACTIONS) -> None:
"""Disable the automation entity.
@@ -942,9 +942,7 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity):
script_execution_set("not_triggered")
async def _async_attach_triggers(
self, home_assistant_start: bool
) -> Callable[[], None] | None:
async def _async_attach_triggers(self) -> Callable[[], None] | None:
"""Set up the triggers."""
this = None
if state := self.hass.states.get(self.entity_id):
@@ -968,8 +966,7 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity):
DOMAIN,
str(self.name),
self._log_callback,
home_assistant_start,
variables,
variables=variables,
did_not_trigger=self._handle_not_triggered,
)
@@ -1,7 +1,7 @@
{
"domain": "bluetooth_adapters",
"name": "Bluetooth Adapters",
"after_dependencies": ["esphome", "shelly", "ruuvi_gateway"],
"after_dependencies": ["esphome", "shelly", "ruuvi_gateway", "smlight"],
"codeowners": ["@bdraco"],
"dependencies": ["bluetooth"],
"documentation": "https://www.home-assistant.io/integrations/bluetooth_adapters",
@@ -6,5 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/ccm15",
"integration_type": "hub",
"iot_class": "local_polling",
"quality_scale": "bronze",
"requirements": ["py_ccm15==0.6.0"]
}
@@ -0,0 +1,90 @@
rules:
# Bronze
action-setup:
status: exempt
comment: This integration does not register any service actions.
appropriate-polling: done
brands: done
common-modules: done
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions:
status: exempt
comment: This integration does not register any service actions.
docs-conditions:
status: exempt
comment: This integration does not have any conditions.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
docs-triggers:
status: exempt
comment: This integration does not have any triggers.
entity-event-setup:
status: exempt
comment: Entities poll through the coordinator and do not subscribe to events.
entity-unique-id: done
has-entity-name: done
runtime-data: done
test-before-configure: done
test-before-setup: done
unique-config-entry: done
# Silver
action-exceptions:
status: exempt
comment: This integration does not register any service actions.
config-entry-unloading: done
docs-configuration-parameters:
status: exempt
comment: This integration has no options flow.
docs-installation-parameters: todo
entity-unavailable: todo
integration-owner: done
log-when-unavailable: todo
parallel-updates: todo
reauthentication-flow:
status: exempt
comment: The device connection is unauthenticated; revisit when optional password (pwd=) support lands.
test-coverage: todo
# Gold
devices: done
diagnostics: done
discovery-update-info: todo
discovery: todo
docs-data-update: todo
docs-examples: todo
docs-known-limitations: todo
docs-supported-devices: todo
docs-supported-functions: todo
docs-troubleshooting: todo
docs-use-cases: todo
dynamic-devices: todo
entity-category:
status: exempt
comment: The single climate entity does not need a non-default category.
entity-device-class:
status: exempt
comment: The climate entity has no applicable device class.
entity-disabled-by-default:
status: exempt
comment: Only primary climate entities are provided.
entity-translations:
status: exempt
comment: The climate entity uses the device name; no entity names to translate.
exception-translations: todo
icon-translations:
status: exempt
comment: The integration uses default entity icons.
reconfiguration-flow: todo
repair-issues:
status: exempt
comment: This integration does not raise repairable issues.
stale-devices: todo
# Platinum
async-dependency: done
inject-websession: done
strict-typing: todo
@@ -12,6 +12,10 @@
"data": {
"host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]"
},
"data_description": {
"host": "The hostname or IP address of your CCM15 controller.",
"port": "The TCP port of the CCM15 controller's HTTP interface."
}
}
}
@@ -0,0 +1,26 @@
"""Diagnostics platform for CentriConnect/MyPropane API integration."""
from typing import Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.core import HomeAssistant
from . import CentriConnectConfigEntry
TO_REDACT = {"Latitude", "Longitude", "Altitude"}
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: CentriConnectConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for the provided config entry."""
coord = entry.runtime_data
return {
"device_info": {
"device_id": coord.device_info.device_id,
"device_name": coord.device_info.device_name,
"hardware_version": coord.device_info.hardware_version,
"lte_version": coord.device_info.lte_version,
},
"tank_data": async_redact_data(coord.data.raw_data, TO_REDACT),
}
@@ -47,7 +47,7 @@ rules:
# Gold
devices: done
diagnostics: todo
diagnostics: done
discovery:
status: exempt
comment: This is a cloud polling integration with no local discovery mechanism.
@@ -8,7 +8,7 @@
"iot_class": "local_polling",
"loggers": ["pyenphase"],
"quality_scale": "platinum",
"requirements": ["pyenphase==3.0.0"],
"requirements": ["pyenphase==3.0.1"],
"zeroconf": [
{
"type": "_enphase-envoy._tcp.local."
@@ -2,8 +2,8 @@
import voluptuous as vol
from homeassistant.const import CONF_EVENT, CONF_PLATFORM
from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant
from homeassistant.const import CONF_EVENT, CONF_PLATFORM, EVENT_HOMEASSISTANT_STARTED
from homeassistant.core import CALLBACK_TYPE, Event, HassJob, HomeAssistant, callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
from homeassistant.helpers.typing import ConfigType
@@ -45,9 +45,12 @@ async def async_attach_trigger(
},
)
# Automation are enabled while hass is starting up, fire right away
# Check state because a config reload shouldn't trigger it.
if trigger_info["home_assistant_start"]:
unsub: CALLBACK_TYPE | None = None
@callback
def hass_started(_: Event) -> None:
nonlocal unsub
unsub = None
hass.async_run_hass_job(
job,
{
@@ -60,4 +63,13 @@ async def async_attach_trigger(
},
)
return lambda: None
# Only fires if armed before EVENT_HOMEASSISTANT_STARTED; if hass is already
# started, the trigger doesn't fire.
unsub = hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, hass_started)
@callback
def remove() -> None:
if unsub is not None:
unsub()
return remove
+7 -8
View File
@@ -18,13 +18,10 @@ from homeassistant.components.climate import (
from homeassistant.components.lock import LockState
from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_MILLION,
CURRENCY_CENT,
CURRENCY_DOLLAR,
DEGREE,
LIGHT_LUX,
PERCENTAGE,
REVOLUTIONS_PER_MINUTE,
SERVICE_LOCK,
SERVICE_UNLOCK,
@@ -40,6 +37,7 @@ from homeassistant.const import (
UV_INDEX,
Platform,
UnitOfApparentPower,
UnitOfDensity,
UnitOfElectricCurrent,
UnitOfElectricPotential,
UnitOfEnergy,
@@ -49,6 +47,7 @@ from homeassistant.const import (
UnitOfMass,
UnitOfPower,
UnitOfPressure,
UnitOfRatio,
UnitOfReactivePower,
UnitOfSoundPressure,
UnitOfSpeed,
@@ -341,8 +340,8 @@ UOM_FRIENDLY_NAME = {
"18": UnitOfLength.FEET,
"19": UnitOfTime.HOURS,
"20": UnitOfTime.HOURS,
"21": PERCENTAGE,
"22": PERCENTAGE,
"21": UnitOfRatio.PERCENTAGE,
"22": UnitOfRatio.PERCENTAGE,
"23": UnitOfPressure.INHG,
"24": UnitOfVolumetricFlux.INCHES_PER_HOUR,
UOM_INDEX: UOM_INDEX, # Index type. Use "node.formatted" for value
@@ -371,10 +370,10 @@ UOM_FRIENDLY_NAME = {
"48": UnitOfSpeed.MILES_PER_HOUR,
"49": UnitOfSpeed.METERS_PER_SECOND,
"50": "",
UOM_PERCENTAGE: PERCENTAGE,
UOM_PERCENTAGE: UnitOfRatio.PERCENTAGE,
"52": UnitOfMass.POUNDS,
"53": "pf",
"54": CONCENTRATION_PARTS_PER_MILLION,
"54": UnitOfRatio.PARTS_PER_MILLION,
"55": "pulse count",
"57": UnitOfTime.SECONDS,
"58": UnitOfTime.SECONDS,
@@ -423,7 +422,7 @@ UOM_FRIENDLY_NAME = {
"118": UnitOfPressure.HPA,
"119": UnitOfEnergy.WATT_HOUR,
"120": UnitOfVolumetricFlux.INCHES_PER_DAY,
"122": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, # Microgram per cubic meter
"122": UnitOfDensity.MICROGRAMS_PER_CUBIC_METER, # Microgram per cubic meter
"123": f"bq/{UnitOfVolume.CUBIC_METERS}", # Becquerel per cubic meter
"124": f"pCi/{UnitOfVolume.LITERS}", # Picocuries per liter
"125": "pH",
+6 -13
View File
@@ -2,14 +2,7 @@
from datetime import timedelta
from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION,
PERCENTAGE,
Platform,
)
from homeassistant.const import Platform, UnitOfDensity, UnitOfRatio
DOMAIN = "kaiterra"
@@ -55,13 +48,13 @@ ATTR_AQI_POLLUTANT = "air_quality_index_pollutant"
AVAILABLE_AQI_STANDARDS = ["us", "cn", "in"]
AVAILABLE_UNITS = [
"x",
PERCENTAGE,
UnitOfRatio.PERCENTAGE,
"C",
"F",
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_MILLION,
CONCENTRATION_PARTS_PER_BILLION,
UnitOfDensity.MILLIGRAMS_PER_CUBIC_METER,
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
UnitOfRatio.PARTS_PER_MILLION,
UnitOfRatio.PARTS_PER_BILLION,
]
AVAILABLE_DEVICE_TYPES = ["laseregg", "sensedge"]
@@ -227,8 +227,8 @@ class MikrotikData:
_LOGGER.debug("Running command %s", cmd)
try:
if params:
return list(self.api(cmd=cmd, **params))
return list(self.api(cmd=cmd))
return list(self.api(cmd, **params))
return list(self.api(cmd))
except (
librouteros.exceptions.ConnectionClosed,
OSError,
@@ -318,8 +318,7 @@ def get_api(entry: dict[str, Any]) -> librouteros.Api:
"""Connect to Mikrotik hub."""
_LOGGER.debug("Connecting to Mikrotik hub [%s]", entry[CONF_HOST])
_login_method = (login_plain, login_token)
kwargs = {"login_methods": _login_method, "port": entry["port"], "encoding": "utf8"}
kwargs = {"port": entry["port"], "encoding": "utf8"}
if entry[CONF_VERIFY_SSL]:
ssl_context = ssl.create_default_context()
@@ -328,22 +327,30 @@ def get_api(entry: dict[str, Any]) -> librouteros.Api:
_ssl_wrapper = ssl_context.wrap_socket
kwargs["ssl_wrapper"] = _ssl_wrapper
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
_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
_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==3.2.1"]
"requirements": ["librouteros==4.1.1"]
}
+1 -2
View File
@@ -71,7 +71,6 @@ from .const import (
DEFAULT_QOS,
DEFAULT_TRANSPORT,
DEFAULT_WILL,
DEFAULT_WS_HEADERS,
DEFAULT_WS_PATH,
DOMAIN,
MQTT_CONNECTION_STATE,
@@ -414,7 +413,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, DEFAULT_WS_HEADERS)
ws_headers: dict[str, str] = config.get(CONF_WS_HEADERS, {})
self._client.ws_set_options(ws_path, ws_headers)
if certificate is not None:
self._client.tls_set(
+251 -365
View File
@@ -373,7 +373,6 @@ 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,
@@ -414,7 +413,6 @@ from .const import (
DEFAULT_TILT_OPEN_POSITION,
DEFAULT_TRANSPORT,
DEFAULT_WILL,
DEFAULT_WS_PATH,
DOMAIN,
REMOTE_CODE,
REMOTE_CODE_TEXT,
@@ -441,7 +439,7 @@ ADDON_SETUP_TIMEOUT_ROUNDS = 5
CONF_CLIENT_KEY_PASSWORD = "client_key_password"
ADVANCED_OPTIONS = "advanced_options"
OTHER_SETTINGS = "other_settings"
SET_CA_CERT = "set_ca_cert"
SET_CLIENT_CERT = "set_client_cert"
@@ -1124,7 +1122,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
@@ -1506,7 +1504,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,
@@ -1678,13 +1676,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: {
@@ -3125,7 +3123,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,
@@ -3133,7 +3131,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,
@@ -3141,7 +3139,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,
@@ -3149,21 +3147,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: {
@@ -3372,7 +3370,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: {
@@ -3798,10 +3796,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),
@@ -4036,24 +4034,22 @@ def subentry_schema_default_data_from_fields(
@callback
def update_password_from_user_input(
entry_password: str | None, user_input: dict[str, Any]
) -> dict[str, Any]:
) -> None:
"""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 = substituted_used_data.pop(CONF_PASSWORD, None)
user_password: str | None = user_input.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:
substituted_used_data[CONF_PASSWORD] = password
return substituted_used_data
user_input[CONF_PASSWORD] = password
REAUTH_SCHEMA = vol.Schema(
@@ -4063,6 +4059,35 @@ 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."""
@@ -4072,24 +4097,26 @@ 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:
@@ -4310,8 +4337,9 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
reauth_entry = self._get_reauth_entry()
if user_input:
substituted_used_data = update_password_from_user_input(
reauth_entry.data.get(CONF_PASSWORD), user_input
substituted_used_data = deepcopy(user_input)
update_password_from_user_input(
reauth_entry.data.get(CONF_PASSWORD), substituted_used_data
)
new_entry_data = {**reauth_entry.data, **substituted_used_data}
if await self.hass.async_add_executor_job(
@@ -4335,49 +4363,76 @@ 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] = {}
fields: OrderedDict[Any, Any] = OrderedDict()
validated_user_input: dict[str, Any] = {}
schema = CONFIG_DATAFLOW_SCHEMA
entry_config_update: dict[str, Any] = {}
entry_defaults: dict[str, Any] | None = None
if is_reconfigure := (self.source == SOURCE_RECONFIGURE):
reconfigure_entry = self._get_reconfigure_entry()
if await async_get_broker_settings(
entry_defaults = self.async_get_entry_defaults()
if await async_validate_broker_settings(
self,
fields,
reconfigure_entry.data if is_reconfigure else None,
user_input,
validated_user_input,
entry_config_update,
errors,
):
if is_reconfigure:
validated_user_input = update_password_from_user_input(
reconfigure_entry.data.get(CONF_PASSWORD), validated_user_input
return self.async_update_and_abort(
reconfigure_entry,
data=entry_config_update,
)
can_connect = await self.hass.async_add_executor_job(
try_connection,
validated_user_input,
return self.async_create_entry(
title=entry_config_update[CONF_BROKER],
data=entry_config_update,
)
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
schema = self.add_suggested_values_to_schema(
schema, (entry_defaults or {}) | (user_input or {})
)
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
@@ -4688,8 +4743,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:
@@ -5250,331 +5305,162 @@ async def _get_uploaded_file(hass: HomeAssistant, id: str) -> bytes:
return await hass.async_add_executor_job(_proces_uploaded_file)
def _validate_pki_file(
file_id: str | None, pem_data: str | None, errors: dict[str, str], error: str
) -> bool:
"""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
async def async_get_broker_settings(
flow: ConfigFlow | OptionsFlow,
fields: OrderedDict[Any, Any],
async def async_validate_broker_settings(
flow: FlowHandler,
entry_config: MappingProxyType[str, Any] | None,
user_input: dict[str, Any] | None,
validated_user_input: dict[str, Any],
entry_config_update: dict[str, Any],
errors: dict[str, str],
) -> bool:
"""Build the config flow schema to collect the broker settings.
"""Validate the broker settings, and return the updated entry dataset."""
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],
async def _async_process_file_upload(
upload_id: str,
field: str,
pem_type: PEMType,
error_code: str,
password: str | None = None,
) -> 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
)
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
"""Get uploaded file, or a preserved copy, and convert to a PEM file."""
try:
validated_user_input[CONF_WS_HEADERS] = json_loads(
validated_user_input.get(CONF_WS_HEADERS, "{}")
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,
):
errors["base"] = error
return False
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, "{}")
)
schema = vol.Schema({cv.string: cv.template})
schema(validated_user_input[CONF_WS_HEADERS])
schema = vol.Schema({str: str})
schema(entry_config_update[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
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
errors["base"] = "cannot_connect"
return False
-1
View File
@@ -315,7 +315,6 @@ 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
+36 -71
View File
@@ -26,46 +26,53 @@
"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",
"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"
"username": "[%key:common::config_flow::data::username%]"
},
"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.",
"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."
"username": "The username to log in to your MQTT broker."
},
"description": "Please enter the connection information of 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"
}
}
},
"hassio_confirm": {
"description": "Do you want to configure Home Assistant to connect to the MQTT broker provided by the {addon} app?",
@@ -1178,48 +1185,6 @@
"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",
+34 -39
View File
@@ -6,15 +6,8 @@ from typing import Final
import voluptuous as vol
from homeassistant.const import (
CONCENTRATION_GRAMS_PER_CUBIC_METER,
CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION,
DEGREE,
LIGHT_LUX,
PERCENTAGE,
SIGNAL_STRENGTH_DECIBELS,
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
UnitOfApparentPower,
@@ -22,6 +15,7 @@ from homeassistant.const import (
UnitOfBloodGlucoseConcentration,
UnitOfConductivity,
UnitOfDataRate,
UnitOfDensity,
UnitOfElectricCurrent,
UnitOfElectricPotential,
UnitOfEnergy,
@@ -34,6 +28,7 @@ from homeassistant.const import (
UnitOfPower,
UnitOfPrecipitationDepth,
UnitOfPressure,
UnitOfRatio,
UnitOfReactiveEnergy,
UnitOfReactivePower,
UnitOfSoundPressure,
@@ -516,22 +511,22 @@ class NumberDeviceClass(StrEnum):
DEVICE_CLASSES_SCHEMA: Final = vol.All(vol.Lower, vol.Coerce(NumberDeviceClass))
DEVICE_CLASS_UNITS: dict[NumberDeviceClass, set[type[StrEnum] | str | None]] = {
NumberDeviceClass.ABSOLUTE_HUMIDITY: {
CONCENTRATION_GRAMS_PER_CUBIC_METER,
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
UnitOfDensity.GRAMS_PER_CUBIC_METER,
UnitOfDensity.MILLIGRAMS_PER_CUBIC_METER,
},
NumberDeviceClass.APPARENT_POWER: set(UnitOfApparentPower),
NumberDeviceClass.AQI: {None},
NumberDeviceClass.AREA: set(UnitOfArea),
NumberDeviceClass.ATMOSPHERIC_PRESSURE: set(UnitOfPressure),
NumberDeviceClass.BATTERY: {PERCENTAGE},
NumberDeviceClass.BATTERY: {UnitOfRatio.PERCENTAGE},
NumberDeviceClass.BLOOD_GLUCOSE_CONCENTRATION: set(UnitOfBloodGlucoseConcentration),
NumberDeviceClass.CO: {
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION,
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
UnitOfRatio.PARTS_PER_BILLION,
UnitOfRatio.PARTS_PER_MILLION,
UnitOfDensity.MILLIGRAMS_PER_CUBIC_METER,
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
},
NumberDeviceClass.CO2: {CONCENTRATION_PARTS_PER_MILLION},
NumberDeviceClass.CO2: {UnitOfRatio.PARTS_PER_MILLION},
NumberDeviceClass.CONDUCTIVITY: set(UnitOfConductivity),
NumberDeviceClass.CURRENT: set(UnitOfElectricCurrent),
NumberDeviceClass.DATA_RATE: set(UnitOfDataRate),
@@ -556,31 +551,31 @@ DEVICE_CLASS_UNITS: dict[NumberDeviceClass, set[type[StrEnum] | str | None]] = {
UnitOfVolume.LITERS,
UnitOfVolume.MILLE_CUBIC_FEET,
},
NumberDeviceClass.HUMIDITY: {PERCENTAGE},
NumberDeviceClass.HUMIDITY: {UnitOfRatio.PERCENTAGE},
NumberDeviceClass.ILLUMINANCE: {LIGHT_LUX},
NumberDeviceClass.IRRADIANCE: set(UnitOfIrradiance),
NumberDeviceClass.MOISTURE: {PERCENTAGE},
NumberDeviceClass.MOISTURE: {UnitOfRatio.PERCENTAGE},
NumberDeviceClass.NITROGEN_DIOXIDE: {
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
UnitOfRatio.PARTS_PER_BILLION,
UnitOfRatio.PARTS_PER_MILLION,
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
},
NumberDeviceClass.NITROGEN_MONOXIDE: {
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
UnitOfRatio.PARTS_PER_BILLION,
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
},
NumberDeviceClass.NITROUS_OXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
NumberDeviceClass.NITROUS_OXIDE: {UnitOfDensity.MICROGRAMS_PER_CUBIC_METER},
NumberDeviceClass.OZONE: {
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
UnitOfRatio.PARTS_PER_BILLION,
UnitOfRatio.PARTS_PER_MILLION,
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
},
NumberDeviceClass.PH: {None},
NumberDeviceClass.PM1: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
NumberDeviceClass.PM10: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
NumberDeviceClass.PM25: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
NumberDeviceClass.PM4: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
NumberDeviceClass.POWER_FACTOR: {PERCENTAGE, None},
NumberDeviceClass.PM1: {UnitOfDensity.MICROGRAMS_PER_CUBIC_METER},
NumberDeviceClass.PM10: {UnitOfDensity.MICROGRAMS_PER_CUBIC_METER},
NumberDeviceClass.PM25: {UnitOfDensity.MICROGRAMS_PER_CUBIC_METER},
NumberDeviceClass.PM4: {UnitOfDensity.MICROGRAMS_PER_CUBIC_METER},
NumberDeviceClass.POWER_FACTOR: {UnitOfRatio.PERCENTAGE, None},
NumberDeviceClass.POWER: {
UnitOfPower.MILLIWATT,
UnitOfPower.WATT,
@@ -601,18 +596,18 @@ DEVICE_CLASS_UNITS: dict[NumberDeviceClass, set[type[StrEnum] | str | None]] = {
NumberDeviceClass.SOUND_PRESSURE: set(UnitOfSoundPressure),
NumberDeviceClass.SPEED: {*UnitOfSpeed, *UnitOfVolumetricFlux},
NumberDeviceClass.SULPHUR_DIOXIDE: {
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
UnitOfRatio.PARTS_PER_BILLION,
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
},
NumberDeviceClass.TEMPERATURE: set(UnitOfTemperature),
NumberDeviceClass.TEMPERATURE_DELTA: set(UnitOfTemperature),
NumberDeviceClass.VOLATILE_ORGANIC_COMPOUNDS: {
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
UnitOfDensity.MILLIGRAMS_PER_CUBIC_METER,
},
NumberDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS: {
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION,
UnitOfRatio.PARTS_PER_BILLION,
UnitOfRatio.PARTS_PER_MILLION,
},
NumberDeviceClass.VOLTAGE: set(UnitOfElectricPotential),
NumberDeviceClass.VOLUME: set(UnitOfVolume),
@@ -681,8 +676,8 @@ AMBIGUOUS_UNITS: dict[str | None, str] = {
"\u00b5Sv/h": "μSv/h", # aranet: radiation rate
"\u00b5S/cm": UnitOfConductivity.MICROSIEMENS_PER_CM,
"\u00b5V": UnitOfElectricPotential.MICROVOLT,
"\u00b5g/ft³": CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT,
"\u00b5g/m³": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
"\u00b5g/ft³": UnitOfDensity.MICROGRAMS_PER_CUBIC_FOOT,
"\u00b5g/m³": UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
"\u00b5mol/s⋅m²": "μmol/s⋅m²", # fyta: light
"\u00b5g": UnitOfMass.MICROGRAMS,
"\u00b5s": UnitOfTime.MICROSECONDS,
@@ -5,11 +5,10 @@ from screenlogicpy.device_const.circuit import FUNCTION
from screenlogicpy.device_const.system import COLOR_MODE
from homeassistant.const import (
CONCENTRATION_PARTS_PER_MILLION,
PERCENTAGE,
REVOLUTIONS_PER_MINUTE,
UnitOfElectricPotential,
UnitOfPower,
UnitOfRatio,
UnitOfTemperature,
UnitOfTime,
)
@@ -53,6 +52,6 @@ SL_UNIT_TO_HA_UNIT = {
UNIT.HOUR: UnitOfTime.HOURS,
UNIT.SECOND: UnitOfTime.SECONDS,
UNIT.REVOLUTIONS_PER_MINUTE: REVOLUTIONS_PER_MINUTE,
UNIT.PARTS_PER_MILLION: CONCENTRATION_PARTS_PER_MILLION,
UNIT.PERCENT: PERCENTAGE,
UNIT.PARTS_PER_MILLION: UnitOfRatio.PARTS_PER_MILLION,
UNIT.PERCENT: UnitOfRatio.PERCENTAGE,
}
+35 -40
View File
@@ -6,15 +6,8 @@ from typing import Final
import voluptuous as vol
from homeassistant.const import (
CONCENTRATION_GRAMS_PER_CUBIC_METER,
CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION,
DEGREE,
LIGHT_LUX,
PERCENTAGE,
SIGNAL_STRENGTH_DECIBELS,
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
UnitOfApparentPower,
@@ -22,6 +15,7 @@ from homeassistant.const import (
UnitOfBloodGlucoseConcentration,
UnitOfConductivity,
UnitOfDataRate,
UnitOfDensity,
UnitOfElectricCurrent,
UnitOfElectricPotential,
UnitOfEnergy,
@@ -34,6 +28,7 @@ from homeassistant.const import (
UnitOfPower,
UnitOfPrecipitationDepth,
UnitOfPressure,
UnitOfRatio,
UnitOfReactiveEnergy,
UnitOfReactivePower,
UnitOfSoundPressure,
@@ -635,22 +630,22 @@ UNIT_CONVERTERS: dict[SensorDeviceClass | str | None, type[BaseUnitConverter]] =
DEVICE_CLASS_UNITS: dict[SensorDeviceClass, set[type[StrEnum] | str | None]] = {
SensorDeviceClass.ABSOLUTE_HUMIDITY: {
CONCENTRATION_GRAMS_PER_CUBIC_METER,
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
UnitOfDensity.GRAMS_PER_CUBIC_METER,
UnitOfDensity.MILLIGRAMS_PER_CUBIC_METER,
},
SensorDeviceClass.APPARENT_POWER: set(UnitOfApparentPower),
SensorDeviceClass.AQI: {None},
SensorDeviceClass.AREA: set(UnitOfArea),
SensorDeviceClass.ATMOSPHERIC_PRESSURE: set(UnitOfPressure),
SensorDeviceClass.BATTERY: {PERCENTAGE},
SensorDeviceClass.BATTERY: {UnitOfRatio.PERCENTAGE},
SensorDeviceClass.BLOOD_GLUCOSE_CONCENTRATION: set(UnitOfBloodGlucoseConcentration),
SensorDeviceClass.CO: {
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION,
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
UnitOfRatio.PARTS_PER_BILLION,
UnitOfRatio.PARTS_PER_MILLION,
UnitOfDensity.MILLIGRAMS_PER_CUBIC_METER,
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
},
SensorDeviceClass.CO2: {CONCENTRATION_PARTS_PER_MILLION},
SensorDeviceClass.CO2: {UnitOfRatio.PARTS_PER_MILLION},
SensorDeviceClass.CONDUCTIVITY: set(UnitOfConductivity),
SensorDeviceClass.CURRENT: set(UnitOfElectricCurrent),
SensorDeviceClass.DATA_RATE: set(UnitOfDataRate),
@@ -675,31 +670,31 @@ DEVICE_CLASS_UNITS: dict[SensorDeviceClass, set[type[StrEnum] | str | None]] = {
UnitOfVolume.LITERS,
UnitOfVolume.MILLE_CUBIC_FEET,
},
SensorDeviceClass.HUMIDITY: {PERCENTAGE},
SensorDeviceClass.HUMIDITY: {UnitOfRatio.PERCENTAGE},
SensorDeviceClass.ILLUMINANCE: {LIGHT_LUX},
SensorDeviceClass.IRRADIANCE: set(UnitOfIrradiance),
SensorDeviceClass.MOISTURE: {PERCENTAGE},
SensorDeviceClass.MOISTURE: {UnitOfRatio.PERCENTAGE},
SensorDeviceClass.NITROGEN_DIOXIDE: {
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
UnitOfRatio.PARTS_PER_BILLION,
UnitOfRatio.PARTS_PER_MILLION,
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
},
SensorDeviceClass.NITROGEN_MONOXIDE: {
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
UnitOfRatio.PARTS_PER_BILLION,
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
},
SensorDeviceClass.NITROUS_OXIDE: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
SensorDeviceClass.NITROUS_OXIDE: {UnitOfDensity.MICROGRAMS_PER_CUBIC_METER},
SensorDeviceClass.OZONE: {
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
UnitOfRatio.PARTS_PER_BILLION,
UnitOfRatio.PARTS_PER_MILLION,
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
},
SensorDeviceClass.PH: {None},
SensorDeviceClass.PM1: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
SensorDeviceClass.PM10: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
SensorDeviceClass.PM25: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
SensorDeviceClass.PM4: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
SensorDeviceClass.POWER_FACTOR: {PERCENTAGE, None},
SensorDeviceClass.PM1: {UnitOfDensity.MICROGRAMS_PER_CUBIC_METER},
SensorDeviceClass.PM10: {UnitOfDensity.MICROGRAMS_PER_CUBIC_METER},
SensorDeviceClass.PM25: {UnitOfDensity.MICROGRAMS_PER_CUBIC_METER},
SensorDeviceClass.PM4: {UnitOfDensity.MICROGRAMS_PER_CUBIC_METER},
SensorDeviceClass.POWER_FACTOR: {UnitOfRatio.PERCENTAGE, None},
SensorDeviceClass.POWER: {
UnitOfPower.MILLIWATT,
UnitOfPower.WATT,
@@ -720,18 +715,18 @@ DEVICE_CLASS_UNITS: dict[SensorDeviceClass, set[type[StrEnum] | str | None]] = {
SensorDeviceClass.SOUND_PRESSURE: set(UnitOfSoundPressure),
SensorDeviceClass.SPEED: {*UnitOfSpeed, *UnitOfVolumetricFlux},
SensorDeviceClass.SULPHUR_DIOXIDE: {
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
UnitOfRatio.PARTS_PER_BILLION,
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
},
SensorDeviceClass.TEMPERATURE: set(UnitOfTemperature),
SensorDeviceClass.TEMPERATURE_DELTA: set(UnitOfTemperature),
SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS: {
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
UnitOfDensity.MILLIGRAMS_PER_CUBIC_METER,
},
SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS: {
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION,
UnitOfRatio.PARTS_PER_BILLION,
UnitOfRatio.PARTS_PER_MILLION,
},
SensorDeviceClass.VOLTAGE: set(UnitOfElectricPotential),
SensorDeviceClass.VOLUME: set(UnitOfVolume),
@@ -758,7 +753,7 @@ DEFAULT_PRECISION_LIMIT = 2
# have 0 decimals, that one should be used and not mW, even though mW also should have
# 0 decimals. Otherwise the smaller units will have more decimals than expected.
UNITS_PRECISION = {
SensorDeviceClass.ABSOLUTE_HUMIDITY: (CONCENTRATION_GRAMS_PER_CUBIC_METER, 1),
SensorDeviceClass.ABSOLUTE_HUMIDITY: (UnitOfDensity.GRAMS_PER_CUBIC_METER, 1),
SensorDeviceClass.APPARENT_POWER: (UnitOfApparentPower.VOLT_AMPERE, 0),
SensorDeviceClass.AREA: (UnitOfArea.SQUARE_CENTIMETERS, 0),
SensorDeviceClass.ATMOSPHERIC_PRESSURE: (UnitOfPressure.PA, 0),
@@ -891,8 +886,8 @@ AMBIGUOUS_UNITS: dict[str | None, str] = {
"\u00b5Sv/h": "μSv/h", # aranet: radiation rate
"\u00b5S/cm": UnitOfConductivity.MICROSIEMENS_PER_CM,
"\u00b5V": UnitOfElectricPotential.MICROVOLT,
"\u00b5g/ft³": CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT,
"\u00b5g/m³": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
"\u00b5g/ft³": UnitOfDensity.MICROGRAMS_PER_CUBIC_FOOT,
"\u00b5g/m³": UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
"\u00b5mol/s⋅m²": "μmol/s⋅m²", # fyta: light
"\u00b5g": UnitOfMass.MICROGRAMS,
"\u00b5s": UnitOfTime.MICROSECONDS,
@@ -13,7 +13,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.2", "bleak-smlight==1.1.0"],
"zeroconf": [
{
"type": "_slzb-06._tcp.local."
@@ -96,7 +96,12 @@ PLATFORMS_BY_TYPE = {
],
SupportedModels.HUBMINI_MATTER.value: [Platform.SENSOR],
SupportedModels.CIRCULATOR_FAN.value: [Platform.FAN, Platform.SENSOR],
SupportedModels.STANDING_FAN.value: [Platform.SWITCH, Platform.SENSOR],
SupportedModels.STANDING_FAN.value: [
Platform.SELECT,
Platform.NUMBER,
Platform.SWITCH,
Platform.SENSOR,
],
SupportedModels.S10_VACUUM.value: [Platform.VACUUM, Platform.SENSOR],
SupportedModels.S20_VACUUM.value: [Platform.VACUUM, Platform.SENSOR],
SupportedModels.K10_VACUUM.value: [Platform.VACUUM, Platform.SENSOR],
+72 -1
View File
@@ -1,5 +1,7 @@
"""Number platform for SwitchBot devices."""
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from datetime import timedelta
import logging
from typing import override
@@ -8,7 +10,11 @@ import switchbot
from switchbot import SwitchbotOperationError
from switchbot.devices.meter_pro import MAX_TIME_OFFSET
from homeassistant.components.number import NumberDeviceClass, NumberEntity
from homeassistant.components.number import (
NumberDeviceClass,
NumberEntity,
NumberEntityDescription,
)
from homeassistant.const import EntityCategory, UnitOfTime
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -35,6 +41,11 @@ async def async_setup_entry(
async_add_entities(
[SwitchBotMeterProCO2DisplayTimeOffsetNumber(coordinator)], True
)
elif isinstance(coordinator.device, switchbot.SwitchbotStandingFan):
async_add_entities(
SwitchBotStandingFanOscillationAngleNumber(coordinator, desc)
for desc in OSCILLATION_NUMBER_DESCRIPTIONS
)
class SwitchBotMeterProCO2DisplayTimeOffsetNumber(SwitchbotEntity, NumberEntity):
@@ -78,3 +89,63 @@ class SwitchBotMeterProCO2DisplayTimeOffsetNumber(SwitchbotEntity, NumberEntity)
)
return
self._attr_native_value = round(offset_seconds / _SECONDS_IN_MINUTE)
@dataclass(frozen=True, kw_only=True)
class SwitchBotOscillationAngleNumberEntityDescription(NumberEntityDescription):
"""Describes a Standing Fan oscillation angle number entity."""
setter: Callable[[switchbot.SwitchbotStandingFan, int], Awaitable[None]]
OSCILLATION_NUMBER_DESCRIPTIONS: tuple[
SwitchBotOscillationAngleNumberEntityDescription, ...
] = (
SwitchBotOscillationAngleNumberEntityDescription(
key="horizontal_oscillation_angle",
translation_key="horizontal_oscillation_angle",
native_min_value=30,
native_max_value=90,
native_step=30,
setter=lambda device, angle: device.set_horizontal_oscillation_angle(angle),
),
SwitchBotOscillationAngleNumberEntityDescription(
key="vertical_oscillation_angle",
translation_key="vertical_oscillation_angle",
native_min_value=30,
native_max_value=90,
native_step=30,
setter=lambda device, angle: device.set_vertical_oscillation_angle(angle),
),
)
class SwitchBotStandingFanOscillationAngleNumber(SwitchbotEntity, NumberEntity):
"""Number entity for oscillation angle on Standing Fan.
Uses assumed_state=True because the device does not report its current
oscillation angle back to HA state is only known after the user sets it.
"""
entity_description: SwitchBotOscillationAngleNumberEntityDescription
_device: switchbot.SwitchbotStandingFan
_attr_assumed_state = True
_attr_entity_category = EntityCategory.CONFIG
def __init__(
self,
coordinator: SwitchbotDataUpdateCoordinator,
description: SwitchBotOscillationAngleNumberEntityDescription,
) -> None:
"""Initialize the oscillation angle number entity."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = f"{coordinator.base_unique_id}_{description.key}"
@exception_handler
@override
async def async_set_native_value(self, value: float) -> None:
"""Set oscillation angle."""
await self.entity_description.setter(self._device, int(value))
self._attr_native_value = value
self.async_write_ha_state()
+47 -1
View File
@@ -5,7 +5,7 @@ import logging
from typing import override
import switchbot
from switchbot import SwitchbotOperationError
from switchbot import NightLightState, SwitchbotOperationError
from homeassistant.components.select import SelectEntity
from homeassistant.const import EntityCategory
@@ -34,6 +34,8 @@ async def async_setup_entry(
if isinstance(coordinator.device, switchbot.SwitchbotMeterProCO2):
async_add_entities([SwitchBotMeterProCO2TimeFormatSelect(coordinator)], True)
elif isinstance(coordinator.device, switchbot.SwitchbotStandingFan):
async_add_entities([SwitchBotStandingFanNightLightSelect(coordinator)])
class SwitchBotMeterProCO2TimeFormatSelect(SwitchbotEntity, SelectEntity):
@@ -74,3 +76,47 @@ class SwitchBotMeterProCO2TimeFormatSelect(SwitchbotEntity, SelectEntity):
self._attr_current_option = (
TIME_FORMAT_12H if device_time["12h_mode"] else TIME_FORMAT_24H
)
NIGHT_LIGHT_OFF = "off"
NIGHT_LIGHT_LEVEL_1 = "level_1"
NIGHT_LIGHT_LEVEL_2 = "level_2"
NIGHT_LIGHT_OPTIONS = [NIGHT_LIGHT_OFF, NIGHT_LIGHT_LEVEL_1, NIGHT_LIGHT_LEVEL_2]
NIGHT_LIGHT_TO_STATE: dict[str, NightLightState] = {
NIGHT_LIGHT_OFF: NightLightState.OFF,
NIGHT_LIGHT_LEVEL_1: NightLightState.LEVEL_1,
NIGHT_LIGHT_LEVEL_2: NightLightState.LEVEL_2,
}
NIGHT_LIGHT_FROM_STATE: dict[int, str] = {
state.value: option for option, state in NIGHT_LIGHT_TO_STATE.items()
}
class SwitchBotStandingFanNightLightSelect(SwitchbotEntity, SelectEntity):
"""Select entity for night light on Standing Fan."""
_device: switchbot.SwitchbotStandingFan
_attr_entity_category = EntityCategory.CONFIG
_attr_translation_key = "night_light"
_attr_options = NIGHT_LIGHT_OPTIONS
def __init__(self, coordinator: SwitchbotDataUpdateCoordinator) -> None:
"""Initialize the select entity."""
super().__init__(coordinator)
self._attr_unique_id = f"{coordinator.base_unique_id}_night_light"
@property
@override
def current_option(self) -> str | None:
"""Return current night light state."""
state = self._device.get_night_light_state()
if state is None:
return None
return NIGHT_LIGHT_FROM_STATE.get(state)
@exception_handler
@override
async def async_select_option(self, option: str) -> None:
"""Set night light state."""
await self._device.set_night_light(NIGHT_LIGHT_TO_STATE[option])
self.async_write_ha_state()
@@ -287,9 +287,23 @@
"number": {
"display_time_offset": {
"name": "Display time offset"
},
"horizontal_oscillation_angle": {
"name": "Horizontal oscillation angle"
},
"vertical_oscillation_angle": {
"name": "Vertical oscillation angle"
}
},
"select": {
"night_light": {
"name": "Night light",
"state": {
"level_1": "Level 1",
"level_2": "Level 2",
"off": "[%key:common::state::off%]"
}
},
"time_format": {
"name": "Time format",
"state": {
@@ -126,7 +126,6 @@ class TriggerUpdateCoordinator(DataUpdateCoordinator):
DOMAIN,
self.name,
self.logger.log,
start_event is not None,
)
async def _handle_triggered_with_script(
+74 -13
View File
@@ -156,6 +156,8 @@ class EventStateReportedData(EventStateEventData):
# How long to wait until things that run on startup have to finish.
TIMEOUT_EVENT_START = 15
# How long to wait until startup jobs have to finish.
TIMEOUT_STARTUP_JOBS = 15
EVENTS_EXCLUDED_FROM_MATCH_ALL = {
@@ -416,6 +418,7 @@ class HomeAssistant:
self.timeout: TimeoutManager = TimeoutManager()
self._stop_future: concurrent.futures.Future[None] | None = None
self._shutdown_jobs: list[HassJobWithArgs] = []
self._startup_jobs: list[HassJobWithArgs] = []
self.import_executor = InterruptibleThreadPoolExecutor(
max_workers=1, thread_name_prefix="ImportExecutor"
)
@@ -503,6 +506,20 @@ class HomeAssistant:
"""
_LOGGER.info("Starting Home Assistant %s", __version__)
def _log_startup_blocked(tasks: set[asyncio.Future[Any]]) -> None:
"""Log when startup is blocked by tasks."""
_LOGGER.warning(
(
"Something is blocking Home Assistant from wrapping up the start up"
" phase. We're going to continue anyway. Please report the"
" following info at"
" https://github.com/home-assistant/core/issues: %s"
" The system is waiting for tasks: %s"
),
", ".join(self.config.components),
tasks,
)
self.set_state(CoreState.starting)
self.bus.async_fire_internal(EVENT_CORE_CONFIG_UPDATE)
self.bus.async_fire_internal(EVENT_HOMEASSISTANT_START)
@@ -515,20 +532,23 @@ class HomeAssistant:
)
if pending:
_LOGGER.warning(
(
"Something is blocking Home Assistant from wrapping up the start up"
" phase. We're going to continue anyway. Please report the"
" following info at"
" https://github.com/home-assistant/core/issues: %s"
" The system is waiting for tasks: %s"
),
", ".join(self.config.components),
self._tasks,
)
_log_startup_blocked(self._tasks)
# Allow automations to set up the start triggers before changing state
await asyncio.sleep(0)
# Run startup jobs
tasks: list[asyncio.Future[Any]] = []
for job in self._startup_jobs:
task_or_none = self.async_run_hass_job(job.job, *job.args)
if task_or_none is None:
continue
tasks.append(task_or_none)
self._startup_jobs.clear()
if not tasks:
pending = None
else:
_done, pending = await asyncio.wait(tasks, timeout=TIMEOUT_STARTUP_JOBS)
if pending:
_log_startup_blocked(pending)
if self.state is not CoreState.starting:
_LOGGER.warning(
@@ -1035,6 +1055,9 @@ class HomeAssistant:
) -> CALLBACK_TYPE:
"""Add a HassJob which will be executed on shutdown.
The job will be called (and awaited if it returns a coroutine) before firing
of event EVENT_HOMEASSISTANT_STOP when Home Assistant is shutting down.
This method must be run in the event loop.
hassjob: HassJob
@@ -1051,6 +1074,44 @@ class HomeAssistant:
return remove_job
@overload
@callback
def async_add_startup_job(
self, hassjob: HassJob[..., Coroutine[Any, Any, Any]], *args: Any
) -> CALLBACK_TYPE: ...
@overload
@callback
def async_add_startup_job(
self, hassjob: HassJob[..., Coroutine[Any, Any, Any] | Any], *args: Any
) -> CALLBACK_TYPE: ...
@callback
def async_add_startup_job(
self, hassjob: HassJob[..., Coroutine[Any, Any, Any] | Any], *args: Any
) -> CALLBACK_TYPE:
"""Add a HassJob which will be executed on startup.
The job will be called (and awaited if it returns a coroutine) before firing
of event EVENT_HOMEASSISTANT_STARTED when Home Assistant is starting.
This method must be run in the event loop.
hassjob: HassJob
args: parameters for method to call.
Returns function to remove the job.
"""
job_with_args = HassJobWithArgs(hassjob, args)
self._startup_jobs.append(job_with_args)
@callback
def remove_job() -> None:
if job_with_args in self._startup_jobs:
self._startup_jobs.remove(job_with_args)
return remove_job
def stop(self) -> None:
"""Stop Home Assistant and shuts down all threads."""
if self.state is CoreState.not_running: # just ignore
+11 -3
View File
@@ -79,6 +79,7 @@ from .automation import (
move_options_fields_to_top_level,
)
from .event import async_call_later
from .frame import report_usage
from .integration_platform import async_process_integration_platforms
from .selector import (
NumericThresholdMode,
@@ -1350,7 +1351,6 @@ class TriggerInfo(TypedDict):
domain: str
name: str
home_assistant_start: bool
variables: TemplateVarsType
trigger_data: TriggerData
@@ -1671,8 +1671,9 @@ async def async_initialize_triggers(
domain: str,
name: str,
log_cb: Callable,
home_assistant_start: bool = False,
home_assistant_start: bool | UndefinedType = UNDEFINED,
variables: TemplateVarsType = None,
*,
did_not_trigger: TriggerNotTriggeredAction | None = None,
) -> CALLBACK_TYPE | None:
"""Initialize triggers.
@@ -1681,6 +1682,14 @@ async def async_initialize_triggers(
invoked - for new-style triggers that support it - when a trigger evaluates
a relevant change but reports it did not fire. Old-style triggers ignore it.
"""
if home_assistant_start is not UNDEFINED:
report_usage(
"passes `home_assistant_start` to `async_initialize_triggers`, which is "
"deprecated and will be removed in Home Assistant 2027.8; the parameter "
"no longer has any effect",
breaks_in_ha_version="2027.8.0",
)
triggers: list[asyncio.Task[CALLBACK_TYPE]] = []
for idx, conf in enumerate(trigger_config):
# Skip triggers that are not enabled
@@ -1704,7 +1713,6 @@ async def async_initialize_triggers(
info = TriggerInfo(
domain=domain,
name=name,
home_assistant_start=home_assistant_start,
variables=variables,
trigger_data=trigger_data,
)
+1 -1
View File
@@ -70,7 +70,7 @@ standard-telnetlib==3.13.0
typing-extensions>=4.15.0,<5.0
ulid-transform==2.2.9
urllib3>=2.0
uv==0.11.21
uv==0.11.25
voluptuous-openapi==0.4.1
voluptuous-serialize==2.7.0
voluptuous==0.15.2
+1 -1
View File
@@ -74,7 +74,7 @@ dependencies = [
"typing-extensions>=4.15.0,<5.0",
"ulid-transform==2.2.9",
"urllib3>=2.0",
"uv==0.11.21",
"uv==0.11.25",
"voluptuous==0.15.2",
"voluptuous-serialize==2.7.0",
"voluptuous-openapi==0.4.1",
+1 -1
View File
@@ -55,7 +55,7 @@ standard-telnetlib==3.13.0
typing-extensions>=4.15.0,<5.0
ulid-transform==2.2.9
urllib3>=2.0
uv==0.11.21
uv==0.11.25
voluptuous-openapi==0.4.1
voluptuous-serialize==2.7.0
voluptuous==0.15.2
+3 -3
View File
@@ -1474,7 +1474,7 @@ libpyvivotek==0.6.1
librehardwaremonitor-api==1.11.1
# homeassistant.components.mikrotik
librouteros==3.2.1
librouteros==4.1.1
# homeassistant.components.soundtouch
libsoundtouch==0.8
@@ -2148,7 +2148,7 @@ pyegps==0.2.5
pyemoncms==0.1.3
# homeassistant.components.enphase_envoy
pyenphase==3.0.0
pyenphase==3.0.1
# homeassistant.components.envertech_evt800
pyenvertechevt800==0.2.4
@@ -2567,7 +2567,7 @@ pysmhi==2.0.0
pysml==0.1.8
# homeassistant.components.smlight
pysmlight==0.5.0
pysmlight==0.5.2
# homeassistant.components.snmp
pysnmp==7.1.27
+1 -1
View File
@@ -10,7 +10,7 @@
# ast-serialize is an internal mypy dependency
ast-serialize==0.3.0
astroid==4.0.4
coverage==7.14.2
coverage==7.14.3
freezegun==1.5.5
# librt is an internal mypy dependency
librt==0.11.0
-2
View File
@@ -212,7 +212,6 @@ INTEGRATIONS_WITHOUT_QUALITY_SCALE_FILE = [
"caldav",
"canary",
"cast",
"ccm15",
"chacon_dio",
"channels",
"circuit",
@@ -1150,7 +1149,6 @@ INTEGRATIONS_WITHOUT_SCALE = [
"caldav",
"canary",
"cast",
"ccm15",
"cert_expiry",
"chacon_dio",
"channels",
@@ -11,7 +11,7 @@ from homeassistant.components.air_quality import (
from homeassistant.const import (
ATTR_ATTRIBUTION,
ATTR_UNIT_OF_MEASUREMENT,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
UnitOfDensity,
)
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
@@ -52,5 +52,5 @@ async def test_attributes(hass: HomeAssistant) -> None:
assert data.get(ATTR_OZONE) is None
assert data.get(ATTR_ATTRIBUTION) == "Powered by Home Assistant"
assert (
data.get(ATTR_UNIT_OF_MEASUREMENT) == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
data.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfDensity.MICROGRAMS_PER_CUBIC_METER
)
+29 -30
View File
@@ -8,11 +8,10 @@ from homeassistant.components.binary_sensor import BinarySensorDeviceClass
from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_UNIT_OF_MEASUREMENT,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION,
STATE_OFF,
STATE_ON,
UnitOfDensity,
UnitOfRatio,
)
from homeassistant.core import HomeAssistant
@@ -31,10 +30,10 @@ from tests.components.common import (
)
_UGM3_UNIT_ATTRIBUTES = {
ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
ATTR_UNIT_OF_MEASUREMENT: UnitOfDensity.MICROGRAMS_PER_CUBIC_METER
}
_PPB_UNIT_ATTRIBUTES = {ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION}
_PPM_UNIT_ATTRIBUTES = {ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_MILLION}
_PPB_UNIT_ATTRIBUTES = {ATTR_UNIT_OF_MEASUREMENT: UnitOfRatio.PARTS_PER_BILLION}
_PPM_UNIT_ATTRIBUTES = {ATTR_UNIT_OF_MEASUREMENT: UnitOfRatio.PARTS_PER_MILLION}
@pytest.fixture
@@ -55,7 +54,7 @@ _PPB_THRESHOLD = {
"type": "above",
"value": {
"number": 50,
"unit_of_measurement": CONCENTRATION_PARTS_PER_BILLION,
"unit_of_measurement": UnitOfRatio.PARTS_PER_BILLION,
},
}
}
@@ -64,7 +63,7 @@ _UGM3_THRESHOLD = {
"type": "above",
"value": {
"number": 50,
"unit_of_measurement": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
"unit_of_measurement": UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
},
}
}
@@ -269,43 +268,43 @@ async def test_air_quality_binary_condition_behavior_all(
*parametrize_numerical_condition_above_below_any(
"air_quality.is_co_value",
device_class="carbon_monoxide",
threshold_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
threshold_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
unit_attributes=_UGM3_UNIT_ATTRIBUTES,
),
*parametrize_numerical_condition_above_below_any(
"air_quality.is_ozone_value",
device_class="ozone",
threshold_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
threshold_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
unit_attributes=_UGM3_UNIT_ATTRIBUTES,
),
*parametrize_numerical_condition_above_below_any(
"air_quality.is_voc_value",
device_class="volatile_organic_compounds",
threshold_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
threshold_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
unit_attributes=_UGM3_UNIT_ATTRIBUTES,
),
*parametrize_numerical_condition_above_below_any(
"air_quality.is_voc_ratio_value",
device_class="volatile_organic_compounds_parts",
threshold_unit=CONCENTRATION_PARTS_PER_BILLION,
threshold_unit=UnitOfRatio.PARTS_PER_BILLION,
unit_attributes=_PPB_UNIT_ATTRIBUTES,
),
*parametrize_numerical_condition_above_below_any(
"air_quality.is_no_value",
device_class="nitrogen_monoxide",
threshold_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
threshold_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
unit_attributes=_UGM3_UNIT_ATTRIBUTES,
),
*parametrize_numerical_condition_above_below_any(
"air_quality.is_no2_value",
device_class="nitrogen_dioxide",
threshold_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
threshold_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
unit_attributes=_UGM3_UNIT_ATTRIBUTES,
),
*parametrize_numerical_condition_above_below_any(
"air_quality.is_so2_value",
device_class="sulphur_dioxide",
threshold_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
threshold_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
unit_attributes=_UGM3_UNIT_ATTRIBUTES,
),
],
@@ -343,43 +342,43 @@ async def test_air_quality_numerical_with_unit_condition_behavior_any(
*parametrize_numerical_condition_above_below_all(
"air_quality.is_co_value",
device_class="carbon_monoxide",
threshold_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
threshold_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
unit_attributes=_UGM3_UNIT_ATTRIBUTES,
),
*parametrize_numerical_condition_above_below_all(
"air_quality.is_ozone_value",
device_class="ozone",
threshold_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
threshold_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
unit_attributes=_UGM3_UNIT_ATTRIBUTES,
),
*parametrize_numerical_condition_above_below_all(
"air_quality.is_voc_value",
device_class="volatile_organic_compounds",
threshold_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
threshold_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
unit_attributes=_UGM3_UNIT_ATTRIBUTES,
),
*parametrize_numerical_condition_above_below_all(
"air_quality.is_voc_ratio_value",
device_class="volatile_organic_compounds_parts",
threshold_unit=CONCENTRATION_PARTS_PER_BILLION,
threshold_unit=UnitOfRatio.PARTS_PER_BILLION,
unit_attributes=_PPB_UNIT_ATTRIBUTES,
),
*parametrize_numerical_condition_above_below_all(
"air_quality.is_no_value",
device_class="nitrogen_monoxide",
threshold_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
threshold_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
unit_attributes=_UGM3_UNIT_ATTRIBUTES,
),
*parametrize_numerical_condition_above_below_all(
"air_quality.is_no2_value",
device_class="nitrogen_dioxide",
threshold_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
threshold_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
unit_attributes=_UGM3_UNIT_ATTRIBUTES,
),
*parametrize_numerical_condition_above_below_all(
"air_quality.is_so2_value",
device_class="sulphur_dioxide",
threshold_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
threshold_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
unit_attributes=_UGM3_UNIT_ATTRIBUTES,
),
],
@@ -541,8 +540,8 @@ async def test_air_quality_condition_unit_conversion_co(
hass: HomeAssistant,
) -> None:
"""Test that the CO condition converts units correctly."""
_unit_ugm3 = {ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}
_unit_ppm = {ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_MILLION}
_unit_ugm3 = {ATTR_UNIT_OF_MEASUREMENT: UnitOfDensity.MICROGRAMS_PER_CUBIC_METER}
_unit_ppm = {ATTR_UNIT_OF_MEASUREMENT: UnitOfRatio.PARTS_PER_MILLION}
_unit_invalid = {ATTR_UNIT_OF_MEASUREMENT: "not_a_valid_unit"}
await assert_numerical_condition_unit_conversion(
@@ -554,7 +553,7 @@ async def test_air_quality_condition_unit_conversion_co(
"state": "500",
"attributes": {
"device_class": "carbon_monoxide",
ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
ATTR_UNIT_OF_MEASUREMENT: UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
},
}
],
@@ -563,7 +562,7 @@ async def test_air_quality_condition_unit_conversion_co(
"state": "100",
"attributes": {
"device_class": "carbon_monoxide",
ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
ATTR_UNIT_OF_MEASUREMENT: UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
},
}
],
@@ -573,11 +572,11 @@ async def test_air_quality_condition_unit_conversion_co(
"type": "between",
"value_min": {
"number": 0.2,
"unit_of_measurement": CONCENTRATION_PARTS_PER_MILLION,
"unit_of_measurement": UnitOfRatio.PARTS_PER_MILLION,
},
"value_max": {
"number": 0.8,
"unit_of_measurement": CONCENTRATION_PARTS_PER_MILLION,
"unit_of_measurement": UnitOfRatio.PARTS_PER_MILLION,
},
}
},
@@ -586,11 +585,11 @@ async def test_air_quality_condition_unit_conversion_co(
"type": "between",
"value_min": {
"number": 200,
"unit_of_measurement": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
"unit_of_measurement": UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
},
"value_max": {
"number": 800,
"unit_of_measurement": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
"unit_of_measurement": UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
},
}
},
+38 -39
View File
@@ -9,12 +9,11 @@ from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_UNIT_OF_MEASUREMENT,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION,
CONF_ENTITY_ID,
STATE_OFF,
STATE_ON,
UnitOfDensity,
UnitOfRatio,
)
from homeassistant.core import HomeAssistant
@@ -33,10 +32,10 @@ from tests.components.common import (
)
_UGM3_UNIT_ATTRIBUTES = {
ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
ATTR_UNIT_OF_MEASUREMENT: UnitOfDensity.MICROGRAMS_PER_CUBIC_METER
}
_PPB_UNIT_ATTRIBUTES = {ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION}
_PPM_UNIT_ATTRIBUTES = {ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_MILLION}
_PPB_UNIT_ATTRIBUTES = {ATTR_UNIT_OF_MEASUREMENT: UnitOfRatio.PARTS_PER_BILLION}
_PPM_UNIT_ATTRIBUTES = {ATTR_UNIT_OF_MEASUREMENT: UnitOfRatio.PARTS_PER_MILLION}
@pytest.fixture
@@ -58,7 +57,7 @@ _PPB_CROSSED_THRESHOLD = {
"type": "above",
"value": {
"number": 50,
"unit_of_measurement": CONCENTRATION_PARTS_PER_BILLION,
"unit_of_measurement": UnitOfRatio.PARTS_PER_BILLION,
},
}
}
@@ -67,7 +66,7 @@ _UGM3_CROSSED_THRESHOLD = {
"type": "above",
"value": {
"number": 50,
"unit_of_measurement": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
"unit_of_measurement": UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
},
}
}
@@ -375,86 +374,86 @@ async def test_air_quality_trigger_binary_sensor_behavior_all(
*parametrize_numerical_state_value_changed_trigger_states(
"air_quality.co_changed",
device_class=SensorDeviceClass.CO,
threshold_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
threshold_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
unit_attributes=_UGM3_UNIT_ATTRIBUTES,
),
*parametrize_numerical_state_value_crossed_threshold_trigger_states(
"air_quality.co_crossed_threshold",
device_class=SensorDeviceClass.CO,
threshold_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
threshold_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
unit_attributes=_UGM3_UNIT_ATTRIBUTES,
),
*parametrize_numerical_state_value_changed_trigger_states(
"air_quality.ozone_changed",
device_class=SensorDeviceClass.OZONE,
threshold_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
threshold_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
unit_attributes=_UGM3_UNIT_ATTRIBUTES,
),
*parametrize_numerical_state_value_crossed_threshold_trigger_states(
"air_quality.ozone_crossed_threshold",
device_class=SensorDeviceClass.OZONE,
threshold_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
threshold_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
unit_attributes=_UGM3_UNIT_ATTRIBUTES,
),
*parametrize_numerical_state_value_changed_trigger_states(
"air_quality.voc_changed",
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS,
threshold_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
threshold_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
unit_attributes=_UGM3_UNIT_ATTRIBUTES,
),
*parametrize_numerical_state_value_crossed_threshold_trigger_states(
"air_quality.voc_crossed_threshold",
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS,
threshold_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
threshold_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
unit_attributes=_UGM3_UNIT_ATTRIBUTES,
),
*parametrize_numerical_state_value_changed_trigger_states(
"air_quality.no_changed",
device_class=SensorDeviceClass.NITROGEN_MONOXIDE,
threshold_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
threshold_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
unit_attributes=_UGM3_UNIT_ATTRIBUTES,
),
*parametrize_numerical_state_value_crossed_threshold_trigger_states(
"air_quality.no_crossed_threshold",
device_class=SensorDeviceClass.NITROGEN_MONOXIDE,
threshold_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
threshold_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
unit_attributes=_UGM3_UNIT_ATTRIBUTES,
),
*parametrize_numerical_state_value_changed_trigger_states(
"air_quality.no2_changed",
device_class=SensorDeviceClass.NITROGEN_DIOXIDE,
threshold_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
threshold_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
unit_attributes=_UGM3_UNIT_ATTRIBUTES,
),
*parametrize_numerical_state_value_crossed_threshold_trigger_states(
"air_quality.no2_crossed_threshold",
device_class=SensorDeviceClass.NITROGEN_DIOXIDE,
threshold_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
threshold_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
unit_attributes=_UGM3_UNIT_ATTRIBUTES,
),
*parametrize_numerical_state_value_changed_trigger_states(
"air_quality.so2_changed",
device_class=SensorDeviceClass.SULPHUR_DIOXIDE,
threshold_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
threshold_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
unit_attributes=_UGM3_UNIT_ATTRIBUTES,
),
*parametrize_numerical_state_value_crossed_threshold_trigger_states(
"air_quality.so2_crossed_threshold",
device_class=SensorDeviceClass.SULPHUR_DIOXIDE,
threshold_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
threshold_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
unit_attributes=_UGM3_UNIT_ATTRIBUTES,
),
# With unit conversion (ppb base unit)
*parametrize_numerical_state_value_changed_trigger_states(
"air_quality.voc_ratio_changed",
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS,
threshold_unit=CONCENTRATION_PARTS_PER_BILLION,
threshold_unit=UnitOfRatio.PARTS_PER_BILLION,
unit_attributes=_PPB_UNIT_ATTRIBUTES,
),
*parametrize_numerical_state_value_crossed_threshold_trigger_states(
"air_quality.voc_ratio_crossed_threshold",
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS,
threshold_unit=CONCENTRATION_PARTS_PER_BILLION,
threshold_unit=UnitOfRatio.PARTS_PER_BILLION,
unit_attributes=_PPB_UNIT_ATTRIBUTES,
),
# Without unit conversion (single-unit device classes)
@@ -554,44 +553,44 @@ async def test_air_quality_trigger_sensor_behavior_each(
*parametrize_numerical_state_value_crossed_threshold_trigger_states(
"air_quality.co_crossed_threshold",
device_class=SensorDeviceClass.CO,
threshold_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
threshold_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
unit_attributes=_UGM3_UNIT_ATTRIBUTES,
),
*parametrize_numerical_state_value_crossed_threshold_trigger_states(
"air_quality.ozone_crossed_threshold",
device_class=SensorDeviceClass.OZONE,
threshold_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
threshold_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
unit_attributes=_UGM3_UNIT_ATTRIBUTES,
),
*parametrize_numerical_state_value_crossed_threshold_trigger_states(
"air_quality.voc_crossed_threshold",
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS,
threshold_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
threshold_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
unit_attributes=_UGM3_UNIT_ATTRIBUTES,
),
*parametrize_numerical_state_value_crossed_threshold_trigger_states(
"air_quality.no_crossed_threshold",
device_class=SensorDeviceClass.NITROGEN_MONOXIDE,
threshold_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
threshold_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
unit_attributes=_UGM3_UNIT_ATTRIBUTES,
),
*parametrize_numerical_state_value_crossed_threshold_trigger_states(
"air_quality.no2_crossed_threshold",
device_class=SensorDeviceClass.NITROGEN_DIOXIDE,
threshold_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
threshold_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
unit_attributes=_UGM3_UNIT_ATTRIBUTES,
),
*parametrize_numerical_state_value_crossed_threshold_trigger_states(
"air_quality.so2_crossed_threshold",
device_class=SensorDeviceClass.SULPHUR_DIOXIDE,
threshold_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
threshold_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
unit_attributes=_UGM3_UNIT_ATTRIBUTES,
),
# With unit conversion (ppb base unit)
*parametrize_numerical_state_value_crossed_threshold_trigger_states(
"air_quality.voc_ratio_crossed_threshold",
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS,
threshold_unit=CONCENTRATION_PARTS_PER_BILLION,
threshold_unit=UnitOfRatio.PARTS_PER_BILLION,
unit_attributes=_PPB_UNIT_ATTRIBUTES,
),
# Without unit conversion (single-unit device classes)
@@ -664,44 +663,44 @@ async def test_air_quality_trigger_sensor_crossed_threshold_behavior_first(
*parametrize_numerical_state_value_crossed_threshold_trigger_states(
"air_quality.co_crossed_threshold",
device_class=SensorDeviceClass.CO,
threshold_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
threshold_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
unit_attributes=_UGM3_UNIT_ATTRIBUTES,
),
*parametrize_numerical_state_value_crossed_threshold_trigger_states(
"air_quality.ozone_crossed_threshold",
device_class=SensorDeviceClass.OZONE,
threshold_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
threshold_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
unit_attributes=_UGM3_UNIT_ATTRIBUTES,
),
*parametrize_numerical_state_value_crossed_threshold_trigger_states(
"air_quality.voc_crossed_threshold",
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS,
threshold_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
threshold_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
unit_attributes=_UGM3_UNIT_ATTRIBUTES,
),
*parametrize_numerical_state_value_crossed_threshold_trigger_states(
"air_quality.no_crossed_threshold",
device_class=SensorDeviceClass.NITROGEN_MONOXIDE,
threshold_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
threshold_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
unit_attributes=_UGM3_UNIT_ATTRIBUTES,
),
*parametrize_numerical_state_value_crossed_threshold_trigger_states(
"air_quality.no2_crossed_threshold",
device_class=SensorDeviceClass.NITROGEN_DIOXIDE,
threshold_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
threshold_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
unit_attributes=_UGM3_UNIT_ATTRIBUTES,
),
*parametrize_numerical_state_value_crossed_threshold_trigger_states(
"air_quality.so2_crossed_threshold",
device_class=SensorDeviceClass.SULPHUR_DIOXIDE,
threshold_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
threshold_unit=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
unit_attributes=_UGM3_UNIT_ATTRIBUTES,
),
# With unit conversion (ppb base unit)
*parametrize_numerical_state_value_crossed_threshold_trigger_states(
"air_quality.voc_ratio_crossed_threshold",
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS,
threshold_unit=CONCENTRATION_PARTS_PER_BILLION,
threshold_unit=UnitOfRatio.PARTS_PER_BILLION,
unit_attributes=_PPB_UNIT_ATTRIBUTES,
),
# Without unit conversion (single-unit device classes)
@@ -777,7 +776,7 @@ async def test_air_quality_trigger_unit_conversion_co_ppm_to_ugm3(
"0.5",
{
ATTR_DEVICE_CLASS: SensorDeviceClass.CO,
ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_MILLION,
ATTR_UNIT_OF_MEASUREMENT: UnitOfRatio.PARTS_PER_MILLION,
},
)
await hass.async_block_till_done()
@@ -801,7 +800,7 @@ async def test_air_quality_trigger_unit_conversion_co_ppm_to_ugm3(
"0.5",
{
ATTR_DEVICE_CLASS: SensorDeviceClass.CO,
ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_MILLION,
ATTR_UNIT_OF_MEASUREMENT: UnitOfRatio.PARTS_PER_MILLION,
},
)
await hass.async_block_till_done()
@@ -813,7 +812,7 @@ async def test_air_quality_trigger_unit_conversion_co_ppm_to_ugm3(
"1",
{
ATTR_DEVICE_CLASS: SensorDeviceClass.CO,
ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_MILLION,
ATTR_UNIT_OF_MEASUREMENT: UnitOfRatio.PARTS_PER_MILLION,
},
)
await hass.async_block_till_done()
+3 -2
View File
@@ -23,7 +23,6 @@ from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_NAME,
CONF_ID,
EVENT_HOMEASSISTANT_STARTED,
SERVICE_RELOAD,
SERVICE_TOGGLE,
SERVICE_TURN_OFF,
@@ -1683,7 +1682,9 @@ async def test_automation_not_trigger_on_bootstrap(hass: HomeAssistant) -> None:
await hass.async_block_till_done()
assert len(calls) == 0
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
# Triggers are armed by a startup job which runs during async_start, before
# EVENT_HOMEASSISTANT_STARTED is fired.
await hass.async_start()
await hass.async_block_till_done()
assert automation.is_on(hass, "automation.hello")
+2 -2
View File
@@ -12,8 +12,8 @@ from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_UNIT_OF_MEASUREMENT,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
STATE_UNKNOWN,
UnitOfDensity,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant
@@ -235,7 +235,7 @@ async def test_airsensor_update(airsensor, hass: HomeAssistant) -> None:
state = hass.states.get(entity_id)
assert (
state.attributes[ATTR_UNIT_OF_MEASUREMENT]
== CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
== UnitOfDensity.MICROGRAMS_PER_CUBIC_METER
)
assert state.state == "49"
@@ -0,0 +1,31 @@
# serializer version: 1
# name: test_entry_diagnostics
dict({
'device_info': dict({
'device_id': '123a4b5c-678d-9e0f-a123-4b567c8d901e',
'device_name': 'My Tank',
'hardware_version': '4.1',
'lte_version': '1.1.2',
}),
'tank_data': dict({
'AlertStatus': 'No Alert',
'Altitude': '**REDACTED**',
'BatteryVolts': 4.19,
'DeviceID': '123a4b5c-678d-9e0f-a123-4b567c8d901e',
'DeviceName': 'My Tank',
'DeviceTempCelsius': 17.0,
'DeviceTempFahrenheit': 63.0,
'LastPostTimeIso': '2026-02-27 22:00:31.000',
'Latitude': '**REDACTED**',
'Longitude': '**REDACTED**',
'NextPostTimeIso': '2026-02-28 10:00:00.000',
'SignalQualLTE': -107.0,
'SolarVolts': 2.46,
'TankLevel': 75.0,
'TankSize': 1000,
'TankSizeUnit': 'Gallons',
'VersionHW': '4.1',
'VersionLTE': '1.1.2',
}),
})
# ---
@@ -0,0 +1,29 @@
"""Tests for the diagnostics data provided by the CentriConnect/MyPropane integration."""
from unittest.mock import AsyncMock
from syrupy.assertion import SnapshotAssertion
from homeassistant.core import HomeAssistant
from . import setup_integration
from tests.common import MockConfigEntry
from tests.components.diagnostics import get_diagnostics_for_config_entry
from tests.typing import ClientSessionGenerator
async def test_entry_diagnostics(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_centriconnect_client: AsyncMock,
hass_client: ClientSessionGenerator,
snapshot: SnapshotAssertion,
) -> None:
"""Test config entry diagnostics."""
await setup_integration(hass, mock_config_entry)
result = await get_diagnostics_for_config_entry(
hass, hass_client, mock_config_entry
)
assert result == snapshot
+6 -12
View File
@@ -8,13 +8,7 @@ import pytest
from homeassistant.components import sensor
from homeassistant.components.foobot import sensor as foobot
from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION,
PERCENTAGE,
UnitOfTemperature,
)
from homeassistant.const import UnitOfDensity, UnitOfRatio, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import PlatformNotReady
from homeassistant.setup import async_setup_component
@@ -45,12 +39,12 @@ async def test_default_setup(
await hass.async_block_till_done()
metrics = {
"co2": ["1232.0", CONCENTRATION_PARTS_PER_MILLION],
"co2": ["1232.0", UnitOfRatio.PARTS_PER_MILLION],
"temperature": ["21.1", UnitOfTemperature.CELSIUS],
"humidity": ["49.5", PERCENTAGE],
"pm2_5": ["144.8", CONCENTRATION_MICROGRAMS_PER_CUBIC_METER],
"voc": ["340.7", CONCENTRATION_PARTS_PER_BILLION],
"index": ["138.9", PERCENTAGE],
"humidity": ["49.5", UnitOfRatio.PERCENTAGE],
"pm2_5": ["144.8", UnitOfDensity.MICROGRAMS_PER_CUBIC_METER],
"voc": ["340.7", UnitOfRatio.PARTS_PER_BILLION],
"index": ["138.9", UnitOfRatio.PERCENTAGE],
}
for name, value in metrics.items():
@@ -90,7 +90,7 @@
'supported_features': 0,
'translation_key': 'benzene',
'unique_id': 'c6h6_10.1_20.1',
'unit_of_measurement': 'μg/m³',
'unit_of_measurement': <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
})
# ---
# name: test_sensor_snapshot[air_quality_data.json-mock_config_entry][sensor.home_benzene-state]
@@ -99,7 +99,7 @@
<EntityStateAttribute.ATTRIBUTION: 'attribution'>: 'Data provided by Google Air Quality',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Home Benzene',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'μg/m³',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
}),
'context': <ANY>,
'entity_id': 'sensor.home_benzene',
@@ -559,7 +559,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': 'pm10_10.1_20.1',
'unit_of_measurement': 'μg/m³',
'unit_of_measurement': <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
})
# ---
# name: test_sensor_snapshot[air_quality_data.json-mock_config_entry][sensor.home_pm10-state]
@@ -569,7 +569,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'pm10',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Home PM10',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'μg/m³',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
}),
'context': <ANY>,
'entity_id': 'sensor.home_pm10',
@@ -615,7 +615,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': 'pm25_10.1_20.1',
'unit_of_measurement': 'μg/m³',
'unit_of_measurement': <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
})
# ---
# name: test_sensor_snapshot[air_quality_data.json-mock_config_entry][sensor.home_pm2_5-state]
@@ -625,7 +625,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'pm25',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Home PM2.5',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'μg/m³',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
}),
'context': <ANY>,
'entity_id': 'sensor.home_pm2_5',
@@ -1280,7 +1280,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': 'pm10_10.1_20.1',
'unit_of_measurement': 'μg/m³',
'unit_of_measurement': <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
})
# ---
# name: test_sensor_snapshot[air_quality_data_custom_laqi.json-mock_config_entry_with_custom_laqi][sensor.home_pm10-state]
@@ -1290,7 +1290,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'pm10',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Home PM10',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'μg/m³',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
}),
'context': <ANY>,
'entity_id': 'sensor.home_pm10',
@@ -1336,7 +1336,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': 'pm25_10.1_20.1',
'unit_of_measurement': 'μg/m³',
'unit_of_measurement': <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
})
# ---
# name: test_sensor_snapshot[air_quality_data_custom_laqi.json-mock_config_entry_with_custom_laqi][sensor.home_pm2_5-state]
@@ -1346,7 +1346,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'pm25',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Home PM2.5',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'μg/m³',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
}),
'context': <ANY>,
'entity_id': 'sensor.home_pm2_5',
@@ -29,7 +29,9 @@ from tests.common import async_mock_service
)
@pytest.mark.usefixtures("mock_hass_config")
async def test_if_fires_on_hass_start(
hass: HomeAssistant, hass_config: ConfigType
hass: HomeAssistant,
hass_config: ConfigType,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test the firing when Home Assistant starts."""
calls = async_mock_service(hass, "test", "automation")
@@ -52,6 +54,33 @@ async def test_if_fires_on_hass_start(
assert len(calls) == 1
assert calls[0].data["id"] == 0
# Detaching the trigger after it fired must not re-invoke the stale once
# listener's remove callback.
assert "Unable to remove unknown job listener" not in caplog.text
async def test_if_not_fires_when_set_up_after_start(hass: HomeAssistant) -> None:
"""Test the start trigger stays silent when armed after Home Assistant started."""
calls = async_mock_service(hass, "test", "automation")
assert hass.state is CoreState.running
assert await async_setup_component(
hass,
automation.DOMAIN,
{
automation.DOMAIN: {
"alias": "hello",
"trigger": {"platform": "homeassistant", "event": "start"},
"action": {"service": "test.automation"},
}
},
)
assert automation.is_on(hass, "automation.hello")
await hass.async_block_till_done()
# EVENT_HOMEASSISTANT_STARTED has already fired, so the trigger must not fire.
assert len(calls) == 0
async def test_if_fires_on_hass_shutdown(hass: HomeAssistant) -> None:
"""Test the firing when Home Assistant shuts down."""
+5 -5
View File
@@ -50,14 +50,14 @@ from homeassistant.const import (
ATTR_DEVICE_ID,
ATTR_ENTITY_ID,
ATTR_UNIT_OF_MEASUREMENT,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONF_NAME,
CONF_PORT,
EVENT_HOMEASSISTANT_STARTED,
PERCENTAGE,
SERVICE_RELOAD,
STATE_ON,
EntityCategory,
UnitOfDensity,
UnitOfRatio,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant, State
@@ -2146,7 +2146,7 @@ async def test_homekit_finds_linked_humidity_sensors(
"42",
{
ATTR_DEVICE_CLASS: SensorDeviceClass.HUMIDITY,
ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE,
ATTR_UNIT_OF_MEASUREMENT: UnitOfRatio.PERCENTAGE,
},
)
hass.states.async_set(humidifier.entity_id, STATE_ON)
@@ -2233,7 +2233,7 @@ async def test_homekit_finds_linked_air_purifier_sensors(
"42",
{
ATTR_DEVICE_CLASS: SensorDeviceClass.HUMIDITY,
ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE,
ATTR_UNIT_OF_MEASUREMENT: UnitOfRatio.PERCENTAGE,
},
)
hass.states.async_set(
@@ -2241,7 +2241,7 @@ async def test_homekit_finds_linked_air_purifier_sensors(
8,
{
ATTR_DEVICE_CLASS: SensorDeviceClass.PM25,
ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
ATTR_UNIT_OF_MEASUREMENT: UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
},
)
hass.states.async_set(
+5 -5
View File
@@ -11,9 +11,9 @@ from homeassistant.const import (
ATTR_FRIENDLY_NAME,
ATTR_ICON,
ATTR_UNIT_OF_MEASUREMENT,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
PERCENTAGE,
UnitOfDensity,
UnitOfPressure,
UnitOfRatio,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant
@@ -55,7 +55,7 @@ async def test_luftdaten_sensors(
assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Sensor 12345 Humidity"
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.HUMIDITY
assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfRatio.PERCENTAGE
assert ATTR_ICON not in state.attributes
entry = entity_registry.async_get("sensor.sensor_12345_pressure")
@@ -101,7 +101,7 @@ async def test_luftdaten_sensors(
assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT
assert (
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
== CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
== UnitOfDensity.MICROGRAMS_PER_CUBIC_METER
)
assert ATTR_ICON not in state.attributes
@@ -118,7 +118,7 @@ async def test_luftdaten_sensors(
assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.MEASUREMENT
assert (
state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
== CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
== UnitOfDensity.MICROGRAMS_PER_CUBIC_METER
)
assert ATTR_ICON not in state.attributes
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -76,10 +76,8 @@ from homeassistant.const import (
ATTR_MODE,
ATTR_TEMPERATURE,
ATTR_UNIT_OF_MEASUREMENT,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONTENT_TYPE_TEXT_PLAIN,
DEGREE,
PERCENTAGE,
STATE_CLOSED,
STATE_CLOSING,
STATE_HOME,
@@ -90,8 +88,10 @@ from homeassistant.const import (
STATE_OPENING,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
UnitOfDensity,
UnitOfEnergy,
UnitOfLength,
UnitOfRatio,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant
@@ -2023,7 +2023,7 @@ async def sensor_fixture(
domain=sensor.DOMAIN,
platform="test",
unique_id="sensor_2",
unit_of_measurement=PERCENTAGE,
unit_of_measurement=UnitOfRatio.PERCENTAGE,
original_device_class=SensorDeviceClass.HUMIDITY,
suggested_object_id="outside_humidity",
original_name="Outside Humidity",
@@ -2081,7 +2081,7 @@ async def sensor_fixture(
domain=sensor.DOMAIN,
platform="test",
unique_id="sensor_7",
unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
suggested_object_id="sps30_pm_1um_weight_concentration",
original_name="SPS30 PM <1µm Weight concentration",
)
@@ -306,7 +306,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '01:03:05:07:12:34-pm1',
'unit_of_measurement': 'μg/m³',
'unit_of_measurement': <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
})
# ---
# name: test_sensors[e1][sensor.ruuvi_air_884f_pm1-state]
@@ -315,7 +315,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'pm1',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Ruuvi Air 884F PM1',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'μg/m³',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
}),
'context': <ANY>,
'entity_id': 'sensor.ruuvi_air_884f_pm1',
@@ -361,7 +361,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '01:03:05:07:12:34-pm10',
'unit_of_measurement': 'μg/m³',
'unit_of_measurement': <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
})
# ---
# name: test_sensors[e1][sensor.ruuvi_air_884f_pm10-state]
@@ -370,7 +370,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'pm10',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Ruuvi Air 884F PM10',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'μg/m³',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
}),
'context': <ANY>,
'entity_id': 'sensor.ruuvi_air_884f_pm10',
@@ -416,7 +416,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '01:03:05:07:12:34-pm25',
'unit_of_measurement': 'μg/m³',
'unit_of_measurement': <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
})
# ---
# name: test_sensors[e1][sensor.ruuvi_air_884f_pm2_5-state]
@@ -425,7 +425,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'pm25',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Ruuvi Air 884F PM2.5',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'μg/m³',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
}),
'context': <ANY>,
'entity_id': 'sensor.ruuvi_air_884f_pm2_5',
@@ -471,7 +471,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '01:03:05:07:12:34-pm4',
'unit_of_measurement': 'μg/m³',
'unit_of_measurement': <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
})
# ---
# name: test_sensors[e1][sensor.ruuvi_air_884f_pm4-state]
@@ -480,7 +480,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'pm4',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'Ruuvi Air 884F PM4',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'μg/m³',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
}),
'context': <ANY>,
'entity_id': 'sensor.ruuvi_air_884f_pm4',
@@ -1574,7 +1574,7 @@
'supported_features': 0,
'translation_key': None,
'unique_id': '01:03:05:07:12:34-pm25',
'unit_of_measurement': 'μg/m³',
'unit_of_measurement': <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
})
# ---
# name: test_sensors[v6][sensor.ruuvitag_884f_pm2_5-state]
@@ -1583,7 +1583,7 @@
<EntityStateAttribute.DEVICE_CLASS: 'device_class'>: 'pm25',
<EntityStateAttribute.FRIENDLY_NAME: 'friendly_name'>: 'RuuviTag 884F PM2.5',
<SensorEntityCapabilityAttribute.STATE_CLASS: 'state_class'>: <SensorStateClass.MEASUREMENT: 'measurement'>,
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: 'μg/m³',
<EntityStateAttribute.UNIT_OF_MEASUREMENT: 'unit_of_measurement'>: <UnitOfDensity.MICROGRAMS_PER_CUBIC_METER: 'μg/m³'>,
}),
'context': <ANY>,
'entity_id': 'sensor.ruuvitag_884f_pm2_5',
+20 -22
View File
@@ -7,18 +7,15 @@ from homeassistant.components.sensor import (
)
from homeassistant.components.sensor.const import DEVICE_CLASS_STATE_CLASSES
from homeassistant.const import (
CONCENTRATION_GRAMS_PER_CUBIC_METER,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_MILLION,
DEGREE,
LIGHT_LUX,
PERCENTAGE,
SIGNAL_STRENGTH_DECIBELS,
UnitOfApparentPower,
UnitOfArea,
UnitOfBloodGlucoseConcentration,
UnitOfConductivity,
UnitOfDataRate,
UnitOfDensity,
UnitOfElectricCurrent,
UnitOfElectricPotential,
UnitOfEnergy,
@@ -31,6 +28,7 @@ from homeassistant.const import (
UnitOfPower,
UnitOfPrecipitationDepth,
UnitOfPressure,
UnitOfRatio,
UnitOfReactiveEnergy,
UnitOfReactivePower,
UnitOfSoundPressure,
@@ -45,17 +43,17 @@ from homeassistant.const import (
from tests.common import MockEntity
UNITS_OF_MEASUREMENT = {
SensorDeviceClass.ABSOLUTE_HUMIDITY: CONCENTRATION_GRAMS_PER_CUBIC_METER,
SensorDeviceClass.ABSOLUTE_HUMIDITY: UnitOfDensity.GRAMS_PER_CUBIC_METER,
SensorDeviceClass.APPARENT_POWER: UnitOfApparentPower.VOLT_AMPERE,
SensorDeviceClass.AQI: None,
SensorDeviceClass.AREA: UnitOfArea.SQUARE_METERS,
SensorDeviceClass.ATMOSPHERIC_PRESSURE: UnitOfPressure.HPA,
SensorDeviceClass.BATTERY: PERCENTAGE,
SensorDeviceClass.BATTERY: UnitOfRatio.PERCENTAGE,
SensorDeviceClass.BLOOD_GLUCOSE_CONCENTRATION: (
UnitOfBloodGlucoseConcentration.MILLIGRAMS_PER_DECILITER
),
SensorDeviceClass.CO2: CONCENTRATION_PARTS_PER_MILLION,
SensorDeviceClass.CO: CONCENTRATION_PARTS_PER_MILLION,
SensorDeviceClass.CO2: UnitOfRatio.PARTS_PER_MILLION,
SensorDeviceClass.CO: UnitOfRatio.PARTS_PER_MILLION,
SensorDeviceClass.CONDUCTIVITY: UnitOfConductivity.SIEMENS_PER_CM,
SensorDeviceClass.CURRENT: UnitOfElectricCurrent.AMPERE,
SensorDeviceClass.DATA_RATE: UnitOfDataRate.BITS_PER_SECOND,
@@ -69,22 +67,22 @@ UNITS_OF_MEASUREMENT = {
SensorDeviceClass.ENUM: None,
SensorDeviceClass.FREQUENCY: UnitOfFrequency.GIGAHERTZ,
SensorDeviceClass.GAS: UnitOfVolume.CUBIC_METERS,
SensorDeviceClass.HUMIDITY: PERCENTAGE,
SensorDeviceClass.HUMIDITY: UnitOfRatio.PERCENTAGE,
SensorDeviceClass.ILLUMINANCE: LIGHT_LUX,
SensorDeviceClass.IRRADIANCE: UnitOfIrradiance.WATTS_PER_SQUARE_METER,
SensorDeviceClass.MOISTURE: PERCENTAGE,
SensorDeviceClass.MOISTURE: UnitOfRatio.PERCENTAGE,
SensorDeviceClass.MONETARY: None,
SensorDeviceClass.NITROGEN_DIOXIDE: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
SensorDeviceClass.NITROGEN_MONOXIDE: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
SensorDeviceClass.NITROUS_OXIDE: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
SensorDeviceClass.OZONE: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
SensorDeviceClass.NITROGEN_DIOXIDE: UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
SensorDeviceClass.NITROGEN_MONOXIDE: UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
SensorDeviceClass.NITROUS_OXIDE: UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
SensorDeviceClass.OZONE: UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
SensorDeviceClass.PH: None,
SensorDeviceClass.PM10: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
SensorDeviceClass.PM1: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
SensorDeviceClass.PM25: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
SensorDeviceClass.PM4: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
SensorDeviceClass.PM10: UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
SensorDeviceClass.PM1: UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
SensorDeviceClass.PM25: UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
SensorDeviceClass.PM4: UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
SensorDeviceClass.POWER: UnitOfPower.KILO_WATT,
SensorDeviceClass.POWER_FACTOR: PERCENTAGE,
SensorDeviceClass.POWER_FACTOR: UnitOfRatio.PERCENTAGE,
SensorDeviceClass.PRECIPITATION: UnitOfPrecipitationDepth.MILLIMETERS,
SensorDeviceClass.PRECIPITATION_INTENSITY: (
UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR
@@ -95,15 +93,15 @@ UNITS_OF_MEASUREMENT = {
SensorDeviceClass.SIGNAL_STRENGTH: SIGNAL_STRENGTH_DECIBELS,
SensorDeviceClass.SOUND_PRESSURE: UnitOfSoundPressure.DECIBEL,
SensorDeviceClass.SPEED: UnitOfSpeed.METERS_PER_SECOND,
SensorDeviceClass.SULPHUR_DIOXIDE: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
SensorDeviceClass.SULPHUR_DIOXIDE: UnitOfDensity.MICROGRAMS_PER_CUBIC_METER,
SensorDeviceClass.TEMPERATURE: UnitOfTemperature.CELSIUS,
SensorDeviceClass.TEMPERATURE_DELTA: UnitOfTemperature.CELSIUS,
SensorDeviceClass.TIMESTAMP: None,
SensorDeviceClass.UPTIME: None,
SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS: (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
UnitOfDensity.MICROGRAMS_PER_CUBIC_METER
),
SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS: CONCENTRATION_PARTS_PER_MILLION,
SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS: UnitOfRatio.PARTS_PER_MILLION,
SensorDeviceClass.VOLTAGE: UnitOfElectricPotential.VOLT,
SensorDeviceClass.VOLUME: UnitOfVolume.LITERS,
SensorDeviceClass.VOLUME_FLOW_RATE: UnitOfVolumeFlowRate.LITERS_PER_MINUTE,
+7 -8
View File
@@ -33,8 +33,6 @@ from homeassistant.components.sensor.const import STATE_CLASS_UNITS, UNIT_CONVER
from homeassistant.config_entries import ConfigEntry, ConfigFlow
from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT,
CONCENTRATION_PARTS_PER_MILLION,
PERCENTAGE,
STATE_UNKNOWN,
EntityCategory,
Platform,
@@ -55,6 +53,7 @@ from homeassistant.const import (
UnitOfPower,
UnitOfPrecipitationDepth,
UnitOfPressure,
UnitOfRatio,
UnitOfReactivePower,
UnitOfSoundPressure,
UnitOfSpeed,
@@ -1076,15 +1075,15 @@ async def test_custom_unit(
# Power factor
(
None,
PERCENTAGE,
PERCENTAGE,
UnitOfRatio.PERCENTAGE,
UnitOfRatio.PERCENTAGE,
1.0,
1.0,
100.0,
SensorDeviceClass.POWER_FACTOR,
),
(
PERCENTAGE,
UnitOfRatio.PERCENTAGE,
None,
None,
100,
@@ -2601,7 +2600,7 @@ async def test_device_classes_with_invalid_state_class(
("custom", None, None, None, False),
(SensorDeviceClass.POWER, None, "V", None, True),
(None, SensorStateClass.MEASUREMENT, None, None, True),
(None, None, PERCENTAGE, None, True),
(None, None, UnitOfRatio.PERCENTAGE, None, True),
(None, None, None, None, False),
],
)
@@ -3088,9 +3087,9 @@ async def test_suggested_unit_guard_invalid_unit(
),
(
SensorDeviceClass.CO2,
CONCENTRATION_PARTS_PER_MILLION,
UnitOfRatio.PARTS_PER_MILLION,
10,
CONCENTRATION_PARTS_PER_MILLION,
UnitOfRatio.PARTS_PER_MILLION,
10,
),
],
+50 -1
View File
@@ -15,7 +15,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from homeassistant.setup import async_setup_component
from . import DOMAIN, WOMETERTHPC_SERVICE_INFO
from . import DOMAIN, STANDING_FAN_SERVICE_INFO, WOMETERTHPC_SERVICE_INFO
from tests.common import MockConfigEntry
from tests.components.bluetooth import inject_bluetooth_service_info
@@ -169,3 +169,52 @@ async def test_set_display_time_offset_out_of_range(
)
mock_set_time_offset.assert_not_awaited()
@pytest.mark.parametrize(
("entity_id", "set_method"),
[
(
"number.test_name_horizontal_oscillation_angle",
"set_horizontal_oscillation_angle",
),
(
"number.test_name_vertical_oscillation_angle",
"set_vertical_oscillation_angle",
),
],
)
async def test_standing_fan_oscillation_angle_number(
hass: HomeAssistant,
mock_entry_factory: Callable[[str], MockConfigEntry],
entity_id: str,
set_method: str,
) -> None:
"""Test horizontal/vertical oscillation angle number entities."""
await async_setup_component(hass, DOMAIN, {})
inject_bluetooth_service_info(hass, STANDING_FAN_SERVICE_INFO)
entry = mock_entry_factory(sensor_type="standing_fan")
entry.add_to_hass(hass)
mocked_set = AsyncMock(return_value=True)
with patch.multiple(
"homeassistant.components.switchbot.number.switchbot.SwitchbotStandingFan",
get_basic_info=AsyncMock(return_value=None),
**{set_method: mocked_set},
):
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
await hass.services.async_call(
NUMBER_DOMAIN,
SERVICE_SET_VALUE,
{ATTR_ENTITY_ID: entity_id, ATTR_VALUE: 60},
blocking=True,
)
mocked_set.assert_awaited_once_with(60)
state = hass.states.get(entity_id)
assert state is not None
assert float(state.state) == 60
+44 -1
View File
@@ -4,6 +4,7 @@ from collections.abc import Callable
from unittest.mock import AsyncMock, patch
import pytest
from switchbot import NightLightState
from homeassistant.components.select import (
ATTR_OPTION,
@@ -14,7 +15,7 @@ from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from . import DOMAIN, WOMETERTHPC_SERVICE_INFO
from . import DOMAIN, STANDING_FAN_SERVICE_INFO, WOMETERTHPC_SERVICE_INFO
from tests.common import MockConfigEntry
from tests.components.bluetooth import inject_bluetooth_service_info
@@ -123,3 +124,45 @@ async def test_set_time_format(
state = hass.states.get("select.test_name_time_format")
assert state is not None
assert state.state == expected_state
@pytest.mark.parametrize(
("device_state", "option", "expected_state"),
[
(NightLightState.OFF.value, "level_1", NightLightState.LEVEL_1),
(NightLightState.LEVEL_1.value, "level_2", NightLightState.LEVEL_2),
(NightLightState.LEVEL_2.value, "off", NightLightState.OFF),
],
)
async def test_standing_fan_night_light_select(
hass: HomeAssistant,
mock_entry_factory: Callable[[str], MockConfigEntry],
device_state: int,
option: str,
expected_state: NightLightState,
) -> None:
"""Test night light select translates options to device commands."""
await async_setup_component(hass, DOMAIN, {})
inject_bluetooth_service_info(hass, STANDING_FAN_SERVICE_INFO)
entry = mock_entry_factory(sensor_type="standing_fan")
entry.add_to_hass(hass)
mocked_set = AsyncMock(return_value=True)
with patch.multiple(
"homeassistant.components.switchbot.select.switchbot.SwitchbotStandingFan",
get_basic_info=AsyncMock(return_value=None),
get_night_light_state=lambda self: device_state,
set_night_light=mocked_set,
):
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
await hass.services.async_call(
SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
{ATTR_ENTITY_ID: "select.test_name_night_light", ATTR_OPTION: option},
blocking=True,
)
mocked_set.assert_awaited_once_with(expected_state)
+6 -6
View File
@@ -20,10 +20,10 @@ from homeassistant.components.tradfri.const import DOMAIN
from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_UNIT_OF_MEASUREMENT,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
PERCENTAGE,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
UnitOfDensity,
UnitOfRatio,
UnitOfTime,
)
from homeassistant.core import HomeAssistant
@@ -54,7 +54,7 @@ async def test_battery_sensor(
state = hass.states.get(entity_id)
assert state
assert state.state == "87"
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfRatio.PERCENTAGE
assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.BATTERY
assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT
@@ -65,7 +65,7 @@ async def test_battery_sensor(
state = hass.states.get(entity_id)
assert state
assert state.state == "60"
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfRatio.PERCENTAGE
assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.BATTERY
assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT
@@ -82,7 +82,7 @@ async def test_cover_battery_sensor(
state = hass.states.get(entity_id)
assert state
assert state.state == "77"
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfRatio.PERCENTAGE
assert state.attributes[ATTR_DEVICE_CLASS] == SensorDeviceClass.BATTERY
assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT
@@ -102,7 +102,7 @@ async def test_air_quality_sensor(
assert state.state == "5"
assert (
state.attributes[ATTR_UNIT_OF_MEASUREMENT]
== CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
== UnitOfDensity.MICROGRAMS_PER_CUBIC_METER
)
assert state.attributes[ATTR_STATE_CLASS] == SensorStateClass.MEASUREMENT
assert ATTR_DEVICE_CLASS not in state.attributes
+31
View File
@@ -3856,6 +3856,37 @@ async def _arm_off_to_on_trigger(
)
@pytest.mark.usefixtures("mock_integration_frame")
async def test_async_initialize_triggers_home_assistant_start_deprecated(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test passing the deprecated home_assistant_start parameter is reported."""
log = logging.getLogger(__name__)
@callback
def action(run_variables: dict[str, Any], context: Context | None = None) -> None:
pass
# The parameter no longer has any effect; passing it is logged, not raised.
assert (
await async_initialize_triggers(
hass,
[],
action,
"test",
"test",
log.log,
home_assistant_start=True,
)
is None
)
assert (
"passes `home_assistant_start` to `async_initialize_triggers`, which is "
"deprecated and will be removed in Home Assistant 2027.8" in caplog.text
)
def _set_or_remove_state(
hass: HomeAssistant, entity_id: str, state: str | None
) -> None:
+12
View File
@@ -2,7 +2,19 @@
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,13 +2,12 @@
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
from tests.pylint import assert_no_messages, walk_checker
@pytest.fixture(name="registration_checker")
@@ -68,11 +67,9 @@ 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):
walker.walk(root_node)
walk_checker(linter, registration_checker, root_node)
def test_hass_services_register_flagged(
@@ -87,9 +84,7 @@ async def async_setup_entry(hass, entry):
""",
"homeassistant.components.test_integration",
)
walker = ASTWalker(linter)
walker.add_checker(registration_checker)
walker.walk(root_node)
walk_checker(linter, registration_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
@@ -108,9 +103,7 @@ async def async_setup_entry(hass, entry):
""",
"homeassistant.components.test_integration",
)
walker = ASTWalker(linter)
walker.add_checker(registration_checker)
walker.walk(root_node)
walk_checker(linter, registration_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
@@ -128,9 +121,7 @@ async def async_setup_entry(hass, entry):
""",
"homeassistant.components.test_integration",
)
walker = ASTWalker(linter)
walker.add_checker(registration_checker)
walker.walk(root_node)
walk_checker(linter, registration_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
@@ -148,9 +139,7 @@ async def async_setup_entry(hass, entry):
""",
"homeassistant.components.test_integration",
)
walker = ASTWalker(linter)
walker.add_checker(registration_checker)
walker.walk(root_node)
walk_checker(linter, registration_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
@@ -170,9 +159,7 @@ async def async_setup_entry(hass, entry):
""",
"homeassistant.components.test_integration",
)
walker = ASTWalker(linter)
walker.add_checker(registration_checker)
walker.walk(root_node)
walk_checker(linter, registration_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 3
@@ -193,9 +180,7 @@ async def async_setup_entry(hass, entry):
""",
"homeassistant.components.test_integration",
)
walker = ASTWalker(linter)
walker.add_checker(registration_checker)
walker.walk(root_node)
walk_checker(linter, registration_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
@@ -219,8 +204,6 @@ async def async_setup_entry(hass, entry):
""",
"homeassistant.components.test_integration",
)
walker = ASTWalker(linter)
walker.add_checker(registration_checker)
with assert_no_messages(linter):
walker.walk(root_node)
walk_checker(linter, registration_checker, root_node)
@@ -2,13 +2,12 @@
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
from tests.pylint import assert_no_messages, walk_checker
@pytest.fixture(name="error_propagation_checker")
@@ -129,11 +128,9 @@ 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):
walker.walk(root_node)
walk_checker(linter, error_propagation_checker, root_node)
def test_log_only_flagged(
@@ -152,9 +149,7 @@ class MySwitch(SwitchEntity):
""",
"homeassistant.components.test_integration.switch",
)
walker = ASTWalker(linter)
walker.add_checker(error_propagation_checker)
walker.walk(root_node)
walk_checker(linter, error_propagation_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
@@ -179,9 +174,7 @@ class MySwitch(SwitchEntity):
""",
"homeassistant.components.test_integration.switch",
)
walker = ASTWalker(linter)
walker.add_checker(error_propagation_checker)
walker.walk(root_node)
walk_checker(linter, error_propagation_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
@@ -203,9 +196,7 @@ class MySwitch(SwitchEntity):
""",
"homeassistant.components.test_integration.switch",
)
walker = ASTWalker(linter)
walker.add_checker(error_propagation_checker)
walker.walk(root_node)
walk_checker(linter, error_propagation_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
@@ -229,9 +220,7 @@ class MySwitch(SwitchEntity):
""",
"homeassistant.components.test_integration.switch",
)
walker = ASTWalker(linter)
walker.add_checker(error_propagation_checker)
walker.walk(root_node)
walk_checker(linter, error_propagation_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
@@ -255,9 +244,7 @@ class MySwitch(SwitchEntity):
""",
"homeassistant.components.test_integration.switch",
)
walker = ASTWalker(linter)
walker.add_checker(error_propagation_checker)
walker.walk(root_node)
walk_checker(linter, error_propagation_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
@@ -280,9 +267,7 @@ class MySwitch(SwitchEntity):
""",
"homeassistant.components.test_integration.switch",
)
walker = ASTWalker(linter)
walker.add_checker(error_propagation_checker)
walker.walk(root_node)
walk_checker(linter, error_propagation_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
@@ -304,9 +289,7 @@ class MySwitch(SwitchEntity):
""",
"homeassistant.components.test_integration.switch",
)
walker = ASTWalker(linter)
walker.add_checker(error_propagation_checker)
walker.walk(root_node)
walk_checker(linter, error_propagation_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
@@ -334,9 +317,7 @@ class MySwitch(SwitchEntity):
""",
"homeassistant.components.test_integration.switch",
)
walker = ASTWalker(linter)
walker.add_checker(error_propagation_checker)
walker.walk(root_node)
walk_checker(linter, error_propagation_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 2
@@ -356,9 +337,7 @@ class MySwitch(SwitchEntity):
""",
"homeassistant.components.test_integration.switch",
)
walker = ASTWalker(linter)
walker.add_checker(error_propagation_checker)
walker.walk(root_node)
walk_checker(linter, error_propagation_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
@@ -379,11 +358,9 @@ class MySwitch(SwitchEntity):
""",
"homeassistant.components.test_integration.switch",
)
walker = ASTWalker(linter)
walker.add_checker(error_propagation_checker)
with assert_no_messages(linter):
walker.walk(root_node)
walk_checker(linter, error_propagation_checker, root_node)
def test_decorator_swallows_flagged(
@@ -408,9 +385,7 @@ class MySwitch(SwitchEntity):
""",
"homeassistant.components.test_integration.switch",
)
walker = ASTWalker(linter)
walker.add_checker(error_propagation_checker)
walker.walk(root_node)
walk_checker(linter, error_propagation_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
@@ -441,9 +416,7 @@ class MySwitch(SwitchEntity):
""",
"homeassistant.components.test_integration.switch",
)
walker = ASTWalker(linter)
walker.add_checker(error_propagation_checker)
walker.walk(root_node)
walk_checker(linter, error_propagation_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
@@ -471,11 +444,9 @@ class MySwitch(SwitchEntity):
""",
"homeassistant.components.test_integration.switch",
)
walker = ASTWalker(linter)
walker.add_checker(error_propagation_checker)
with assert_no_messages(linter):
walker.walk(root_node)
walk_checker(linter, error_propagation_checker, root_node)
def test_custom_service_method_flagged(
@@ -502,9 +473,7 @@ class MyFan(FanEntity):
""",
"homeassistant.components.test_integration.fan",
)
walker = ASTWalker(linter)
walker.add_checker(error_propagation_checker)
walker.walk(root_node)
walk_checker(linter, error_propagation_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
@@ -535,11 +504,9 @@ class MyFan(FanEntity):
""",
"homeassistant.components.test_integration.fan",
)
walker = ASTWalker(linter)
walker.add_checker(error_propagation_checker)
with assert_no_messages(linter):
walker.walk(root_node)
walk_checker(linter, error_propagation_checker, root_node)
def test_unregistered_custom_method_ignored(
@@ -558,11 +525,9 @@ class MyFan(FanEntity):
""",
"homeassistant.components.test_integration.fan",
)
walker = ASTWalker(linter)
walker.add_checker(error_propagation_checker)
with assert_no_messages(linter):
walker.walk(root_node)
walk_checker(linter, error_propagation_checker, root_node)
def test_standalone_service_handler_flagged(
@@ -583,9 +548,7 @@ async def _handle_do_thing(call):
""",
"homeassistant.components.test_integration.services",
)
walker = ASTWalker(linter)
walker.add_checker(error_propagation_checker)
walker.walk(root_node)
walk_checker(linter, error_propagation_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
@@ -610,9 +573,7 @@ async def _handle_reset(call):
""",
"homeassistant.components.test_integration.services",
)
walker = ASTWalker(linter)
walker.add_checker(error_propagation_checker)
walker.walk(root_node)
walk_checker(linter, error_propagation_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
@@ -637,11 +598,9 @@ 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):
walker.walk(root_node)
walk_checker(linter, error_propagation_checker, root_node)
def test_not_integration_module_ignored(
@@ -660,8 +619,6 @@ class MySwitch(SwitchEntity):
""",
"tests.components.test_integration.test_switch",
)
walker = ASTWalker(linter)
walker.add_checker(error_propagation_checker)
with assert_no_messages(linter):
walker.walk(root_node)
walk_checker(linter, error_propagation_checker, root_node)
+6 -19
View File
@@ -8,10 +8,9 @@ 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
from tests.pylint import assert_no_messages, walk_checker
@pytest.mark.parametrize(
@@ -62,11 +61,9 @@ 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):
walker.walk(root_node)
walk_checker(linter, enforce_config_flow_no_name_checker, root_node)
@pytest.mark.parametrize(
@@ -110,10 +107,8 @@ 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)
walker.walk(root_node)
walk_checker(linter, enforce_config_flow_no_name_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
assert messages[0].msg_id == "home-assistant-config-flow-name-field"
@@ -134,11 +129,9 @@ 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):
walker.walk(root_node)
walk_checker(linter, enforce_config_flow_no_name_checker, root_node)
def test_enforce_config_flow_no_name_helper_integration(
@@ -160,11 +153,8 @@ 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):
walker.walk(root_node)
walk_checker(linter, enforce_config_flow_no_name_checker, root_node)
def test_enforce_config_flow_no_name_non_helper_integration(
@@ -184,10 +174,7 @@ 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")
walker = ASTWalker(linter)
walker.add_checker(enforce_config_flow_no_name_checker)
walker.walk(root_node)
walk_checker(linter, enforce_config_flow_no_name_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
assert messages[0].msg_id == "home-assistant-config-flow-name-field"
+3 -8
View File
@@ -3,10 +3,9 @@
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
from tests.pylint import assert_no_messages, walk_checker
@pytest.mark.parametrize(
@@ -71,11 +70,9 @@ 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):
walker.walk(root_node)
walk_checker(linter, enforce_config_flow_no_polling_checker, root_node)
@pytest.mark.parametrize(
@@ -133,10 +130,8 @@ 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)
walker.walk(root_node)
walk_checker(linter, enforce_config_flow_no_polling_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
assert messages[0].msg_id == "home-assistant-config-flow-polling-field"
@@ -3,10 +3,9 @@
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
from tests.pylint import assert_no_messages, walk_checker
@pytest.mark.parametrize(
@@ -64,11 +63,9 @@ 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):
walker.walk(root_node)
walk_checker(linter, enforce_config_entry_unique_id_no_ip_checker, root_node)
@pytest.mark.parametrize(
@@ -121,10 +118,8 @@ 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)
walker.walk(root_node)
walk_checker(linter, enforce_config_entry_unique_id_no_ip_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
assert messages[0].msg_id == "home-assistant-unique-id-ip-based"
@@ -182,10 +177,8 @@ 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)
walker.walk(root_node)
walk_checker(linter, enforce_config_entry_unique_id_no_ip_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
assert messages[0].msg_id == "home-assistant-unique-id-ip-based"
@@ -4,7 +4,6 @@ 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,
)
@@ -12,7 +11,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
from tests.pylint import assert_adds_messages, assert_no_messages, walk_checker
@pytest.fixture(name="unloading_checker")
@@ -56,11 +55,8 @@ 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):
walker.walk(root_node)
walk_checker(linter, unloading_checker, root_node)
def test_unload_entry_missing_fires(
@@ -81,9 +77,6 @@ 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(
@@ -93,7 +86,7 @@ async def async_setup_entry(hass, entry):
col_offset=0,
),
):
walker.walk(root_node)
walk_checker(linter, unloading_checker, root_node)
@pytest.mark.parametrize(
@@ -146,8 +139,5 @@ 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):
walker.walk(root_node)
walk_checker(linter, unloading_checker, root_node)
+4 -14
View File
@@ -4,13 +4,12 @@ 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
from tests.pylint import assert_adds_messages, assert_no_messages, walk_checker
@pytest.fixture(name="diagnostics_checker")
@@ -75,11 +74,8 @@ 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):
walker.walk(root_node)
walk_checker(linter, diagnostics_checker, root_node)
def test_diagnostics_missing_fires(
@@ -100,9 +96,6 @@ 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(
@@ -112,7 +105,7 @@ async def async_setup(hass, config):
col_offset=0,
),
):
walker.walk(root_node)
walk_checker(linter, diagnostics_checker, root_node)
@pytest.mark.parametrize(
@@ -165,8 +158,5 @@ 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):
walker.walk(root_node)
walk_checker(linter, diagnostics_checker, root_node)
@@ -6,7 +6,6 @@ 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,
)
@@ -15,7 +14,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
from tests.pylint import assert_adds_messages, assert_no_messages, walk_checker
@pytest.fixture(name="entity_unique_id_checker")
@@ -258,10 +257,8 @@ 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):
walker.walk(root_node)
walk_checker(linter, entity_unique_id_checker, root_node)
@pytest.mark.parametrize(
@@ -351,10 +348,8 @@ 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):
walker.walk(root_node)
walk_checker(linter, entity_unique_id_checker, root_node)
def test_missing_fires(
@@ -377,10 +372,8 @@ 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)):
walker.walk(root_node)
walk_checker(linter, entity_unique_id_checker, root_node)
@pytest.mark.parametrize(
@@ -444,10 +437,8 @@ 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)):
walker.walk(root_node)
walk_checker(linter, entity_unique_id_checker, root_node)
def test_explicit_none_class_body_fires(
@@ -470,10 +461,8 @@ 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)):
walker.walk(root_node)
walk_checker(linter, entity_unique_id_checker, root_node)
def test_subclass_nullifies_ancestor_value(
@@ -510,10 +499,8 @@ 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)):
walker.walk(root_node)
walk_checker(linter, entity_unique_id_checker, root_node)
def test_explicit_none_self_assign_fires(
@@ -537,10 +524,8 @@ 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)):
walker.walk(root_node)
walk_checker(linter, entity_unique_id_checker, root_node)
def test_bare_annotation_only_fires(
@@ -563,10 +548,8 @@ 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)):
walker.walk(root_node)
walk_checker(linter, entity_unique_id_checker, root_node)
def test_entity_default_does_not_satisfy(
@@ -596,10 +579,8 @@ 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)):
walker.walk(root_node)
walk_checker(linter, entity_unique_id_checker, root_node)
@pytest.mark.parametrize(
@@ -638,10 +619,8 @@ 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):
walker.walk(root_node)
walk_checker(linter, entity_unique_id_checker, root_node)
def test_leaf_class_still_fires(
@@ -671,10 +650,8 @@ 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)):
walker.walk(root_node)
walk_checker(linter, entity_unique_id_checker, root_node)
def test_dict_status_done_fires(
@@ -700,10 +677,8 @@ 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)):
walker.walk(root_node)
walk_checker(linter, entity_unique_id_checker, root_node)
@pytest.mark.parametrize(
@@ -773,10 +748,8 @@ 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):
walker.walk(root_node)
walk_checker(linter, entity_unique_id_checker, root_node)
def _find_attr_value_node(
@@ -848,10 +821,8 @@ 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")):
walker.walk(root_node)
walk_checker(linter, entity_unique_id_checker, root_node)
def test_static_class_body_string_no_manifest_fires(
@@ -875,10 +846,8 @@ 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")):
walker.walk(root_node)
walk_checker(linter, entity_unique_id_checker, root_node)
_STATIC_CLASS_BODY_STRING = """
@@ -946,7 +915,5 @@ 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):
walker.walk(root_node)
walk_checker(linter, entity_unique_id_checker, root_node)
@@ -5,7 +5,6 @@ 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,
)
@@ -13,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
from tests.pylint import assert_adds_messages, assert_no_messages, walk_checker
@pytest.fixture(name="has_entity_name_checker")
@@ -145,10 +144,8 @@ 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):
walker.walk(root_node)
walk_checker(linter, has_entity_name_checker, root_node)
def test_ancestor_class_level(
@@ -180,10 +177,8 @@ class MySensor(TestIntegrationBaseEntity):
integration_dir,
)
walker = ASTWalker(linter)
walker.add_checker(has_entity_name_checker)
with assert_no_messages(linter):
walker.walk(root_node)
walk_checker(linter, has_entity_name_checker, root_node)
def test_ancestor_self_assign(
@@ -216,10 +211,8 @@ class MySensor(TestIntegrationBaseEntity):
integration_dir,
)
walker = ASTWalker(linter)
walker.add_checker(has_entity_name_checker)
with assert_no_messages(linter):
walker.walk(root_node)
walk_checker(linter, has_entity_name_checker, root_node)
def test_missing_fires(
@@ -242,10 +235,8 @@ 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)):
walker.walk(root_node)
walk_checker(linter, has_entity_name_checker, root_node)
@pytest.mark.parametrize(
@@ -333,10 +324,8 @@ 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)):
walker.walk(root_node)
walk_checker(linter, has_entity_name_checker, root_node)
def test_explicit_false_fires(
@@ -359,10 +348,8 @@ 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)):
walker.walk(root_node)
walk_checker(linter, has_entity_name_checker, root_node)
def test_generic_subscript_base_sets_flag(
@@ -394,10 +381,8 @@ class MySensor(TestIntegrationGenericBase[int]):
integration_dir,
)
walker = ASTWalker(linter)
walker.add_checker(has_entity_name_checker)
with assert_no_messages(linter):
walker.walk(root_node)
walk_checker(linter, has_entity_name_checker, root_node)
def test_two_level_subscript_chain(
@@ -438,10 +423,8 @@ class MyLight(TestIntegrationLightBase):
file_name="light.py",
)
walker = ASTWalker(linter)
walker.add_checker(has_entity_name_checker)
with assert_no_messages(linter):
walker.walk(root_node)
walk_checker(linter, has_entity_name_checker, root_node)
def test_entity_description_fallback(
@@ -471,10 +454,8 @@ class MyEntity(Entity):
integration_dir,
)
walker = ASTWalker(linter)
walker.add_checker(has_entity_name_checker)
with assert_no_messages(linter):
walker.walk(root_node)
walk_checker(linter, has_entity_name_checker, root_node)
def test_entity_description_subscripted_annotation(
@@ -504,10 +485,8 @@ class MyEntity[T](Entity):
integration_dir,
)
walker = ASTWalker(linter)
walker.add_checker(has_entity_name_checker)
with assert_no_messages(linter):
walker.walk(root_node)
walk_checker(linter, has_entity_name_checker, root_node)
def test_entity_description_without_flag_still_fires(
@@ -542,10 +521,8 @@ 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)):
walker.walk(root_node)
walk_checker(linter, has_entity_name_checker, root_node)
def test_entity_description_set_in_ancestor(
@@ -585,10 +562,8 @@ class MySensor(TestIntegrationBaseEntity):
integration_dir,
)
walker = ASTWalker(linter)
walker.add_checker(has_entity_name_checker)
with assert_no_messages(linter):
walker.walk(root_node)
walk_checker(linter, has_entity_name_checker, root_node)
def test_mixin_subclassed_in_same_module_ignored(
@@ -613,10 +588,8 @@ class ActualEntity(MyClimateMixin):
integration_dir,
)
walker = ASTWalker(linter)
walker.add_checker(has_entity_name_checker)
with assert_no_messages(linter):
walker.walk(root_node)
walk_checker(linter, has_entity_name_checker, root_node)
def test_subclassed_via_subscript_ignored(
@@ -641,10 +614,8 @@ class ConcreteEntity(GenericBase[int]):
integration_dir,
)
walker = ASTWalker(linter)
walker.add_checker(has_entity_name_checker)
with assert_no_messages(linter):
walker.walk(root_node)
walk_checker(linter, has_entity_name_checker, root_node)
def test_leaf_class_still_fires(
@@ -674,10 +645,8 @@ 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)):
walker.walk(root_node)
walk_checker(linter, has_entity_name_checker, root_node)
def test_non_entity_class_ignored(
@@ -697,10 +666,8 @@ class NotAnEntity:
integration_dir,
)
walker = ASTWalker(linter)
walker.add_checker(has_entity_name_checker)
with assert_no_messages(linter):
walker.walk(root_node)
walk_checker(linter, has_entity_name_checker, root_node)
def test_dict_status_done_fires(
@@ -726,10 +693,8 @@ 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)):
walker.walk(root_node)
walk_checker(linter, has_entity_name_checker, root_node)
@pytest.mark.parametrize(
@@ -799,7 +764,5 @@ 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):
walker.walk(root_node)
walk_checker(linter, has_entity_name_checker, root_node)
@@ -4,7 +4,6 @@ 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,
)
@@ -12,7 +11,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
from tests.pylint import assert_adds_messages, assert_no_messages, walk_checker
@pytest.fixture(name="parallel_updates_checker")
@@ -61,11 +60,8 @@ 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):
walker.walk(root_node)
walk_checker(linter, parallel_updates_checker, root_node)
def test_parallel_updates_zero(
@@ -82,11 +78,8 @@ 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):
walker.walk(root_node)
walk_checker(linter, parallel_updates_checker, root_node)
def test_parallel_updates_annotated_assignment(
@@ -104,11 +97,8 @@ 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):
walker.walk(root_node)
walk_checker(linter, parallel_updates_checker, root_node)
def test_parallel_updates_missing_fires(
@@ -126,9 +116,6 @@ 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(
@@ -138,7 +125,7 @@ def test_parallel_updates_missing_fires(
col_offset=0,
),
):
walker.walk(root_node)
walk_checker(linter, parallel_updates_checker, root_node)
def test_parallel_updates_missing_status_done_dict(
@@ -159,9 +146,6 @@ 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(
@@ -171,7 +155,7 @@ def test_parallel_updates_missing_status_done_dict(
col_offset=0,
),
):
walker.walk(root_node)
walk_checker(linter, parallel_updates_checker, root_node)
@pytest.mark.parametrize(
@@ -231,8 +215,5 @@ 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):
walker.walk(root_node)
walk_checker(linter, parallel_updates_checker, root_node)
@@ -4,7 +4,6 @@ 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,
)
@@ -12,7 +11,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
from tests.pylint import assert_adds_messages, assert_no_messages, walk_checker
@pytest.fixture(name="reauth_checker")
@@ -54,11 +53,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):
walker.walk(root_node)
walk_checker(linter, reauth_checker, root_node)
def test_reauth_missing_fires(
@@ -80,9 +76,6 @@ 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(
@@ -92,7 +85,7 @@ class MyConfigFlow(ConfigFlow, domain=DOMAIN):
col_offset=0,
),
):
walker.walk(root_node)
walk_checker(linter, reauth_checker, root_node)
@pytest.mark.parametrize(
@@ -146,8 +139,5 @@ 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):
walker.walk(root_node)
walk_checker(linter, reauth_checker, root_node)
+7 -20
View File
@@ -5,10 +5,9 @@ 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
from . import assert_adds_messages, assert_no_messages, walk_checker
@pytest.mark.parametrize(
@@ -54,11 +53,9 @@ 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):
walker.walk(root_node)
walk_checker(linter, enforce_class_module_checker, root_node)
@pytest.mark.parametrize(
@@ -90,11 +87,9 @@ 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):
walker.walk(root_node)
walk_checker(linter, enforce_class_module_checker, root_node)
@pytest.mark.parametrize(
@@ -128,8 +123,6 @@ def test_enforce_class_module_bad_simple(
""",
path,
)
walker = ASTWalker(linter)
walker.add_checker(enforce_class_module_checker)
with assert_adds_messages(
linter,
@@ -154,7 +147,7 @@ def test_enforce_class_module_bad_simple(
end_col_offset=35,
),
):
walker.walk(root_node)
walk_checker(linter, enforce_class_module_checker, root_node)
@pytest.mark.parametrize(
@@ -185,8 +178,6 @@ def test_enforce_class_module_bad_nested(
""",
path,
)
walker = ASTWalker(linter)
walker.add_checker(enforce_class_module_checker)
with assert_adds_messages(
linter,
@@ -211,7 +202,7 @@ def test_enforce_class_module_bad_nested(
end_col_offset=21,
),
):
walker.walk(root_node)
walk_checker(linter, enforce_class_module_checker, root_node)
@pytest.mark.parametrize(
@@ -236,11 +227,9 @@ 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):
walker.walk(root_node)
walk_checker(linter, enforce_class_module_checker, root_node)
@pytest.mark.parametrize(
@@ -265,8 +254,6 @@ 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,
@@ -281,4 +268,4 @@ def test_enforce_entity_bad(
end_col_offset=18,
),
):
walker.walk(root_node)
walk_checker(linter, enforce_class_module_checker, root_node)
+7 -20
View File
@@ -5,10 +5,9 @@ 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
from . import assert_adds_messages, assert_no_messages, walk_checker
def test_good_callback(linter: UnittestLinter, decorator_checker: BaseChecker) -> None:
@@ -24,11 +23,9 @@ 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):
walker.walk(root_node)
walk_checker(linter, decorator_checker, root_node)
def test_bad_callback(linter: UnittestLinter, decorator_checker: BaseChecker) -> None:
@@ -44,8 +41,6 @@ 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,
@@ -60,7 +55,7 @@ def test_bad_callback(linter: UnittestLinter, decorator_checker: BaseChecker) ->
end_col_offset=15,
),
):
walker.walk(root_node)
walk_checker(linter, decorator_checker, root_node)
@pytest.mark.parametrize(
@@ -110,11 +105,9 @@ def test_good_fixture(
"""
root_node = astroid.parse(code, path)
walker = ASTWalker(linter)
walker.add_checker(decorator_checker)
with assert_no_messages(linter):
walker.walk(root_node)
walk_checker(linter, decorator_checker, root_node)
@pytest.mark.parametrize(
@@ -146,8 +139,6 @@ 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,
@@ -162,7 +153,7 @@ def test_bad_fixture_session_scope(
end_col_offset=32,
),
):
walker.walk(root_node)
walk_checker(linter, decorator_checker, root_node)
@pytest.mark.parametrize(
@@ -193,8 +184,6 @@ 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,
@@ -209,7 +198,7 @@ def test_bad_fixture_package_scope(
end_col_offset=32,
),
):
walker.walk(root_node)
walk_checker(linter, decorator_checker, root_node)
@pytest.mark.parametrize(
@@ -247,8 +236,6 @@ def test_bad_fixture_autouse(
"""
root_node = astroid.parse(code, path)
walker = ASTWalker(linter)
walker.add_checker(decorator_checker)
with assert_adds_messages(
linter,
@@ -263,4 +250,4 @@ def test_bad_fixture_autouse(
end_col_offset=17 + len(keywords),
),
):
walker.walk(root_node)
walk_checker(linter, decorator_checker, root_node)
+6 -17
View File
@@ -2,11 +2,10 @@
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
from . import assert_no_messages, walk_checker
@pytest.fixture(name="determinism_checker")
@@ -144,11 +143,9 @@ 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):
walker.walk(root_node)
walk_checker(linter, determinism_checker, root_node)
def test_if_statement_flagged(
@@ -167,9 +164,7 @@ def test_sensor_value(hass) -> None:
""",
"tests.components.test_integration.test_sensor",
)
walker = ASTWalker(linter)
walker.add_checker(determinism_checker)
walker.walk(root_node)
walk_checker(linter, determinism_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
@@ -193,9 +188,7 @@ def test_something(hass) -> None:
""",
"tests.components.test_integration.test_init",
)
walker = ASTWalker(linter)
walker.add_checker(determinism_checker)
walker.walk(root_node)
walk_checker(linter, determinism_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 2
@@ -216,9 +209,7 @@ async def test_something(hass) -> None:
""",
"tests.components.test_integration.test_init",
)
walker = ASTWalker(linter)
walker.add_checker(determinism_checker)
walker.walk(root_node)
walk_checker(linter, determinism_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
@@ -241,9 +232,7 @@ def test_something(state) -> None:
""",
"tests.components.test_integration.test_init",
)
walker = ASTWalker(linter)
walker.add_checker(determinism_checker)
walker.walk(root_node)
walk_checker(linter, determinism_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
+4 -11
View File
@@ -2,11 +2,10 @@
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
from . import assert_no_messages, walk_checker
@pytest.fixture(name="domain_constant_checker")
@@ -221,11 +220,9 @@ 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):
walker.walk(root_node)
walk_checker(linter, domain_constant_checker, root_node)
@pytest.mark.parametrize(
@@ -255,9 +252,7 @@ def test_domain_argument_flagged(
) -> None:
"""Test that non-domain arguments are flagged."""
root_node = astroid.parse(code, "tests.components.test_integration.test_init")
walker = ASTWalker(linter)
walker.add_checker(domain_constant_checker)
walker.walk(root_node)
walk_checker(linter, domain_constant_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
@@ -276,8 +271,6 @@ async_setup_component(hass, OTHER, {})
""",
"homeassistant.components.test_integration",
)
walker = ASTWalker(linter)
walker.add_checker(domain_constant_checker)
with assert_no_messages(linter):
walker.walk(root_node)
walk_checker(linter, domain_constant_checker, root_node)
+4 -11
View File
@@ -2,11 +2,10 @@
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
from . import assert_no_messages, walk_checker
# Pre-load homeassistant.const so astroid can resolve it.
astroid.MANAGER.ast_from_module_name("homeassistant.const")
@@ -64,11 +63,9 @@ 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):
walker.walk(root_node)
walk_checker(linter, duplicate_const_checker, root_node)
@pytest.mark.parametrize(
@@ -121,9 +118,7 @@ def test_duplicate_const_flagged(
) -> None:
"""Test that duplicate constants are flagged."""
root_node = astroid.parse(code, "homeassistant.components.test_integration.const")
walker = ASTWalker(linter)
walker.add_checker(duplicate_const_checker)
walker.walk(root_node)
walk_checker(linter, duplicate_const_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
@@ -142,8 +137,6 @@ CONF_HOST = "host"
""",
"tests.components.test_integration.test_const",
)
walker = ASTWalker(linter)
walker.add_checker(duplicate_const_checker)
with assert_no_messages(linter):
walker.walk(root_node)
walk_checker(linter, duplicate_const_checker, root_node)
@@ -2,13 +2,12 @@
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
from . import assert_no_messages, walk_checker
# Pre-load EntityDescription so astroid can resolve it in parsed snippets.
# This avoids depending on component-level imports which may not be
@@ -75,11 +74,9 @@ 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):
walker.walk(root_node)
walk_checker(linter, defaults_checker, root_node)
@pytest.mark.parametrize(
@@ -161,9 +158,7 @@ def test_redundant_default_flagged(
) -> None:
"""Test that redundant defaults are flagged."""
root_node = astroid.parse(code, "homeassistant.components.test_integration.sensor")
walker = ASTWalker(linter)
walker.add_checker(defaults_checker)
walker.walk(root_node)
walk_checker(linter, defaults_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
@@ -185,11 +180,9 @@ JobDescription(icon=None)
""",
"homeassistant.components.test_integration.sensor",
)
walker = ASTWalker(linter)
walker.add_checker(defaults_checker)
with assert_no_messages(linter):
walker.walk(root_node)
walk_checker(linter, defaults_checker, root_node)
def test_local_entity_description_name_ignored(
@@ -209,11 +202,9 @@ MyDescription(entity_registry_enabled_default=True)
""",
"homeassistant.components.test_integration.sensor",
)
walker = ASTWalker(linter)
walker.add_checker(defaults_checker)
with assert_no_messages(linter):
walker.walk(root_node)
walk_checker(linter, defaults_checker, root_node)
def test_aliased_description_flagged(
@@ -230,9 +221,7 @@ Alias(key="temperature", icon=None)
""",
"homeassistant.components.test_integration.sensor",
)
walker = ASTWalker(linter)
walker.add_checker(defaults_checker)
walker.walk(root_node)
walk_checker(linter, defaults_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
@@ -256,8 +245,6 @@ EntityDescription(
""",
"tests.components.test_integration.test_sensor",
)
walker = ASTWalker(linter)
walker.add_checker(defaults_checker)
with assert_no_messages(linter):
walker.walk(root_node)
walk_checker(linter, defaults_checker, root_node)
+11 -32
View File
@@ -6,14 +6,13 @@ 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
from . import assert_adds_messages, assert_no_messages, walk_checker
@pytest.fixture(name="checker")
@@ -181,10 +180,8 @@ 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")):
walker.walk(root_node)
walk_checker(linter, checker, root_node)
def test_redundant_domain_fires_in_both_branches(
@@ -223,14 +220,12 @@ 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"),
):
walker.walk(root_node)
walk_checker(linter, checker, root_node)
@pytest.mark.parametrize(
@@ -280,10 +275,8 @@ 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):
walker.walk(root_node)
walk_checker(linter, checker, root_node)
@pytest.mark.parametrize(
@@ -370,9 +363,7 @@ def test_redundant_domain_literal_fires(
integration_dir = _make_integration(tmp_path, domain="myhub")
root_node = _parse(code, integration_dir)
walker = ASTWalker(linter)
walker.add_checker(checker)
walker.walk(root_node)
walk_checker(linter, checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
assert messages[0].msg_id == "home-assistant-entity-unique-id-redundant-domain"
@@ -410,9 +401,7 @@ class MySensor(Entity):
""",
integration_dir,
)
walker = ASTWalker(linter)
walker.add_checker(checker)
walker.walk(root_node)
walk_checker(linter, checker, root_node)
messages = linter.release_messages()
assert len(messages) == (1 if fires else 0)
@@ -492,10 +481,8 @@ 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):
walker.walk(root_node)
walk_checker(linter, checker, root_node)
@pytest.mark.parametrize(
@@ -539,12 +526,10 @@ 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")
):
walker.walk(root_node)
walk_checker(linter, checker, root_node)
@pytest.mark.parametrize(
@@ -591,10 +576,8 @@ 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):
walker.walk(root_node)
walk_checker(linter, checker, root_node)
@pytest.mark.parametrize(
@@ -640,10 +623,8 @@ 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")):
walker.walk(root_node)
walk_checker(linter, checker, root_node)
def test_same_module_mixin_base_fires(
@@ -674,9 +655,7 @@ 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")
):
walker.walk(root_node)
walk_checker(linter, checker, root_node)
+19 -63
View File
@@ -5,7 +5,6 @@ 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,
)
@@ -14,7 +13,7 @@ from pylint_home_assistant.helpers.translations import clear_translations_cache
import pytest
import yaml
from . import assert_no_messages
from . import assert_no_messages, walk_checker
# Pre-load so astroid can resolve exception classes in parsed snippets.
astroid.MANAGER.ast_from_module_name("homeassistant.exceptions")
@@ -125,11 +124,8 @@ 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):
walker.walk(root_node)
walk_checker(linter, translations_checker, root_node)
@pytest.mark.parametrize(
@@ -164,9 +160,7 @@ def test_hardcoded_string_flagged(
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)
walker.walk(root_node)
walk_checker(linter, translations_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
@@ -218,9 +212,7 @@ def test_translation_key_domain_mismatch_flagged(
)
root_node.file = str(integration_dir / "coordinator.py")
walker = ASTWalker(linter)
walker.add_checker(translations_checker)
walker.walk(root_node)
walk_checker(linter, translations_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
@@ -249,9 +241,7 @@ raise HomeAssistantError(
)
root_node.file = str(integration_dir / "coordinator.py")
walker = ASTWalker(linter)
walker.add_checker(translations_checker)
walker.walk(root_node)
walk_checker(linter, translations_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
@@ -280,9 +270,7 @@ raise HomeAssistantError(
)
root_node.file = str(integration_dir / "coordinator.py")
walker = ASTWalker(linter)
walker.add_checker(translations_checker)
walker.walk(root_node)
walk_checker(linter, translations_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
@@ -312,11 +300,8 @@ raise HomeAssistantError(
)
root_node.file = str(integration_dir / "coordinator.py")
walker = ASTWalker(linter)
walker.add_checker(translations_checker)
with assert_no_messages(linter):
walker.walk(root_node)
walk_checker(linter, translations_checker, root_node)
def test_extra_placeholders_flagged(
@@ -344,9 +329,7 @@ raise HomeAssistantError(
)
root_node.file = str(integration_dir / "coordinator.py")
walker = ASTWalker(linter)
walker.add_checker(translations_checker)
walker.walk(root_node)
walk_checker(linter, translations_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
@@ -378,9 +361,7 @@ raise HomeAssistantError(
)
root_node.file = str(integration_dir / "coordinator.py")
walker = ASTWalker(linter)
walker.add_checker(translations_checker)
walker.walk(root_node)
walk_checker(linter, translations_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
@@ -412,11 +393,8 @@ raise HomeAssistantError(
)
root_node.file = str(integration_dir / "coordinator.py")
walker = ASTWalker(linter)
walker.add_checker(translations_checker)
with assert_no_messages(linter):
walker.walk(root_node)
walk_checker(linter, translations_checker, root_node)
def test_placeholder_variable_resolved(
@@ -445,11 +423,8 @@ raise HomeAssistantError(
)
root_node.file = str(integration_dir / "coordinator.py")
walker = ASTWalker(linter)
walker.add_checker(translations_checker)
with assert_no_messages(linter):
walker.walk(root_node)
walk_checker(linter, translations_checker, root_node)
def test_placeholder_variable_mismatch_flagged(
@@ -478,9 +453,7 @@ raise HomeAssistantError(
)
root_node.file = str(integration_dir / "coordinator.py")
walker = ASTWalker(linter)
walker.add_checker(translations_checker)
walker.walk(root_node)
walk_checker(linter, translations_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
@@ -513,11 +486,8 @@ raise HomeAssistantError(
)
root_node.file = str(integration_dir / "coordinator.py")
walker = ASTWalker(linter)
walker.add_checker(translations_checker)
with assert_no_messages(linter):
walker.walk(root_node)
walk_checker(linter, translations_checker, root_node)
def test_constant_placeholder_keys_ok(
@@ -546,11 +516,8 @@ raise HomeAssistantError(
)
root_node.file = str(integration_dir / "coordinator.py")
walker = ASTWalker(linter)
walker.add_checker(translations_checker)
with assert_no_messages(linter):
walker.walk(root_node)
walk_checker(linter, translations_checker, root_node)
def test_key_reference_resolution(
@@ -588,11 +555,8 @@ raise HomeAssistantError(
)
root_node.file = str(integration_dir / "coordinator.py")
walker = ASTWalker(linter)
walker.add_checker(translations_checker)
with assert_no_messages(linter):
walker.walk(root_node)
walk_checker(linter, translations_checker, root_node)
def test_no_strings_json_flags_missing_key(
@@ -614,9 +578,7 @@ raise HomeAssistantError(
)
root_node.file = str(integration_dir / "coordinator.py")
walker = ASTWalker(linter)
walker.add_checker(translations_checker)
walker.walk(root_node)
walk_checker(linter, translations_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
@@ -647,9 +609,7 @@ raise HomeAssistantError(
)
root_node.file = str(integration_dir / "coordinator.py")
walker = ASTWalker(linter)
walker.add_checker(translations_checker)
walker.walk(root_node)
walk_checker(linter, translations_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
@@ -682,9 +642,7 @@ raise HomeAssistantError(
)
root_node.file = str(integration_dir / "coordinator.py")
walker = ASTWalker(linter)
walker.add_checker(translations_checker)
walker.walk(root_node)
walk_checker(linter, translations_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
@@ -700,8 +658,6 @@ 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):
walker.walk(root_node)
walk_checker(linter, translations_checker, root_node)
+3 -8
View File
@@ -3,10 +3,9 @@
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
from . import assert_no_messages, walk_checker
@pytest.mark.parametrize(
@@ -77,11 +76,9 @@ 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):
walker.walk(root_node)
walk_checker(linter, enforce_greek_micro_char_checker, root_node)
@pytest.mark.parametrize(
@@ -153,10 +150,8 @@ 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)
walker.walk(root_node)
walk_checker(linter, enforce_greek_micro_char_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
message = next(iter(messages))
+8 -25
View File
@@ -5,12 +5,11 @@ 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
from . import assert_no_messages, walk_checker
@pytest.fixture(name="mdi_checker")
@@ -78,11 +77,9 @@ 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):
walker.walk(root_node)
walk_checker(linter, mdi_checker, root_node)
@pytest.mark.parametrize(
@@ -118,9 +115,7 @@ 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")
walker = ASTWalker(linter)
walker.add_checker(mdi_checker)
walker.walk(root_node)
walk_checker(linter, mdi_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
@@ -137,11 +132,9 @@ 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):
walker.walk(root_node)
walk_checker(linter, mdi_checker, root_node)
# --- icons.json tests ---
@@ -173,11 +166,8 @@ 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):
walker.walk(root_node)
walk_checker(linter, mdi_checker, root_node)
def test_icons_json_invalid_flagged(
@@ -203,9 +193,7 @@ def test_icons_json_invalid_flagged(
)
root_node.file = str(integration_dir / "__init__.py")
walker = ASTWalker(linter)
walker.add_checker(mdi_checker)
walker.walk(root_node)
walk_checker(linter, mdi_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
@@ -227,11 +215,8 @@ 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):
walker.walk(root_node)
walk_checker(linter, mdi_checker, root_node)
def test_icons_json_nested_invalid_flagged(
@@ -265,9 +250,7 @@ def test_icons_json_nested_invalid_flagged(
)
root_node.file = str(integration_dir / "__init__.py")
walker = ASTWalker(linter)
walker.add_checker(mdi_checker)
walker.walk(root_node)
walk_checker(linter, mdi_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
+4 -11
View File
@@ -3,10 +3,9 @@
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
from . import assert_no_messages, walk_checker
@pytest.mark.parametrize(
@@ -71,11 +70,9 @@ 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):
walker.walk(root_node)
walk_checker(linter, enforce_naive_now_checker, root_node)
@pytest.mark.parametrize(
@@ -158,10 +155,8 @@ 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)
walker.walk(root_node)
walk_checker(linter, enforce_naive_now_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
assert messages[0].msg_id == "home-assistant-enforce-naive-now"
@@ -197,8 +192,6 @@ 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):
walker.walk(root_node)
walk_checker(linter, enforce_naive_now_checker, root_node)
+4 -11
View File
@@ -3,10 +3,9 @@
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
from . import assert_no_messages, walk_checker
@pytest.mark.parametrize(
@@ -136,11 +135,9 @@ 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):
walker.walk(root_node)
walk_checker(linter, enforce_now_checker, root_node)
@pytest.mark.parametrize(
@@ -230,10 +227,8 @@ 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)
walker.walk(root_node)
walk_checker(linter, enforce_now_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
assert messages[0].msg_id == "home-assistant-enforce-now"
@@ -270,8 +265,6 @@ 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):
walker.walk(root_node)
walk_checker(linter, enforce_now_checker, root_node)
+5 -16
View File
@@ -5,10 +5,9 @@ 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
from . import assert_no_messages, walk_checker
@pytest.mark.parametrize(
@@ -104,11 +103,9 @@ 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):
walker.walk(root_node)
walk_checker(linter, enforce_runtime_data_checker, root_node)
@pytest.mark.parametrize(
@@ -160,10 +157,8 @@ 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)
walker.walk(root_node)
walk_checker(linter, enforce_runtime_data_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
assert messages[0].msg_id == "home-assistant-use-runtime-data"
@@ -187,11 +182,8 @@ 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):
walker.walk(root_node)
walk_checker(linter, enforce_runtime_data_checker, root_node)
def test_enforce_runtime_data_with_config_flow(
@@ -213,10 +205,7 @@ def test_enforce_runtime_data_with_config_flow(
root_node = astroid.parse(code, "homeassistant.components.modern")
root_node.file = str(init_file)
walker = ASTWalker(linter)
walker.add_checker(enforce_runtime_data_checker)
walker.walk(root_node)
walk_checker(linter, enforce_runtime_data_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
assert messages[0].msg_id == "home-assistant-use-runtime-data"
+11 -32
View File
@@ -2,13 +2,12 @@
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
from . import assert_no_messages, walk_checker
@pytest.fixture(name="executor_checker")
@@ -56,11 +55,9 @@ 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):
walker.walk(root_node)
walk_checker(linter, executor_checker, root_node)
def test_two_sequential_flagged(
@@ -76,9 +73,7 @@ async def async_setup(hass, config):
""",
"homeassistant.components.test_integration",
)
walker = ASTWalker(linter)
walker.add_checker(executor_checker)
walker.walk(root_node)
walk_checker(linter, executor_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
@@ -99,9 +94,7 @@ async def async_setup(hass, config):
""",
"homeassistant.components.test_integration",
)
walker = ASTWalker(linter)
walker.add_checker(executor_checker)
walker.walk(root_node)
walk_checker(linter, executor_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 2
@@ -120,9 +113,7 @@ async def async_setup(hass, config):
""",
"homeassistant.components.test_integration",
)
walker = ASTWalker(linter)
walker.add_checker(executor_checker)
walker.walk(root_node)
walk_checker(linter, executor_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
@@ -144,9 +135,7 @@ async def async_setup(hass, config):
""",
"homeassistant.components.test_integration",
)
walker = ASTWalker(linter)
walker.add_checker(executor_checker)
walker.walk(root_node)
walk_checker(linter, executor_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
@@ -168,9 +157,7 @@ async def async_setup(hass, config):
""",
"homeassistant.components.test_integration",
)
walker = ASTWalker(linter)
walker.add_checker(executor_checker)
walker.walk(root_node)
walk_checker(linter, executor_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
@@ -193,9 +180,7 @@ async def async_setup(hass, config):
""",
"homeassistant.components.test_integration",
)
walker = ASTWalker(linter)
walker.add_checker(executor_checker)
walker.walk(root_node)
walk_checker(linter, executor_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
@@ -218,9 +203,7 @@ async def async_setup(hass, config):
""",
"homeassistant.components.test_integration",
)
walker = ASTWalker(linter)
walker.add_checker(executor_checker)
walker.walk(root_node)
walk_checker(linter, executor_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
@@ -240,9 +223,7 @@ async def async_setup(hass, config):
""",
"homeassistant.components.test_integration",
)
walker = ASTWalker(linter)
walker.add_checker(executor_checker)
walker.walk(root_node)
walk_checker(linter, executor_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
@@ -262,8 +243,6 @@ async def async_setup(hass, config):
""",
"tests.components.test_integration",
)
walker = ASTWalker(linter)
walker.add_checker(executor_checker)
with assert_no_messages(linter):
walker.walk(root_node)
walk_checker(linter, executor_checker, root_node)
+2 -5
View File
@@ -5,10 +5,9 @@ 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
from . import assert_adds_messages, assert_no_messages, walk_checker
@pytest.mark.parametrize(
@@ -75,11 +74,9 @@ 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):
walker.walk(root_node)
walk_checker(linter, enforce_sorted_platforms_checker, root_node)
def test_enforce_sorted_platforms_bad(
+3 -8
View File
@@ -7,10 +7,9 @@ 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
from . import assert_adds_messages, assert_no_messages, walk_checker
@pytest.mark.parametrize(
@@ -117,8 +116,6 @@ 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(
@@ -127,7 +124,7 @@ def test_enforce_super_call(
),
assert_no_messages(linter),
):
walker.walk(root_node)
walk_checker(linter, super_call_checker, root_node)
@pytest.mark.parametrize(
@@ -199,8 +196,6 @@ 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 (
@@ -222,4 +217,4 @@ def test_enforce_super_call_bad(
),
),
):
walker.walk(root_node)
walk_checker(linter, super_call_checker, root_node)
+4 -13
View File
@@ -2,13 +2,12 @@
import astroid
from pylint.testutils import UnittestLinter
from pylint.utils.ast_walker import ASTWalker
from pylint_home_assistant.checkers.unnecessary_format_mac import (
UnnecessaryFormatMacChecker,
)
import pytest
from . import assert_no_messages
from . import assert_no_messages, walk_checker
_IMPORTS = """\
from homeassistant.helpers.device_registry import (
@@ -95,11 +94,8 @@ def test_no_warning(
"""Test cases that should not trigger a warning."""
root_node = astroid.parse(code, "homeassistant.components.test_integration.sensor")
walker = ASTWalker(linter)
walker.add_checker(format_mac_checker)
with assert_no_messages(linter):
walker.walk(root_node)
walk_checker(linter, format_mac_checker, root_node)
@pytest.mark.parametrize(
@@ -152,9 +148,7 @@ def test_format_mac_flagged(
"""Warning when format_mac is used in connections= keyword argument."""
root_node = astroid.parse(code, "homeassistant.components.test_integration.sensor")
walker = ASTWalker(linter)
walker.add_checker(format_mac_checker)
walker.walk(root_node)
walk_checker(linter, format_mac_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
@@ -174,8 +168,5 @@ device = DeviceInfo(
"""
root_node = astroid.parse(code, "some_other.module")
walker = ASTWalker(linter)
walker.add_checker(format_mac_checker)
with assert_no_messages(linter):
walker.walk(root_node)
walk_checker(linter, format_mac_checker, root_node)
+6 -17
View File
@@ -2,13 +2,12 @@
import astroid
from pylint.testutils import UnittestLinter
from pylint.utils.ast_walker import ASTWalker
from pylint_home_assistant.checkers.unused_test_fixture_args import (
UnusedTestFixtureArgsChecker,
)
import pytest
from . import assert_no_messages
from . import assert_no_messages, walk_checker
@pytest.fixture(name="unused_args_checker")
@@ -68,11 +67,9 @@ 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(unused_args_checker)
with assert_no_messages(linter):
walker.walk(root_node)
walk_checker(linter, unused_args_checker, root_node)
def test_unused_single_arg(
@@ -87,9 +84,7 @@ def test_something(hass: HomeAssistant, enable_bluetooth: None) -> None:
""",
"tests.components.test_integration.test_init",
)
walker = ASTWalker(linter)
walker.add_checker(unused_args_checker)
walker.walk(root_node)
walk_checker(linter, unused_args_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
@@ -115,9 +110,7 @@ def test_something(
""",
"tests.components.test_integration.test_init",
)
walker = ASTWalker(linter)
walker.add_checker(unused_args_checker)
walker.walk(root_node)
walk_checker(linter, unused_args_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 2
@@ -137,11 +130,9 @@ def test_something(unused: str) -> None:
""",
"homeassistant.components.test_integration",
)
walker = ASTWalker(linter)
walker.add_checker(unused_args_checker)
with assert_no_messages(linter):
walker.walk(root_node)
walk_checker(linter, unused_args_checker, root_node)
def test_async_test_function(
@@ -156,9 +147,7 @@ async def test_something(hass: HomeAssistant, enable_bluetooth: None) -> None:
""",
"tests.components.test_integration.test_init",
)
walker = ASTWalker(linter)
walker.add_checker(unused_args_checker)
walker.walk(root_node)
walk_checker(linter, unused_args_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
+4 -11
View File
@@ -3,10 +3,9 @@
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
from . import assert_no_messages, walk_checker
@pytest.mark.parametrize(
@@ -72,11 +71,9 @@ def test_enforce_utcnow_good(
) -> None:
"""Good test cases -- no message expected."""
root_node = astroid.parse(code, "homeassistant.components.pylint_test")
walker = ASTWalker(linter)
walker.add_checker(enforce_utcnow_checker)
with assert_no_messages(linter):
walker.walk(root_node)
walk_checker(linter, enforce_utcnow_checker, root_node)
@pytest.mark.parametrize(
@@ -243,10 +240,8 @@ def test_enforce_utcnow_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_utcnow_checker)
walker.walk(root_node)
walk_checker(linter, enforce_utcnow_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
assert messages[0].msg_id == "home-assistant-enforce-utcnow"
@@ -282,8 +277,6 @@ def test_enforce_utcnow_skips_util_dt(
) -> None:
"""``homeassistant.util.dt`` defines ``utcnow`` itself, so it is skipped."""
root_node = astroid.parse(code, "homeassistant.util.dt")
walker = ASTWalker(linter)
walker.add_checker(enforce_utcnow_checker)
with assert_no_messages(linter):
walker.walk(root_node)
walk_checker(linter, enforce_utcnow_checker, root_node)
@@ -2,13 +2,12 @@
import astroid
from pylint.testutils import UnittestLinter
from pylint.utils.ast_walker import ASTWalker
from pylint_home_assistant.checkers.tests.direct_async_migrate_entry import (
DirectAsyncMigrateEntry,
)
import pytest
from tests.pylint import assert_no_messages
from tests.pylint import assert_no_messages, walk_checker
# Pre-load so astroid can resolve ``async_migrate_entry`` in parsed snippets.
astroid.MANAGER.ast_from_module_name("homeassistant.components.ps4")
@@ -70,11 +69,9 @@ 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(checker)
with assert_no_messages(linter):
walker.walk(root_node)
walk_checker(linter, checker, root_node)
@pytest.mark.parametrize(
@@ -110,9 +107,7 @@ def test_warning(
) -> None:
"""Test cases that should trigger a warning."""
root_node = astroid.parse(code, module_name)
walker = ASTWalker(linter)
walker.add_checker(checker)
walker.walk(root_node)
walk_checker(linter, checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
@@ -136,9 +131,7 @@ async def test_b(hass, mock_config_entry):
""",
"tests.components.ps4.test_init",
)
walker = ASTWalker(linter)
walker.add_checker(checker)
walker.walk(root_node)
walk_checker(linter, checker, root_node)
messages = linter.release_messages()
assert len(messages) == 2
+4 -11
View File
@@ -2,11 +2,10 @@
import astroid
from pylint.testutils import UnittestLinter
from pylint.utils.ast_walker import ASTWalker
from pylint_home_assistant.checkers.tests.direct_async_setup import DirectAsyncSetup
import pytest
from tests.pylint import assert_no_messages
from tests.pylint import assert_no_messages, walk_checker
# Pre-load so astroid can resolve ``async_setup`` in parsed snippets.
astroid.MANAGER.ast_from_module_name("homeassistant.components.ps4")
@@ -78,11 +77,9 @@ 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(checker)
with assert_no_messages(linter):
walker.walk(root_node)
walk_checker(linter, checker, root_node)
@pytest.mark.parametrize(
@@ -118,9 +115,7 @@ def test_warning(
) -> None:
"""Test cases that should trigger a warning."""
root_node = astroid.parse(code, module_name)
walker = ASTWalker(linter)
walker.add_checker(checker)
walker.walk(root_node)
walk_checker(linter, checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
@@ -144,9 +139,7 @@ async def test_b(hass):
""",
"tests.components.ps4.test_init",
)
walker = ASTWalker(linter)
walker.add_checker(checker)
walker.walk(root_node)
walk_checker(linter, checker, root_node)
messages = linter.release_messages()
assert len(messages) == 2
@@ -2,13 +2,12 @@
import astroid
from pylint.testutils import UnittestLinter
from pylint.utils.ast_walker import ASTWalker
from pylint_home_assistant.checkers.tests.direct_async_setup_entry import (
DirectAsyncSetupEntry,
)
import pytest
from tests.pylint import assert_no_messages
from tests.pylint import assert_no_messages, walk_checker
# Pre-load so astroid can resolve ``async_setup_entry`` in parsed snippets.
astroid.MANAGER.ast_from_module_name("homeassistant.components.sun")
@@ -71,11 +70,9 @@ 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(checker)
with assert_no_messages(linter):
walker.walk(root_node)
walk_checker(linter, checker, root_node)
@pytest.mark.parametrize(
@@ -136,9 +133,7 @@ def test_warning(
) -> None:
"""Test cases that should trigger a warning."""
root_node = astroid.parse(code, module_name)
walker = ASTWalker(linter)
walker.add_checker(checker)
walker.walk(root_node)
walk_checker(linter, checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
@@ -162,9 +157,7 @@ async def test_b(hass, mock_config_entry):
""",
"tests.components.sun.test_init",
)
walker = ASTWalker(linter)
walker.add_checker(checker)
walker.walk(root_node)
walk_checker(linter, checker, root_node)
messages = linter.release_messages()
assert len(messages) == 2
@@ -2,13 +2,12 @@
import astroid
from pylint.testutils import UnittestLinter
from pylint.utils.ast_walker import ASTWalker
from pylint_home_assistant.checkers.tests.direct_async_unload_entry import (
DirectAsyncUnloadEntry,
)
import pytest
from tests.pylint import assert_no_messages
from tests.pylint import assert_no_messages, walk_checker
# Pre-load so astroid can resolve ``async_unload_entry`` in parsed snippets.
astroid.MANAGER.ast_from_module_name("homeassistant.components.ps4")
@@ -70,11 +69,9 @@ 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(checker)
with assert_no_messages(linter):
walker.walk(root_node)
walk_checker(linter, checker, root_node)
@pytest.mark.parametrize(
@@ -110,9 +107,7 @@ def test_warning(
) -> None:
"""Test cases that should trigger a warning."""
root_node = astroid.parse(code, module_name)
walker = ASTWalker(linter)
walker.add_checker(checker)
walker.walk(root_node)
walk_checker(linter, checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
@@ -136,9 +131,7 @@ async def test_b(hass, mock_config_entry):
""",
"tests.components.ps4.test_init",
)
walker = ASTWalker(linter)
walker.add_checker(checker)
walker.walk(root_node)
walk_checker(linter, checker, root_node)
messages = linter.release_messages()
assert len(messages) == 2
@@ -4,13 +4,12 @@ from pathlib import Path
import astroid
from pylint.testutils import UnittestLinter
from pylint.utils.ast_walker import ASTWalker
from pylint_home_assistant.checkers.tests.redundant_usefixtures import (
RedundantUsefixtures,
)
import pytest
from tests.pylint import assert_no_messages
from tests.pylint import assert_no_messages, walk_checker
@pytest.mark.parametrize(
@@ -73,11 +72,9 @@ 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(usefixtures_checker)
with assert_no_messages(linter):
walker.walk(root_node)
walk_checker(linter, usefixtures_checker, root_node)
def test_single_fixture_redundant(
@@ -95,9 +92,7 @@ async def test_something(hass: HomeAssistant) -> None:
""",
"tests.components.test_integration.test_init",
)
walker = ASTWalker(linter)
walker.add_checker(usefixtures_checker)
walker.walk(root_node)
walk_checker(linter, usefixtures_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
@@ -120,9 +115,7 @@ async def test_something(hass: HomeAssistant) -> None:
""",
"tests.components.test_integration.test_init",
)
walker = ASTWalker(linter)
walker.add_checker(usefixtures_checker)
walker.walk(root_node)
walk_checker(linter, usefixtures_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
@@ -144,9 +137,7 @@ async def test_something(hass: HomeAssistant) -> None:
""",
"tests.components.test_integration.test_init",
)
walker = ASTWalker(linter)
walker.add_checker(usefixtures_checker)
walker.walk(root_node)
walk_checker(linter, usefixtures_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
@@ -171,9 +162,7 @@ async def test_b(hass: HomeAssistant) -> None:
""",
"tests.components.test_integration.test_init",
)
walker = ASTWalker(linter)
walker.add_checker(usefixtures_checker)
walker.walk(root_node)
walk_checker(linter, usefixtures_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 2
@@ -202,9 +191,7 @@ async def test_something(hass: HomeAssistant) -> None:
)
root_node.file = str(test_dir / "test_init.py")
walker = ASTWalker(linter)
walker.add_checker(usefixtures_checker)
walker.walk(root_node)
walk_checker(linter, usefixtures_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
@@ -235,9 +222,7 @@ async def test_something(hass: HomeAssistant) -> None:
)
root_node.file = str(test_dir / "test_init.py")
walker = ASTWalker(linter)
walker.add_checker(usefixtures_checker)
walker.walk(root_node)
walk_checker(linter, usefixtures_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
@@ -268,9 +253,7 @@ async def test_something(hass: HomeAssistant) -> None:
)
root_node.file = str(test_dir / "test_init.py")
walker = ASTWalker(linter)
walker.add_checker(usefixtures_checker)
walker.walk(root_node)
walk_checker(linter, usefixtures_checker, root_node)
messages = linter.release_messages()
assert len(messages) == 1
@@ -292,8 +275,6 @@ async def test_something(hass: HomeAssistant) -> None:
""",
"homeassistant.components.test_integration",
)
walker = ASTWalker(linter)
walker.add_checker(usefixtures_checker)
with assert_no_messages(linter):
walker.walk(root_node)
walk_checker(linter, usefixtures_checker, root_node)
+78
View File
@@ -3211,6 +3211,84 @@ async def test_cancel_shutdown_job(hass: HomeAssistant) -> None:
assert not evt.is_set()
async def test_shutdown_job_runs_before_stop(hass: HomeAssistant) -> None:
"""Test shutdown jobs run before EVENT_HOMEASSISTANT_STOP is fired."""
order: list[str] = []
@callback
def stop_listener(event: ha.Event) -> None:
order.append("stop_listener")
async def shutdown_func() -> None:
order.append("shutdown_job")
assert hass.state is CoreState.running
assert "stop_listener" not in order
hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, stop_listener)
hass.async_add_shutdown_job(HassJob(shutdown_func, "shutdown_job"))
await hass.async_stop()
assert order == ["shutdown_job", "stop_listener"]
async def test_startup_job(hass: HomeAssistant) -> None:
"""Test async_add_startup_job."""
evt = asyncio.Event()
async def startup_func() -> None:
# Sleep to ensure core is waiting for the task to finish
await asyncio.sleep(0.01)
evt.set()
job = HassJob(startup_func, "startup_job")
hass.async_add_startup_job(job)
await hass.async_start()
assert evt.is_set()
async def test_cancel_startup_job(hass: HomeAssistant) -> None:
"""Test cancelling a job added to async_add_startup_job."""
evt = asyncio.Event()
async def startup_func() -> None:
evt.set()
job = HassJob(startup_func, "startup_job")
cancel = hass.async_add_startup_job(job)
cancel()
await hass.async_start()
assert not evt.is_set()
async def test_startup_job_runs_after_start_before_started(hass: HomeAssistant) -> None:
"""Test startup jobs run after START listeners finish and before STARTED is fired."""
order: list[str] = []
async def start_listener(event: ha.Event) -> None:
# Yield control to prove startup jobs wait for in-flight START listeners.
await asyncio.sleep(0.01)
order.append("start_listener")
@callback
def started_listener(event: ha.Event) -> None:
order.append("started_listener")
async def startup_func() -> None:
order.append("startup_job")
assert hass.state is CoreState.starting
assert "started_listener" not in order
hass.bus.async_listen(EVENT_HOMEASSISTANT_START, start_listener)
hass.bus.async_listen(EVENT_HOMEASSISTANT_STARTED, started_listener)
hass.async_add_startup_job(HassJob(startup_func, "startup_job"))
hass.set_state(CoreState.not_running)
await hass.async_start()
await hass.async_block_till_done()
assert order == ["start_listener", "startup_job", "started_listener"]
def test_one_time_listener_repr(hass: HomeAssistant) -> None:
"""Test one time listener repr."""