mirror of
https://github.com/home-assistant/core.git
synced 2026-03-25 08:48:19 +01:00
Compare commits
19 Commits
add_air_qu
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
171b8dfa89 | ||
|
|
f299b009fa | ||
|
|
91e9eb0ab3 | ||
|
|
a2b91a9ac0 | ||
|
|
a3add179a0 | ||
|
|
6075becbab | ||
|
|
193f519366 | ||
|
|
b6508c2ca4 | ||
|
|
3dc478a357 | ||
|
|
bd407872b0 | ||
|
|
8b696044c3 | ||
|
|
1a772b6df2 | ||
|
|
a880ad2904 | ||
|
|
ea73f2d0f1 | ||
|
|
11351500ea | ||
|
|
86901bfd80 | ||
|
|
d2ef60125f | ||
|
|
471b49f12b | ||
|
|
33e9e663da |
2
CODEOWNERS
generated
2
CODEOWNERS
generated
@@ -1762,6 +1762,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/tomorrowio/ @raman325 @lymanepp
|
||||
/homeassistant/components/totalconnect/ @austinmroczek
|
||||
/tests/components/totalconnect/ @austinmroczek
|
||||
/homeassistant/components/touchline/ @mnordseth
|
||||
/tests/components/touchline/ @mnordseth
|
||||
/homeassistant/components/touchline_sl/ @jnsgruk
|
||||
/tests/components/touchline_sl/ @jnsgruk
|
||||
/homeassistant/components/tplink/ @rytilahti @bdraco @sdb9696
|
||||
|
||||
@@ -9,6 +9,6 @@
|
||||
},
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["jaraco.abode", "lomond"],
|
||||
"requirements": ["jaraco.abode==6.2.1"],
|
||||
"requirements": ["jaraco.abode==6.4.0"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
"""Provides conditions for air quality."""
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DOMAIN as BINARY_SENSOR_DOMAIN,
|
||||
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.core import HomeAssistant
|
||||
from homeassistant.helpers.automation import DomainSpec, NumericalDomainSpec
|
||||
from homeassistant.helpers.condition import (
|
||||
Condition,
|
||||
make_entity_numerical_condition,
|
||||
make_entity_numerical_condition_with_unit,
|
||||
make_entity_state_condition,
|
||||
)
|
||||
from homeassistant.util.unit_conversion import (
|
||||
CarbonMonoxideConcentrationConverter,
|
||||
MassVolumeConcentrationConverter,
|
||||
NitrogenDioxideConcentrationConverter,
|
||||
NitrogenMonoxideConcentrationConverter,
|
||||
OzoneConcentrationConverter,
|
||||
SulphurDioxideConcentrationConverter,
|
||||
UnitlessRatioConverter,
|
||||
)
|
||||
|
||||
|
||||
def _make_detected_condition(
|
||||
device_class: BinarySensorDeviceClass,
|
||||
) -> type[Condition]:
|
||||
"""Create a detected condition for a binary sensor device class."""
|
||||
return make_entity_state_condition(
|
||||
{BINARY_SENSOR_DOMAIN: DomainSpec(device_class=device_class)}, STATE_ON
|
||||
)
|
||||
|
||||
|
||||
def _make_cleared_condition(
|
||||
device_class: BinarySensorDeviceClass,
|
||||
) -> type[Condition]:
|
||||
"""Create a cleared condition for a binary sensor device class."""
|
||||
return make_entity_state_condition(
|
||||
{BINARY_SENSOR_DOMAIN: DomainSpec(device_class=device_class)}, STATE_OFF
|
||||
)
|
||||
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
# Binary sensor conditions (detected/cleared)
|
||||
"is_gas_detected": _make_detected_condition(BinarySensorDeviceClass.GAS),
|
||||
"is_gas_cleared": _make_cleared_condition(BinarySensorDeviceClass.GAS),
|
||||
"is_co_detected": _make_detected_condition(BinarySensorDeviceClass.CO),
|
||||
"is_co_cleared": _make_cleared_condition(BinarySensorDeviceClass.CO),
|
||||
"is_smoke_detected": _make_detected_condition(BinarySensorDeviceClass.SMOKE),
|
||||
"is_smoke_cleared": _make_cleared_condition(BinarySensorDeviceClass.SMOKE),
|
||||
# Numerical sensor conditions with unit conversion
|
||||
"is_co_value": make_entity_numerical_condition_with_unit(
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.CO)},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
CarbonMonoxideConcentrationConverter,
|
||||
),
|
||||
"is_ozone_value": make_entity_numerical_condition_with_unit(
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.OZONE)},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
OzoneConcentrationConverter,
|
||||
),
|
||||
"is_voc_value": make_entity_numerical_condition_with_unit(
|
||||
{
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS
|
||||
)
|
||||
},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
MassVolumeConcentrationConverter,
|
||||
),
|
||||
"is_voc_ratio_value": make_entity_numerical_condition_with_unit(
|
||||
{
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS
|
||||
)
|
||||
},
|
||||
CONCENTRATION_PARTS_PER_BILLION,
|
||||
UnitlessRatioConverter,
|
||||
),
|
||||
"is_no_value": make_entity_numerical_condition_with_unit(
|
||||
{
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
device_class=SensorDeviceClass.NITROGEN_MONOXIDE
|
||||
)
|
||||
},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
NitrogenMonoxideConcentrationConverter,
|
||||
),
|
||||
"is_no2_value": make_entity_numerical_condition_with_unit(
|
||||
{
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
device_class=SensorDeviceClass.NITROGEN_DIOXIDE
|
||||
)
|
||||
},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
NitrogenDioxideConcentrationConverter,
|
||||
),
|
||||
"is_so2_value": make_entity_numerical_condition_with_unit(
|
||||
{
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
device_class=SensorDeviceClass.SULPHUR_DIOXIDE
|
||||
)
|
||||
},
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
SulphurDioxideConcentrationConverter,
|
||||
),
|
||||
# Numerical sensor conditions without unit conversion (single-unit device classes)
|
||||
"is_co2_value": make_entity_numerical_condition(
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.CO2)},
|
||||
valid_unit=CONCENTRATION_PARTS_PER_MILLION,
|
||||
),
|
||||
"is_pm1_value": make_entity_numerical_condition(
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM1)},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
"is_pm25_value": make_entity_numerical_condition(
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM25)},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
"is_pm4_value": make_entity_numerical_condition(
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM4)},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
"is_pm10_value": make_entity_numerical_condition(
|
||||
{SENSOR_DOMAIN: NumericalDomainSpec(device_class=SensorDeviceClass.PM10)},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
"is_n2o_value": make_entity_numerical_condition(
|
||||
{
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
device_class=SensorDeviceClass.NITROUS_OXIDE
|
||||
)
|
||||
},
|
||||
valid_unit=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
async def async_get_conditions(hass: HomeAssistant) -> dict[str, type[Condition]]:
|
||||
"""Return the air quality conditions."""
|
||||
return CONDITIONS
|
||||
@@ -1,588 +0,0 @@
|
||||
# --- Common condition fields ---
|
||||
|
||||
.condition_behavior: &condition_behavior
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
|
||||
# --- Number or entity selectors ---
|
||||
|
||||
.number_or_entity_co: &number_or_entity_co
|
||||
required: false
|
||||
selector:
|
||||
choose:
|
||||
choices:
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
entity:
|
||||
selector:
|
||||
entity:
|
||||
filter:
|
||||
- domain: input_number
|
||||
unit_of_measurement:
|
||||
- "ppb"
|
||||
- "ppm"
|
||||
- "mg/m³"
|
||||
- "μg/m³"
|
||||
- domain: sensor
|
||||
device_class: carbon_monoxide
|
||||
- domain: number
|
||||
device_class: carbon_monoxide
|
||||
translation_key: number_or_entity
|
||||
|
||||
.number_or_entity_co2: &number_or_entity_co2
|
||||
required: false
|
||||
selector:
|
||||
choose:
|
||||
choices:
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: "ppm"
|
||||
entity:
|
||||
selector:
|
||||
entity:
|
||||
filter:
|
||||
- domain: input_number
|
||||
unit_of_measurement: "ppm"
|
||||
- domain: sensor
|
||||
device_class: carbon_dioxide
|
||||
- domain: number
|
||||
device_class: carbon_dioxide
|
||||
translation_key: number_or_entity
|
||||
|
||||
.number_or_entity_pm1: &number_or_entity_pm1
|
||||
required: false
|
||||
selector:
|
||||
choose:
|
||||
choices:
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: "μg/m³"
|
||||
entity:
|
||||
selector:
|
||||
entity:
|
||||
filter:
|
||||
- domain: input_number
|
||||
unit_of_measurement: "μg/m³"
|
||||
- domain: sensor
|
||||
device_class: pm1
|
||||
- domain: number
|
||||
device_class: pm1
|
||||
translation_key: number_or_entity
|
||||
|
||||
.number_or_entity_pm25: &number_or_entity_pm25
|
||||
required: false
|
||||
selector:
|
||||
choose:
|
||||
choices:
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: "μg/m³"
|
||||
entity:
|
||||
selector:
|
||||
entity:
|
||||
filter:
|
||||
- domain: input_number
|
||||
unit_of_measurement: "μg/m³"
|
||||
- domain: sensor
|
||||
device_class: pm25
|
||||
- domain: number
|
||||
device_class: pm25
|
||||
translation_key: number_or_entity
|
||||
|
||||
.number_or_entity_pm4: &number_or_entity_pm4
|
||||
required: false
|
||||
selector:
|
||||
choose:
|
||||
choices:
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: "μg/m³"
|
||||
entity:
|
||||
selector:
|
||||
entity:
|
||||
filter:
|
||||
- domain: input_number
|
||||
unit_of_measurement: "μg/m³"
|
||||
- domain: sensor
|
||||
device_class: pm4
|
||||
- domain: number
|
||||
device_class: pm4
|
||||
translation_key: number_or_entity
|
||||
|
||||
.number_or_entity_pm10: &number_or_entity_pm10
|
||||
required: false
|
||||
selector:
|
||||
choose:
|
||||
choices:
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: "μg/m³"
|
||||
entity:
|
||||
selector:
|
||||
entity:
|
||||
filter:
|
||||
- domain: input_number
|
||||
unit_of_measurement: "μg/m³"
|
||||
- domain: sensor
|
||||
device_class: pm10
|
||||
- domain: number
|
||||
device_class: pm10
|
||||
translation_key: number_or_entity
|
||||
|
||||
.number_or_entity_ozone: &number_or_entity_ozone
|
||||
required: false
|
||||
selector:
|
||||
choose:
|
||||
choices:
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
entity:
|
||||
selector:
|
||||
entity:
|
||||
filter:
|
||||
- domain: input_number
|
||||
unit_of_measurement:
|
||||
- "ppb"
|
||||
- "ppm"
|
||||
- "μg/m³"
|
||||
- domain: sensor
|
||||
device_class: ozone
|
||||
- domain: number
|
||||
device_class: ozone
|
||||
translation_key: number_or_entity
|
||||
|
||||
.number_or_entity_voc: &number_or_entity_voc
|
||||
required: false
|
||||
selector:
|
||||
choose:
|
||||
choices:
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
entity:
|
||||
selector:
|
||||
entity:
|
||||
filter:
|
||||
- domain: input_number
|
||||
unit_of_measurement:
|
||||
- "μg/m³"
|
||||
- "mg/m³"
|
||||
- domain: sensor
|
||||
device_class: volatile_organic_compounds
|
||||
- domain: number
|
||||
device_class: volatile_organic_compounds
|
||||
translation_key: number_or_entity
|
||||
|
||||
.number_or_entity_voc_ratio: &number_or_entity_voc_ratio
|
||||
required: false
|
||||
selector:
|
||||
choose:
|
||||
choices:
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
entity:
|
||||
selector:
|
||||
entity:
|
||||
filter:
|
||||
- domain: input_number
|
||||
unit_of_measurement:
|
||||
- "ppb"
|
||||
- "ppm"
|
||||
- domain: sensor
|
||||
device_class: volatile_organic_compounds_parts
|
||||
- domain: number
|
||||
device_class: volatile_organic_compounds_parts
|
||||
translation_key: number_or_entity
|
||||
|
||||
.number_or_entity_no: &number_or_entity_no
|
||||
required: false
|
||||
selector:
|
||||
choose:
|
||||
choices:
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
entity:
|
||||
selector:
|
||||
entity:
|
||||
filter:
|
||||
- domain: input_number
|
||||
unit_of_measurement:
|
||||
- "ppb"
|
||||
- "μg/m³"
|
||||
- domain: sensor
|
||||
device_class: nitrogen_monoxide
|
||||
- domain: number
|
||||
device_class: nitrogen_monoxide
|
||||
translation_key: number_or_entity
|
||||
|
||||
.number_or_entity_no2: &number_or_entity_no2
|
||||
required: false
|
||||
selector:
|
||||
choose:
|
||||
choices:
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
entity:
|
||||
selector:
|
||||
entity:
|
||||
filter:
|
||||
- domain: input_number
|
||||
unit_of_measurement:
|
||||
- "ppb"
|
||||
- "ppm"
|
||||
- "μg/m³"
|
||||
- domain: sensor
|
||||
device_class: nitrogen_dioxide
|
||||
- domain: number
|
||||
device_class: nitrogen_dioxide
|
||||
translation_key: number_or_entity
|
||||
|
||||
.number_or_entity_n2o: &number_or_entity_n2o
|
||||
required: false
|
||||
selector:
|
||||
choose:
|
||||
choices:
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: "μg/m³"
|
||||
entity:
|
||||
selector:
|
||||
entity:
|
||||
filter:
|
||||
- domain: input_number
|
||||
unit_of_measurement: "μg/m³"
|
||||
- domain: sensor
|
||||
device_class: nitrous_oxide
|
||||
- domain: number
|
||||
device_class: nitrous_oxide
|
||||
translation_key: number_or_entity
|
||||
|
||||
.number_or_entity_so2: &number_or_entity_so2
|
||||
required: false
|
||||
selector:
|
||||
choose:
|
||||
choices:
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
entity:
|
||||
selector:
|
||||
entity:
|
||||
filter:
|
||||
- domain: input_number
|
||||
unit_of_measurement:
|
||||
- "ppb"
|
||||
- "μg/m³"
|
||||
- domain: sensor
|
||||
device_class: sulphur_dioxide
|
||||
- domain: number
|
||||
device_class: sulphur_dioxide
|
||||
translation_key: number_or_entity
|
||||
|
||||
# --- Unit selectors ---
|
||||
|
||||
.unit_co: &unit_co
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "ppb"
|
||||
- "ppm"
|
||||
- "mg/m³"
|
||||
- "μg/m³"
|
||||
|
||||
.unit_ozone: &unit_ozone
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "ppb"
|
||||
- "ppm"
|
||||
- "μg/m³"
|
||||
|
||||
.unit_no2: &unit_no2
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "ppb"
|
||||
- "ppm"
|
||||
- "μg/m³"
|
||||
|
||||
.unit_no: &unit_no
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "ppb"
|
||||
- "μg/m³"
|
||||
|
||||
.unit_so2: &unit_so2
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "ppb"
|
||||
- "μg/m³"
|
||||
|
||||
.unit_voc: &unit_voc
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "μg/m³"
|
||||
- "mg/m³"
|
||||
|
||||
.unit_voc_ratio: &unit_voc_ratio
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "ppb"
|
||||
- "ppm"
|
||||
|
||||
# --- Binary sensor targets ---
|
||||
|
||||
.target_gas: &target_gas
|
||||
entity:
|
||||
- domain: binary_sensor
|
||||
device_class: gas
|
||||
|
||||
.target_co_binary: &target_co_binary
|
||||
entity:
|
||||
- domain: binary_sensor
|
||||
device_class: carbon_monoxide
|
||||
|
||||
.target_smoke: &target_smoke
|
||||
entity:
|
||||
- domain: binary_sensor
|
||||
device_class: smoke
|
||||
|
||||
# --- Sensor targets ---
|
||||
|
||||
.target_co_sensor: &target_co_sensor
|
||||
entity:
|
||||
- domain: sensor
|
||||
device_class: carbon_monoxide
|
||||
|
||||
.target_co2: &target_co2
|
||||
entity:
|
||||
- domain: sensor
|
||||
device_class: carbon_dioxide
|
||||
|
||||
.target_pm1: &target_pm1
|
||||
entity:
|
||||
- domain: sensor
|
||||
device_class: pm1
|
||||
|
||||
.target_pm25: &target_pm25
|
||||
entity:
|
||||
- domain: sensor
|
||||
device_class: pm25
|
||||
|
||||
.target_pm4: &target_pm4
|
||||
entity:
|
||||
- domain: sensor
|
||||
device_class: pm4
|
||||
|
||||
.target_pm10: &target_pm10
|
||||
entity:
|
||||
- domain: sensor
|
||||
device_class: pm10
|
||||
|
||||
.target_ozone: &target_ozone
|
||||
entity:
|
||||
- domain: sensor
|
||||
device_class: ozone
|
||||
|
||||
.target_voc: &target_voc
|
||||
entity:
|
||||
- domain: sensor
|
||||
device_class: volatile_organic_compounds
|
||||
|
||||
.target_voc_ratio: &target_voc_ratio
|
||||
entity:
|
||||
- domain: sensor
|
||||
device_class: volatile_organic_compounds_parts
|
||||
|
||||
.target_no: &target_no
|
||||
entity:
|
||||
- domain: sensor
|
||||
device_class: nitrogen_monoxide
|
||||
|
||||
.target_no2: &target_no2
|
||||
entity:
|
||||
- domain: sensor
|
||||
device_class: nitrogen_dioxide
|
||||
|
||||
.target_n2o: &target_n2o
|
||||
entity:
|
||||
- domain: sensor
|
||||
device_class: nitrous_oxide
|
||||
|
||||
.target_so2: &target_so2
|
||||
entity:
|
||||
- domain: sensor
|
||||
device_class: sulphur_dioxide
|
||||
|
||||
# --- Binary sensor conditions ---
|
||||
|
||||
.condition_binary_common: &condition_binary_common
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
|
||||
is_gas_detected:
|
||||
<<: *condition_binary_common
|
||||
target: *target_gas
|
||||
|
||||
is_gas_cleared:
|
||||
<<: *condition_binary_common
|
||||
target: *target_gas
|
||||
|
||||
is_co_detected:
|
||||
<<: *condition_binary_common
|
||||
target: *target_co_binary
|
||||
|
||||
is_co_cleared:
|
||||
<<: *condition_binary_common
|
||||
target: *target_co_binary
|
||||
|
||||
is_smoke_detected:
|
||||
<<: *condition_binary_common
|
||||
target: *target_smoke
|
||||
|
||||
is_smoke_cleared:
|
||||
<<: *condition_binary_common
|
||||
target: *target_smoke
|
||||
|
||||
# --- Numerical sensor conditions with unit conversion ---
|
||||
|
||||
is_co_value:
|
||||
target: *target_co_sensor
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
above: *number_or_entity_co
|
||||
below: *number_or_entity_co
|
||||
unit: *unit_co
|
||||
|
||||
is_ozone_value:
|
||||
target: *target_ozone
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
above: *number_or_entity_ozone
|
||||
below: *number_or_entity_ozone
|
||||
unit: *unit_ozone
|
||||
|
||||
is_voc_value:
|
||||
target: *target_voc
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
above: *number_or_entity_voc
|
||||
below: *number_or_entity_voc
|
||||
unit: *unit_voc
|
||||
|
||||
is_voc_ratio_value:
|
||||
target: *target_voc_ratio
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
above: *number_or_entity_voc_ratio
|
||||
below: *number_or_entity_voc_ratio
|
||||
unit: *unit_voc_ratio
|
||||
|
||||
is_no_value:
|
||||
target: *target_no
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
above: *number_or_entity_no
|
||||
below: *number_or_entity_no
|
||||
unit: *unit_no
|
||||
|
||||
is_no2_value:
|
||||
target: *target_no2
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
above: *number_or_entity_no2
|
||||
below: *number_or_entity_no2
|
||||
unit: *unit_no2
|
||||
|
||||
is_so2_value:
|
||||
target: *target_so2
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
above: *number_or_entity_so2
|
||||
below: *number_or_entity_so2
|
||||
unit: *unit_so2
|
||||
|
||||
# --- Numerical sensor conditions without unit conversion ---
|
||||
|
||||
is_co2_value:
|
||||
target: *target_co2
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
above: *number_or_entity_co2
|
||||
below: *number_or_entity_co2
|
||||
|
||||
is_pm1_value:
|
||||
target: *target_pm1
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
above: *number_or_entity_pm1
|
||||
below: *number_or_entity_pm1
|
||||
|
||||
is_pm25_value:
|
||||
target: *target_pm25
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
above: *number_or_entity_pm25
|
||||
below: *number_or_entity_pm25
|
||||
|
||||
is_pm4_value:
|
||||
target: *target_pm4
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
above: *number_or_entity_pm4
|
||||
below: *number_or_entity_pm4
|
||||
|
||||
is_pm10_value:
|
||||
target: *target_pm10
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
above: *number_or_entity_pm10
|
||||
below: *number_or_entity_pm10
|
||||
|
||||
is_n2o_value:
|
||||
target: *target_n2o
|
||||
fields:
|
||||
behavior: *condition_behavior
|
||||
above: *number_or_entity_n2o
|
||||
below: *number_or_entity_n2o
|
||||
@@ -1,63 +1,4 @@
|
||||
{
|
||||
"conditions": {
|
||||
"is_co2_value": {
|
||||
"condition": "mdi:molecule-co2"
|
||||
},
|
||||
"is_co_cleared": {
|
||||
"condition": "mdi:check-circle"
|
||||
},
|
||||
"is_co_detected": {
|
||||
"condition": "mdi:molecule-co"
|
||||
},
|
||||
"is_co_value": {
|
||||
"condition": "mdi:molecule-co"
|
||||
},
|
||||
"is_gas_cleared": {
|
||||
"condition": "mdi:check-circle"
|
||||
},
|
||||
"is_gas_detected": {
|
||||
"condition": "mdi:gas-cylinder"
|
||||
},
|
||||
"is_n2o_value": {
|
||||
"condition": "mdi:factory"
|
||||
},
|
||||
"is_no2_value": {
|
||||
"condition": "mdi:factory"
|
||||
},
|
||||
"is_no_value": {
|
||||
"condition": "mdi:factory"
|
||||
},
|
||||
"is_ozone_value": {
|
||||
"condition": "mdi:weather-sunny-alert"
|
||||
},
|
||||
"is_pm10_value": {
|
||||
"condition": "mdi:blur"
|
||||
},
|
||||
"is_pm1_value": {
|
||||
"condition": "mdi:blur"
|
||||
},
|
||||
"is_pm25_value": {
|
||||
"condition": "mdi:blur"
|
||||
},
|
||||
"is_pm4_value": {
|
||||
"condition": "mdi:blur"
|
||||
},
|
||||
"is_smoke_cleared": {
|
||||
"condition": "mdi:check-circle"
|
||||
},
|
||||
"is_smoke_detected": {
|
||||
"condition": "mdi:smoke-detector-variant"
|
||||
},
|
||||
"is_so2_value": {
|
||||
"condition": "mdi:factory"
|
||||
},
|
||||
"is_voc_ratio_value": {
|
||||
"condition": "mdi:air-filter"
|
||||
},
|
||||
"is_voc_value": {
|
||||
"condition": "mdi:air-filter"
|
||||
}
|
||||
},
|
||||
"entity_component": {
|
||||
"_": {
|
||||
"default": "mdi:air-filter"
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
{
|
||||
"common": {
|
||||
"condition_above_description": "Require the value to be above this value.",
|
||||
"condition_above_name": "Above",
|
||||
"condition_behavior_description": "How the value should match on the targeted entities.",
|
||||
"condition_behavior_name": "Behavior",
|
||||
"condition_below_description": "Require the value to be below this value.",
|
||||
"condition_below_name": "Below",
|
||||
"condition_unit_description": "All values will be converted to this unit when evaluating the condition.",
|
||||
"condition_unit_name": "Unit of measurement",
|
||||
"trigger_behavior_description": "The behavior of the targeted entities to trigger on.",
|
||||
"trigger_behavior_name": "Behavior",
|
||||
"trigger_changed_above_name": "Above",
|
||||
@@ -21,337 +13,7 @@
|
||||
"trigger_unit_description": "All values will be converted to this unit when evaluating the trigger.",
|
||||
"trigger_unit_name": "Unit of measurement"
|
||||
},
|
||||
"conditions": {
|
||||
"is_co2_value": {
|
||||
"description": "Tests the carbon dioxide level of one or more entities.",
|
||||
"fields": {
|
||||
"above": {
|
||||
"description": "[%key:component::air_quality::common::condition_above_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_above_name%]"
|
||||
},
|
||||
"behavior": {
|
||||
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"below": {
|
||||
"description": "[%key:component::air_quality::common::condition_below_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_below_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Carbon dioxide value"
|
||||
},
|
||||
"is_co_cleared": {
|
||||
"description": "Tests if one or more carbon monoxide sensors are cleared.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Carbon monoxide cleared"
|
||||
},
|
||||
"is_co_detected": {
|
||||
"description": "Tests if one or more carbon monoxide sensors are detecting carbon monoxide.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Carbon monoxide detected"
|
||||
},
|
||||
"is_co_value": {
|
||||
"description": "Tests the carbon monoxide level of one or more entities.",
|
||||
"fields": {
|
||||
"above": {
|
||||
"description": "[%key:component::air_quality::common::condition_above_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_above_name%]"
|
||||
},
|
||||
"behavior": {
|
||||
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"below": {
|
||||
"description": "[%key:component::air_quality::common::condition_below_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_below_name%]"
|
||||
},
|
||||
"unit": {
|
||||
"description": "[%key:component::air_quality::common::condition_unit_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_unit_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Carbon monoxide value"
|
||||
},
|
||||
"is_gas_cleared": {
|
||||
"description": "Tests if one or more gas sensors are cleared.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Gas cleared"
|
||||
},
|
||||
"is_gas_detected": {
|
||||
"description": "Tests if one or more gas sensors are detecting gas.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Gas detected"
|
||||
},
|
||||
"is_n2o_value": {
|
||||
"description": "Tests the nitrous oxide level of one or more entities.",
|
||||
"fields": {
|
||||
"above": {
|
||||
"description": "[%key:component::air_quality::common::condition_above_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_above_name%]"
|
||||
},
|
||||
"behavior": {
|
||||
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"below": {
|
||||
"description": "[%key:component::air_quality::common::condition_below_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_below_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Nitrous oxide value"
|
||||
},
|
||||
"is_no2_value": {
|
||||
"description": "Tests the nitrogen dioxide level of one or more entities.",
|
||||
"fields": {
|
||||
"above": {
|
||||
"description": "[%key:component::air_quality::common::condition_above_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_above_name%]"
|
||||
},
|
||||
"behavior": {
|
||||
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"below": {
|
||||
"description": "[%key:component::air_quality::common::condition_below_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_below_name%]"
|
||||
},
|
||||
"unit": {
|
||||
"description": "[%key:component::air_quality::common::condition_unit_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_unit_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Nitrogen dioxide value"
|
||||
},
|
||||
"is_no_value": {
|
||||
"description": "Tests the nitrogen monoxide level of one or more entities.",
|
||||
"fields": {
|
||||
"above": {
|
||||
"description": "[%key:component::air_quality::common::condition_above_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_above_name%]"
|
||||
},
|
||||
"behavior": {
|
||||
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"below": {
|
||||
"description": "[%key:component::air_quality::common::condition_below_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_below_name%]"
|
||||
},
|
||||
"unit": {
|
||||
"description": "[%key:component::air_quality::common::condition_unit_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_unit_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Nitrogen monoxide value"
|
||||
},
|
||||
"is_ozone_value": {
|
||||
"description": "Tests the ozone level of one or more entities.",
|
||||
"fields": {
|
||||
"above": {
|
||||
"description": "[%key:component::air_quality::common::condition_above_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_above_name%]"
|
||||
},
|
||||
"behavior": {
|
||||
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"below": {
|
||||
"description": "[%key:component::air_quality::common::condition_below_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_below_name%]"
|
||||
},
|
||||
"unit": {
|
||||
"description": "[%key:component::air_quality::common::condition_unit_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_unit_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Ozone value"
|
||||
},
|
||||
"is_pm10_value": {
|
||||
"description": "Tests the PM10 level of one or more entities.",
|
||||
"fields": {
|
||||
"above": {
|
||||
"description": "[%key:component::air_quality::common::condition_above_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_above_name%]"
|
||||
},
|
||||
"behavior": {
|
||||
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"below": {
|
||||
"description": "[%key:component::air_quality::common::condition_below_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_below_name%]"
|
||||
}
|
||||
},
|
||||
"name": "PM10 value"
|
||||
},
|
||||
"is_pm1_value": {
|
||||
"description": "Tests the PM1 level of one or more entities.",
|
||||
"fields": {
|
||||
"above": {
|
||||
"description": "[%key:component::air_quality::common::condition_above_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_above_name%]"
|
||||
},
|
||||
"behavior": {
|
||||
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"below": {
|
||||
"description": "[%key:component::air_quality::common::condition_below_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_below_name%]"
|
||||
}
|
||||
},
|
||||
"name": "PM1 value"
|
||||
},
|
||||
"is_pm25_value": {
|
||||
"description": "Tests the PM2.5 level of one or more entities.",
|
||||
"fields": {
|
||||
"above": {
|
||||
"description": "[%key:component::air_quality::common::condition_above_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_above_name%]"
|
||||
},
|
||||
"behavior": {
|
||||
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"below": {
|
||||
"description": "[%key:component::air_quality::common::condition_below_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_below_name%]"
|
||||
}
|
||||
},
|
||||
"name": "PM2.5 value"
|
||||
},
|
||||
"is_pm4_value": {
|
||||
"description": "Tests the PM4 level of one or more entities.",
|
||||
"fields": {
|
||||
"above": {
|
||||
"description": "[%key:component::air_quality::common::condition_above_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_above_name%]"
|
||||
},
|
||||
"behavior": {
|
||||
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"below": {
|
||||
"description": "[%key:component::air_quality::common::condition_below_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_below_name%]"
|
||||
}
|
||||
},
|
||||
"name": "PM4 value"
|
||||
},
|
||||
"is_smoke_cleared": {
|
||||
"description": "Tests if one or more smoke sensors are cleared.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Smoke cleared"
|
||||
},
|
||||
"is_smoke_detected": {
|
||||
"description": "Tests if one or more smoke sensors are detecting smoke.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Smoke detected"
|
||||
},
|
||||
"is_so2_value": {
|
||||
"description": "Tests the sulphur dioxide level of one or more entities.",
|
||||
"fields": {
|
||||
"above": {
|
||||
"description": "[%key:component::air_quality::common::condition_above_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_above_name%]"
|
||||
},
|
||||
"behavior": {
|
||||
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"below": {
|
||||
"description": "[%key:component::air_quality::common::condition_below_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_below_name%]"
|
||||
},
|
||||
"unit": {
|
||||
"description": "[%key:component::air_quality::common::condition_unit_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_unit_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Sulphur dioxide value"
|
||||
},
|
||||
"is_voc_ratio_value": {
|
||||
"description": "Tests the volatile organic compounds ratio of one or more entities.",
|
||||
"fields": {
|
||||
"above": {
|
||||
"description": "[%key:component::air_quality::common::condition_above_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_above_name%]"
|
||||
},
|
||||
"behavior": {
|
||||
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"below": {
|
||||
"description": "[%key:component::air_quality::common::condition_below_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_below_name%]"
|
||||
},
|
||||
"unit": {
|
||||
"description": "[%key:component::air_quality::common::condition_unit_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_unit_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Volatile organic compounds ratio value"
|
||||
},
|
||||
"is_voc_value": {
|
||||
"description": "Tests the volatile organic compounds level of one or more entities.",
|
||||
"fields": {
|
||||
"above": {
|
||||
"description": "[%key:component::air_quality::common::condition_above_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_above_name%]"
|
||||
},
|
||||
"behavior": {
|
||||
"description": "[%key:component::air_quality::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
|
||||
},
|
||||
"below": {
|
||||
"description": "[%key:component::air_quality::common::condition_below_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_below_name%]"
|
||||
},
|
||||
"unit": {
|
||||
"description": "[%key:component::air_quality::common::condition_unit_description%]",
|
||||
"name": "[%key:component::air_quality::common::condition_unit_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Volatile organic compounds value"
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"condition_behavior": {
|
||||
"options": {
|
||||
"all": "All",
|
||||
"any": "Any"
|
||||
}
|
||||
},
|
||||
"number_or_entity": {
|
||||
"choices": {
|
||||
"entity": "Entity",
|
||||
|
||||
@@ -118,7 +118,6 @@ SERVICE_TRIGGER = "trigger"
|
||||
NEW_TRIGGERS_CONDITIONS_FEATURE_FLAG = "new_triggers_conditions"
|
||||
|
||||
_EXPERIMENTAL_CONDITION_PLATFORMS = {
|
||||
"air_quality",
|
||||
"alarm_control_panel",
|
||||
"assist_satellite",
|
||||
"battery",
|
||||
|
||||
@@ -39,10 +39,7 @@ rules:
|
||||
log-when-unavailable: done
|
||||
parallel-updates: done
|
||||
reauthentication-flow: done
|
||||
test-coverage:
|
||||
status: todo
|
||||
comment: |
|
||||
PLANNED: Use freezer-based time advancement instead of directly calling async_refresh().
|
||||
test-coverage: todo
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
|
||||
@@ -6,7 +6,7 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
PLATFORMS = [Platform.MEDIA_PLAYER]
|
||||
PLATFORMS = [Platform.BUTTON, Platform.MEDIA_PLAYER]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
147
homeassistant/components/lg_infrared/button.py
Normal file
147
homeassistant/components/lg_infrared/button.py
Normal file
@@ -0,0 +1,147 @@
|
||||
"""Button platform for LG IR integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from infrared_protocols.codes.lg.tv import LGTVCode
|
||||
|
||||
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import CONF_DEVICE_TYPE, CONF_INFRARED_ENTITY_ID, LGDeviceType
|
||||
from .entity import LgIrEntity
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class LgIrButtonEntityDescription(ButtonEntityDescription):
|
||||
"""Describes LG IR button entity."""
|
||||
|
||||
command_code: LGTVCode
|
||||
|
||||
|
||||
TV_BUTTON_DESCRIPTIONS: tuple[LgIrButtonEntityDescription, ...] = (
|
||||
LgIrButtonEntityDescription(
|
||||
key="power_on", translation_key="power_on", command_code=LGTVCode.POWER_ON
|
||||
),
|
||||
LgIrButtonEntityDescription(
|
||||
key="power_off", translation_key="power_off", command_code=LGTVCode.POWER_OFF
|
||||
),
|
||||
LgIrButtonEntityDescription(
|
||||
key="hdmi_1", translation_key="hdmi_1", command_code=LGTVCode.HDMI_1
|
||||
),
|
||||
LgIrButtonEntityDescription(
|
||||
key="hdmi_2", translation_key="hdmi_2", command_code=LGTVCode.HDMI_2
|
||||
),
|
||||
LgIrButtonEntityDescription(
|
||||
key="hdmi_3", translation_key="hdmi_3", command_code=LGTVCode.HDMI_3
|
||||
),
|
||||
LgIrButtonEntityDescription(
|
||||
key="hdmi_4", translation_key="hdmi_4", command_code=LGTVCode.HDMI_4
|
||||
),
|
||||
LgIrButtonEntityDescription(
|
||||
key="exit", translation_key="exit", command_code=LGTVCode.EXIT
|
||||
),
|
||||
LgIrButtonEntityDescription(
|
||||
key="info", translation_key="info", command_code=LGTVCode.INFO
|
||||
),
|
||||
LgIrButtonEntityDescription(
|
||||
key="guide", translation_key="guide", command_code=LGTVCode.GUIDE
|
||||
),
|
||||
LgIrButtonEntityDescription(
|
||||
key="up", translation_key="up", command_code=LGTVCode.NAV_UP
|
||||
),
|
||||
LgIrButtonEntityDescription(
|
||||
key="down", translation_key="down", command_code=LGTVCode.NAV_DOWN
|
||||
),
|
||||
LgIrButtonEntityDescription(
|
||||
key="left", translation_key="left", command_code=LGTVCode.NAV_LEFT
|
||||
),
|
||||
LgIrButtonEntityDescription(
|
||||
key="right", translation_key="right", command_code=LGTVCode.NAV_RIGHT
|
||||
),
|
||||
LgIrButtonEntityDescription(
|
||||
key="ok", translation_key="ok", command_code=LGTVCode.OK
|
||||
),
|
||||
LgIrButtonEntityDescription(
|
||||
key="back", translation_key="back", command_code=LGTVCode.BACK
|
||||
),
|
||||
LgIrButtonEntityDescription(
|
||||
key="home", translation_key="home", command_code=LGTVCode.HOME
|
||||
),
|
||||
LgIrButtonEntityDescription(
|
||||
key="menu", translation_key="menu", command_code=LGTVCode.MENU
|
||||
),
|
||||
LgIrButtonEntityDescription(
|
||||
key="input", translation_key="input", command_code=LGTVCode.INPUT
|
||||
),
|
||||
LgIrButtonEntityDescription(
|
||||
key="num_0", translation_key="num_0", command_code=LGTVCode.NUM_0
|
||||
),
|
||||
LgIrButtonEntityDescription(
|
||||
key="num_1", translation_key="num_1", command_code=LGTVCode.NUM_1
|
||||
),
|
||||
LgIrButtonEntityDescription(
|
||||
key="num_2", translation_key="num_2", command_code=LGTVCode.NUM_2
|
||||
),
|
||||
LgIrButtonEntityDescription(
|
||||
key="num_3", translation_key="num_3", command_code=LGTVCode.NUM_3
|
||||
),
|
||||
LgIrButtonEntityDescription(
|
||||
key="num_4", translation_key="num_4", command_code=LGTVCode.NUM_4
|
||||
),
|
||||
LgIrButtonEntityDescription(
|
||||
key="num_5", translation_key="num_5", command_code=LGTVCode.NUM_5
|
||||
),
|
||||
LgIrButtonEntityDescription(
|
||||
key="num_6", translation_key="num_6", command_code=LGTVCode.NUM_6
|
||||
),
|
||||
LgIrButtonEntityDescription(
|
||||
key="num_7", translation_key="num_7", command_code=LGTVCode.NUM_7
|
||||
),
|
||||
LgIrButtonEntityDescription(
|
||||
key="num_8", translation_key="num_8", command_code=LGTVCode.NUM_8
|
||||
),
|
||||
LgIrButtonEntityDescription(
|
||||
key="num_9", translation_key="num_9", command_code=LGTVCode.NUM_9
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up LG IR buttons from config entry."""
|
||||
infrared_entity_id = entry.data[CONF_INFRARED_ENTITY_ID]
|
||||
device_type = entry.data[CONF_DEVICE_TYPE]
|
||||
if device_type == LGDeviceType.TV:
|
||||
async_add_entities(
|
||||
LgIrButton(entry, infrared_entity_id, description)
|
||||
for description in TV_BUTTON_DESCRIPTIONS
|
||||
)
|
||||
|
||||
|
||||
class LgIrButton(LgIrEntity, ButtonEntity):
|
||||
"""LG IR button entity."""
|
||||
|
||||
entity_description: LgIrButtonEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
entry: ConfigEntry,
|
||||
infrared_entity_id: str,
|
||||
description: LgIrButtonEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize LG IR button."""
|
||||
super().__init__(entry, infrared_entity_id, unique_id_suffix=description.key)
|
||||
self.entity_description = description
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Press the button."""
|
||||
await self._send_command(self.entity_description.command_code)
|
||||
@@ -19,6 +19,94 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"button": {
|
||||
"back": {
|
||||
"name": "Back"
|
||||
},
|
||||
"down": {
|
||||
"name": "Down"
|
||||
},
|
||||
"exit": {
|
||||
"name": "Exit"
|
||||
},
|
||||
"guide": {
|
||||
"name": "Guide"
|
||||
},
|
||||
"hdmi_1": {
|
||||
"name": "HDMI 1"
|
||||
},
|
||||
"hdmi_2": {
|
||||
"name": "HDMI 2"
|
||||
},
|
||||
"hdmi_3": {
|
||||
"name": "HDMI 3"
|
||||
},
|
||||
"hdmi_4": {
|
||||
"name": "HDMI 4"
|
||||
},
|
||||
"home": {
|
||||
"name": "Home"
|
||||
},
|
||||
"info": {
|
||||
"name": "Info"
|
||||
},
|
||||
"input": {
|
||||
"name": "Input"
|
||||
},
|
||||
"left": {
|
||||
"name": "Left"
|
||||
},
|
||||
"menu": {
|
||||
"name": "Menu"
|
||||
},
|
||||
"num_0": {
|
||||
"name": "Number 0"
|
||||
},
|
||||
"num_1": {
|
||||
"name": "Number 1"
|
||||
},
|
||||
"num_2": {
|
||||
"name": "Number 2"
|
||||
},
|
||||
"num_3": {
|
||||
"name": "Number 3"
|
||||
},
|
||||
"num_4": {
|
||||
"name": "Number 4"
|
||||
},
|
||||
"num_5": {
|
||||
"name": "Number 5"
|
||||
},
|
||||
"num_6": {
|
||||
"name": "Number 6"
|
||||
},
|
||||
"num_7": {
|
||||
"name": "Number 7"
|
||||
},
|
||||
"num_8": {
|
||||
"name": "Number 8"
|
||||
},
|
||||
"num_9": {
|
||||
"name": "Number 9"
|
||||
},
|
||||
"ok": {
|
||||
"name": "OK"
|
||||
},
|
||||
"power_off": {
|
||||
"name": "Power off"
|
||||
},
|
||||
"power_on": {
|
||||
"name": "Power on"
|
||||
},
|
||||
"right": {
|
||||
"name": "Right"
|
||||
},
|
||||
"up": {
|
||||
"name": "Up"
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"device_type": {
|
||||
"options": {
|
||||
|
||||
@@ -26,6 +26,7 @@ from .coordinator import LiebherrConfigEntry, LiebherrCoordinator, LiebherrData
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS: list[Platform] = [
|
||||
Platform.LIGHT,
|
||||
Platform.NUMBER,
|
||||
Platform.SELECT,
|
||||
Platform.SENSOR,
|
||||
|
||||
132
homeassistant/components/liebherr/light.py
Normal file
132
homeassistant/components/liebherr/light.py
Normal file
@@ -0,0 +1,132 @@
|
||||
"""Light platform for Liebherr integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from pyliebherrhomeapi import PresentationLightControl
|
||||
from pyliebherrhomeapi.const import CONTROL_PRESENTATION_LIGHT
|
||||
|
||||
from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import LiebherrConfigEntry, LiebherrCoordinator
|
||||
from .entity import LiebherrEntity
|
||||
|
||||
DEFAULT_MAX_BRIGHTNESS_LEVEL = 5
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
def _create_light_entities(
|
||||
coordinators: list[LiebherrCoordinator],
|
||||
) -> list[LiebherrPresentationLight]:
|
||||
"""Create light entities for the given coordinators."""
|
||||
return [
|
||||
LiebherrPresentationLight(coordinator=coordinator)
|
||||
for coordinator in coordinators
|
||||
for control in coordinator.data.controls
|
||||
if isinstance(control, PresentationLightControl)
|
||||
and control.name == CONTROL_PRESENTATION_LIGHT
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: LiebherrConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Liebherr light entities."""
|
||||
async_add_entities(
|
||||
_create_light_entities(list(entry.runtime_data.coordinators.values()))
|
||||
)
|
||||
|
||||
@callback
|
||||
def _async_new_device(coordinators: list[LiebherrCoordinator]) -> None:
|
||||
"""Add light entities for new devices."""
|
||||
async_add_entities(_create_light_entities(coordinators))
|
||||
|
||||
entry.async_on_unload(
|
||||
async_dispatcher_connect(
|
||||
hass, f"{DOMAIN}_new_device_{entry.entry_id}", _async_new_device
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class LiebherrPresentationLight(LiebherrEntity, LightEntity):
|
||||
"""Representation of a Liebherr presentation light."""
|
||||
|
||||
_attr_translation_key = "presentation_light"
|
||||
_attr_color_mode = ColorMode.BRIGHTNESS
|
||||
_attr_supported_color_modes = {ColorMode.BRIGHTNESS}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: LiebherrCoordinator,
|
||||
) -> None:
|
||||
"""Initialize the presentation light entity."""
|
||||
super().__init__(coordinator)
|
||||
self._attr_unique_id = f"{coordinator.device_id}_presentation_light"
|
||||
|
||||
@property
|
||||
def _light_control(self) -> PresentationLightControl | None:
|
||||
"""Get the presentation light control."""
|
||||
controls = self.coordinator.data.get_presentation_light_controls()
|
||||
return controls.get(CONTROL_PRESENTATION_LIGHT)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if entity is available."""
|
||||
return super().available and self._light_control is not None
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return true if the light is on."""
|
||||
control = self._light_control
|
||||
if TYPE_CHECKING:
|
||||
assert control is not None
|
||||
if control.value is None:
|
||||
return None
|
||||
return control.value > 0
|
||||
|
||||
@property
|
||||
def brightness(self) -> int | None:
|
||||
"""Return the brightness of the light (0-255)."""
|
||||
control = self._light_control
|
||||
if TYPE_CHECKING:
|
||||
assert control is not None
|
||||
if control.value is None or control.max is None or control.max == 0:
|
||||
return None
|
||||
return math.ceil(control.value * 255 / control.max)
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the light on."""
|
||||
control = self._light_control
|
||||
if TYPE_CHECKING:
|
||||
assert control is not None
|
||||
max_level = control.max or DEFAULT_MAX_BRIGHTNESS_LEVEL
|
||||
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
target = max(1, round(kwargs[ATTR_BRIGHTNESS] * max_level / 255))
|
||||
else:
|
||||
target = max_level
|
||||
|
||||
await self._async_send_command(
|
||||
self.coordinator.client.set_presentation_light(
|
||||
device_id=self.coordinator.device_id,
|
||||
target=target,
|
||||
)
|
||||
)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the light off."""
|
||||
await self._async_send_command(
|
||||
self.coordinator.client.set_presentation_light(
|
||||
device_id=self.coordinator.device_id,
|
||||
target=0,
|
||||
)
|
||||
)
|
||||
@@ -33,6 +33,11 @@
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"light": {
|
||||
"presentation_light": {
|
||||
"name": "Presentation light"
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"setpoint_temperature": {
|
||||
"name": "Setpoint"
|
||||
|
||||
@@ -7,6 +7,7 @@ from aiohttp import ClientSession
|
||||
from linkplay.bridge import LinkPlayBridge
|
||||
from linkplay.discovery import linkplay_factory_httpapi_bridge
|
||||
from linkplay.exceptions import LinkPlayRequestException
|
||||
from linkplay.manufacturers import MANUFACTURER_WIIM
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
@@ -45,6 +46,9 @@ class LinkPlayConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
|
||||
if bridge.device.manufacturer == MANUFACTURER_WIIM:
|
||||
return self.async_abort(reason="not_linkplay_device")
|
||||
|
||||
self.data[CONF_HOST] = discovery_info.host
|
||||
self.data[CONF_MODEL] = bridge.device.name
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]"
|
||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
||||
"not_linkplay_device": "This device should be set up with the WiiM integration."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
|
||||
@@ -82,6 +82,14 @@ NODE_BUTTONS: tuple[ProxmoxNodeButtonNodeEntityDescription, ...] = (
|
||||
).stopall.post(),
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
ProxmoxNodeButtonNodeEntityDescription(
|
||||
key="suspend_all",
|
||||
translation_key="suspend_all",
|
||||
press_action=lambda coordinator, node: coordinator.proxmox.nodes(
|
||||
node
|
||||
).suspendall.post(),
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
)
|
||||
|
||||
VM_BUTTONS: tuple[ProxmoxVMButtonEntityDescription, ...] = (
|
||||
|
||||
@@ -13,8 +13,17 @@
|
||||
"start": {
|
||||
"default": "mdi:play"
|
||||
},
|
||||
"start_all": {
|
||||
"default": "mdi:play"
|
||||
},
|
||||
"stop": {
|
||||
"default": "mdi:stop"
|
||||
},
|
||||
"stop_all": {
|
||||
"default": "mdi:stop"
|
||||
},
|
||||
"suspend_all": {
|
||||
"default": "mdi:pause"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
|
||||
@@ -130,6 +130,9 @@
|
||||
},
|
||||
"stop_all": {
|
||||
"name": "Stop all"
|
||||
},
|
||||
"suspend_all": {
|
||||
"name": "Suspend all"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
|
||||
@@ -159,7 +159,11 @@ async def async_setup_entry(
|
||||
)
|
||||
for coordinator in config_entry.runtime_data.v1
|
||||
for description in BINARY_SENSOR_DESCRIPTIONS
|
||||
if description.value_fn(coordinator.data) is not None
|
||||
# Note: Currently coordinator.data is always available on startup but won't be in the future
|
||||
if (
|
||||
coordinator.data is not None
|
||||
and description.value_fn(coordinator.data) is not None
|
||||
)
|
||||
]
|
||||
entities.extend(
|
||||
RoborockBinarySensorEntityA01(
|
||||
@@ -193,9 +197,11 @@ class RoborockBinarySensorEntity(RoborockCoordinatedEntityV1, BinarySensorEntity
|
||||
self.entity_description = description
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return the value reported by the sensor."""
|
||||
return bool(self.entity_description.value_fn(self.coordinator.data))
|
||||
if (data := self.coordinator.data) is not None:
|
||||
return bool(self.entity_description.value_fn(data))
|
||||
return None
|
||||
|
||||
|
||||
class RoborockBinarySensorEntityA01(RoborockCoordinatedEntityA01, BinarySensorEntity):
|
||||
|
||||
@@ -83,7 +83,7 @@ class RoborockCoordinators:
|
||||
type RoborockConfigEntry = ConfigEntry[RoborockCoordinators]
|
||||
|
||||
|
||||
class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceState]):
|
||||
class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceState | None]):
|
||||
"""Class to manage fetching data from the API."""
|
||||
|
||||
config_entry: RoborockConfigEntry
|
||||
@@ -229,7 +229,7 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceState]):
|
||||
)
|
||||
_LOGGER.debug("Updated device properties")
|
||||
|
||||
async def _async_update_data(self) -> DeviceState:
|
||||
async def _async_update_data(self) -> DeviceState | None:
|
||||
"""Update data via library."""
|
||||
await self._verify_api()
|
||||
try:
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
from typing import Any
|
||||
|
||||
from roborock.devices.traits.v1.command import CommandTrait
|
||||
from roborock.devices.traits.v1.status import StatusTrait
|
||||
from roborock.exceptions import RoborockException
|
||||
from roborock.roborock_typing import RoborockCommand
|
||||
|
||||
@@ -94,12 +93,6 @@ class RoborockCoordinatedEntityV1(
|
||||
CoordinatorEntity.__init__(self, coordinator=coordinator)
|
||||
self._attr_unique_id = unique_id
|
||||
|
||||
@property
|
||||
def _device_status(self) -> StatusTrait:
|
||||
"""Return the status of the device."""
|
||||
data = self.coordinator.data
|
||||
return data.status
|
||||
|
||||
async def send(
|
||||
self,
|
||||
command: RoborockCommand | str,
|
||||
|
||||
@@ -9,7 +9,7 @@ from roborock.devices.traits.v1.map_content import MapContent
|
||||
from homeassistant.components.image import ImageEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
@@ -73,6 +73,7 @@ class RoborockMap(RoborockCoordinatedEntityV1, ImageEntity):
|
||||
self.map_flag = map_flag
|
||||
self.cached_map: bytes | None = None
|
||||
self._attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||
self._attr_image_last_updated = None
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""When entity is added to hass load any previously cached maps from disk."""
|
||||
@@ -88,17 +89,17 @@ class RoborockMap(RoborockCoordinatedEntityV1, ImageEntity):
|
||||
return map_content
|
||||
return None
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator.
|
||||
|
||||
If the coordinator has updated the map, we can update the image.
|
||||
"""
|
||||
if (map_content := self._map_content) is None:
|
||||
if self.coordinator.data is None or (map_content := self._map_content) is None:
|
||||
return
|
||||
if self.cached_map != map_content.image_content:
|
||||
self.cached_map = map_content.image_content
|
||||
self._attr_image_last_updated = self.coordinator.last_home_update
|
||||
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
async def async_image(self) -> bytes | None:
|
||||
|
||||
@@ -427,7 +427,11 @@ async def async_setup_entry(
|
||||
)
|
||||
for coordinator in coordinators.v1
|
||||
for description in SENSOR_DESCRIPTIONS
|
||||
if description.value_fn(coordinator.data) is not None
|
||||
# Note: Currently coordinator.data is always available on startup but won't be in the future
|
||||
if (
|
||||
coordinator.data is not None
|
||||
and description.value_fn(coordinator.data) is not None
|
||||
)
|
||||
]
|
||||
entities.extend(RoborockCurrentRoom(coordinator) for coordinator in coordinators.v1)
|
||||
entities.extend(
|
||||
@@ -480,6 +484,8 @@ class RoborockSensorEntity(RoborockCoordinatedEntityV1, SensorEntity):
|
||||
@property
|
||||
def native_value(self) -> StateType | datetime.datetime:
|
||||
"""Return the value reported by the sensor."""
|
||||
if self.coordinator.data is None:
|
||||
return None
|
||||
return self.entity_description.value_fn(self.coordinator.data)
|
||||
|
||||
|
||||
|
||||
@@ -150,6 +150,7 @@ class RoborockVacuum(RoborockCoordinatedEntityV1, StateVacuumEntity):
|
||||
coordinator.duid_slug,
|
||||
coordinator,
|
||||
)
|
||||
self._status_trait = coordinator.properties_api.status
|
||||
self._home_trait = coordinator.properties_api.home
|
||||
self._maps_trait = coordinator.properties_api.maps
|
||||
|
||||
@@ -176,31 +177,37 @@ class RoborockVacuum(RoborockCoordinatedEntityV1, StateVacuumEntity):
|
||||
@property
|
||||
def fan_speed_list(self) -> list[str]:
|
||||
"""Get the list of available fan speeds."""
|
||||
return [mode.value for mode in self._device_status.fan_speed_options]
|
||||
if self.coordinator.data is None:
|
||||
return []
|
||||
return [mode.value for mode in self._status_trait.fan_speed_options]
|
||||
|
||||
@property
|
||||
def activity(self) -> VacuumActivity | None:
|
||||
"""Return the status of the vacuum cleaner."""
|
||||
assert self._device_status.state is not None
|
||||
return STATE_CODE_TO_STATE.get(self._device_status.state)
|
||||
if self.coordinator.data is None or self._status_trait.state is None:
|
||||
return None
|
||||
return STATE_CODE_TO_STATE.get(self._status_trait.state)
|
||||
|
||||
@property
|
||||
def fan_speed(self) -> str | None:
|
||||
"""Return the fan speed of the vacuum cleaner."""
|
||||
return self._device_status.fan_speed_name
|
||||
if self.coordinator.data is None:
|
||||
return None
|
||||
return self._status_trait.fan_speed_name
|
||||
|
||||
async def async_start(self) -> None:
|
||||
"""Start the vacuum."""
|
||||
if self._device_status.in_returning == 1:
|
||||
await self.send(RoborockCommand.APP_CHARGE)
|
||||
elif self._device_status.in_cleaning == 2:
|
||||
await self.send(RoborockCommand.RESUME_ZONED_CLEAN)
|
||||
elif self._device_status.in_cleaning == 3:
|
||||
await self.send(RoborockCommand.RESUME_SEGMENT_CLEAN)
|
||||
elif self._device_status.in_cleaning == 4:
|
||||
await self.send(RoborockCommand.APP_RESUME_BUILD_MAP)
|
||||
else:
|
||||
await self.send(RoborockCommand.APP_START)
|
||||
command = RoborockCommand.APP_START
|
||||
if self.coordinator.data is not None:
|
||||
if self._status_trait.in_returning == 1:
|
||||
command = RoborockCommand.APP_CHARGE
|
||||
elif self._status_trait.in_cleaning == 2:
|
||||
command = RoborockCommand.RESUME_ZONED_CLEAN
|
||||
elif self._status_trait.in_cleaning == 3:
|
||||
command = RoborockCommand.RESUME_SEGMENT_CLEAN
|
||||
elif self._status_trait.in_cleaning == 4:
|
||||
command = RoborockCommand.APP_RESUME_BUILD_MAP
|
||||
await self.send(command)
|
||||
|
||||
async def async_pause(self) -> None:
|
||||
"""Pause the vacuum."""
|
||||
@@ -224,10 +231,15 @@ class RoborockVacuum(RoborockCoordinatedEntityV1, StateVacuumEntity):
|
||||
|
||||
async def async_set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None:
|
||||
"""Set vacuum fan speed."""
|
||||
if self.coordinator.data is None:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="update_options_failed",
|
||||
)
|
||||
await self.send(
|
||||
RoborockCommand.SET_CUSTOM_MODE,
|
||||
[
|
||||
{v: k for k, v in self._device_status.fan_speed_mapping.items()}[
|
||||
{v: k for k, v in self._status_trait.fan_speed_mapping.items()}[
|
||||
fan_speed
|
||||
]
|
||||
],
|
||||
|
||||
@@ -38,42 +38,42 @@
|
||||
},
|
||||
"services": {
|
||||
"select_first": {
|
||||
"description": "Selects the first option.",
|
||||
"name": "First"
|
||||
"description": "Selects the first option of a select.",
|
||||
"name": "Select first option"
|
||||
},
|
||||
"select_last": {
|
||||
"description": "Selects the last option.",
|
||||
"name": "Last"
|
||||
"description": "Selects the last option of a select.",
|
||||
"name": "Select last option"
|
||||
},
|
||||
"select_next": {
|
||||
"description": "Selects the next option.",
|
||||
"description": "Selects the next option of a select.",
|
||||
"fields": {
|
||||
"cycle": {
|
||||
"description": "If the option should cycle from the last to the first.",
|
||||
"name": "Cycle"
|
||||
}
|
||||
},
|
||||
"name": "Next"
|
||||
"name": "Select next option"
|
||||
},
|
||||
"select_option": {
|
||||
"description": "Selects an option.",
|
||||
"description": "Selects an option of a select.",
|
||||
"fields": {
|
||||
"option": {
|
||||
"description": "Option to be selected.",
|
||||
"name": "Option"
|
||||
}
|
||||
},
|
||||
"name": "Select"
|
||||
"name": "Select option"
|
||||
},
|
||||
"select_previous": {
|
||||
"description": "Selects the previous option.",
|
||||
"description": "Selects the previous option of a select.",
|
||||
"fields": {
|
||||
"cycle": {
|
||||
"description": "If the option should cycle from the first to the last.",
|
||||
"name": "Cycle"
|
||||
}
|
||||
},
|
||||
"name": "Previous"
|
||||
"name": "Select previous option"
|
||||
}
|
||||
},
|
||||
"title": "Select",
|
||||
|
||||
@@ -58,15 +58,15 @@
|
||||
},
|
||||
"services": {
|
||||
"toggle": {
|
||||
"description": "Toggles the siren on/off.",
|
||||
"name": "[%key:common::action::toggle%]"
|
||||
"description": "Toggles a siren on/off.",
|
||||
"name": "Toggle siren"
|
||||
},
|
||||
"turn_off": {
|
||||
"description": "Turns the siren off.",
|
||||
"name": "[%key:common::action::turn_off%]"
|
||||
"description": "Turns off a siren.",
|
||||
"name": "Turn off siren"
|
||||
},
|
||||
"turn_on": {
|
||||
"description": "Turns the siren on.",
|
||||
"description": "Turns on a siren.",
|
||||
"fields": {
|
||||
"duration": {
|
||||
"description": "Number of seconds the sound is played. Must be supported by the integration.",
|
||||
@@ -81,7 +81,7 @@
|
||||
"name": "Volume"
|
||||
}
|
||||
},
|
||||
"name": "[%key:common::action::turn_on%]"
|
||||
"name": "Turn on siren"
|
||||
}
|
||||
},
|
||||
"title": "Siren",
|
||||
|
||||
@@ -9,8 +9,14 @@ from typing import Any, cast
|
||||
|
||||
import jwt
|
||||
from tesla_fleet_api import TeslaFleetApi
|
||||
from tesla_fleet_api.const import SERVERS
|
||||
from tesla_fleet_api.exceptions import PreconditionFailed, TeslaFleetError
|
||||
from tesla_fleet_api.const import SERVERS, Scope
|
||||
from tesla_fleet_api.exceptions import (
|
||||
InvalidToken,
|
||||
LoginRequired,
|
||||
OAuthExpired,
|
||||
PreconditionFailed,
|
||||
TeslaFleetError,
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult
|
||||
@@ -69,6 +75,7 @@ class OAuth2FlowHandler(
|
||||
# OAuth done, setup Partner API connections for all regions
|
||||
implementation = cast(TeslaUserImplementation, self.flow_impl)
|
||||
session = async_get_clientsession(self.hass)
|
||||
failed_regions: list[str] = []
|
||||
|
||||
for region, server_url in SERVERS.items():
|
||||
if region == "cn":
|
||||
@@ -84,11 +91,37 @@ class OAuth2FlowHandler(
|
||||
vehicle_scope=False,
|
||||
)
|
||||
await api.get_private_key(self.hass.config.path("tesla_fleet.key"))
|
||||
await api.partner_login(
|
||||
implementation.client_id, implementation.client_secret
|
||||
)
|
||||
try:
|
||||
await api.partner_login(
|
||||
implementation.client_id,
|
||||
implementation.client_secret,
|
||||
[Scope.OPENID],
|
||||
)
|
||||
except (InvalidToken, OAuthExpired, LoginRequired) as err:
|
||||
LOGGER.warning(
|
||||
"Partner login failed for %s due to an authentication error: %s",
|
||||
server_url,
|
||||
err,
|
||||
)
|
||||
return self.async_abort(reason="oauth_error")
|
||||
except TeslaFleetError as err:
|
||||
LOGGER.warning("Partner login failed for %s: %s", server_url, err)
|
||||
failed_regions.append(server_url)
|
||||
continue
|
||||
self.apis.append(api)
|
||||
|
||||
if not self.apis:
|
||||
LOGGER.warning(
|
||||
"Partner login failed for all regions: %s", ", ".join(failed_regions)
|
||||
)
|
||||
return self.async_abort(reason="oauth_error")
|
||||
|
||||
if failed_regions:
|
||||
LOGGER.warning(
|
||||
"Partner login succeeded on some regions but failed on: %s",
|
||||
", ".join(failed_regions),
|
||||
)
|
||||
|
||||
return await self.async_step_domain_input()
|
||||
|
||||
async def async_step_domain_input(
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["tesla-fleet-api"],
|
||||
"requirements": ["tesla-fleet-api==1.4.3"]
|
||||
"requirements": ["tesla-fleet-api==1.4.5"]
|
||||
}
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["tesla-fleet-api"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["tesla-fleet-api==1.4.3", "teslemetry-stream==0.9.0"]
|
||||
"requirements": ["tesla-fleet-api==1.4.5", "teslemetry-stream==0.9.0"]
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["tessie", "tesla-fleet-api"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["tessie-api==0.1.1", "tesla-fleet-api==1.4.3"]
|
||||
"requirements": ["tessie-api==0.1.1", "tesla-fleet-api==1.4.5"]
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ rules:
|
||||
status: exempt
|
||||
comment: |
|
||||
Cloud-based service without local discovery capabilities.
|
||||
docs-data-update: todo
|
||||
docs-data-update: done
|
||||
docs-examples: done
|
||||
docs-known-limitations: done
|
||||
docs-supported-devices: done
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "touchline",
|
||||
"name": "Roth Touchline",
|
||||
"codeowners": [],
|
||||
"codeowners": ["@mnordseth"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/touchline",
|
||||
"integration_type": "hub",
|
||||
|
||||
@@ -15,6 +15,8 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from .coordinator import TouchlineSLConfigEntry, TouchlineSLModuleCoordinator
|
||||
from .entity import TouchlineSLZoneEntity
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -17,6 +17,7 @@ from homeassistant.const import (
|
||||
SIGNAL_STRENGTH_DECIBELS,
|
||||
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
Platform,
|
||||
UnitOfConductivity,
|
||||
UnitOfElectricCurrent,
|
||||
UnitOfElectricPotential,
|
||||
UnitOfEnergy,
|
||||
@@ -705,6 +706,7 @@ class DPCode(StrEnum):
|
||||
DOORCONTACT_STATE_2 = "doorcontact_state_2"
|
||||
DOORCONTACT_STATE_3 = "doorcontact_state_3"
|
||||
DUSTER_CLOTH = "duster_cloth"
|
||||
EC_CURRENT = "ec_current"
|
||||
ECO2 = "eco2"
|
||||
EDGE_BRUSH = "edge_brush"
|
||||
ELECTRICITY_LEFT = "electricity_left"
|
||||
@@ -784,6 +786,7 @@ class DPCode(StrEnum):
|
||||
MUFFLING = "muffling" # Muffling
|
||||
NEAR_DETECTION = "near_detection"
|
||||
OPPOSITE = "opposite"
|
||||
ORP_CURRENT = "orp_current"
|
||||
OUTPUT_POWER_LIMIT = "output_power_limit"
|
||||
OXYGEN = "oxygen" # Oxygen bar
|
||||
PAUSE = "pause"
|
||||
@@ -796,6 +799,7 @@ class DPCode(StrEnum):
|
||||
PHASE_A = "phase_a"
|
||||
PHASE_B = "phase_b"
|
||||
PHASE_C = "phase_c"
|
||||
PH_CURRENT = "ph_current"
|
||||
PIR = "pir" # Motion sensor
|
||||
PM1 = "pm1"
|
||||
PM10 = "pm10"
|
||||
@@ -1167,6 +1171,20 @@ UNITS = (
|
||||
aliases={"mv", "millivolt"},
|
||||
device_classes={SensorDeviceClass.VOLTAGE},
|
||||
),
|
||||
UnitOfMeasurement(
|
||||
unit="",
|
||||
aliases={"ph"},
|
||||
device_classes={
|
||||
SensorDeviceClass.PH,
|
||||
},
|
||||
),
|
||||
UnitOfMeasurement(
|
||||
unit=UnitOfConductivity.MICROSIEMENS_PER_CM,
|
||||
aliases={"us"},
|
||||
device_classes={
|
||||
SensorDeviceClass.CONDUCTIVITY,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -315,6 +315,11 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
|
||||
),
|
||||
TuyaSensorEntityDescription(
|
||||
key=DPCode.EC_CURRENT,
|
||||
device_class=SensorDeviceClass.CONDUCTIVITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
TuyaSensorEntityDescription(
|
||||
key=DPCode.CH2O_VALUE,
|
||||
translation_key="formaldehyde",
|
||||
@@ -342,6 +347,17 @@ SENSORS: dict[DeviceCategory, tuple[TuyaSensorEntityDescription, ...]] = {
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
TuyaSensorEntityDescription(
|
||||
key=DPCode.ORP_CURRENT,
|
||||
translation_key="oxydo_reduction_potential",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
TuyaSensorEntityDescription(
|
||||
key=DPCode.PH_CURRENT,
|
||||
device_class=SensorDeviceClass.PH,
|
||||
# pH is unitless; treat any Tuya-reported "pH"/"ph" unit as unitless
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
TuyaSensorEntityDescription(
|
||||
key=DPCode.SMOKE_SENSOR_VALUE,
|
||||
translation_key="smoke_amount",
|
||||
|
||||
@@ -756,6 +756,9 @@
|
||||
"work": "Working"
|
||||
}
|
||||
},
|
||||
"oxydo_reduction_potential": {
|
||||
"name": "Oxydo reduction potential"
|
||||
},
|
||||
"phase_a_current": {
|
||||
"name": "Phase A current"
|
||||
},
|
||||
|
||||
@@ -300,13 +300,22 @@ def make_wan_latency_sensors() -> tuple[UnifiSensorEntityDescription, ...]:
|
||||
@callback
|
||||
def async_device_temperatures_value_fn(
|
||||
temperature_name: str, hub: UnifiHub, device: Device
|
||||
) -> float:
|
||||
) -> float | None:
|
||||
"""Retrieve the temperature of the device."""
|
||||
return_value: float = 0
|
||||
if device.temperatures:
|
||||
temperature = _device_temperature(temperature_name, device.temperatures)
|
||||
return_value = temperature if temperature is not None else 0
|
||||
return return_value
|
||||
return _device_temperature(temperature_name, device.temperatures)
|
||||
return None
|
||||
|
||||
|
||||
@callback
|
||||
def async_device_temperatures_available_fn(
|
||||
temperature_name: str, hub: UnifiHub, obj_id: str
|
||||
) -> bool:
|
||||
"""Determine if a device temperature has a value."""
|
||||
device = hub.api.devices[obj_id]
|
||||
if not async_device_available_fn(hub, obj_id):
|
||||
return False
|
||||
return _device_temperature(temperature_name, device.temperatures or []) is not None
|
||||
|
||||
|
||||
@callback
|
||||
@@ -315,7 +324,11 @@ def async_device_temperatures_supported_fn(
|
||||
) -> bool:
|
||||
"""Determine if an device have a temperatures."""
|
||||
if (device := hub.api.devices[obj_id]) and device.temperatures:
|
||||
return _device_temperature(temperature_name, device.temperatures) is not None
|
||||
return any(
|
||||
temperature_name in temperature["name"]
|
||||
for temperature in device.temperatures
|
||||
)
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@@ -326,7 +339,8 @@ def _device_temperature(
|
||||
"""Return the temperature of the device."""
|
||||
for temperature in temperatures:
|
||||
if temperature_name in temperature["name"]:
|
||||
return temperature["value"]
|
||||
if (value := temperature.get("value")) is not None:
|
||||
return value
|
||||
return None
|
||||
|
||||
|
||||
@@ -344,7 +358,7 @@ def make_device_temperatur_sensors() -> tuple[UnifiSensorEntityDescription, ...]
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
entity_registry_enabled_default=False,
|
||||
api_handler_fn=lambda api: api.devices,
|
||||
available_fn=async_device_available_fn,
|
||||
available_fn=partial(async_device_temperatures_available_fn, name),
|
||||
device_info_fn=async_device_device_info_fn,
|
||||
name_fn=lambda device: f"{device.name} {name} Temperature",
|
||||
object_fn=lambda api, obj_id: api.devices[obj_id],
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
"name": "Install update"
|
||||
},
|
||||
"skip": {
|
||||
"description": "Marks currently available update as skipped.",
|
||||
"description": "Marks a currently available update as skipped.",
|
||||
"name": "Skip update"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"universal_silabs_flasher",
|
||||
"serialx"
|
||||
],
|
||||
"requirements": ["zha==1.0.2", "serialx==0.6.2"],
|
||||
"requirements": ["zha==1.1.0", "serialx==0.6.2"],
|
||||
"usb": [
|
||||
{
|
||||
"description": "*2652*",
|
||||
|
||||
@@ -294,30 +294,51 @@
|
||||
"ias_zone": {
|
||||
"name": "IAS zone"
|
||||
},
|
||||
"interconnectable": {
|
||||
"name": "Interconnectable"
|
||||
},
|
||||
"led_indicator": {
|
||||
"name": "LED indicator"
|
||||
},
|
||||
"lifecycle": {
|
||||
"name": "Lifecycle"
|
||||
},
|
||||
"linkage_alarm_state": {
|
||||
"name": "Linkage alarm state"
|
||||
},
|
||||
"mounting_mode_active": {
|
||||
"name": "Mounting mode active"
|
||||
},
|
||||
"muted": {
|
||||
"name": "Muted"
|
||||
},
|
||||
"open_window_detection_status": {
|
||||
"name": "Open window detection status"
|
||||
},
|
||||
"preheat": {
|
||||
"name": "Preheat"
|
||||
},
|
||||
"preheat_active": {
|
||||
"name": "Preheat active"
|
||||
},
|
||||
"preheat_status": {
|
||||
"name": "Pre-heat status"
|
||||
},
|
||||
"rain_detected": {
|
||||
"name": "Rain detected"
|
||||
},
|
||||
"replace_filter": {
|
||||
"name": "Replace filter"
|
||||
},
|
||||
"self_test_state": {
|
||||
"name": "Self-test"
|
||||
},
|
||||
"silence_alarm": {
|
||||
"name": "Silence alarm"
|
||||
},
|
||||
"test": {
|
||||
"name": "Test"
|
||||
},
|
||||
"valve_alarm": {
|
||||
"name": "Valve alarm"
|
||||
},
|
||||
@@ -335,15 +356,27 @@
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
"allow_remote_binding": {
|
||||
"name": "Allow remote binding"
|
||||
},
|
||||
"boost_mode": {
|
||||
"name": "Boost mode"
|
||||
},
|
||||
"calibrate_sensor": {
|
||||
"name": "Calibrate sensor"
|
||||
},
|
||||
"calibrate_tvoc_sensor": {
|
||||
"name": "Calibrate TVOC sensor"
|
||||
},
|
||||
"calibrate_valve": {
|
||||
"name": "Calibrate valve"
|
||||
},
|
||||
"calibrate_z_axis": {
|
||||
"name": "Calibrate Z axis"
|
||||
},
|
||||
"configure_limits": {
|
||||
"name": "Configure cover limits"
|
||||
},
|
||||
"delete_all_limits": {
|
||||
"name": "Delete all limits"
|
||||
},
|
||||
@@ -368,6 +401,9 @@
|
||||
"prepare_manual_calibration": {
|
||||
"name": "Prepare manual calibration"
|
||||
},
|
||||
"remote_test": {
|
||||
"name": "Remote test"
|
||||
},
|
||||
"reset_alarm": {
|
||||
"name": "Reset alarm"
|
||||
},
|
||||
@@ -461,6 +497,9 @@
|
||||
"additional_steps": {
|
||||
"name": "Additional steps"
|
||||
},
|
||||
"air_threshold": {
|
||||
"name": "Air threshold"
|
||||
},
|
||||
"alarm_duration": {
|
||||
"name": "Alarm duration"
|
||||
},
|
||||
@@ -677,6 +716,9 @@
|
||||
"installation_height": {
|
||||
"name": "Height from sensor to tank bottom"
|
||||
},
|
||||
"integral_factor": {
|
||||
"name": "Integral factor"
|
||||
},
|
||||
"interval_time": {
|
||||
"name": "Interval time"
|
||||
},
|
||||
@@ -707,6 +749,9 @@
|
||||
"led_intensity_when_on": {
|
||||
"name": "Default all LED on intensity"
|
||||
},
|
||||
"length": {
|
||||
"name": "Length"
|
||||
},
|
||||
"lift_drive_down_time": {
|
||||
"name": "Lift drive down time"
|
||||
},
|
||||
@@ -869,9 +914,18 @@
|
||||
"presence_sensitivity": {
|
||||
"name": "Presence sensitivity"
|
||||
},
|
||||
"presence_sensor_sensitivity": {
|
||||
"name": "Presence sensor sensitivity"
|
||||
},
|
||||
"presence_timeout": {
|
||||
"name": "Fade time"
|
||||
},
|
||||
"proportional_gain": {
|
||||
"name": "Proportional gain (Kp)"
|
||||
},
|
||||
"proportional_shift": {
|
||||
"name": "Proportional shift (N)"
|
||||
},
|
||||
"pulse_configuration": {
|
||||
"name": "Pulse configuration"
|
||||
},
|
||||
@@ -911,6 +965,9 @@
|
||||
"sensitivity_level": {
|
||||
"name": "Sensitivity level"
|
||||
},
|
||||
"sensor_sensitivity": {
|
||||
"name": "Sensor sensitivity"
|
||||
},
|
||||
"serving_size": {
|
||||
"name": "Serving to dispense"
|
||||
},
|
||||
@@ -938,6 +995,9 @@
|
||||
"sound_volume": {
|
||||
"name": "Sound volume"
|
||||
},
|
||||
"speed": {
|
||||
"name": "Speed"
|
||||
},
|
||||
"start_up_color_temperature": {
|
||||
"name": "Start-up color temperature"
|
||||
},
|
||||
@@ -956,6 +1016,9 @@
|
||||
"static_detection_sensitivity": {
|
||||
"name": "Static detection sensitivity"
|
||||
},
|
||||
"summer_backup_heating_demand": {
|
||||
"name": "Summer backup heating demand"
|
||||
},
|
||||
"sustain_time": {
|
||||
"name": "Sustain time"
|
||||
},
|
||||
@@ -998,6 +1061,9 @@
|
||||
"timer_time_left": {
|
||||
"name": "Timer time left"
|
||||
},
|
||||
"total_cycle_times": {
|
||||
"name": "Total cycle times"
|
||||
},
|
||||
"transmit_power": {
|
||||
"name": "Transmit power"
|
||||
},
|
||||
@@ -1060,6 +1126,9 @@
|
||||
},
|
||||
"water_interval": {
|
||||
"name": "Water interval"
|
||||
},
|
||||
"winter_backup_heating_demand": {
|
||||
"name": "Winter backup heating demand"
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
@@ -1087,6 +1156,15 @@
|
||||
"approach_distance": {
|
||||
"name": "Approach distance"
|
||||
},
|
||||
"audio": {
|
||||
"name": "Audio"
|
||||
},
|
||||
"audio_effect": {
|
||||
"name": "Audio effect"
|
||||
},
|
||||
"audio_sensitivity": {
|
||||
"name": "Audio sensitivity"
|
||||
},
|
||||
"backlight_mode": {
|
||||
"name": "Backlight mode"
|
||||
},
|
||||
@@ -1105,6 +1183,9 @@
|
||||
"click_mode": {
|
||||
"name": "Click mode"
|
||||
},
|
||||
"color_temperature_channel": {
|
||||
"name": "Color temperature channel"
|
||||
},
|
||||
"control_type": {
|
||||
"name": "Control type"
|
||||
},
|
||||
@@ -1126,6 +1207,9 @@
|
||||
"default_strobe_level": {
|
||||
"name": "Default strobe level"
|
||||
},
|
||||
"detach_relay": {
|
||||
"name": "Detach relay"
|
||||
},
|
||||
"detection_distance": {
|
||||
"name": "Detection distance"
|
||||
},
|
||||
@@ -1401,6 +1485,12 @@
|
||||
"brightness_level": {
|
||||
"name": "Brightness level"
|
||||
},
|
||||
"calibrated": {
|
||||
"name": "Calibrated"
|
||||
},
|
||||
"chamber_contamination": {
|
||||
"name": "Chamber contamination"
|
||||
},
|
||||
"control_status": {
|
||||
"name": "Control status"
|
||||
},
|
||||
@@ -1464,6 +1554,9 @@
|
||||
"formaldehyde": {
|
||||
"name": "Formaldehyde concentration"
|
||||
},
|
||||
"gust_speed": {
|
||||
"name": "Gust speed"
|
||||
},
|
||||
"heating_demand": {
|
||||
"name": "Heating demand"
|
||||
},
|
||||
@@ -1532,6 +1625,9 @@
|
||||
"last_pin_code": {
|
||||
"name": "Last PIN code"
|
||||
},
|
||||
"last_remaining_battery_percentage": {
|
||||
"name": "Last remaining battery percentage"
|
||||
},
|
||||
"last_valve_open_duration": {
|
||||
"name": "Last valve open duration"
|
||||
},
|
||||
@@ -1544,6 +1640,9 @@
|
||||
"lifetime": {
|
||||
"name": "Lifetime"
|
||||
},
|
||||
"light_level": {
|
||||
"name": "Light level"
|
||||
},
|
||||
"liquid_depth": {
|
||||
"name": "Liquid depth"
|
||||
},
|
||||
@@ -1607,9 +1706,18 @@
|
||||
"preheat_time": {
|
||||
"name": "Pre-heat time"
|
||||
},
|
||||
"rebooted_count": {
|
||||
"name": "Rebooted count"
|
||||
},
|
||||
"rejoined_count": {
|
||||
"name": "Rejoined count"
|
||||
},
|
||||
"remaining_watering_time": {
|
||||
"name": "Remaining watering time"
|
||||
},
|
||||
"reported_packages": {
|
||||
"name": "Reported packages"
|
||||
},
|
||||
"rms_current_ph_b": {
|
||||
"name": "Current phase B"
|
||||
},
|
||||
@@ -1646,6 +1754,9 @@
|
||||
"smoke_density": {
|
||||
"name": "Smoke density"
|
||||
},
|
||||
"smoke_level": {
|
||||
"name": "Smoke level"
|
||||
},
|
||||
"software_error": {
|
||||
"name": "Software error",
|
||||
"state": {
|
||||
@@ -1742,6 +1853,12 @@
|
||||
"total_power_factor": {
|
||||
"name": "Total power factor"
|
||||
},
|
||||
"total_volatile_organic_compounds": {
|
||||
"name": "Total volatile organic compounds"
|
||||
},
|
||||
"uv_index": {
|
||||
"name": "UV index"
|
||||
},
|
||||
"valve_adapt_status": {
|
||||
"name": "Valve adaptation status"
|
||||
},
|
||||
@@ -1771,6 +1888,9 @@
|
||||
},
|
||||
"window_covering_type": {
|
||||
"name": "Window covering type"
|
||||
},
|
||||
"work_mode": {
|
||||
"name": "Work mode"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
@@ -1780,6 +1900,9 @@
|
||||
"adaptive_mode": {
|
||||
"name": "Adaptive mode"
|
||||
},
|
||||
"alarm_switch": {
|
||||
"name": "Alarm switch"
|
||||
},
|
||||
"auto_clean": {
|
||||
"name": "Autoclean"
|
||||
},
|
||||
@@ -1810,6 +1933,9 @@
|
||||
"detach_relay": {
|
||||
"name": "Detach relay"
|
||||
},
|
||||
"detach_relay_id": {
|
||||
"name": "Detach relay {id}"
|
||||
},
|
||||
"detached": {
|
||||
"name": "Detached mode"
|
||||
},
|
||||
@@ -1822,6 +1948,9 @@
|
||||
"disable_clear_notifications_double_tap": {
|
||||
"name": "Disable config 2x tap to clear notifications"
|
||||
},
|
||||
"disable_double_click": {
|
||||
"name": "Disable double click"
|
||||
},
|
||||
"disable_led": {
|
||||
"name": "Disable LED"
|
||||
},
|
||||
@@ -1921,6 +2050,9 @@
|
||||
"mute_siren": {
|
||||
"name": "Mute siren"
|
||||
},
|
||||
"network_led": {
|
||||
"name": "Network LED"
|
||||
},
|
||||
"on_only_when_dark": {
|
||||
"name": "On only when dark"
|
||||
},
|
||||
@@ -1972,6 +2104,9 @@
|
||||
"sound_enabled": {
|
||||
"name": "Sound enabled"
|
||||
},
|
||||
"summer_mode": {
|
||||
"name": "Summer mode"
|
||||
},
|
||||
"switch": {
|
||||
"name": "[%key:component::switch::title%]"
|
||||
},
|
||||
|
||||
@@ -235,3 +235,12 @@ auth0-python<5.0
|
||||
|
||||
# Setuptools >=82.0.0 doesn't contain pkg_resources anymore
|
||||
setuptools<82.0.0
|
||||
|
||||
# Pin dependencies with '.pth' files to exact versions, only update manually!
|
||||
# https://github.com/Azure/azure-kusto-python/ -> '.pth' files removed with >=5.0.5
|
||||
# https://github.com/xolox/python-coloredlogs -> unmaintained
|
||||
# https://github.com/pypa/setuptools
|
||||
azure-kusto-data==4.5.1
|
||||
azure-kusto-ingest==4.5.1
|
||||
coloredlogs==15.0.1
|
||||
setuptools==81.0.0
|
||||
|
||||
6
requirements_all.txt
generated
6
requirements_all.txt
generated
@@ -1355,7 +1355,7 @@ ismartgate==5.0.2
|
||||
israel-rail-api==0.1.4
|
||||
|
||||
# homeassistant.components.abode
|
||||
jaraco.abode==6.2.1
|
||||
jaraco.abode==6.4.0
|
||||
|
||||
# homeassistant.components.jellyfin
|
||||
jellyfin-apiclient-python==1.11.0
|
||||
@@ -3085,7 +3085,7 @@ temperusb==1.6.1
|
||||
# homeassistant.components.tesla_fleet
|
||||
# homeassistant.components.teslemetry
|
||||
# homeassistant.components.tessie
|
||||
tesla-fleet-api==1.4.3
|
||||
tesla-fleet-api==1.4.5
|
||||
|
||||
# homeassistant.components.powerwall
|
||||
tesla-powerwall==0.5.2
|
||||
@@ -3389,7 +3389,7 @@ zeroconf==0.148.0
|
||||
zeversolar==0.3.2
|
||||
|
||||
# homeassistant.components.zha
|
||||
zha==1.0.2
|
||||
zha==1.1.0
|
||||
|
||||
# homeassistant.components.zhong_hong
|
||||
zhong-hong-hvac==1.0.13
|
||||
|
||||
6
requirements_test_all.txt
generated
6
requirements_test_all.txt
generated
@@ -1201,7 +1201,7 @@ ismartgate==5.0.2
|
||||
israel-rail-api==0.1.4
|
||||
|
||||
# homeassistant.components.abode
|
||||
jaraco.abode==6.2.1
|
||||
jaraco.abode==6.4.0
|
||||
|
||||
# homeassistant.components.jellyfin
|
||||
jellyfin-apiclient-python==1.11.0
|
||||
@@ -2603,7 +2603,7 @@ temperusb==1.6.1
|
||||
# homeassistant.components.tesla_fleet
|
||||
# homeassistant.components.teslemetry
|
||||
# homeassistant.components.tessie
|
||||
tesla-fleet-api==1.4.3
|
||||
tesla-fleet-api==1.4.5
|
||||
|
||||
# homeassistant.components.powerwall
|
||||
tesla-powerwall==0.5.2
|
||||
@@ -2865,7 +2865,7 @@ zeroconf==0.148.0
|
||||
zeversolar==0.3.2
|
||||
|
||||
# homeassistant.components.zha
|
||||
zha==1.0.2
|
||||
zha==1.1.0
|
||||
|
||||
# homeassistant.components.zinvolt
|
||||
zinvolt==0.3.0
|
||||
|
||||
@@ -225,6 +225,15 @@ auth0-python<5.0
|
||||
|
||||
# Setuptools >=82.0.0 doesn't contain pkg_resources anymore
|
||||
setuptools<82.0.0
|
||||
|
||||
# Pin dependencies with '.pth' files to exact versions, only update manually!
|
||||
# https://github.com/Azure/azure-kusto-python/ -> '.pth' files removed with >=5.0.5
|
||||
# https://github.com/xolox/python-coloredlogs -> unmaintained
|
||||
# https://github.com/pypa/setuptools
|
||||
azure-kusto-data==4.5.1
|
||||
azure-kusto-ingest==4.5.1
|
||||
coloredlogs==15.0.1
|
||||
setuptools==81.0.0
|
||||
"""
|
||||
|
||||
GENERATED_MESSAGE = (
|
||||
|
||||
@@ -95,6 +95,8 @@ FORBIDDEN_PACKAGES = {
|
||||
"async-timeout": "be replaced by asyncio.timeout (Python 3.11+)",
|
||||
# Only needed for tests
|
||||
"codecov": "not be a runtime dependency",
|
||||
# Coloredlogs is unmaintained and contains a '.pth' file
|
||||
"coloredlogs": "be replaced with colorlog",
|
||||
# Only needed for docs
|
||||
"mkdocs": "not be a runtime dependency",
|
||||
# Does blocking I/O and should be replaced by pyserial-asyncio-fast
|
||||
@@ -149,11 +151,13 @@ FORBIDDEN_PACKAGE_EXCEPTIONS: dict[str, dict[str, set[str]]] = {
|
||||
},
|
||||
"flux_led": {"flux-led": {"async-timeout"}},
|
||||
"foobot": {"foobot-async": {"async-timeout"}},
|
||||
"google_maps": {"locationsharinglib": {"coloredlogs"}},
|
||||
"harmony": {"aioharmony": {"async-timeout"}},
|
||||
"here_travel_time": {
|
||||
"here-routing": {"async-timeout"},
|
||||
"here-transit": {"async-timeout"},
|
||||
},
|
||||
"homeassistant_hardware": {"universal-silabs-flasher": {"coloredlogs"}},
|
||||
"homewizard": {"python-homewizard-energy": {"async-timeout"}},
|
||||
"imeon_inverter": {"imeon-inverter-api": {"async-timeout"}},
|
||||
"izone": {"python-izone": {"async-timeout"}},
|
||||
@@ -217,6 +221,7 @@ FORBIDDEN_PACKAGE_EXCEPTIONS: dict[str, dict[str, set[str]]] = {
|
||||
# https://github.com/waveform80/colorzero/issues/9
|
||||
# zha > zigpy-zigate > gpiozero > colorzero > setuptools
|
||||
"colorzero": {"setuptools"},
|
||||
"zigpy-znp": {"coloredlogs"},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -236,16 +241,42 @@ FORBIDDEN_PACKAGE_FILES_EXCEPTIONS = {
|
||||
# - reasonX should be the name of the invalid dependency
|
||||
# https://github.com/jaraco/jaraco.net
|
||||
"abode": {"jaraco-abode": {"jaraco-net"}},
|
||||
# https://github.com/Azure/azure-kusto-python/
|
||||
"azure_data_explorer": {
|
||||
# Legacy namespace packages, resolved with >=5.0.5
|
||||
# azure_kusto_data-*-nspkg.pth
|
||||
# azure_kusto_ingest-*-nspkg.pth
|
||||
"homeassistant": {"azure-kusto-data", "azure-kusto-ingest"},
|
||||
"azure-kusto-ingest": {"azure-kusto-data"},
|
||||
},
|
||||
# https://github.com/coinbase/coinbase-advanced-py
|
||||
"cmus": {
|
||||
# Setuptools - distutils-precedence.pth
|
||||
"pbr": {"setuptools"}
|
||||
},
|
||||
"coinbase": {"homeassistant": {"coinbase-advanced-py"}},
|
||||
# https://github.com/u9n/dlms-cosem
|
||||
"dsmr": {"dsmr-parser": {"dlms-cosem"}},
|
||||
# https://github.com/ChrisMandich/PyFlume # Fixed with >=0.7.1
|
||||
"fitbit": {
|
||||
# Setuptools - distutils-precedence.pth
|
||||
"fitbit": {"setuptools"}
|
||||
},
|
||||
"flume": {"homeassistant": {"pyflume"}},
|
||||
# https://github.com/fortinet-solutions-cse/fortiosapi
|
||||
"fortios": {"homeassistant": {"fortiosapi"}},
|
||||
# https://github.com/manzanotti/geniushub-client
|
||||
"geniushub": {"homeassistant": {"geniushub-client"}},
|
||||
# https://github.com/costastf/locationsharinglib
|
||||
"google_maps": {
|
||||
# Coloredlogs, unmaintained - coloredlogs.pth
|
||||
"locationsharinglib": {"coloredlogs"},
|
||||
},
|
||||
# https://github.com/NabuCasa/universal-silabs-flasher
|
||||
"homeassistant_hardware": {
|
||||
# Coloredlogs, unmaintained - coloredlogs.pth
|
||||
"universal-silabs-flasher": {"coloredlogs"},
|
||||
},
|
||||
# https://github.com/basnijholt/aiokef
|
||||
"kef": {"homeassistant": {"aiokef"}},
|
||||
# https://github.com/danifus/pyzipper
|
||||
@@ -257,11 +288,26 @@ FORBIDDEN_PACKAGE_FILES_EXCEPTIONS = {
|
||||
# https://github.com/timmo001/aiolyric
|
||||
"lyric": {"homeassistant": {"aiolyric"}},
|
||||
# https://github.com/microBeesTech/pythonSDK/
|
||||
"microbees": {"homeassistant": {"microbeespy"}},
|
||||
"microbees": {
|
||||
"homeassistant": {"microbeespy"},
|
||||
"microbeespy": {"setuptools"},
|
||||
},
|
||||
"mochad": {
|
||||
# Setuptools - distutils-precedence.pth
|
||||
"pbr": {"setuptools"}
|
||||
},
|
||||
# https://github.com/ejpenney/pyobihai
|
||||
"obihai": {"homeassistant": {"pyobihai"}},
|
||||
"opnsense": {
|
||||
# Setuptools - distutils-precedence.pth
|
||||
"pbr": {"setuptools"}
|
||||
},
|
||||
# https://github.com/iamkubi/pydactyl
|
||||
"pterodactyl": {"homeassistant": {"py-dactyl"}},
|
||||
"remote_rpi_gpio": {
|
||||
# Setuptools - distutils-precedence.pth
|
||||
"colorzero": {"setuptools"}
|
||||
},
|
||||
# https://github.com/sstallion/sensorpush-api
|
||||
"sensorpush_cloud": {
|
||||
"homeassistant": {"sensorpush-api"},
|
||||
@@ -273,6 +319,14 @@ FORBIDDEN_PACKAGE_FILES_EXCEPTIONS = {
|
||||
"watergate": {"homeassistant": {"watergate-local-api"}},
|
||||
# https://github.com/markusressel/xs1-api-client
|
||||
"xs1": {"homeassistant": {"xs1-api-client"}},
|
||||
# https://github.com/zigpy/zigpy-znp
|
||||
"zha": {
|
||||
# Setuptools - distutils-precedence.pth
|
||||
"colorzero": {"setuptools"},
|
||||
# Coloredlogs, unmaintained - coloredlogs.pth
|
||||
# https://github.com/xolox/python-coloredlogs/blob/15.0.1/coloredlogs.pth
|
||||
"zigpy-znp": {"coloredlogs"},
|
||||
},
|
||||
}
|
||||
|
||||
PYTHON_VERSION_CHECK_EXCEPTIONS: dict[str, dict[str, set[str]]] = {
|
||||
@@ -670,8 +724,10 @@ def check_dependency_files(
|
||||
for file in files(pkg) or ():
|
||||
if not (top := file.parts[0].lower()).endswith((".dist-info", ".py")):
|
||||
top_level.add(top)
|
||||
if (name := str(file)).lower() in FORBIDDEN_FILE_NAMES:
|
||||
file_names.add(name)
|
||||
if (name := str(file).lower()) in FORBIDDEN_FILE_NAMES or (
|
||||
name.endswith(".pth") and len(file.parts) == 1
|
||||
):
|
||||
file_names.add(str(file))
|
||||
results = _PackageFilesCheckResult(
|
||||
top_level=FORBIDDEN_PACKAGE_NAMES & top_level,
|
||||
file_names=file_names,
|
||||
@@ -687,7 +743,8 @@ def check_dependency_files(
|
||||
f"Package {pkg} has a forbidden top level directory '{dir_name}' in {package}",
|
||||
)
|
||||
for file_name in results["file_names"]:
|
||||
integration.add_error(
|
||||
integration.add_warning_or_error(
|
||||
pkg in package_exceptions,
|
||||
"requirements",
|
||||
f"Package {pkg} has a forbidden file '{file_name}' in {package}",
|
||||
)
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
"""Tests for the Abode lock device."""
|
||||
|
||||
import json
|
||||
from unittest.mock import patch
|
||||
|
||||
from jaraco.abode.helpers import urls as URL
|
||||
from requests_mock import Mocker
|
||||
|
||||
from homeassistant.components.abode import ATTR_DEVICE_ID
|
||||
from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN, LockState
|
||||
from homeassistant.const import (
|
||||
@@ -15,6 +19,8 @@ from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from .common import setup_platform
|
||||
|
||||
from tests.common import async_load_fixture
|
||||
|
||||
DEVICE_ID = "lock.test_lock"
|
||||
|
||||
|
||||
@@ -63,3 +69,23 @@ async def test_unlock(hass: HomeAssistant) -> None:
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
mock_unlock.assert_called_once()
|
||||
|
||||
|
||||
async def test_retrofit_lock_discovered(
|
||||
hass: HomeAssistant, requests_mock: Mocker
|
||||
) -> None:
|
||||
"""Test retrofit locks are discovered as lock entities."""
|
||||
devices = json.loads(await async_load_fixture(hass, "devices.json", "abode"))
|
||||
for device in devices:
|
||||
if device["type_tag"] == "device_type.door_lock":
|
||||
device["type_tag"] = "device_type.retrofit_lock"
|
||||
device["type"] = "Retrofit Lock"
|
||||
break
|
||||
|
||||
requests_mock.get(URL.DEVICES, text=json.dumps(devices))
|
||||
|
||||
await setup_platform(hass, LOCK_DOMAIN)
|
||||
|
||||
state = hass.states.get(DEVICE_ID)
|
||||
assert state is not None
|
||||
assert state.state == LockState.LOCKED
|
||||
|
||||
@@ -1,577 +0,0 @@
|
||||
"""Test air quality conditions."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
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,
|
||||
CONF_ABOVE,
|
||||
CONF_BELOW,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.components.common import (
|
||||
ConditionStateDescription,
|
||||
assert_condition_behavior_all,
|
||||
assert_condition_behavior_any,
|
||||
assert_condition_gated_by_labs_flag,
|
||||
assert_numerical_condition_unit_conversion,
|
||||
parametrize_condition_states_all,
|
||||
parametrize_condition_states_any,
|
||||
parametrize_numerical_condition_above_below_all,
|
||||
parametrize_numerical_condition_above_below_any,
|
||||
parametrize_target_entities,
|
||||
target_entities,
|
||||
)
|
||||
|
||||
_UGM3_CONDITION_OPTIONS = {"unit": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER}
|
||||
_UGM3_UNIT_ATTRIBUTES = {
|
||||
ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
|
||||
}
|
||||
_PPB_CONDITION_OPTIONS = {"unit": CONCENTRATION_PARTS_PER_BILLION}
|
||||
_PPB_UNIT_ATTRIBUTES = {ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION}
|
||||
_PPM_UNIT_ATTRIBUTES = {ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_MILLION}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def target_binary_sensors(hass: HomeAssistant) -> dict[str, list[str]]:
|
||||
"""Create multiple binary sensor entities associated with different targets."""
|
||||
return await target_entities(hass, "binary_sensor")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def target_sensors(hass: HomeAssistant) -> dict[str, list[str]]:
|
||||
"""Create multiple sensor entities associated with different targets."""
|
||||
return await target_entities(hass, "sensor")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"condition",
|
||||
[
|
||||
"air_quality.is_gas_detected",
|
||||
"air_quality.is_gas_cleared",
|
||||
"air_quality.is_co_detected",
|
||||
"air_quality.is_co_cleared",
|
||||
"air_quality.is_smoke_detected",
|
||||
"air_quality.is_smoke_cleared",
|
||||
"air_quality.is_co_value",
|
||||
"air_quality.is_co2_value",
|
||||
"air_quality.is_pm1_value",
|
||||
"air_quality.is_pm25_value",
|
||||
"air_quality.is_pm4_value",
|
||||
"air_quality.is_pm10_value",
|
||||
"air_quality.is_ozone_value",
|
||||
"air_quality.is_voc_value",
|
||||
"air_quality.is_voc_ratio_value",
|
||||
"air_quality.is_no_value",
|
||||
"air_quality.is_no2_value",
|
||||
"air_quality.is_n2o_value",
|
||||
"air_quality.is_so2_value",
|
||||
],
|
||||
)
|
||||
async def test_air_quality_conditions_gated_by_labs_flag(
|
||||
hass: HomeAssistant, caplog: pytest.LogCaptureFixture, condition: str
|
||||
) -> None:
|
||||
"""Test the air quality conditions are gated by the labs flag."""
|
||||
await assert_condition_gated_by_labs_flag(hass, caplog, condition)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("condition_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities("binary_sensor"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("condition", "condition_options", "states"),
|
||||
[
|
||||
*parametrize_condition_states_any(
|
||||
condition="air_quality.is_gas_detected",
|
||||
target_states=[STATE_ON],
|
||||
other_states=[STATE_OFF],
|
||||
required_filter_attributes={ATTR_DEVICE_CLASS: BinarySensorDeviceClass.GAS},
|
||||
),
|
||||
*parametrize_condition_states_any(
|
||||
condition="air_quality.is_gas_cleared",
|
||||
target_states=[STATE_OFF],
|
||||
other_states=[STATE_ON],
|
||||
required_filter_attributes={ATTR_DEVICE_CLASS: BinarySensorDeviceClass.GAS},
|
||||
),
|
||||
*parametrize_condition_states_any(
|
||||
condition="air_quality.is_co_detected",
|
||||
target_states=[STATE_ON],
|
||||
other_states=[STATE_OFF],
|
||||
required_filter_attributes={ATTR_DEVICE_CLASS: BinarySensorDeviceClass.CO},
|
||||
),
|
||||
*parametrize_condition_states_any(
|
||||
condition="air_quality.is_co_cleared",
|
||||
target_states=[STATE_OFF],
|
||||
other_states=[STATE_ON],
|
||||
required_filter_attributes={ATTR_DEVICE_CLASS: BinarySensorDeviceClass.CO},
|
||||
),
|
||||
*parametrize_condition_states_any(
|
||||
condition="air_quality.is_smoke_detected",
|
||||
target_states=[STATE_ON],
|
||||
other_states=[STATE_OFF],
|
||||
required_filter_attributes={
|
||||
ATTR_DEVICE_CLASS: BinarySensorDeviceClass.SMOKE
|
||||
},
|
||||
),
|
||||
*parametrize_condition_states_any(
|
||||
condition="air_quality.is_smoke_cleared",
|
||||
target_states=[STATE_OFF],
|
||||
other_states=[STATE_ON],
|
||||
required_filter_attributes={
|
||||
ATTR_DEVICE_CLASS: BinarySensorDeviceClass.SMOKE
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_air_quality_binary_condition_behavior_any(
|
||||
hass: HomeAssistant,
|
||||
target_binary_sensors: dict[str, list[str]],
|
||||
condition_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
condition: str,
|
||||
condition_options: dict[str, Any],
|
||||
states: list[ConditionStateDescription],
|
||||
) -> None:
|
||||
"""Test the air quality binary sensor condition with 'any' behavior."""
|
||||
await assert_condition_behavior_any(
|
||||
hass,
|
||||
target_entities=target_binary_sensors,
|
||||
condition_target_config=condition_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
condition=condition,
|
||||
condition_options=condition_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("condition_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities("binary_sensor"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("condition", "condition_options", "states"),
|
||||
[
|
||||
*parametrize_condition_states_all(
|
||||
condition="air_quality.is_gas_detected",
|
||||
target_states=[STATE_ON],
|
||||
other_states=[STATE_OFF],
|
||||
required_filter_attributes={ATTR_DEVICE_CLASS: BinarySensorDeviceClass.GAS},
|
||||
),
|
||||
*parametrize_condition_states_all(
|
||||
condition="air_quality.is_gas_cleared",
|
||||
target_states=[STATE_OFF],
|
||||
other_states=[STATE_ON],
|
||||
required_filter_attributes={ATTR_DEVICE_CLASS: BinarySensorDeviceClass.GAS},
|
||||
),
|
||||
*parametrize_condition_states_all(
|
||||
condition="air_quality.is_co_detected",
|
||||
target_states=[STATE_ON],
|
||||
other_states=[STATE_OFF],
|
||||
required_filter_attributes={ATTR_DEVICE_CLASS: BinarySensorDeviceClass.CO},
|
||||
),
|
||||
*parametrize_condition_states_all(
|
||||
condition="air_quality.is_co_cleared",
|
||||
target_states=[STATE_OFF],
|
||||
other_states=[STATE_ON],
|
||||
required_filter_attributes={ATTR_DEVICE_CLASS: BinarySensorDeviceClass.CO},
|
||||
),
|
||||
*parametrize_condition_states_all(
|
||||
condition="air_quality.is_smoke_detected",
|
||||
target_states=[STATE_ON],
|
||||
other_states=[STATE_OFF],
|
||||
required_filter_attributes={
|
||||
ATTR_DEVICE_CLASS: BinarySensorDeviceClass.SMOKE
|
||||
},
|
||||
),
|
||||
*parametrize_condition_states_all(
|
||||
condition="air_quality.is_smoke_cleared",
|
||||
target_states=[STATE_OFF],
|
||||
other_states=[STATE_ON],
|
||||
required_filter_attributes={
|
||||
ATTR_DEVICE_CLASS: BinarySensorDeviceClass.SMOKE
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_air_quality_binary_condition_behavior_all(
|
||||
hass: HomeAssistant,
|
||||
target_binary_sensors: dict[str, list[str]],
|
||||
condition_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
condition: str,
|
||||
condition_options: dict[str, Any],
|
||||
states: list[ConditionStateDescription],
|
||||
) -> None:
|
||||
"""Test the air quality binary sensor condition with 'all' behavior."""
|
||||
await assert_condition_behavior_all(
|
||||
hass,
|
||||
target_entities=target_binary_sensors,
|
||||
condition_target_config=condition_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
condition=condition,
|
||||
condition_options=condition_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("condition_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities("sensor"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("condition", "condition_options", "states"),
|
||||
[
|
||||
*parametrize_numerical_condition_above_below_any(
|
||||
"air_quality.is_co_value",
|
||||
device_class="carbon_monoxide",
|
||||
condition_options=_UGM3_CONDITION_OPTIONS,
|
||||
unit_attributes=_UGM3_UNIT_ATTRIBUTES,
|
||||
),
|
||||
*parametrize_numerical_condition_above_below_any(
|
||||
"air_quality.is_ozone_value",
|
||||
device_class="ozone",
|
||||
condition_options=_UGM3_CONDITION_OPTIONS,
|
||||
unit_attributes=_UGM3_UNIT_ATTRIBUTES,
|
||||
),
|
||||
*parametrize_numerical_condition_above_below_any(
|
||||
"air_quality.is_voc_value",
|
||||
device_class="volatile_organic_compounds",
|
||||
condition_options=_UGM3_CONDITION_OPTIONS,
|
||||
unit_attributes=_UGM3_UNIT_ATTRIBUTES,
|
||||
),
|
||||
*parametrize_numerical_condition_above_below_any(
|
||||
"air_quality.is_voc_ratio_value",
|
||||
device_class="volatile_organic_compounds_parts",
|
||||
condition_options=_PPB_CONDITION_OPTIONS,
|
||||
unit_attributes=_PPB_UNIT_ATTRIBUTES,
|
||||
),
|
||||
*parametrize_numerical_condition_above_below_any(
|
||||
"air_quality.is_no_value",
|
||||
device_class="nitrogen_monoxide",
|
||||
condition_options=_UGM3_CONDITION_OPTIONS,
|
||||
unit_attributes=_UGM3_UNIT_ATTRIBUTES,
|
||||
),
|
||||
*parametrize_numerical_condition_above_below_any(
|
||||
"air_quality.is_no2_value",
|
||||
device_class="nitrogen_dioxide",
|
||||
condition_options=_UGM3_CONDITION_OPTIONS,
|
||||
unit_attributes=_UGM3_UNIT_ATTRIBUTES,
|
||||
),
|
||||
*parametrize_numerical_condition_above_below_any(
|
||||
"air_quality.is_so2_value",
|
||||
device_class="sulphur_dioxide",
|
||||
condition_options=_UGM3_CONDITION_OPTIONS,
|
||||
unit_attributes=_UGM3_UNIT_ATTRIBUTES,
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_air_quality_numerical_with_unit_condition_behavior_any(
|
||||
hass: HomeAssistant,
|
||||
target_sensors: dict[str, list[str]],
|
||||
condition_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
condition: str,
|
||||
condition_options: dict[str, Any],
|
||||
states: list[ConditionStateDescription],
|
||||
) -> None:
|
||||
"""Test air quality numerical conditions with unit conversion and 'any' behavior."""
|
||||
await assert_condition_behavior_any(
|
||||
hass,
|
||||
target_entities=target_sensors,
|
||||
condition_target_config=condition_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
condition=condition,
|
||||
condition_options=condition_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("condition_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities("sensor"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("condition", "condition_options", "states"),
|
||||
[
|
||||
*parametrize_numerical_condition_above_below_all(
|
||||
"air_quality.is_co_value",
|
||||
device_class="carbon_monoxide",
|
||||
condition_options=_UGM3_CONDITION_OPTIONS,
|
||||
unit_attributes=_UGM3_UNIT_ATTRIBUTES,
|
||||
),
|
||||
*parametrize_numerical_condition_above_below_all(
|
||||
"air_quality.is_ozone_value",
|
||||
device_class="ozone",
|
||||
condition_options=_UGM3_CONDITION_OPTIONS,
|
||||
unit_attributes=_UGM3_UNIT_ATTRIBUTES,
|
||||
),
|
||||
*parametrize_numerical_condition_above_below_all(
|
||||
"air_quality.is_voc_value",
|
||||
device_class="volatile_organic_compounds",
|
||||
condition_options=_UGM3_CONDITION_OPTIONS,
|
||||
unit_attributes=_UGM3_UNIT_ATTRIBUTES,
|
||||
),
|
||||
*parametrize_numerical_condition_above_below_all(
|
||||
"air_quality.is_voc_ratio_value",
|
||||
device_class="volatile_organic_compounds_parts",
|
||||
condition_options=_PPB_CONDITION_OPTIONS,
|
||||
unit_attributes=_PPB_UNIT_ATTRIBUTES,
|
||||
),
|
||||
*parametrize_numerical_condition_above_below_all(
|
||||
"air_quality.is_no_value",
|
||||
device_class="nitrogen_monoxide",
|
||||
condition_options=_UGM3_CONDITION_OPTIONS,
|
||||
unit_attributes=_UGM3_UNIT_ATTRIBUTES,
|
||||
),
|
||||
*parametrize_numerical_condition_above_below_all(
|
||||
"air_quality.is_no2_value",
|
||||
device_class="nitrogen_dioxide",
|
||||
condition_options=_UGM3_CONDITION_OPTIONS,
|
||||
unit_attributes=_UGM3_UNIT_ATTRIBUTES,
|
||||
),
|
||||
*parametrize_numerical_condition_above_below_all(
|
||||
"air_quality.is_so2_value",
|
||||
device_class="sulphur_dioxide",
|
||||
condition_options=_UGM3_CONDITION_OPTIONS,
|
||||
unit_attributes=_UGM3_UNIT_ATTRIBUTES,
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_air_quality_numerical_with_unit_condition_behavior_all(
|
||||
hass: HomeAssistant,
|
||||
target_sensors: dict[str, list[str]],
|
||||
condition_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
condition: str,
|
||||
condition_options: dict[str, Any],
|
||||
states: list[ConditionStateDescription],
|
||||
) -> None:
|
||||
"""Test air quality numerical conditions with unit conversion and 'all' behavior."""
|
||||
await assert_condition_behavior_all(
|
||||
hass,
|
||||
target_entities=target_sensors,
|
||||
condition_target_config=condition_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
condition=condition,
|
||||
condition_options=condition_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("condition_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities("sensor"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("condition", "condition_options", "states"),
|
||||
[
|
||||
*parametrize_numerical_condition_above_below_any(
|
||||
"air_quality.is_co2_value",
|
||||
device_class="carbon_dioxide",
|
||||
unit_attributes=_PPM_UNIT_ATTRIBUTES,
|
||||
),
|
||||
*parametrize_numerical_condition_above_below_any(
|
||||
"air_quality.is_pm1_value",
|
||||
device_class="pm1",
|
||||
unit_attributes=_UGM3_UNIT_ATTRIBUTES,
|
||||
),
|
||||
*parametrize_numerical_condition_above_below_any(
|
||||
"air_quality.is_pm25_value",
|
||||
device_class="pm25",
|
||||
unit_attributes=_UGM3_UNIT_ATTRIBUTES,
|
||||
),
|
||||
*parametrize_numerical_condition_above_below_any(
|
||||
"air_quality.is_pm4_value",
|
||||
device_class="pm4",
|
||||
unit_attributes=_UGM3_UNIT_ATTRIBUTES,
|
||||
),
|
||||
*parametrize_numerical_condition_above_below_any(
|
||||
"air_quality.is_pm10_value",
|
||||
device_class="pm10",
|
||||
unit_attributes=_UGM3_UNIT_ATTRIBUTES,
|
||||
),
|
||||
*parametrize_numerical_condition_above_below_any(
|
||||
"air_quality.is_n2o_value",
|
||||
device_class="nitrous_oxide",
|
||||
unit_attributes=_UGM3_UNIT_ATTRIBUTES,
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_air_quality_numerical_no_unit_condition_behavior_any(
|
||||
hass: HomeAssistant,
|
||||
target_sensors: dict[str, list[str]],
|
||||
condition_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
condition: str,
|
||||
condition_options: dict[str, Any],
|
||||
states: list[ConditionStateDescription],
|
||||
) -> None:
|
||||
"""Test air quality numerical conditions without unit conversion and 'any' behavior."""
|
||||
await assert_condition_behavior_any(
|
||||
hass,
|
||||
target_entities=target_sensors,
|
||||
condition_target_config=condition_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
condition=condition,
|
||||
condition_options=condition_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
@pytest.mark.parametrize(
|
||||
("condition_target_config", "entity_id", "entities_in_target"),
|
||||
parametrize_target_entities("sensor"),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("condition", "condition_options", "states"),
|
||||
[
|
||||
*parametrize_numerical_condition_above_below_all(
|
||||
"air_quality.is_co2_value",
|
||||
device_class="carbon_dioxide",
|
||||
unit_attributes=_PPM_UNIT_ATTRIBUTES,
|
||||
),
|
||||
*parametrize_numerical_condition_above_below_all(
|
||||
"air_quality.is_pm1_value",
|
||||
device_class="pm1",
|
||||
unit_attributes=_UGM3_UNIT_ATTRIBUTES,
|
||||
),
|
||||
*parametrize_numerical_condition_above_below_all(
|
||||
"air_quality.is_pm25_value",
|
||||
device_class="pm25",
|
||||
unit_attributes=_UGM3_UNIT_ATTRIBUTES,
|
||||
),
|
||||
*parametrize_numerical_condition_above_below_all(
|
||||
"air_quality.is_pm4_value",
|
||||
device_class="pm4",
|
||||
unit_attributes=_UGM3_UNIT_ATTRIBUTES,
|
||||
),
|
||||
*parametrize_numerical_condition_above_below_all(
|
||||
"air_quality.is_pm10_value",
|
||||
device_class="pm10",
|
||||
unit_attributes=_UGM3_UNIT_ATTRIBUTES,
|
||||
),
|
||||
*parametrize_numerical_condition_above_below_all(
|
||||
"air_quality.is_n2o_value",
|
||||
device_class="nitrous_oxide",
|
||||
unit_attributes=_UGM3_UNIT_ATTRIBUTES,
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_air_quality_numerical_no_unit_condition_behavior_all(
|
||||
hass: HomeAssistant,
|
||||
target_sensors: dict[str, list[str]],
|
||||
condition_target_config: dict,
|
||||
entity_id: str,
|
||||
entities_in_target: int,
|
||||
condition: str,
|
||||
condition_options: dict[str, Any],
|
||||
states: list[ConditionStateDescription],
|
||||
) -> None:
|
||||
"""Test air quality numerical conditions without unit conversion and 'all' behavior."""
|
||||
await assert_condition_behavior_all(
|
||||
hass,
|
||||
target_entities=target_sensors,
|
||||
condition_target_config=condition_target_config,
|
||||
entity_id=entity_id,
|
||||
entities_in_target=entities_in_target,
|
||||
condition=condition,
|
||||
condition_options=condition_options,
|
||||
states=states,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("enable_labs_preview_features")
|
||||
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_invalid = {ATTR_UNIT_OF_MEASUREMENT: "not_a_valid_unit"}
|
||||
|
||||
await assert_numerical_condition_unit_conversion(
|
||||
hass,
|
||||
condition="air_quality.is_co_value",
|
||||
entity_id="sensor.test",
|
||||
pass_states=[
|
||||
{
|
||||
"state": "500",
|
||||
"attributes": {
|
||||
"device_class": "carbon_monoxide",
|
||||
ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
},
|
||||
}
|
||||
],
|
||||
fail_states=[
|
||||
{
|
||||
"state": "100",
|
||||
"attributes": {
|
||||
"device_class": "carbon_monoxide",
|
||||
ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
},
|
||||
}
|
||||
],
|
||||
numerical_condition_options=[
|
||||
{
|
||||
CONF_ABOVE: 0.2,
|
||||
CONF_BELOW: 0.8,
|
||||
"unit": CONCENTRATION_PARTS_PER_MILLION,
|
||||
},
|
||||
{
|
||||
CONF_ABOVE: 200,
|
||||
CONF_BELOW: 800,
|
||||
"unit": CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
},
|
||||
],
|
||||
limit_entity_condition_options={
|
||||
CONF_ABOVE: "sensor.above",
|
||||
CONF_BELOW: "sensor.below",
|
||||
},
|
||||
limit_entities=("sensor.above", "sensor.below"),
|
||||
limit_entity_states=[
|
||||
(
|
||||
{"state": "0.2", "attributes": _unit_ppm},
|
||||
{"state": "0.8", "attributes": _unit_ppm},
|
||||
),
|
||||
(
|
||||
{"state": "200", "attributes": _unit_ugm3},
|
||||
{"state": "800", "attributes": _unit_ugm3},
|
||||
),
|
||||
],
|
||||
invalid_limit_entity_states=[
|
||||
(
|
||||
{"state": "0.2", "attributes": _unit_invalid},
|
||||
{"state": "0.8", "attributes": _unit_invalid},
|
||||
),
|
||||
(
|
||||
{"state": "200", "attributes": _unit_invalid},
|
||||
{"state": "800", "attributes": _unit_invalid},
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -1,18 +1 @@
|
||||
"""Tests for the huum integration."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def setup_with_selected_platforms(
|
||||
hass: HomeAssistant, entry: MockConfigEntry, platforms: list[Platform]
|
||||
) -> None:
|
||||
"""Set up the Huum integration with the selected platforms."""
|
||||
entry.add_to_hass(hass)
|
||||
with patch("homeassistant.components.huum.PLATFORMS", platforms):
|
||||
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
@@ -1,93 +1,54 @@
|
||||
"""Configuration for Huum tests."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from huum.const import SaunaStatus
|
||||
from huum.schemas import HuumStatusResponse, SaunaConfig
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.huum.const import DOMAIN
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_huum() -> Generator[AsyncMock]:
|
||||
"""Mock data from the API."""
|
||||
huum = AsyncMock()
|
||||
|
||||
def mock_huum_client() -> Generator[AsyncMock]:
|
||||
"""Mock the Huum API client."""
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.huum.config_flow.Huum.status",
|
||||
return_value=huum,
|
||||
"homeassistant.components.huum.coordinator.Huum",
|
||||
autospec=True,
|
||||
) as mock_cls,
|
||||
patch(
|
||||
"homeassistant.components.huum.config_flow.Huum",
|
||||
new=mock_cls,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.huum.coordinator.Huum.status",
|
||||
return_value=huum,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.huum.coordinator.Huum.turn_on",
|
||||
return_value=huum,
|
||||
) as turn_on,
|
||||
patch(
|
||||
"homeassistant.components.huum.coordinator.Huum.toggle_light",
|
||||
return_value=huum,
|
||||
) as toggle_light,
|
||||
):
|
||||
huum.status = SaunaStatus.ONLINE_NOT_HEATING
|
||||
huum.config = 3
|
||||
huum.door_closed = True
|
||||
huum.temperature = 30
|
||||
huum.sauna_name = "Home sauna"
|
||||
huum.target_temperature = 80
|
||||
huum.payment_end_date = "2026-12-31"
|
||||
huum.light = 1
|
||||
huum.humidity = 0
|
||||
huum.target_humidity = 5
|
||||
huum.sauna_config.child_lock = "OFF"
|
||||
huum.sauna_config.max_heating_time = 3
|
||||
huum.sauna_config.min_heating_time = 0
|
||||
huum.sauna_config.max_temp = 110
|
||||
huum.sauna_config.min_temp = 40
|
||||
huum.sauna_config.max_timer = 0
|
||||
huum.sauna_config.min_timer = 0
|
||||
|
||||
def _to_dict() -> dict[str, object]:
|
||||
return {
|
||||
"status": huum.status,
|
||||
"config": huum.config,
|
||||
"door_closed": huum.door_closed,
|
||||
"temperature": huum.temperature,
|
||||
"sauna_name": huum.sauna_name,
|
||||
"target_temperature": huum.target_temperature,
|
||||
"start_date": None,
|
||||
"end_date": None,
|
||||
"duration": None,
|
||||
"steamer_error": None,
|
||||
"payment_end_date": huum.payment_end_date,
|
||||
"is_private": None,
|
||||
"show_modal": None,
|
||||
"light": huum.light,
|
||||
"humidity": huum.humidity,
|
||||
"target_humidity": huum.target_humidity,
|
||||
"remote_safety_state": None,
|
||||
"sauna_config": {
|
||||
"child_lock": huum.sauna_config.child_lock,
|
||||
"max_heating_time": huum.sauna_config.max_heating_time,
|
||||
"min_heating_time": huum.sauna_config.min_heating_time,
|
||||
"max_temp": huum.sauna_config.max_temp,
|
||||
"min_temp": huum.sauna_config.min_temp,
|
||||
"max_timer": huum.sauna_config.max_timer,
|
||||
"min_timer": huum.sauna_config.min_timer,
|
||||
},
|
||||
}
|
||||
|
||||
huum.to_dict = Mock(side_effect=_to_dict)
|
||||
huum.turn_on = turn_on
|
||||
huum.toggle_light = toggle_light
|
||||
|
||||
yield huum
|
||||
client = mock_cls.return_value
|
||||
client.status.return_value = HuumStatusResponse(
|
||||
status=SaunaStatus.ONLINE_NOT_HEATING,
|
||||
door_closed=True,
|
||||
temperature=30,
|
||||
sauna_name="123456",
|
||||
target_temperature=80,
|
||||
config=3,
|
||||
light=1,
|
||||
humidity=0,
|
||||
target_humidity=5,
|
||||
sauna_config=SaunaConfig(
|
||||
child_lock="OFF",
|
||||
max_heating_time=3,
|
||||
min_heating_time=0,
|
||||
max_temp=110,
|
||||
min_temp=40,
|
||||
max_timer=0,
|
||||
min_timer=0,
|
||||
),
|
||||
)
|
||||
yield client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -110,3 +71,32 @@ def mock_config_entry() -> MockConfigEntry:
|
||||
},
|
||||
entry_id="AABBCC112233",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def platforms() -> list[Platform]:
|
||||
"""Fixture to specify platforms to test."""
|
||||
return [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.CLIMATE,
|
||||
Platform.LIGHT,
|
||||
Platform.NUMBER,
|
||||
Platform.SENSOR,
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def init_integration(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_huum_client: AsyncMock,
|
||||
platforms: list[Platform],
|
||||
) -> MockConfigEntry:
|
||||
"""Set up the Huum integration for testing."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
with patch("homeassistant.components.huum.PLATFORMS", platforms):
|
||||
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
return mock_config_entry
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
'humidity': 0,
|
||||
'is_private': None,
|
||||
'light': 1,
|
||||
'payment_end_date': '**REDACTED**',
|
||||
'payment_end_date': None,
|
||||
'remote_safety_state': None,
|
||||
'sauna_config': dict({
|
||||
'child_lock': 'OFF',
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
"""Tests for the Huum climate entity."""
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
"""Tests for the Huum binary sensor entity."""
|
||||
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import setup_with_selected_platforms
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
ENTITY_ID = "binary_sensor.huum_sauna_door"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def platforms() -> list[Platform]:
|
||||
"""Fixture to specify platforms to test."""
|
||||
return [Platform.BINARY_SENSOR]
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_binary_sensor(
|
||||
hass: HomeAssistant,
|
||||
mock_huum: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
snapshot: SnapshotAssertion,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test the initial parameters."""
|
||||
await setup_with_selected_platforms(
|
||||
hass, mock_config_entry, [Platform.BINARY_SENSOR]
|
||||
)
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
"""Tests for the Huum climate entity."""
|
||||
|
||||
from datetime import timedelta
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
from huum.const import SaunaStatus
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
@@ -20,34 +23,35 @@ from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import setup_with_selected_platforms
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
|
||||
|
||||
ENTITY_ID = "climate.huum_sauna"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def platforms() -> list[Platform]:
|
||||
"""Fixture to specify platforms to test."""
|
||||
return [Platform.CLIMATE]
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_climate_entity(
|
||||
hass: HomeAssistant,
|
||||
mock_huum: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
snapshot: SnapshotAssertion,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test the initial parameters."""
|
||||
await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE])
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_set_hvac_mode(
|
||||
hass: HomeAssistant,
|
||||
mock_huum: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_huum_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test setting HVAC mode."""
|
||||
await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE])
|
||||
|
||||
mock_huum.status = SaunaStatus.ONLINE_HEATING
|
||||
mock_huum_client.status.return_value.status = SaunaStatus.ONLINE_HEATING
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
@@ -58,18 +62,16 @@ async def test_set_hvac_mode(
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state.state == HVACMode.HEAT
|
||||
|
||||
mock_huum.turn_on.assert_called_once()
|
||||
mock_huum_client.turn_on.assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_set_temperature(
|
||||
hass: HomeAssistant,
|
||||
mock_huum: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_huum_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test setting the temperature."""
|
||||
await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE])
|
||||
|
||||
mock_huum.status = SaunaStatus.ONLINE_HEATING
|
||||
mock_huum_client.status.return_value.status = SaunaStatus.ONLINE_HEATING
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
@@ -80,39 +82,40 @@ async def test_set_temperature(
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_huum.turn_on.assert_called_once_with(60)
|
||||
mock_huum_client.turn_on.assert_awaited_once_with(60)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_temperature_range(
|
||||
hass: HomeAssistant,
|
||||
mock_huum: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_huum_client: AsyncMock,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test the temperature range."""
|
||||
await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE])
|
||||
|
||||
# API response.
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state.attributes["min_temp"] == 40
|
||||
assert state.attributes["max_temp"] == 110
|
||||
|
||||
# Empty/unconfigured API response should return default values.
|
||||
mock_huum.sauna_config.min_temp = 0
|
||||
mock_huum.sauna_config.max_temp = 0
|
||||
mock_huum_client.status.return_value.sauna_config.min_temp = 0
|
||||
mock_huum_client.status.return_value.sauna_config.max_temp = 0
|
||||
|
||||
await mock_config_entry.runtime_data.async_refresh()
|
||||
await hass.async_block_till_done()
|
||||
freezer.tick(timedelta(seconds=30))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state.attributes["min_temp"] == CONFIG_DEFAULT_MIN_TEMP
|
||||
assert state.attributes["max_temp"] == CONFIG_DEFAULT_MAX_TEMP
|
||||
|
||||
# Custom configured API response.
|
||||
mock_huum.sauna_config.min_temp = 50
|
||||
mock_huum.sauna_config.max_temp = 80
|
||||
mock_huum_client.status.return_value.sauna_config.min_temp = 50
|
||||
mock_huum_client.status.return_value.sauna_config.max_temp = 80
|
||||
|
||||
await mock_config_entry.runtime_data.async_refresh()
|
||||
await hass.async_block_till_done()
|
||||
freezer.tick(timedelta(seconds=30))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state.attributes["min_temp"] == 50
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Test the huum config flow."""
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from huum.exceptions import Forbidden
|
||||
import pytest
|
||||
@@ -17,9 +17,8 @@ TEST_USERNAME = "huum@sauna.org"
|
||||
TEST_PASSWORD = "ukuuku"
|
||||
|
||||
|
||||
async def test_form(
|
||||
hass: HomeAssistant, mock_huum: AsyncMock, mock_setup_entry: AsyncMock
|
||||
) -> None:
|
||||
@pytest.mark.usefixtures("mock_huum_client")
|
||||
async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None:
|
||||
"""Test we get the form."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
@@ -46,9 +45,9 @@ async def test_form(
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_huum_client")
|
||||
async def test_signup_flow_already_set_up(
|
||||
hass: HomeAssistant,
|
||||
mock_huum: AsyncMock,
|
||||
mock_setup_entry: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
@@ -82,7 +81,7 @@ async def test_signup_flow_already_set_up(
|
||||
)
|
||||
async def test_huum_errors(
|
||||
hass: HomeAssistant,
|
||||
mock_huum: AsyncMock,
|
||||
mock_huum_client: AsyncMock,
|
||||
mock_setup_entry: AsyncMock,
|
||||
raises: Exception,
|
||||
error_base: str,
|
||||
@@ -92,21 +91,19 @@ async def test_huum_errors(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.huum.config_flow.Huum.status",
|
||||
side_effect=raises,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_USERNAME: TEST_USERNAME,
|
||||
CONF_PASSWORD: TEST_PASSWORD,
|
||||
},
|
||||
)
|
||||
mock_huum_client.status.side_effect = raises
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_USERNAME: TEST_USERNAME,
|
||||
CONF_PASSWORD: TEST_PASSWORD,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": error_base}
|
||||
|
||||
mock_huum_client.status.side_effect = None
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
@@ -117,9 +114,9 @@ async def test_huum_errors(
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_huum_client")
|
||||
async def test_reauth_flow(
|
||||
hass: HomeAssistant,
|
||||
mock_huum: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test reauthentication flow succeeds with valid credentials."""
|
||||
@@ -155,7 +152,7 @@ async def test_reauth_flow(
|
||||
)
|
||||
async def test_reauth_errors(
|
||||
hass: HomeAssistant,
|
||||
mock_huum: AsyncMock,
|
||||
mock_huum_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
raises: Exception,
|
||||
error_base: str,
|
||||
@@ -165,19 +162,17 @@ async def test_reauth_errors(
|
||||
|
||||
result = await mock_config_entry.start_reauth_flow(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.huum.config_flow.Huum.status",
|
||||
side_effect=raises,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_PASSWORD: "wrong_password"},
|
||||
)
|
||||
mock_huum_client.status.side_effect = raises
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_PASSWORD: "wrong_password"},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": error_base}
|
||||
|
||||
# Recover with valid credentials
|
||||
mock_huum_client.status.side_effect = None
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_PASSWORD: "new_password"},
|
||||
|
||||
@@ -2,30 +2,31 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import setup_with_selected_platforms
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.components.diagnostics import get_diagnostics_for_config_entry
|
||||
from tests.typing import ClientSessionGenerator
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def platforms() -> list[Platform]:
|
||||
"""Fixture to specify platforms to test."""
|
||||
return [Platform.CLIMATE]
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_diagnostics(
|
||||
hass: HomeAssistant,
|
||||
hass_client: ClientSessionGenerator,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_huum: AsyncMock,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test diagnostics."""
|
||||
await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE])
|
||||
|
||||
diagnostics = await get_diagnostics_for_config_entry(
|
||||
hass, hass_client, mock_config_entry
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Tests for the Huum __init__."""
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from huum.exceptions import Forbidden, NotAuthenticated
|
||||
import pytest
|
||||
@@ -8,20 +8,16 @@ import pytest
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.huum.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import setup_with_selected_platforms
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_loading_and_unloading_config_entry(
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_huum: AsyncMock
|
||||
hass: HomeAssistant, mock_config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test loading and unloading a config entry."""
|
||||
await setup_with_selected_platforms(hass, mock_config_entry, [Platform.CLIMATE])
|
||||
|
||||
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
@@ -35,17 +31,15 @@ async def test_loading_and_unloading_config_entry(
|
||||
async def test_auth_error_triggers_reauth(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_huum_client: AsyncMock,
|
||||
side_effect: type[Exception],
|
||||
) -> None:
|
||||
"""Test that an auth error during coordinator refresh triggers reauth."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
mock_huum_client.status.side_effect = side_effect
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.huum.coordinator.Huum.status",
|
||||
side_effect=side_effect,
|
||||
):
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR
|
||||
assert any(
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"""Tests for the Huum light entity."""
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
|
||||
@@ -16,33 +17,34 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import setup_with_selected_platforms
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
ENTITY_ID = "light.huum_sauna_light"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def platforms() -> list[Platform]:
|
||||
"""Fixture to specify platforms to test."""
|
||||
return [Platform.LIGHT]
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_light(
|
||||
hass: HomeAssistant,
|
||||
mock_huum: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
snapshot: SnapshotAssertion,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test the initial parameters."""
|
||||
await setup_with_selected_platforms(hass, mock_config_entry, [Platform.LIGHT])
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_light_turn_off(
|
||||
hass: HomeAssistant,
|
||||
mock_huum: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_huum_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test turning off light."""
|
||||
await setup_with_selected_platforms(hass, mock_config_entry, [Platform.LIGHT])
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state.state == STATE_ON
|
||||
|
||||
@@ -52,18 +54,21 @@ async def test_light_turn_off(
|
||||
{ATTR_ENTITY_ID: ENTITY_ID},
|
||||
blocking=True,
|
||||
)
|
||||
mock_huum.toggle_light.assert_called_once()
|
||||
mock_huum_client.toggle_light.assert_awaited_once()
|
||||
|
||||
|
||||
async def test_light_turn_on(
|
||||
hass: HomeAssistant,
|
||||
mock_huum: AsyncMock,
|
||||
mock_huum_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test turning on light."""
|
||||
mock_huum.light = 0
|
||||
mock_huum_client.status.return_value.light = 0
|
||||
|
||||
await setup_with_selected_platforms(hass, mock_config_entry, [Platform.LIGHT])
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
with patch("homeassistant.components.huum.PLATFORMS", [Platform.LIGHT]):
|
||||
assert await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state.state == STATE_OFF
|
||||
@@ -74,4 +79,4 @@ async def test_light_turn_on(
|
||||
{ATTR_ENTITY_ID: ENTITY_ID},
|
||||
blocking=True,
|
||||
)
|
||||
mock_huum.toggle_light.assert_called_once()
|
||||
mock_huum_client.toggle_light.assert_awaited_once()
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from huum.const import SaunaStatus
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.number import (
|
||||
@@ -14,34 +15,35 @@ from homeassistant.const import ATTR_ENTITY_ID, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import setup_with_selected_platforms
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
ENTITY_ID = "number.huum_sauna_humidity"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def platforms() -> list[Platform]:
|
||||
"""Fixture to specify platforms to test."""
|
||||
return [Platform.NUMBER]
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_number_entity(
|
||||
hass: HomeAssistant,
|
||||
mock_huum: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
snapshot: SnapshotAssertion,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test the initial parameters."""
|
||||
await setup_with_selected_platforms(hass, mock_config_entry, [Platform.NUMBER])
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_set_humidity(
|
||||
hass: HomeAssistant,
|
||||
mock_huum: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_huum_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test setting the humidity."""
|
||||
await setup_with_selected_platforms(hass, mock_config_entry, [Platform.NUMBER])
|
||||
|
||||
mock_huum.status = SaunaStatus.ONLINE_HEATING
|
||||
mock_huum_client.status.return_value.status = SaunaStatus.ONLINE_HEATING
|
||||
await hass.services.async_call(
|
||||
NUMBER_DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
@@ -52,18 +54,16 @@ async def test_set_humidity(
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_huum.turn_on.assert_called_once_with(temperature=80, humidity=5)
|
||||
mock_huum_client.turn_on.assert_awaited_once_with(temperature=80, humidity=5)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_dont_set_humidity_when_sauna_not_heating(
|
||||
hass: HomeAssistant,
|
||||
mock_huum: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_huum_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Test setting the humidity."""
|
||||
await setup_with_selected_platforms(hass, mock_config_entry, [Platform.NUMBER])
|
||||
|
||||
mock_huum.status = SaunaStatus.ONLINE_NOT_HEATING
|
||||
mock_huum_client.status.return_value.status = SaunaStatus.ONLINE_NOT_HEATING
|
||||
await hass.services.async_call(
|
||||
NUMBER_DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
@@ -74,4 +74,4 @@ async def test_dont_set_humidity_when_sauna_not_heating(
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_huum.turn_on.assert_not_called()
|
||||
mock_huum_client.turn_on.assert_not_called()
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
"""Tests for the Huum sensor entity."""
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import setup_with_selected_platforms
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def platforms() -> list[Platform]:
|
||||
"""Fixture to specify platforms to test."""
|
||||
return [Platform.SENSOR]
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_sensor(
|
||||
hass: HomeAssistant,
|
||||
mock_huum: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
snapshot: SnapshotAssertion,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test the temperature sensor."""
|
||||
await setup_with_selected_platforms(hass, mock_config_entry, [Platform.SENSOR])
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
@@ -13,6 +13,7 @@ from homeassistant.components.infrared import (
|
||||
DOMAIN as INFRARED_DOMAIN,
|
||||
InfraredEntity,
|
||||
)
|
||||
from homeassistant.components.lg_infrared import PLATFORMS
|
||||
from homeassistant.components.lg_infrared.const import (
|
||||
CONF_DEVICE_TYPE,
|
||||
CONF_INFRARED_ENTITY_ID,
|
||||
@@ -68,7 +69,7 @@ def mock_infrared_entity() -> MockInfraredEntity:
|
||||
@pytest.fixture
|
||||
def platforms() -> list[Platform]:
|
||||
"""Return platforms to set up."""
|
||||
return [Platform.MEDIA_PLAYER]
|
||||
return PLATFORMS
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
1401
tests/components/lg_infrared/snapshots/test_button.ambr
Normal file
1401
tests/components/lg_infrared/snapshots/test_button.ambr
Normal file
File diff suppressed because it is too large
Load Diff
107
tests/components/lg_infrared/test_button.py
Normal file
107
tests/components/lg_infrared/test_button.py
Normal file
@@ -0,0 +1,107 @@
|
||||
"""Tests for the LG Infrared button platform."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from infrared_protocols.codes.lg.tv import LGTVCode
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS
|
||||
from homeassistant.const import ATTR_ENTITY_ID, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
|
||||
from .conftest import MockInfraredEntity
|
||||
from .utils import check_availability_follows_ir_entity
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def platforms() -> list[Platform]:
|
||||
"""Return platforms to set up."""
|
||||
return [Platform.BUTTON]
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_entities(
|
||||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
entity_registry: er.EntityRegistry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test all button entities are created with correct attributes."""
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
# Verify all entities belong to the same device
|
||||
device_entry = device_registry.async_get_device(
|
||||
identifiers={("lg_infrared", mock_config_entry.entry_id)}
|
||||
)
|
||||
assert device_entry
|
||||
entity_entries = er.async_entries_for_config_entry(
|
||||
entity_registry, mock_config_entry.entry_id
|
||||
)
|
||||
for entity_entry in entity_entries:
|
||||
assert entity_entry.device_id == device_entry.id
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("entity_id", "expected_code"),
|
||||
[
|
||||
("button.lg_tv_power_on", LGTVCode.POWER_ON),
|
||||
("button.lg_tv_power_off", LGTVCode.POWER_OFF),
|
||||
("button.lg_tv_hdmi_1", LGTVCode.HDMI_1),
|
||||
("button.lg_tv_hdmi_2", LGTVCode.HDMI_2),
|
||||
("button.lg_tv_hdmi_3", LGTVCode.HDMI_3),
|
||||
("button.lg_tv_hdmi_4", LGTVCode.HDMI_4),
|
||||
("button.lg_tv_exit", LGTVCode.EXIT),
|
||||
("button.lg_tv_info", LGTVCode.INFO),
|
||||
("button.lg_tv_guide", LGTVCode.GUIDE),
|
||||
("button.lg_tv_up", LGTVCode.NAV_UP),
|
||||
("button.lg_tv_down", LGTVCode.NAV_DOWN),
|
||||
("button.lg_tv_left", LGTVCode.NAV_LEFT),
|
||||
("button.lg_tv_right", LGTVCode.NAV_RIGHT),
|
||||
("button.lg_tv_ok", LGTVCode.OK),
|
||||
("button.lg_tv_back", LGTVCode.BACK),
|
||||
("button.lg_tv_home", LGTVCode.HOME),
|
||||
("button.lg_tv_menu", LGTVCode.MENU),
|
||||
("button.lg_tv_input", LGTVCode.INPUT),
|
||||
("button.lg_tv_number_0", LGTVCode.NUM_0),
|
||||
("button.lg_tv_number_1", LGTVCode.NUM_1),
|
||||
("button.lg_tv_number_2", LGTVCode.NUM_2),
|
||||
("button.lg_tv_number_3", LGTVCode.NUM_3),
|
||||
("button.lg_tv_number_4", LGTVCode.NUM_4),
|
||||
("button.lg_tv_number_5", LGTVCode.NUM_5),
|
||||
("button.lg_tv_number_6", LGTVCode.NUM_6),
|
||||
("button.lg_tv_number_7", LGTVCode.NUM_7),
|
||||
("button.lg_tv_number_8", LGTVCode.NUM_8),
|
||||
("button.lg_tv_number_9", LGTVCode.NUM_9),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_button_press_sends_correct_code(
|
||||
hass: HomeAssistant,
|
||||
mock_infrared_entity: MockInfraredEntity,
|
||||
entity_id: str,
|
||||
expected_code: LGTVCode,
|
||||
) -> None:
|
||||
"""Test pressing a button sends the correct IR code."""
|
||||
await hass.services.async_call(
|
||||
BUTTON_DOMAIN,
|
||||
SERVICE_PRESS,
|
||||
{ATTR_ENTITY_ID: entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert len(mock_infrared_entity.send_command_calls) == 1
|
||||
assert mock_infrared_entity.send_command_calls[0] == expected_code
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_button_availability_follows_ir_entity(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test button becomes unavailable when IR entity is unavailable."""
|
||||
entity_id = "button.lg_tv_power_on"
|
||||
await check_availability_follows_ir_entity(hass, entity_id)
|
||||
@@ -19,11 +19,12 @@ from homeassistant.components.media_player import (
|
||||
SERVICE_VOLUME_MUTE,
|
||||
SERVICE_VOLUME_UP,
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, Platform
|
||||
from homeassistant.const import ATTR_ENTITY_ID, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
|
||||
from .conftest import MOCK_INFRARED_ENTITY_ID, MockInfraredEntity
|
||||
from .conftest import MockInfraredEntity
|
||||
from .utils import check_availability_follows_ir_entity
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
@@ -99,23 +100,4 @@ async def test_media_player_availability_follows_ir_entity(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test media player becomes unavailable when IR entity is unavailable."""
|
||||
# Initially available
|
||||
state = hass.states.get(MEDIA_PLAYER_ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
|
||||
# Make IR entity unavailable
|
||||
hass.states.async_set(MOCK_INFRARED_ENTITY_ID, STATE_UNAVAILABLE)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(MEDIA_PLAYER_ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
# Restore IR entity
|
||||
hass.states.async_set(MOCK_INFRARED_ENTITY_ID, "2026-01-01T00:00:00.000")
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(MEDIA_PLAYER_ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
await check_availability_follows_ir_entity(hass, MEDIA_PLAYER_ENTITY_ID)
|
||||
|
||||
33
tests/components/lg_infrared/utils.py
Normal file
33
tests/components/lg_infrared/utils.py
Normal file
@@ -0,0 +1,33 @@
|
||||
"""Tests for the LG Infrared integration."""
|
||||
|
||||
from homeassistant.const import STATE_UNAVAILABLE
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .conftest import MOCK_INFRARED_ENTITY_ID
|
||||
|
||||
|
||||
async def check_availability_follows_ir_entity(
|
||||
hass: HomeAssistant,
|
||||
entity_id: str,
|
||||
) -> None:
|
||||
"""Check that entity becomes unavailable when IR entity is unavailable."""
|
||||
# Initially available
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
|
||||
# Make IR entity unavailable
|
||||
hass.states.async_set(MOCK_INFRARED_ENTITY_ID, STATE_UNAVAILABLE)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
# Restore IR entity
|
||||
hass.states.async_set(MOCK_INFRARED_ENTITY_ID, "2026-01-01T00:00:00.000")
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
@@ -15,6 +15,7 @@ from pyliebherrhomeapi import (
|
||||
HydroBreezeMode,
|
||||
IceMakerControl,
|
||||
IceMakerMode,
|
||||
PresentationLightControl,
|
||||
TemperatureControl,
|
||||
TemperatureUnit,
|
||||
ToggleControl,
|
||||
@@ -115,6 +116,12 @@ MOCK_DEVICE_STATE = DeviceState(
|
||||
BioFreshPlusMode.MINUS_TWO_ZERO,
|
||||
],
|
||||
),
|
||||
PresentationLightControl(
|
||||
name="presentationlight",
|
||||
type="PresentationLightControl",
|
||||
value=3,
|
||||
max=5,
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -175,6 +182,7 @@ def mock_liebherr_client() -> Generator[MagicMock]:
|
||||
client.set_ice_maker = AsyncMock()
|
||||
client.set_hydro_breeze = AsyncMock()
|
||||
client.set_bio_fresh_plus = AsyncMock()
|
||||
client.set_presentation_light = AsyncMock()
|
||||
yield client
|
||||
|
||||
|
||||
|
||||
@@ -87,6 +87,12 @@
|
||||
'type': 'BioFreshPlusControl',
|
||||
'zone_id': 1,
|
||||
}),
|
||||
dict({
|
||||
'max': 5,
|
||||
'name': 'presentationlight',
|
||||
'type': 'PresentationLightControl',
|
||||
'value': 3,
|
||||
}),
|
||||
]),
|
||||
'device': dict({
|
||||
'device_id': 'test_device_id',
|
||||
|
||||
61
tests/components/liebherr/snapshots/test_light.ambr
Normal file
61
tests/components/liebherr/snapshots/test_light.ambr
Normal file
@@ -0,0 +1,61 @@
|
||||
# serializer version: 1
|
||||
# name: test_lights[light.test_fridge_presentation_light-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'supported_color_modes': list([
|
||||
<ColorMode.BRIGHTNESS: 'brightness'>,
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'light',
|
||||
'entity_category': None,
|
||||
'entity_id': 'light.test_fridge_presentation_light',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Presentation light',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Presentation light',
|
||||
'platform': 'liebherr',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'presentation_light',
|
||||
'unique_id': 'test_device_id_presentation_light',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_lights[light.test_fridge_presentation_light-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'brightness': 153,
|
||||
'color_mode': <ColorMode.BRIGHTNESS: 'brightness'>,
|
||||
'friendly_name': 'Test Fridge Presentation light',
|
||||
'supported_color_modes': list([
|
||||
<ColorMode.BRIGHTNESS: 'brightness'>,
|
||||
]),
|
||||
'supported_features': <LightEntityFeature: 0>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'light.test_fridge_presentation_light',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
267
tests/components/liebherr/test_light.py
Normal file
267
tests/components/liebherr/test_light.py
Normal file
@@ -0,0 +1,267 @@
|
||||
"""Test the Liebherr light platform."""
|
||||
|
||||
import copy
|
||||
from datetime import timedelta
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
from pyliebherrhomeapi import Device, DeviceState, DeviceType, PresentationLightControl
|
||||
from pyliebherrhomeapi.exceptions import LiebherrConnectionError
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.liebherr.const import DOMAIN
|
||||
from homeassistant.components.light import ATTR_BRIGHTNESS, DOMAIN as LIGHT_DOMAIN
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
SERVICE_TURN_OFF,
|
||||
SERVICE_TURN_ON,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from .conftest import MOCK_DEVICE, MOCK_DEVICE_STATE
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
|
||||
|
||||
pytestmark = pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def platforms() -> list[Platform]:
|
||||
"""Fixture to specify platforms to test."""
|
||||
return [Platform.LIGHT]
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_lights(
|
||||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test all light entities."""
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_light_state(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test light entity reports correct state."""
|
||||
entity_id = "light.test_fridge_presentation_light"
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
assert state.state == STATE_ON
|
||||
# value=3, max=5 → brightness = ceil(3 * 255 / 5) = 153
|
||||
assert state.attributes[ATTR_BRIGHTNESS] == 153
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("service", "service_data", "expected_target"),
|
||||
[
|
||||
(SERVICE_TURN_ON, {}, 5),
|
||||
(SERVICE_TURN_ON, {ATTR_BRIGHTNESS: 255}, 5),
|
||||
(SERVICE_TURN_ON, {ATTR_BRIGHTNESS: 128}, 3),
|
||||
(SERVICE_TURN_ON, {ATTR_BRIGHTNESS: 51}, 1),
|
||||
(SERVICE_TURN_ON, {ATTR_BRIGHTNESS: 1}, 1),
|
||||
(SERVICE_TURN_OFF, {}, 0),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_light_service_calls(
|
||||
hass: HomeAssistant,
|
||||
mock_liebherr_client: MagicMock,
|
||||
service: str,
|
||||
service_data: dict[str, Any],
|
||||
expected_target: int,
|
||||
) -> None:
|
||||
"""Test light turn on/off service calls."""
|
||||
entity_id = "light.test_fridge_presentation_light"
|
||||
initial_call_count = mock_liebherr_client.get_device_state.call_count
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
service,
|
||||
{ATTR_ENTITY_ID: entity_id, **service_data},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_liebherr_client.set_presentation_light.assert_called_once_with(
|
||||
device_id="test_device_id",
|
||||
target=expected_target,
|
||||
)
|
||||
|
||||
# Verify coordinator refresh was triggered
|
||||
assert mock_liebherr_client.get_device_state.call_count > initial_call_count
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_light_failure(
|
||||
hass: HomeAssistant,
|
||||
mock_liebherr_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test light fails gracefully on connection error."""
|
||||
entity_id = "light.test_fridge_presentation_light"
|
||||
mock_liebherr_client.set_presentation_light.side_effect = LiebherrConnectionError(
|
||||
"Connection failed"
|
||||
)
|
||||
|
||||
with pytest.raises(
|
||||
HomeAssistantError,
|
||||
match="An error occurred while communicating with the device",
|
||||
):
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_light_when_control_missing(
|
||||
hass: HomeAssistant,
|
||||
mock_liebherr_client: MagicMock,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test light entity behavior when control is removed."""
|
||||
entity_id = "light.test_fridge_presentation_light"
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
assert state.state == STATE_ON
|
||||
|
||||
# Device stops reporting presentation light control
|
||||
mock_liebherr_client.get_device_state.side_effect = lambda *a, **kw: DeviceState(
|
||||
device=MOCK_DEVICE, controls=[]
|
||||
)
|
||||
|
||||
freezer.tick(timedelta(seconds=61))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("value", "max_value", "expected_state", "expected_brightness"),
|
||||
[
|
||||
(0, 5, STATE_OFF, None),
|
||||
(None, 5, STATE_UNKNOWN, None),
|
||||
(1, 0, STATE_ON, None),
|
||||
],
|
||||
ids=["off", "null_value", "zero_max"],
|
||||
)
|
||||
@pytest.mark.usefixtures("init_integration")
|
||||
async def test_light_state_updates(
|
||||
hass: HomeAssistant,
|
||||
mock_liebherr_client: MagicMock,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
value: int | None,
|
||||
max_value: int,
|
||||
expected_state: str,
|
||||
expected_brightness: int | None,
|
||||
) -> None:
|
||||
"""Test light entity state after coordinator update."""
|
||||
entity_id = "light.test_fridge_presentation_light"
|
||||
|
||||
mock_liebherr_client.get_device_state.side_effect = lambda *a, **kw: DeviceState(
|
||||
device=MOCK_DEVICE,
|
||||
controls=[
|
||||
PresentationLightControl(
|
||||
name="presentationlight",
|
||||
type="PresentationLightControl",
|
||||
value=value,
|
||||
max=max_value,
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
freezer.tick(timedelta(seconds=61))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
assert state.state == expected_state
|
||||
assert state.attributes.get(ATTR_BRIGHTNESS) == expected_brightness
|
||||
|
||||
|
||||
async def test_no_light_entity_without_control(
|
||||
hass: HomeAssistant,
|
||||
mock_liebherr_client: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
platforms: list[Platform],
|
||||
) -> None:
|
||||
"""Test no light entity created when device has no presentation light control."""
|
||||
mock_liebherr_client.get_device_state.side_effect = lambda *a, **kw: DeviceState(
|
||||
device=MOCK_DEVICE, controls=[]
|
||||
)
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
with patch("homeassistant.components.liebherr.PLATFORMS", platforms):
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get("light.test_fridge_presentation_light") is None
|
||||
|
||||
|
||||
async def test_dynamic_device_discovery_light(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_liebherr_client: MagicMock,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test new devices with presentation light are automatically discovered."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
with patch(f"homeassistant.components.{DOMAIN}.PLATFORMS", [Platform.LIGHT]):
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get("light.test_fridge_presentation_light") is not None
|
||||
assert hass.states.get("light.new_fridge_presentation_light") is None
|
||||
|
||||
new_device = Device(
|
||||
device_id="new_device_id",
|
||||
nickname="New Fridge",
|
||||
device_type=DeviceType.FRIDGE,
|
||||
device_name="K2601",
|
||||
)
|
||||
new_device_state = DeviceState(
|
||||
device=new_device,
|
||||
controls=[
|
||||
PresentationLightControl(
|
||||
name="presentationlight",
|
||||
type="PresentationLightControl",
|
||||
value=2,
|
||||
max=5,
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
mock_liebherr_client.get_devices.return_value = [MOCK_DEVICE, new_device]
|
||||
mock_liebherr_client.get_device_state.side_effect = lambda device_id, **kw: (
|
||||
copy.deepcopy(
|
||||
new_device_state if device_id == "new_device_id" else MOCK_DEVICE_STATE
|
||||
)
|
||||
)
|
||||
|
||||
freezer.tick(timedelta(minutes=5, seconds=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("light.new_fridge_presentation_light")
|
||||
assert state is not None
|
||||
assert state.state == STATE_ON
|
||||
@@ -41,6 +41,7 @@ def mock_linkplay_factory_bridge() -> Generator[AsyncMock]:
|
||||
bridge.device = AsyncMock(spec=LinkPlayDevice)
|
||||
bridge.device.uuid = UUID
|
||||
bridge.device.name = NAME
|
||||
bridge.device.manufacturer = "Linkplay"
|
||||
conf_factory.return_value = bridge
|
||||
yield conf_factory
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ from ipaddress import ip_address
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from linkplay.exceptions import LinkPlayRequestException
|
||||
from linkplay.manufacturers import MANUFACTURER_WIIM
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.linkplay.const import DOMAIN
|
||||
@@ -179,6 +180,24 @@ async def test_zeroconf_flow_errors(
|
||||
assert result["reason"] == "cannot_connect"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_setup_entry")
|
||||
async def test_zeroconf_flow_ignores_wiim_device(
|
||||
hass: HomeAssistant,
|
||||
mock_linkplay_factory_bridge: AsyncMock,
|
||||
) -> None:
|
||||
"""Test Zeroconf discovery is ignored for WiiM devices."""
|
||||
mock_linkplay_factory_bridge.return_value.device.manufacturer = MANUFACTURER_WIIM
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_ZEROCONF},
|
||||
data=ZEROCONF_DISCOVERY,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "not_linkplay_device"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_setup_entry")
|
||||
async def test_user_flow_errors(
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -502,6 +502,56 @@
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_button_entities[button.pve1_suspend_all-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'button',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'button.pve1_suspend_all',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Suspend all',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Suspend all',
|
||||
'platform': 'proxmoxve',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'suspend_all',
|
||||
'unique_id': '1234_node/pve1_suspend_all',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_button_entities[button.pve1_suspend_all-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'pve1 Suspend all',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'button.pve1_suspend_all',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_button_entities[button.vm_db_hibernate-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
|
||||
@@ -82,9 +82,10 @@ async def test_node_buttons(
|
||||
[
|
||||
("button.pve1_start_all", "startall"),
|
||||
("button.pve1_stop_all", "stopall"),
|
||||
("button.pve1_suspend_all", "suspendall"),
|
||||
],
|
||||
)
|
||||
async def test_node_startall_stopall_buttons(
|
||||
async def test_node_all_actions_buttons(
|
||||
hass: HomeAssistant,
|
||||
mock_proxmox_client: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
|
||||
@@ -6,6 +6,7 @@ from urllib.parse import parse_qs, urlparse
|
||||
import pytest
|
||||
from tesla_fleet_api.exceptions import (
|
||||
InvalidResponse,
|
||||
LoginRequired,
|
||||
PreconditionFailed,
|
||||
TeslaFleetError,
|
||||
)
|
||||
@@ -87,6 +88,121 @@ def mock_private_key():
|
||||
return private_key
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("current_request_with_host")
|
||||
async def test_partner_login_auth_error(
|
||||
hass: HomeAssistant,
|
||||
hass_client_no_auth: ClientSessionGenerator,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
access_token: str,
|
||||
mock_private_key,
|
||||
) -> None:
|
||||
"""Test partner login auth errors abort the flow cleanly."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
state = config_entry_oauth2_flow._encode_jwt(
|
||||
hass,
|
||||
{
|
||||
"flow_id": result["flow_id"],
|
||||
"redirect_uri": REDIRECT,
|
||||
},
|
||||
)
|
||||
|
||||
client = await hass_client_no_auth()
|
||||
await client.get(f"/auth/external/callback?code=abcd&state={state}")
|
||||
|
||||
aioclient_mock.post(
|
||||
TOKEN_URL,
|
||||
json={
|
||||
"refresh_token": "mock-refresh-token",
|
||||
"access_token": access_token,
|
||||
"type": "Bearer",
|
||||
"expires_in": 60,
|
||||
},
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.tesla_fleet.config_flow.TeslaFleetApi"
|
||||
) as mock_api_class:
|
||||
mock_api = AsyncMock()
|
||||
mock_api.private_key = mock_private_key
|
||||
mock_api.get_private_key = AsyncMock()
|
||||
mock_api.partner_login = AsyncMock(side_effect=LoginRequired)
|
||||
mock_api_class.return_value = mock_api
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"])
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "oauth_error"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("current_request_with_host")
|
||||
async def test_partner_login_partial_failure(
|
||||
hass: HomeAssistant,
|
||||
hass_client_no_auth: ClientSessionGenerator,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
access_token: str,
|
||||
mock_private_key,
|
||||
) -> None:
|
||||
"""Test partner login succeeds when one region fails."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
state = config_entry_oauth2_flow._encode_jwt(
|
||||
hass,
|
||||
{
|
||||
"flow_id": result["flow_id"],
|
||||
"redirect_uri": REDIRECT,
|
||||
},
|
||||
)
|
||||
|
||||
client = await hass_client_no_auth()
|
||||
await client.get(f"/auth/external/callback?code=abcd&state={state}")
|
||||
|
||||
aioclient_mock.post(
|
||||
TOKEN_URL,
|
||||
json={
|
||||
"refresh_token": "mock-refresh-token",
|
||||
"access_token": access_token,
|
||||
"type": "Bearer",
|
||||
"expires_in": 60,
|
||||
},
|
||||
)
|
||||
|
||||
public_key = "0404112233445566778899aabbccddeeff112233445566778899aabbccddeeff112233445566778899aabbccddeeff112233445566778899aabbccddeeff1122"
|
||||
|
||||
mock_api_na = AsyncMock()
|
||||
mock_api_na.private_key = mock_private_key
|
||||
mock_api_na.get_private_key = AsyncMock()
|
||||
mock_api_na.partner_login = AsyncMock()
|
||||
mock_api_na.public_uncompressed_point = public_key
|
||||
mock_api_na.partner.register.return_value = {"response": {"public_key": public_key}}
|
||||
|
||||
mock_api_eu = AsyncMock()
|
||||
mock_api_eu.private_key = mock_private_key
|
||||
mock_api_eu.get_private_key = AsyncMock()
|
||||
mock_api_eu.partner_login = AsyncMock(
|
||||
side_effect=TeslaFleetError("EU partner login failed")
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.tesla_fleet.config_flow.TeslaFleetApi",
|
||||
side_effect=[mock_api_na, mock_api_eu],
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"])
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "domain_input"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {CONF_DOMAIN: "example.com"}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "registration_complete"
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("current_request_with_host")
|
||||
async def test_full_flow_with_domain_registration(
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -24384,6 +24384,173 @@
|
||||
'state': '100.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[sensor.wifi_smart_online_8_in_1_tester_conductivity-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.wifi_smart_online_8_in_1_tester_conductivity',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Conductivity',
|
||||
'options': dict({
|
||||
'sensor': dict({
|
||||
'suggested_display_precision': 1,
|
||||
}),
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.CONDUCTIVITY: 'conductivity'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Conductivity',
|
||||
'platform': 'tuya',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'tuya.frmfrbds0jixxyaljbngdec_current',
|
||||
'unit_of_measurement': <UnitOfConductivity.MICROSIEMENS_PER_CM: 'μS/cm'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[sensor.wifi_smart_online_8_in_1_tester_conductivity-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'conductivity',
|
||||
'friendly_name': 'WiFi smart online 8 in 1 tester Conductivity',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': <UnitOfConductivity.MICROSIEMENS_PER_CM: 'μS/cm'>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.wifi_smart_online_8_in_1_tester_conductivity',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unavailable',
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[sensor.wifi_smart_online_8_in_1_tester_oxydo_reduction_potential-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.wifi_smart_online_8_in_1_tester_oxydo_reduction_potential',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Oxydo reduction potential',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Oxydo reduction potential',
|
||||
'platform': 'tuya',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'oxydo_reduction_potential',
|
||||
'unique_id': 'tuya.frmfrbds0jixxyaljbngdorp_current',
|
||||
'unit_of_measurement': 'mV',
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[sensor.wifi_smart_online_8_in_1_tester_oxydo_reduction_potential-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'WiFi smart online 8 in 1 tester Oxydo reduction potential',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': 'mV',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.wifi_smart_online_8_in_1_tester_oxydo_reduction_potential',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unavailable',
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[sensor.wifi_smart_online_8_in_1_tester_ph-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.wifi_smart_online_8_in_1_tester_ph',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'pH',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <SensorDeviceClass.PH: 'ph'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'pH',
|
||||
'platform': 'tuya',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'tuya.frmfrbds0jixxyaljbngdph_current',
|
||||
'unit_of_measurement': '',
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[sensor.wifi_smart_online_8_in_1_tester_ph-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'ph',
|
||||
'friendly_name': 'WiFi smart online 8 in 1 tester pH',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': '',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.wifi_smart_online_8_in_1_tester_ph',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unavailable',
|
||||
})
|
||||
# ---
|
||||
# name: test_platform_setup_and_discovery[sensor.wifi_smart_online_8_in_1_tester_temperature-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
|
||||
@@ -1774,6 +1774,75 @@ async def test_device_temperatures(
|
||||
assert hass.states.get(entity_id).state == updated_state
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"device_payload",
|
||||
[
|
||||
[
|
||||
{
|
||||
"board_rev": 3,
|
||||
"device_id": "mock-id",
|
||||
"has_fan": True,
|
||||
"fan_level": 0,
|
||||
"ip": "10.0.1.1",
|
||||
"last_seen": 1562600145,
|
||||
"mac": "00:00:00:00:01:01",
|
||||
"model": "US16P150",
|
||||
"name": "Device",
|
||||
"next_interval": 20,
|
||||
"overheating": True,
|
||||
"state": 1,
|
||||
"type": "usw",
|
||||
"upgradable": True,
|
||||
"uptime": 60,
|
||||
"version": "4.0.42.10433",
|
||||
"temperatures": [
|
||||
{"name": "CPU", "type": "cpu", "value": 66.0},
|
||||
],
|
||||
}
|
||||
]
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("config_entry_setup")
|
||||
async def test_device_temperature_with_missing_value(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_websocket_message,
|
||||
device_payload: list[dict[str, Any]],
|
||||
) -> None:
|
||||
"""Verify that device temperatures sensors are working as expected."""
|
||||
entity_id = "sensor.device_device_cpu_temperature"
|
||||
|
||||
temperature_entity = entity_registry.async_get(entity_id)
|
||||
assert temperature_entity.disabled_by == RegistryEntryDisabler.INTEGRATION
|
||||
|
||||
# Enable entity
|
||||
entity_registry.async_update_entity(entity_id=entity_id, disabled_by=None)
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
async_fire_time_changed(
|
||||
hass,
|
||||
dt_util.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1),
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Verify sensor state
|
||||
assert hass.states.get(entity_id).state == "66.0"
|
||||
|
||||
# Remove temperature value from payload
|
||||
device = deepcopy(device_payload[0])
|
||||
device["temperatures"][0].pop("value")
|
||||
|
||||
mock_websocket_message(message=MessageKey.DEVICE, data=device)
|
||||
|
||||
assert hass.states.get(entity_id).state == STATE_UNAVAILABLE
|
||||
|
||||
# Send original payload again to verify sensor recovers
|
||||
mock_websocket_message(message=MessageKey.DEVICE, data=device_payload[0])
|
||||
|
||||
assert hass.states.get(entity_id).state == "66.0"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"device_payload",
|
||||
[
|
||||
|
||||
@@ -375,10 +375,17 @@ async def test_firmware_update_success(
|
||||
attrs[ATTR_LATEST_VERSION] == f"0x{fw_image.firmware.header.file_version:08x}"
|
||||
)
|
||||
|
||||
ota_completed = False
|
||||
|
||||
async def endpoint_reply(cluster, sequence, data, **kwargs):
|
||||
nonlocal ota_completed
|
||||
if cluster == general.Ota.cluster_id:
|
||||
_hdr, cmd = ota_cluster.deserialize(data)
|
||||
if isinstance(cmd, general.Ota.ImageNotifyCommand):
|
||||
if ota_completed:
|
||||
# Post-OTA image_notify: ignore or don't respond
|
||||
return
|
||||
|
||||
zha_device.device.device.packet_received(
|
||||
make_packet(
|
||||
zha_device.device.device,
|
||||
@@ -394,6 +401,12 @@ async def test_firmware_update_success(
|
||||
elif isinstance(
|
||||
cmd, general.Ota.ClientCommandDefs.query_next_image_response.schema
|
||||
):
|
||||
# After a successful OTA, zigpy sends a post-OTA image_notify
|
||||
# which triggers a query_next_image -> NO_IMAGE_AVAILABLE exchange
|
||||
if cmd.status == foundation.Status.NO_IMAGE_AVAILABLE:
|
||||
assert ota_completed
|
||||
return
|
||||
|
||||
assert cmd.status == foundation.Status.SUCCESS
|
||||
assert cmd.manufacturer_code == fw_image.firmware.header.manufacturer_id
|
||||
assert cmd.image_type == fw_image.firmware.header.image_type
|
||||
@@ -486,6 +499,8 @@ async def test_firmware_update_success(
|
||||
assert cmd.current_time == 0
|
||||
assert cmd.upgrade_time == 0
|
||||
|
||||
ota_completed = True
|
||||
|
||||
def read_new_fw_version(*args, **kwargs):
|
||||
ota_cluster.update_attribute(
|
||||
attrid=general.Ota.AttributeDefs.current_file_version.id,
|
||||
@@ -590,6 +605,9 @@ async def test_firmware_update_raises(
|
||||
elif isinstance(
|
||||
cmd, general.Ota.ClientCommandDefs.query_next_image_response.schema
|
||||
):
|
||||
if cmd.status == foundation.Status.NO_IMAGE_AVAILABLE:
|
||||
return
|
||||
|
||||
assert cmd.status == foundation.Status.SUCCESS
|
||||
assert cmd.manufacturer_code == fw_image.firmware.header.manufacturer_id
|
||||
assert cmd.image_type == fw_image.firmware.header.image_type
|
||||
|
||||
@@ -275,6 +275,7 @@ def test_check_dependency_file_names(integration: Integration) -> None:
|
||||
pkg_files = [
|
||||
PackagePath("py.typed"),
|
||||
PackagePath("my_package.py"),
|
||||
PackagePath("some_script.Pth"),
|
||||
PackagePath("my_package-1.0.0.dist-info/METADATA"),
|
||||
]
|
||||
with (
|
||||
@@ -285,17 +286,23 @@ def test_check_dependency_file_names(integration: Integration) -> None:
|
||||
):
|
||||
assert not _packages_checked_files_cache
|
||||
assert check_dependency_files(integration, package, pkg, ()) is False
|
||||
assert _packages_checked_files_cache[pkg]["file_names"] == {"py.typed"}
|
||||
assert len(integration.errors) == 1
|
||||
assert _packages_checked_files_cache[pkg]["file_names"] == {
|
||||
"py.typed",
|
||||
"some_script.Pth",
|
||||
}
|
||||
assert len(integration.errors) == 2
|
||||
assert f"Package {pkg} has a forbidden file 'py.typed' in {package}" in [
|
||||
x.error for x in integration.errors
|
||||
]
|
||||
assert f"Package {pkg} has a forbidden file 'some_script.Pth' in {package}" in [
|
||||
x.error for x in integration.errors
|
||||
]
|
||||
integration.errors.clear()
|
||||
|
||||
# Repeated call should use cache
|
||||
assert check_dependency_files(integration, package, pkg, ()) is False
|
||||
assert mock_files.call_count == 1
|
||||
assert len(integration.errors) == 1
|
||||
assert len(integration.errors) == 2
|
||||
integration.errors.clear()
|
||||
|
||||
# All good
|
||||
|
||||
Reference in New Issue
Block a user