mirror of
https://github.com/home-assistant/core.git
synced 2026-06-30 10:35:54 +02:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a2e00eb0b5 | |||
| 0ea1ec6fc7 | |||
| de6b679e6e | |||
| 3bcdb621ec | |||
| 003aed3d44 | |||
| 1d0beeeef9 | |||
| f0850b67f2 | |||
| fbd83f8a52 | |||
| bc18d0678a | |||
| c0971e99e9 | |||
| 771da03707 | |||
| 80a04d2820 | |||
| feadcc057d | |||
| cf1e0f0aa5 | |||
| cfa49bb0dd | |||
| a0d96a2c62 | |||
| eb9ebd17b6 | |||
| 5ea38fcc07 | |||
| 15ef228cfa | |||
| e815c9f0cc | |||
| 434b3ca309 | |||
| a557e96c53 | |||
| 324c95140b |
@@ -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
@@ -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
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
Generated
+1
-1
@@ -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
|
||||
|
||||
Generated
+3
-3
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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."""
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,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,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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user