Compare commits

..

2 Commits

Author SHA1 Message Date
farmio
a166da552b fix type 2026-01-13 22:31:56 +01:00
farmio
31ae6951db KNX Expose: Add support for sending value periodically 2026-01-13 22:16:57 +01:00
21 changed files with 209 additions and 300 deletions

View File

@@ -407,7 +407,6 @@ homeassistant.components.person.*
homeassistant.components.pi_hole.*
homeassistant.components.ping.*
homeassistant.components.plugwise.*
homeassistant.components.pooldose.*
homeassistant.components.portainer.*
homeassistant.components.powerfox.*
homeassistant.components.powerwall.*

View File

@@ -7,5 +7,6 @@
"integration_type": "service",
"iot_class": "local_push",
"loggers": ["datadog"],
"quality_scale": "legacy",
"requirements": ["datadog==0.52.0"]
}

View File

@@ -112,7 +112,6 @@ AIR_QUALITY_SENSOR_TYPES: tuple[AirQualitySensorEntityDescription, ...] = (
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.CO,
native_unit_of_measurement_fn=lambda x: x.pollutants.co.concentration.units,
exists_fn=lambda x: "co" in {p.code for p in x.pollutants},
value_fn=lambda x: x.pollutants.co.concentration.value,
),
AirQualitySensorEntityDescription(
@@ -144,7 +143,6 @@ AIR_QUALITY_SENSOR_TYPES: tuple[AirQualitySensorEntityDescription, ...] = (
translation_key="nitrogen_dioxide",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement_fn=lambda x: x.pollutants.no2.concentration.units,
exists_fn=lambda x: "no2" in {p.code for p in x.pollutants},
value_fn=lambda x: x.pollutants.no2.concentration.value,
),
AirQualitySensorEntityDescription(
@@ -152,7 +150,6 @@ AIR_QUALITY_SENSOR_TYPES: tuple[AirQualitySensorEntityDescription, ...] = (
translation_key="ozone",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement_fn=lambda x: x.pollutants.o3.concentration.units,
exists_fn=lambda x: "o3" in {p.code for p in x.pollutants},
value_fn=lambda x: x.pollutants.o3.concentration.value,
),
AirQualitySensorEntityDescription(
@@ -160,7 +157,6 @@ AIR_QUALITY_SENSOR_TYPES: tuple[AirQualitySensorEntityDescription, ...] = (
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.PM10,
native_unit_of_measurement_fn=lambda x: x.pollutants.pm10.concentration.units,
exists_fn=lambda x: "pm10" in {p.code for p in x.pollutants},
value_fn=lambda x: x.pollutants.pm10.concentration.value,
),
AirQualitySensorEntityDescription(
@@ -168,7 +164,6 @@ AIR_QUALITY_SENSOR_TYPES: tuple[AirQualitySensorEntityDescription, ...] = (
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.PM25,
native_unit_of_measurement_fn=lambda x: x.pollutants.pm25.concentration.units,
exists_fn=lambda x: "pm25" in {p.code for p in x.pollutants},
value_fn=lambda x: x.pollutants.pm25.concentration.value,
),
AirQualitySensorEntityDescription(
@@ -176,7 +171,6 @@ AIR_QUALITY_SENSOR_TYPES: tuple[AirQualitySensorEntityDescription, ...] = (
translation_key="sulphur_dioxide",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement_fn=lambda x: x.pollutants.so2.concentration.units,
exists_fn=lambda x: "so2" in {p.code for p in x.pollutants},
value_fn=lambda x: x.pollutants.so2.concentration.value,
),
)

View File

@@ -116,6 +116,7 @@ class KnxExposeOptions:
dpt: type[DPTBase]
respond_to_read: bool
cooldown: float
periodic_send: float
default: Any | None
value_template: Template | None
@@ -130,12 +131,17 @@ def _yaml_config_to_expose_options(config: ConfigType) -> KnxExposeOptions:
else:
dpt = DPTBase.parse_transcoder(config[ExposeSchema.CONF_KNX_EXPOSE_TYPE]) # type: ignore[assignment] # checked by schema validation
ga = parse_device_group_address(config[KNX_ADDRESS])
cooldown_seconds = config[ExposeSchema.CONF_KNX_EXPOSE_COOLDOWN].total_seconds()
periodic_send_seconds = config[
ExposeSchema.CONF_KNX_EXPOSE_PERIODIC_SEND
].total_seconds()
return KnxExposeOptions(
attribute=config.get(ExposeSchema.CONF_KNX_EXPOSE_ATTRIBUTE),
group_address=ga,
dpt=dpt,
respond_to_read=config[CONF_RESPOND_TO_READ],
cooldown=config[ExposeSchema.CONF_KNX_EXPOSE_COOLDOWN],
cooldown=cooldown_seconds,
periodic_send=periodic_send_seconds,
default=config.get(ExposeSchema.CONF_KNX_EXPOSE_DEFAULT),
value_template=config.get(CONF_VALUE_TEMPLATE),
)
@@ -167,6 +173,7 @@ class KnxExposeEntity:
respond_to_read=option.respond_to_read,
value_type=option.dpt,
cooldown=option.cooldown,
periodic_send=option.periodic_send,
),
)
for option in options

View File

@@ -4,6 +4,7 @@ from __future__ import annotations
from abc import ABC
from collections import OrderedDict
from datetime import timedelta
import math
from typing import ClassVar, Final
@@ -538,6 +539,7 @@ class ExposeSchema(KNXPlatformSchema):
CONF_KNX_EXPOSE_ATTRIBUTE = "attribute"
CONF_KNX_EXPOSE_BINARY = "binary"
CONF_KNX_EXPOSE_COOLDOWN = "cooldown"
CONF_KNX_EXPOSE_PERIODIC_SEND = "periodic_send"
CONF_KNX_EXPOSE_DEFAULT = "default"
CONF_TIME = "time"
CONF_DATE = "date"
@@ -554,7 +556,12 @@ class ExposeSchema(KNXPlatformSchema):
)
EXPOSE_SENSOR_SCHEMA = vol.Schema(
{
vol.Optional(CONF_KNX_EXPOSE_COOLDOWN, default=0): cv.positive_float,
vol.Optional(
CONF_KNX_EXPOSE_COOLDOWN, default=timedelta(0)
): cv.positive_time_period,
vol.Optional(
CONF_KNX_EXPOSE_PERIODIC_SEND, default=timedelta(0)
): cv.positive_time_period,
vol.Optional(CONF_RESPOND_TO_READ, default=True): cv.boolean,
vol.Required(CONF_KNX_EXPOSE_TYPE): vol.Any(
CONF_KNX_EXPOSE_BINARY, sensor_type_validator

View File

@@ -7,6 +7,7 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["london_tube_status"],
"quality_scale": "legacy",
"requirements": ["london-tube-status==0.5"],
"single_config_entry": true
}

View File

@@ -528,10 +528,7 @@ DISCOVERY_SCHEMAS = [
),
),
entity_class=MatterBinarySensor,
required_attributes=(
clusters.Thermostat.Attributes.RemoteSensing,
clusters.Thermostat.Attributes.OutdoorTemperature,
),
required_attributes=(clusters.Thermostat.Attributes.RemoteSensing,),
allow_multi=True,
),
MatterDiscoverySchema(

View File

@@ -9,7 +9,7 @@ from aiohttp import ClientError
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_DOMAIN, CONF_HOST, CONF_NAME, CONF_PASSWORD
from homeassistant.const import CONF_DOMAIN, CONF_HOST, CONF_PASSWORD
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.selector import (
@@ -37,16 +37,6 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
}
)
STEP_RECONFIGURE_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_PASSWORD): TextSelector(
TextSelectorConfig(
type=TextSelectorType.PASSWORD, autocomplete="current-password"
)
),
}
)
class NamecheapDnsConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Namecheap DynamicDNS."""
@@ -99,41 +89,3 @@ class NamecheapDnsConfigFlow(ConfigFlow, domain=DOMAIN):
deprecate_yaml_issue(self.hass, import_success=True)
return result
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle reconfigure flow."""
errors: dict[str, str] = {}
entry = self._get_reconfigure_entry()
if user_input is not None:
session = async_get_clientsession(self.hass)
try:
if not await update_namecheapdns(
session,
entry.data[CONF_HOST],
entry.data[CONF_DOMAIN],
user_input[CONF_PASSWORD],
):
errors["base"] = "update_failed"
except ClientError:
_LOGGER.debug("Cannot connect", exc_info=True)
errors["base"] = "cannot_connect"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
if not errors:
return self.async_update_reload_and_abort(
entry,
data_updates=user_input,
)
return self.async_show_form(
step_id="reconfigure",
data_schema=STEP_RECONFIGURE_DATA_SCHEMA,
errors=errors,
description_placeholders={CONF_NAME: entry.title},
)

View File

@@ -1,8 +1,7 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
@@ -10,15 +9,6 @@
"update_failed": "Updating DNS failed"
},
"step": {
"reconfigure": {
"data": {
"password": "[%key:component::namecheapdns::config::step::user::data::password%]"
},
"data_description": {
"password": "[%key:component::namecheapdns::config::step::user::data_description::password%]"
},
"title": "Re-configure {name}"
},
"user": {
"data": {
"domain": "[%key:common::config_flow::data::username%]",

View File

@@ -6,5 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/nederlandse_spoorwegen",
"integration_type": "service",
"iot_class": "cloud_polling",
"quality_scale": "legacy",
"requirements": ["nsapi==3.1.3"]
}

View File

@@ -7,5 +7,6 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["pyrail"],
"quality_scale": "legacy",
"requirements": ["pyrail==0.4.1"]
}

View File

@@ -2,8 +2,7 @@
from __future__ import annotations
from collections.abc import Callable, Coroutine
from typing import Any, Literal
from typing import Literal
from pooldose.type_definitions import DeviceInfoDict, ValueDict
@@ -81,10 +80,7 @@ class PooldoseEntity(CoordinatorEntity[PooldoseCoordinator]):
return platform_data.get(self.entity_description.key)
async def _async_perform_write(
self,
api_call: Callable[[str, Any], Coroutine[Any, Any, bool]],
key: str,
value: bool | str | float,
self, api_call, key: str, value: bool | str | float
) -> None:
"""Perform a write call to the API with unified error handling.

View File

@@ -11,6 +11,6 @@
"documentation": "https://www.home-assistant.io/integrations/pooldose",
"integration_type": "device",
"iot_class": "local_polling",
"quality_scale": "platinum",
"quality_scale": "gold",
"requirements": ["python-pooldose==0.8.2"]
}

View File

@@ -71,4 +71,4 @@ rules:
# Platinum
async-dependency: done
inject-websession: done
strict-typing: done
strict-typing: todo

View File

@@ -7,5 +7,6 @@
"integration_type": "service",
"iot_class": "cloud_push",
"loggers": ["prowl"],
"quality_scale": "legacy",
"requirements": ["prowlpy==1.1.1"]
}

View File

@@ -7,5 +7,6 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["wsdot"],
"quality_scale": "legacy",
"requirements": ["wsdot==0.0.1"]
}

10
mypy.ini generated
View File

@@ -3826,16 +3826,6 @@ disallow_untyped_defs = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.pooldose.*]
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.portainer.*]
check_untyped_defs = true
disallow_incomplete_defs = true

View File

@@ -41,20 +41,7 @@ from homeassistant.setup import async_setup_component
from tests.common import get_fixture_path
VALUES = [17, 20, 15.3]
STATES_ONE_ERROR = ["17", "string", "15.3"]
STATES_ONE_MISSING = ["17", None, "15.3"]
STATES_ONE_UNKNOWN = ["17", STATE_UNKNOWN, "15.3"]
STATES_ONE_UNAVAILABLE = ["17", STATE_UNAVAILABLE, "15.3"]
STATES_ALL_ERROR = ["string", "string", "string"]
STATES_ALL_MISSING = [None, None, None]
STATES_ALL_UNKNOWN = [STATE_UNKNOWN, STATE_UNKNOWN, STATE_UNKNOWN]
STATES_ALL_UNAVAILABLE = [STATE_UNAVAILABLE, STATE_UNAVAILABLE, STATE_UNAVAILABLE]
STATES_MIX_MISSING_UNAVAILABLE_UNKNOWN = [None, STATE_UNAVAILABLE, STATE_UNKNOWN]
STATES_MIX_MISSING_UNAVAILABLE = [None, STATE_UNAVAILABLE, STATE_UNAVAILABLE]
STATES_MIX_MISSING_UNKNOWN = [None, STATE_UNKNOWN, STATE_UNKNOWN]
STATES_MIX_UNAVAILABLE_UNKNOWN = [STATE_UNAVAILABLE, STATE_UNKNOWN, STATE_UNKNOWN]
VALUES_ERROR = [17, "string", 15.3]
COUNT = len(VALUES)
MIN_VALUE = min(VALUES)
MAX_VALUE = max(VALUES)
@@ -66,18 +53,6 @@ SUM_VALUE = sum(VALUES)
PRODUCT_VALUE = prod(VALUES)
def set_or_remove_state(
hass: HomeAssistant,
entity_id: str,
state: str | None,
) -> None:
"""Set or remove the state of an entity."""
if state is None:
hass.states.async_remove(entity_id)
else:
hass.states.async_set(entity_id, state)
@pytest.mark.parametrize(
("sensor_type", "result", "attributes"),
[
@@ -115,7 +90,7 @@ async def test_sensors2(
for entity_id, value in dict(zip(entity_ids, VALUES, strict=False)).items():
hass.states.async_set(
entity_id,
str(value),
value,
{
ATTR_DEVICE_CLASS: SensorDeviceClass.VOLUME,
ATTR_STATE_CLASS: SensorStateClass.TOTAL,
@@ -165,7 +140,7 @@ async def test_sensors_attributes_defined(hass: HomeAssistant) -> None:
for entity_id, value in dict(zip(entity_ids, VALUES, strict=False)).items():
hass.states.async_set(
entity_id,
str(value),
value,
{
ATTR_DEVICE_CLASS: SensorDeviceClass.VOLUME,
ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT,
@@ -210,7 +185,7 @@ async def test_not_enough_sensor_value(hass: HomeAssistant) -> None:
assert state.attributes.get("min_entity_id") is None
assert state.attributes.get("max_entity_id") is None
hass.states.async_set(entity_ids[1], str(VALUES[1]))
hass.states.async_set(entity_ids[1], VALUES[1])
await hass.async_block_till_done()
state = hass.states.get("sensor.test_max")
@@ -235,8 +210,8 @@ async def test_not_enough_sensor_value(hass: HomeAssistant) -> None:
async def test_reload(hass: HomeAssistant) -> None:
"""Verify we can reload sensors."""
hass.states.async_set("sensor.test_1", "12345")
hass.states.async_set("sensor.test_2", "45678")
hass.states.async_set("sensor.test_1", 12345)
hass.states.async_set("sensor.test_2", 45678)
await async_setup_component(
hass,
@@ -274,28 +249,8 @@ async def test_reload(hass: HomeAssistant) -> None:
assert hass.states.get("sensor.second_test")
@pytest.mark.parametrize(
("states_list", "expected_group_state"),
[
(STATES_ONE_ERROR, "17.0"),
(STATES_ONE_MISSING, "17.0"),
(STATES_ONE_UNKNOWN, "17.0"),
(STATES_ONE_UNAVAILABLE, "17.0"),
(STATES_ALL_ERROR, STATE_UNAVAILABLE),
(STATES_ALL_MISSING, STATE_UNAVAILABLE),
(STATES_ALL_UNKNOWN, STATE_UNAVAILABLE),
(STATES_ALL_UNAVAILABLE, STATE_UNAVAILABLE),
(STATES_MIX_MISSING_UNAVAILABLE, STATE_UNAVAILABLE),
(STATES_MIX_MISSING_UNKNOWN, STATE_UNAVAILABLE),
(STATES_MIX_UNAVAILABLE_UNKNOWN, STATE_UNAVAILABLE),
(STATES_MIX_MISSING_UNAVAILABLE_UNKNOWN, STATE_UNAVAILABLE),
],
)
async def test_sensor_incorrect_state_with_ignore_non_numeric(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
states_list: list[str | None],
expected_group_state: str,
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test that non numeric values are ignored in a group."""
config = {
@@ -316,48 +271,27 @@ async def test_sensor_incorrect_state_with_ignore_non_numeric(
entity_ids = config["sensor"]["entities"]
# Check that the final sensor value ignores the non numeric input
for entity_id, value in dict(zip(entity_ids, states_list, strict=False)).items():
set_or_remove_state(hass, entity_id, value)
for entity_id, value in dict(zip(entity_ids, VALUES_ERROR, strict=False)).items():
hass.states.async_set(entity_id, value)
await hass.async_block_till_done()
state = hass.states.get("sensor.test_ignore_non_numeric")
assert state.state == expected_group_state
assert state.state == "17.0"
assert (
"Unable to use state. Only numerical states are supported," not in caplog.text
)
# Check that the final sensor value with all numeric inputs
for entity_id, value in dict(zip(entity_ids, VALUES, strict=False)).items():
hass.states.async_set(entity_id, str(value))
hass.states.async_set(entity_id, value)
await hass.async_block_till_done()
state = hass.states.get("sensor.test_ignore_non_numeric")
assert state.state == "20.0"
@pytest.mark.parametrize(
("states_list", "expected_group_state", "error_count"),
[
(STATES_ONE_ERROR, STATE_UNKNOWN, 1),
(STATES_ONE_MISSING, "17.0", 0),
(STATES_ONE_UNKNOWN, STATE_UNKNOWN, 1),
(STATES_ONE_UNAVAILABLE, STATE_UNKNOWN, 1),
(STATES_ALL_ERROR, STATE_UNAVAILABLE, 3),
(STATES_ALL_MISSING, STATE_UNAVAILABLE, 0),
(STATES_ALL_UNKNOWN, STATE_UNAVAILABLE, 3),
(STATES_ALL_UNAVAILABLE, STATE_UNAVAILABLE, 3),
(STATES_MIX_MISSING_UNKNOWN, STATE_UNAVAILABLE, 2),
(STATES_MIX_UNAVAILABLE_UNKNOWN, STATE_UNAVAILABLE, 3),
(STATES_MIX_MISSING_UNAVAILABLE, STATE_UNAVAILABLE, 2),
(STATES_MIX_MISSING_UNAVAILABLE_UNKNOWN, STATE_UNAVAILABLE, 2),
],
)
async def test_sensor_incorrect_state_with_not_ignore_non_numeric(
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
states_list: list[str | None],
expected_group_state: str,
error_count: int,
hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
"""Test that non numeric values cause a group to be unknown."""
config = {
@@ -378,46 +312,24 @@ async def test_sensor_incorrect_state_with_not_ignore_non_numeric(
entity_ids = config["sensor"]["entities"]
# Check that the final sensor value is unavailable if a non numeric input exists
for entity_id, value in dict(zip(entity_ids, states_list, strict=False)).items():
set_or_remove_state(hass, entity_id, value)
for entity_id, value in dict(zip(entity_ids, VALUES_ERROR, strict=False)).items():
hass.states.async_set(entity_id, value)
await hass.async_block_till_done()
state = hass.states.get("sensor.test_failure")
assert state.state == expected_group_state
assert (
caplog.text.count("Unable to use state. Only numerical states are supported")
== error_count
)
assert state.state == "unknown"
assert "Unable to use state. Only numerical states are supported" in caplog.text
# Check that the final sensor value is correct with all numeric inputs
for entity_id, value in dict(zip(entity_ids, VALUES, strict=False)).items():
hass.states.async_set(entity_id, str(value))
hass.states.async_set(entity_id, value)
await hass.async_block_till_done()
state = hass.states.get("sensor.test_failure")
assert state.state == "20.0"
@pytest.mark.parametrize(
("states_list", "expected_group_state"),
[
(STATES_ONE_ERROR, STATE_UNKNOWN),
(STATES_ONE_MISSING, "32.3"),
(STATES_ONE_UNKNOWN, STATE_UNKNOWN),
(STATES_ONE_UNAVAILABLE, STATE_UNKNOWN),
(STATES_ALL_ERROR, STATE_UNAVAILABLE),
(STATES_ALL_MISSING, STATE_UNAVAILABLE),
(STATES_ALL_UNKNOWN, STATE_UNAVAILABLE),
(STATES_ALL_UNAVAILABLE, STATE_UNAVAILABLE),
(STATES_MIX_MISSING_UNAVAILABLE, STATE_UNAVAILABLE),
(STATES_MIX_MISSING_UNKNOWN, STATE_UNAVAILABLE),
(STATES_MIX_UNAVAILABLE_UNKNOWN, STATE_UNAVAILABLE),
(STATES_MIX_MISSING_UNAVAILABLE_UNKNOWN, STATE_UNAVAILABLE),
],
)
async def test_sensor_require_all_states(
hass: HomeAssistant, states_list: list[str | None], expected_group_state: str
) -> None:
async def test_sensor_require_all_states(hass: HomeAssistant) -> None:
"""Test the sum sensor with missing state require all."""
config = {
SENSOR_DOMAIN: {
@@ -436,13 +348,13 @@ async def test_sensor_require_all_states(
entity_ids = config["sensor"]["entities"]
for entity_id, value in dict(zip(entity_ids, states_list, strict=False)).items():
set_or_remove_state(hass, entity_id, value)
for entity_id, value in dict(zip(entity_ids, VALUES_ERROR, strict=False)).items():
hass.states.async_set(entity_id, value)
await hass.async_block_till_done()
state = hass.states.get("sensor.test_sum")
assert state.state == expected_group_state
assert state.state == STATE_UNKNOWN
async def test_sensor_calculated_properties(hass: HomeAssistant) -> None:
@@ -461,7 +373,7 @@ async def test_sensor_calculated_properties(hass: HomeAssistant) -> None:
hass.states.async_set(
entity_ids[0],
str(VALUES[0]),
VALUES[0],
{
"device_class": SensorDeviceClass.ENERGY,
"state_class": SensorStateClass.TOTAL,
@@ -470,7 +382,7 @@ async def test_sensor_calculated_properties(hass: HomeAssistant) -> None:
)
hass.states.async_set(
entity_ids[1],
str(VALUES[1]),
VALUES[1],
{
"device_class": SensorDeviceClass.ENERGY,
"state_class": SensorStateClass.TOTAL,
@@ -479,7 +391,7 @@ async def test_sensor_calculated_properties(hass: HomeAssistant) -> None:
)
hass.states.async_set(
entity_ids[2],
str(VALUES[2]),
VALUES[2],
{
"device_class": SensorDeviceClass.ENERGY,
"state_class": SensorStateClass.TOTAL,
@@ -501,7 +413,7 @@ async def test_sensor_calculated_properties(hass: HomeAssistant) -> None:
# is converted correctly by the group sensor
hass.states.async_set(
entity_ids[2],
str(VALUES[2]),
VALUES[2],
{
"device_class": SensorDeviceClass.ENERGY,
"state_class": SensorStateClass.TOTAL,
@@ -534,7 +446,7 @@ async def test_sensor_with_uoms_but_no_device_class(
hass.states.async_set(
entity_ids[0],
str(VALUES[0]),
VALUES[0],
{
"device_class": SensorDeviceClass.POWER,
"state_class": SensorStateClass.MEASUREMENT,
@@ -543,7 +455,7 @@ async def test_sensor_with_uoms_but_no_device_class(
)
hass.states.async_set(
entity_ids[1],
str(VALUES[1]),
VALUES[1],
{
"device_class": SensorDeviceClass.POWER,
"state_class": SensorStateClass.MEASUREMENT,
@@ -552,7 +464,7 @@ async def test_sensor_with_uoms_but_no_device_class(
)
hass.states.async_set(
entity_ids[2],
str(VALUES[2]),
VALUES[2],
{
"unit_of_measurement": "W",
},
@@ -575,7 +487,7 @@ async def test_sensor_with_uoms_but_no_device_class(
hass.states.async_set(
entity_ids[0],
str(VALUES[0]),
VALUES[0],
{
"device_class": SensorDeviceClass.POWER,
"state_class": SensorStateClass.MEASUREMENT,
@@ -596,7 +508,7 @@ async def test_sensor_with_uoms_but_no_device_class(
hass.states.async_set(
entity_ids[0],
str(VALUES[0]),
VALUES[0],
{
"device_class": SensorDeviceClass.POWER,
"state_class": SensorStateClass.MEASUREMENT,
@@ -629,7 +541,7 @@ async def test_sensor_calculated_properties_not_same(
hass.states.async_set(
entity_ids[0],
str(VALUES[0]),
VALUES[0],
{
"device_class": SensorDeviceClass.ENERGY,
"state_class": SensorStateClass.TOTAL,
@@ -638,7 +550,7 @@ async def test_sensor_calculated_properties_not_same(
)
hass.states.async_set(
entity_ids[1],
str(VALUES[1]),
VALUES[1],
{
"device_class": SensorDeviceClass.ENERGY,
"state_class": SensorStateClass.TOTAL,
@@ -647,7 +559,7 @@ async def test_sensor_calculated_properties_not_same(
)
hass.states.async_set(
entity_ids[2],
str(VALUES[2]),
VALUES[2],
{
"device_class": SensorDeviceClass.CURRENT,
"state_class": SensorStateClass.MEASUREMENT,
@@ -692,7 +604,7 @@ async def test_sensor_calculated_result_fails_on_uom(hass: HomeAssistant) -> Non
hass.states.async_set(
entity_ids[0],
str(VALUES[0]),
VALUES[0],
{
"device_class": SensorDeviceClass.ENERGY,
"state_class": SensorStateClass.TOTAL,
@@ -701,7 +613,7 @@ async def test_sensor_calculated_result_fails_on_uom(hass: HomeAssistant) -> Non
)
hass.states.async_set(
entity_ids[1],
str(VALUES[1]),
VALUES[1],
{
"device_class": SensorDeviceClass.ENERGY,
"state_class": SensorStateClass.TOTAL,
@@ -710,7 +622,7 @@ async def test_sensor_calculated_result_fails_on_uom(hass: HomeAssistant) -> Non
)
hass.states.async_set(
entity_ids[2],
str(VALUES[2]),
VALUES[2],
{
"device_class": SensorDeviceClass.ENERGY,
"state_class": SensorStateClass.TOTAL,
@@ -730,7 +642,7 @@ async def test_sensor_calculated_result_fails_on_uom(hass: HomeAssistant) -> Non
hass.states.async_set(
entity_ids[2],
"12",
12,
{
"device_class": SensorDeviceClass.ENERGY,
"state_class": SensorStateClass.TOTAL,
@@ -765,7 +677,7 @@ async def test_sensor_calculated_properties_not_convertible_device_class(
hass.states.async_set(
entity_ids[0],
str(VALUES[0]),
VALUES[0],
{
"device_class": SensorDeviceClass.HUMIDITY,
"state_class": SensorStateClass.MEASUREMENT,
@@ -774,7 +686,7 @@ async def test_sensor_calculated_properties_not_convertible_device_class(
)
hass.states.async_set(
entity_ids[1],
str(VALUES[1]),
VALUES[1],
{
"device_class": SensorDeviceClass.HUMIDITY,
"state_class": SensorStateClass.MEASUREMENT,
@@ -783,7 +695,7 @@ async def test_sensor_calculated_properties_not_convertible_device_class(
)
hass.states.async_set(
entity_ids[2],
str(VALUES[2]),
VALUES[2],
{
"device_class": SensorDeviceClass.HUMIDITY,
"state_class": SensorStateClass.MEASUREMENT,
@@ -808,7 +720,7 @@ async def test_sensor_calculated_properties_not_convertible_device_class(
hass.states.async_set(
entity_ids[2],
str(VALUES[2]),
VALUES[2],
{
"device_class": SensorDeviceClass.HUMIDITY,
"state_class": SensorStateClass.MEASUREMENT,
@@ -848,7 +760,7 @@ async def test_last_sensor(hass: HomeAssistant) -> None:
entity_ids = config["sensor"]["entities"]
for entity_id, value in dict(zip(entity_ids, VALUES, strict=False)).items():
hass.states.async_set(entity_id, str(value))
hass.states.async_set(entity_id, value)
await hass.async_block_till_done()
state = hass.states.get("sensor.test_last")
assert str(float(value)) == state.state
@@ -885,7 +797,7 @@ async def test_sensors_attributes_added_when_entity_info_available(
for entity_id, value in dict(zip(entity_ids, VALUES, strict=False)).items():
hass.states.async_set(
entity_id,
str(value),
value,
{
ATTR_DEVICE_CLASS: SensorDeviceClass.VOLUME,
ATTR_STATE_CLASS: SensorStateClass.TOTAL,
@@ -931,9 +843,9 @@ async def test_sensor_state_class_no_uom_not_available(
"unit_of_measurement": PERCENTAGE,
}
hass.states.async_set(entity_ids[0], str(VALUES[0]), input_attributes)
hass.states.async_set(entity_ids[1], str(VALUES[1]), input_attributes)
hass.states.async_set(entity_ids[2], str(VALUES[2]), input_attributes)
hass.states.async_set(entity_ids[0], VALUES[0], input_attributes)
hass.states.async_set(entity_ids[1], VALUES[1], input_attributes)
hass.states.async_set(entity_ids[2], VALUES[2], input_attributes)
await hass.async_block_till_done()
assert await async_setup_component(hass, "sensor", config)
@@ -952,7 +864,7 @@ async def test_sensor_state_class_no_uom_not_available(
# sensor.test_3 drops the unit of measurement
hass.states.async_set(
entity_ids[2],
str(VALUES[2]),
VALUES[2],
{
"state_class": SensorStateClass.MEASUREMENT,
},
@@ -1002,7 +914,7 @@ async def test_sensor_different_attributes_ignore_non_numeric(
test_cases = [
{
"entity": entity_ids[0],
"value": str(VALUES[0]),
"value": VALUES[0],
"attributes": {
"state_class": SensorStateClass.MEASUREMENT,
"unit_of_measurement": PERCENTAGE,
@@ -1014,7 +926,7 @@ async def test_sensor_different_attributes_ignore_non_numeric(
},
{
"entity": entity_ids[1],
"value": str(VALUES[1]),
"value": VALUES[1],
"attributes": {
"state_class": SensorStateClass.MEASUREMENT,
"device_class": SensorDeviceClass.HUMIDITY,
@@ -1027,7 +939,7 @@ async def test_sensor_different_attributes_ignore_non_numeric(
},
{
"entity": entity_ids[2],
"value": str(VALUES[2]),
"value": VALUES[2],
"attributes": {
"state_class": SensorStateClass.MEASUREMENT,
"device_class": SensorDeviceClass.TEMPERATURE,
@@ -1040,7 +952,7 @@ async def test_sensor_different_attributes_ignore_non_numeric(
},
{
"entity": entity_ids[2],
"value": str(VALUES[2]),
"value": VALUES[2],
"attributes": {
"state_class": SensorStateClass.MEASUREMENT,
"device_class": SensorDeviceClass.HUMIDITY,
@@ -1054,7 +966,7 @@ async def test_sensor_different_attributes_ignore_non_numeric(
},
{
"entity": entity_ids[0],
"value": str(VALUES[0]),
"value": VALUES[0],
"attributes": {
"state_class": SensorStateClass.MEASUREMENT,
"device_class": SensorDeviceClass.HUMIDITY,
@@ -1068,7 +980,7 @@ async def test_sensor_different_attributes_ignore_non_numeric(
},
{
"entity": entity_ids[0],
"value": str(VALUES[0]),
"value": VALUES[0],
"attributes": {
"state_class": SensorStateClass.MEASUREMENT,
},

View File

@@ -78,6 +78,11 @@ async def test_expose_attribute(hass: HomeAssistant, knx: KNXTestKit) -> None:
await hass.async_block_till_done()
await knx.assert_write("1/1/8", (1,))
# Change attribute below resolution of DPT; expect no telegram
hass.states.async_set(entity_id, "on", {attribute: 1.2})
await hass.async_block_till_done()
await knx.assert_no_telegram()
# Read in between
await knx.receive_read("1/1/8")
await knx.assert_response("1/1/8", (1,))
@@ -251,6 +256,32 @@ async def test_expose_cooldown(
await knx.assert_write("1/1/8", (3,))
async def test_expose_periodic_send(
hass: HomeAssistant, knx: KNXTestKit, freezer: FrozenDateTimeFactory
) -> None:
"""Test an expose with periodic send."""
entity_id = "fake.entity"
await knx.setup_integration(
{
CONF_KNX_EXPOSE: {
CONF_TYPE: "percentU8",
KNX_ADDRESS: "1/1/8",
CONF_ENTITY_ID: entity_id,
ExposeSchema.CONF_KNX_EXPOSE_PERIODIC_SEND: {"minutes": 1},
}
},
)
# Initialize state
hass.states.async_set(entity_id, "15", {})
await hass.async_block_till_done()
await knx.assert_write("1/1/8", (15,))
# Wait for time to pass
freezer.tick(timedelta(seconds=60))
async_fire_time_changed(hass)
await hass.async_block_till_done()
await knx.assert_write("1/1/8", (15,))
async def test_expose_value_template(
hass: HomeAssistant, knx: KNXTestKit, caplog: pytest.LogCaptureFixture
) -> None:

View File

@@ -439,6 +439,54 @@
'state': 'off',
})
# ---
# name: test_binary_sensors[eve_thermo_v4][binary_sensor.eve_thermo_20ebp1701_outdoor_temperature_remote_sensing-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.eve_thermo_20ebp1701_outdoor_temperature_remote_sensing',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Outdoor temperature remote sensing',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'thermostat_remote_sensing_outdoor_temperature',
'unique_id': '00000000000004D2-0000000000000021-MatterNodeDevice-1-ThermostatRemoteSensing_OutdoorTemperature-513-26',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensors[eve_thermo_v4][binary_sensor.eve_thermo_20ebp1701_outdoor_temperature_remote_sensing-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Eve Thermo 20EBP1701 Outdoor temperature remote sensing',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.eve_thermo_20ebp1701_outdoor_temperature_remote_sensing',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_binary_sensors[eve_thermo_v5][binary_sensor.eve_thermo_20ecd1701_local_temperature_remote_sensing-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
@@ -487,6 +535,54 @@
'state': 'off',
})
# ---
# name: test_binary_sensors[eve_thermo_v5][binary_sensor.eve_thermo_20ecd1701_outdoor_temperature_remote_sensing-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'binary_sensor',
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
'entity_id': 'binary_sensor.eve_thermo_20ecd1701_outdoor_temperature_remote_sensing',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Outdoor temperature remote sensing',
'platform': 'matter',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'thermostat_remote_sensing_outdoor_temperature',
'unique_id': '00000000000004D2-000000000000000C-MatterNodeDevice-1-ThermostatRemoteSensing_OutdoorTemperature-513-26',
'unit_of_measurement': None,
})
# ---
# name: test_binary_sensors[eve_thermo_v5][binary_sensor.eve_thermo_20ecd1701_outdoor_temperature_remote_sensing-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Eve Thermo 20ECD1701 Outdoor temperature remote sensing',
}),
'context': <ANY>,
'entity_id': 'binary_sensor.eve_thermo_20ecd1701_outdoor_temperature_remote_sensing',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_binary_sensors[heiman_motion_sensor_m1][binary_sensor.smart_motion_sensor_occupancy-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@@ -7,7 +7,6 @@ import pytest
from homeassistant.components.namecheapdns.const import DOMAIN
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER
from homeassistant.const import CONF_PASSWORD
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers import issue_registry as ir
@@ -15,8 +14,6 @@ from homeassistant.setup import async_setup_component
from .conftest import TEST_USER_INPUT
from tests.common import MockConfigEntry
@pytest.mark.usefixtures("mock_namecheap")
async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None:
@@ -143,68 +140,3 @@ async def test_init_import_flow(
)
assert len(mock_setup_entry.mock_calls) == 1
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
@pytest.mark.usefixtures("mock_namecheap")
async def test_reconfigure(
hass: HomeAssistant,
config_entry: MockConfigEntry,
) -> None:
"""Test reconfigure flow."""
config_entry.add_to_hass(hass)
result = await config_entry.start_reconfigure_flow(hass)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "reconfigure"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_PASSWORD: "new-password"}
)
await hass.async_block_till_done()
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "reconfigure_successful"
assert config_entry.data[CONF_PASSWORD] == "new-password"
@pytest.mark.parametrize(
("side_effect", "text_error"),
[
(ValueError, "unknown"),
(False, "update_failed"),
(ClientError, "cannot_connect"),
],
)
async def test_reconfigure_errors(
hass: HomeAssistant,
config_entry: MockConfigEntry,
mock_namecheap: AsyncMock,
side_effect: Exception | bool,
text_error: str,
) -> None:
"""Test we handle errors."""
config_entry.add_to_hass(hass)
result = await config_entry.start_reconfigure_flow(hass)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "reconfigure"
mock_namecheap.side_effect = [side_effect]
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_PASSWORD: "new-password"}
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {"base": text_error}
mock_namecheap.side_effect = None
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_PASSWORD: "new-password"}
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "reconfigure_successful"
assert config_entry.data[CONF_PASSWORD] == "new-password"