mirror of
https://github.com/home-assistant/core.git
synced 2026-01-14 03:27:32 +01:00
Compare commits
2 Commits
dev
...
knx-expose
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a166da552b | ||
|
|
31ae6951db |
@@ -39,7 +39,7 @@ repos:
|
||||
- id: prettier
|
||||
additional_dependencies:
|
||||
- prettier@3.6.2
|
||||
- prettier-plugin-sort-json@4.2.0
|
||||
- prettier-plugin-sort-json@4.1.1
|
||||
- repo: https://github.com/cdce8p/python-typing-update
|
||||
rev: v0.6.0
|
||||
hooks:
|
||||
|
||||
@@ -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.*
|
||||
|
||||
@@ -7,5 +7,6 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["datadog"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["datadog==0.52.0"]
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
@@ -99,29 +98,16 @@ class FireflyDataUpdateCoordinator(DataUpdateCoordinator[FireflyCoordinatorData]
|
||||
|
||||
try:
|
||||
accounts = await self.firefly.get_accounts()
|
||||
|
||||
(
|
||||
categories,
|
||||
primary_currency,
|
||||
budgets,
|
||||
bills,
|
||||
) = await asyncio.gather(
|
||||
self.firefly.get_categories(),
|
||||
self.firefly.get_currency_primary(),
|
||||
self.firefly.get_budgets(start=start_date, end=end_date),
|
||||
self.firefly.get_bills(),
|
||||
)
|
||||
|
||||
category_details = await asyncio.gather(
|
||||
*(
|
||||
self.firefly.get_category(
|
||||
category_id=int(category.id),
|
||||
start=start_date,
|
||||
end=end_date,
|
||||
)
|
||||
for category in categories
|
||||
categories = await self.firefly.get_categories()
|
||||
category_details = [
|
||||
await self.firefly.get_category(
|
||||
category_id=int(category.id), start=start_date, end=end_date
|
||||
)
|
||||
)
|
||||
for category in categories
|
||||
]
|
||||
primary_currency = await self.firefly.get_currency_primary()
|
||||
budgets = await self.firefly.get_budgets(start=start_date, end=end_date)
|
||||
bills = await self.firefly.get_bills()
|
||||
except FireflyAuthenticationError as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pyfirefly==0.1.11"]
|
||||
"requirements": ["pyfirefly==0.1.10"]
|
||||
}
|
||||
|
||||
@@ -66,7 +66,6 @@ from .const import (
|
||||
CONF_COLD_TOLERANCE,
|
||||
CONF_HEATER,
|
||||
CONF_HOT_TOLERANCE,
|
||||
CONF_KEEP_ALIVE,
|
||||
CONF_MAX_TEMP,
|
||||
CONF_MIN_DUR,
|
||||
CONF_MIN_TEMP,
|
||||
@@ -82,6 +81,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
DEFAULT_NAME = "Generic Thermostat"
|
||||
|
||||
CONF_INITIAL_HVAC_MODE = "initial_hvac_mode"
|
||||
CONF_KEEP_ALIVE = "keep_alive"
|
||||
CONF_PRECISION = "precision"
|
||||
CONF_TARGET_TEMP = "target_temp"
|
||||
CONF_TEMP_STEP = "target_temp_step"
|
||||
|
||||
@@ -21,7 +21,6 @@ from .const import (
|
||||
CONF_COLD_TOLERANCE,
|
||||
CONF_HEATER,
|
||||
CONF_HOT_TOLERANCE,
|
||||
CONF_KEEP_ALIVE,
|
||||
CONF_MAX_TEMP,
|
||||
CONF_MIN_DUR,
|
||||
CONF_MIN_TEMP,
|
||||
@@ -60,9 +59,6 @@ OPTIONS_SCHEMA = {
|
||||
vol.Optional(CONF_MIN_DUR): selector.DurationSelector(
|
||||
selector.DurationSelectorConfig(allow_negative=False)
|
||||
),
|
||||
vol.Optional(CONF_KEEP_ALIVE): selector.DurationSelector(
|
||||
selector.DurationSelectorConfig(allow_negative=False)
|
||||
),
|
||||
vol.Optional(CONF_MIN_TEMP): selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(
|
||||
mode=selector.NumberSelectorMode.BOX, unit_of_measurement=DEGREE, step=0.1
|
||||
|
||||
@@ -33,5 +33,4 @@ CONF_PRESETS = {
|
||||
)
|
||||
}
|
||||
CONF_SENSOR = "target_sensor"
|
||||
CONF_KEEP_ALIVE = "keep_alive"
|
||||
DEFAULT_TOLERANCE = 0.3
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
"cold_tolerance": "Cold tolerance",
|
||||
"heater": "Actuator switch",
|
||||
"hot_tolerance": "Hot tolerance",
|
||||
"keep_alive": "Keep-alive interval",
|
||||
"max_temp": "Maximum target temperature",
|
||||
"min_cycle_duration": "Minimum cycle duration",
|
||||
"min_temp": "Minimum target temperature",
|
||||
@@ -30,7 +29,6 @@
|
||||
"cold_tolerance": "Minimum amount of difference between the temperature read by the temperature sensor the target temperature that must change prior to being switched on. For example, if the target temperature is 25 and the tolerance is 0.5 the heater will start when the sensor goes below 24.5.",
|
||||
"heater": "Switch entity used to cool or heat depending on A/C mode.",
|
||||
"hot_tolerance": "Minimum amount of difference between the temperature read by the temperature sensor the target temperature that must change prior to being switched off. For example, if the target temperature is 25 and the tolerance is 0.5 the heater will stop when the sensor equals or goes above 25.5.",
|
||||
"keep_alive": "Trigger the heater periodically to keep devices from losing state. When set, min cycle duration is ignored.",
|
||||
"min_cycle_duration": "Set a minimum amount of time that the switch specified must be in its current state prior to being switched either off or on.",
|
||||
"target_sensor": "Temperature sensor that reflects the current temperature."
|
||||
},
|
||||
@@ -47,7 +45,6 @@
|
||||
"cold_tolerance": "[%key:component::generic_thermostat::config::step::user::data::cold_tolerance%]",
|
||||
"heater": "[%key:component::generic_thermostat::config::step::user::data::heater%]",
|
||||
"hot_tolerance": "[%key:component::generic_thermostat::config::step::user::data::hot_tolerance%]",
|
||||
"keep_alive": "[%key:component::generic_thermostat::config::step::user::data::keep_alive%]",
|
||||
"max_temp": "[%key:component::generic_thermostat::config::step::user::data::max_temp%]",
|
||||
"min_cycle_duration": "[%key:component::generic_thermostat::config::step::user::data::min_cycle_duration%]",
|
||||
"min_temp": "[%key:component::generic_thermostat::config::step::user::data::min_temp%]",
|
||||
@@ -58,7 +55,6 @@
|
||||
"cold_tolerance": "[%key:component::generic_thermostat::config::step::user::data_description::cold_tolerance%]",
|
||||
"heater": "[%key:component::generic_thermostat::config::step::user::data_description::heater%]",
|
||||
"hot_tolerance": "[%key:component::generic_thermostat::config::step::user::data_description::hot_tolerance%]",
|
||||
"keep_alive": "[%key:component::generic_thermostat::config::step::user::data_description::keep_alive%]",
|
||||
"min_cycle_duration": "[%key:component::generic_thermostat::config::step::user::data_description::min_cycle_duration%]",
|
||||
"target_sensor": "[%key:component::generic_thermostat::config::step::user::data_description::target_sensor%]"
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -8,7 +8,6 @@ from .coordinator import HDFuryConfigEntry, HDFuryCoordinator
|
||||
PLATFORMS = [
|
||||
Platform.BUTTON,
|
||||
Platform.SELECT,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
]
|
||||
|
||||
|
||||
@@ -16,50 +16,6 @@
|
||||
"default": "mdi:hdmi-port"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"aud0": {
|
||||
"default": "mdi:audio-input-rca"
|
||||
},
|
||||
"aud1": {
|
||||
"default": "mdi:audio-input-rca"
|
||||
},
|
||||
"audout": {
|
||||
"default": "mdi:television-speaker"
|
||||
},
|
||||
"earcrx": {
|
||||
"default": "mdi:audio-video"
|
||||
},
|
||||
"edida0": {
|
||||
"default": "mdi:format-list-text"
|
||||
},
|
||||
"edida1": {
|
||||
"default": "mdi:format-list-text"
|
||||
},
|
||||
"edida2": {
|
||||
"default": "mdi:format-list-text"
|
||||
},
|
||||
"rx0": {
|
||||
"default": "mdi:video-input-hdmi"
|
||||
},
|
||||
"rx1": {
|
||||
"default": "mdi:video-input-hdmi"
|
||||
},
|
||||
"sink0": {
|
||||
"default": "mdi:television"
|
||||
},
|
||||
"sink1": {
|
||||
"default": "mdi:television"
|
||||
},
|
||||
"sink2": {
|
||||
"default": "mdi:audio-video"
|
||||
},
|
||||
"tx0": {
|
||||
"default": "mdi:cable-data"
|
||||
},
|
||||
"tx1": {
|
||||
"default": "mdi:cable-data"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"autosw": {
|
||||
"default": "mdi:import"
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
"""Sensor platform for HDFury Integration."""
|
||||
|
||||
from homeassistant.components.sensor import SensorEntity, SensorEntityDescription
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import HDFuryConfigEntry
|
||||
from .entity import HDFuryEntity
|
||||
|
||||
SENSORS: tuple[SensorEntityDescription, ...] = (
|
||||
SensorEntityDescription(
|
||||
key="RX0",
|
||||
translation_key="rx0",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="RX1",
|
||||
translation_key="rx1",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="TX0",
|
||||
translation_key="tx0",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="TX1",
|
||||
translation_key="tx1",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="AUD0",
|
||||
translation_key="aud0",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="AUD1",
|
||||
translation_key="aud1",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="AUDOUT",
|
||||
translation_key="audout",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="EARCRX",
|
||||
translation_key="earcrx",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="SINK0",
|
||||
translation_key="sink0",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="SINK1",
|
||||
translation_key="sink1",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="SINK2",
|
||||
translation_key="sink2",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="EDIDA0",
|
||||
translation_key="edida0",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="EDIDA1",
|
||||
translation_key="edida1",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="EDIDA2",
|
||||
translation_key="edida2",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: HDFuryConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up sensors using the platform schema."""
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
HDFurySensor(coordinator, description)
|
||||
for description in SENSORS
|
||||
if description.key in coordinator.data.info
|
||||
)
|
||||
|
||||
|
||||
class HDFurySensor(HDFuryEntity, SensorEntity):
|
||||
"""Base HDFury Sensor Class."""
|
||||
|
||||
entity_description: SensorEntityDescription
|
||||
|
||||
@property
|
||||
def native_value(self) -> str:
|
||||
"""Set Sensor Value."""
|
||||
|
||||
return self.coordinator.data.info[self.entity_description.key]
|
||||
@@ -57,50 +57,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"aud0": {
|
||||
"name": "Audio TX0"
|
||||
},
|
||||
"aud1": {
|
||||
"name": "Audio TX1"
|
||||
},
|
||||
"audout": {
|
||||
"name": "Audio output"
|
||||
},
|
||||
"earcrx": {
|
||||
"name": "eARC/ARC status"
|
||||
},
|
||||
"edida0": {
|
||||
"name": "EDID TXA0"
|
||||
},
|
||||
"edida1": {
|
||||
"name": "EDID TXA1"
|
||||
},
|
||||
"edida2": {
|
||||
"name": "EDID AUDA"
|
||||
},
|
||||
"rx0": {
|
||||
"name": "Input RX0"
|
||||
},
|
||||
"rx1": {
|
||||
"name": "Input RX1"
|
||||
},
|
||||
"sink0": {
|
||||
"name": "EDID TX0"
|
||||
},
|
||||
"sink1": {
|
||||
"name": "EDID TX1"
|
||||
},
|
||||
"sink2": {
|
||||
"name": "EDID AUD"
|
||||
},
|
||||
"tx0": {
|
||||
"name": "Output TX0"
|
||||
},
|
||||
"tx1": {
|
||||
"name": "Output TX1"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"autosw": {
|
||||
"name": "Auto switch inputs"
|
||||
|
||||
@@ -220,33 +220,31 @@ def get_accessory( # noqa: C901
|
||||
a_type = "TemperatureSensor"
|
||||
elif device_class == SensorDeviceClass.HUMIDITY and unit == PERCENTAGE:
|
||||
a_type = "HumiditySensor"
|
||||
elif device_class == SensorDeviceClass.PM10:
|
||||
elif (
|
||||
device_class == SensorDeviceClass.PM10
|
||||
or SensorDeviceClass.PM10 in state.entity_id
|
||||
):
|
||||
a_type = "PM10Sensor"
|
||||
elif device_class == SensorDeviceClass.PM25:
|
||||
elif (
|
||||
device_class == SensorDeviceClass.PM25
|
||||
or SensorDeviceClass.PM25 in state.entity_id
|
||||
):
|
||||
a_type = "PM25Sensor"
|
||||
elif device_class == SensorDeviceClass.NITROGEN_DIOXIDE:
|
||||
a_type = "NitrogenDioxideSensor"
|
||||
elif device_class == SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS:
|
||||
a_type = "VolatileOrganicCompoundsSensor"
|
||||
elif device_class == SensorDeviceClass.GAS:
|
||||
elif (
|
||||
device_class == SensorDeviceClass.GAS
|
||||
or SensorDeviceClass.GAS in state.entity_id
|
||||
):
|
||||
a_type = "AirQualitySensor"
|
||||
elif device_class == SensorDeviceClass.CO:
|
||||
a_type = "CarbonMonoxideSensor"
|
||||
elif device_class == SensorDeviceClass.CO2:
|
||||
elif device_class == SensorDeviceClass.CO2 or "co2" in state.entity_id:
|
||||
a_type = "CarbonDioxideSensor"
|
||||
elif device_class == SensorDeviceClass.ILLUMINANCE or unit == LIGHT_LUX:
|
||||
a_type = "LightSensor"
|
||||
|
||||
# Fallbacks based on entity_id
|
||||
elif SensorDeviceClass.PM10 in state.entity_id:
|
||||
a_type = "PM10Sensor"
|
||||
elif SensorDeviceClass.PM25 in state.entity_id:
|
||||
a_type = "PM25Sensor"
|
||||
elif SensorDeviceClass.GAS in state.entity_id:
|
||||
a_type = "AirQualitySensor"
|
||||
elif "co2" in state.entity_id:
|
||||
a_type = "CarbonDioxideSensor"
|
||||
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"%s: Unsupported sensor type (device_class=%s) (unit=%s)",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -642,7 +642,6 @@ DISCOVERY_SCHEMAS = [
|
||||
list_attribute=clusters.DoorLock.Attributes.SupportedOperatingModes,
|
||||
device_to_ha=DOOR_LOCK_OPERATING_MODE_MAP.get,
|
||||
ha_to_device=DOOR_LOCK_OPERATING_MODE_MAP_REVERSE.get,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
entity_class=MatterDoorLockOperatingModeSelectEntity,
|
||||
required_attributes=(
|
||||
|
||||
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
from http import HTTPStatus
|
||||
|
||||
from aiohttp import ClientConnectionError, ClientResponseError
|
||||
from pymelcloud import get_devices
|
||||
@@ -24,18 +23,21 @@ PLATFORMS = [Platform.CLIMATE, Platform.SENSOR, Platform.WATER_HEATER]
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: MelCloudConfigEntry) -> bool:
|
||||
"""Establish connection with MELCloud."""
|
||||
token = entry.data[CONF_TOKEN]
|
||||
session = async_get_clientsession(hass)
|
||||
|
||||
try:
|
||||
async with asyncio.timeout(10):
|
||||
all_devices = await get_devices(
|
||||
token=entry.data[CONF_TOKEN],
|
||||
session=async_get_clientsession(hass),
|
||||
token,
|
||||
session,
|
||||
conf_update_interval=timedelta(minutes=30),
|
||||
device_set_debounce=timedelta(seconds=2),
|
||||
)
|
||||
except ClientResponseError as ex:
|
||||
if ex.status in (HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN):
|
||||
if ex.status in (401, 403):
|
||||
raise ConfigEntryAuthFailed from ex
|
||||
if ex.status == HTTPStatus.TOO_MANY_REQUESTS:
|
||||
if ex.status == 429:
|
||||
raise UpdateFailed(
|
||||
"MELCloud rate limit exceeded. Your account may be temporarily blocked"
|
||||
) from ex
|
||||
@@ -47,21 +49,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: MelCloudConfigEntry) ->
|
||||
coordinators: dict[str, list[MelCloudDeviceUpdateCoordinator]] = {}
|
||||
device_registry = dr.async_get(hass)
|
||||
for device_type, devices in all_devices.items():
|
||||
# Build coordinators for this device_type
|
||||
coordinators[device_type] = [
|
||||
MelCloudDeviceUpdateCoordinator(hass, device, entry) for device in devices
|
||||
]
|
||||
|
||||
# Perform initial refreshes concurrently
|
||||
await asyncio.gather(
|
||||
*(
|
||||
coordinator.async_config_entry_first_refresh()
|
||||
for coordinator in coordinators[device_type]
|
||||
)
|
||||
)
|
||||
|
||||
# Register parent devices so zone entities can reference via_device
|
||||
for coordinator in coordinators[device_type]:
|
||||
coordinators[device_type] = []
|
||||
for device in devices:
|
||||
coordinator = MelCloudDeviceUpdateCoordinator(hass, device, entry)
|
||||
# Perform initial refresh for this device
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
coordinators[device_type].append(coordinator)
|
||||
# Register parent device now so zone entities can reference it via via_device
|
||||
device_registry.async_get_or_create(
|
||||
config_entry_id=entry.entry_id,
|
||||
**coordinator.device_info,
|
||||
|
||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
from collections.abc import Mapping
|
||||
from http import HTTPStatus
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aiohttp import ClientError, ClientResponseError
|
||||
@@ -17,6 +18,8 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow."""
|
||||
@@ -34,7 +37,8 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
async def _create_client(
|
||||
self,
|
||||
username: str,
|
||||
password: str,
|
||||
*,
|
||||
password: str | None = None,
|
||||
token: str | None = None,
|
||||
) -> ConfigFlowResult:
|
||||
"""Create client."""
|
||||
@@ -42,13 +46,13 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
async with asyncio.timeout(10):
|
||||
if (acquired_token := token) is None:
|
||||
acquired_token = await pymelcloud.login(
|
||||
email=username,
|
||||
password=password,
|
||||
session=async_get_clientsession(self.hass),
|
||||
username,
|
||||
password,
|
||||
async_get_clientsession(self.hass),
|
||||
)
|
||||
await pymelcloud.get_devices(
|
||||
token=acquired_token,
|
||||
session=async_get_clientsession(self.hass),
|
||||
acquired_token,
|
||||
async_get_clientsession(self.hass),
|
||||
)
|
||||
except ClientResponseError as err:
|
||||
if err.status in (HTTPStatus.UNAUTHORIZED, HTTPStatus.FORBIDDEN):
|
||||
@@ -74,9 +78,8 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
{vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str}
|
||||
),
|
||||
)
|
||||
return await self._create_client(
|
||||
username=user_input[CONF_USERNAME], password=user_input[CONF_PASSWORD]
|
||||
)
|
||||
username = user_input[CONF_USERNAME]
|
||||
return await self._create_client(username, password=user_input[CONF_PASSWORD])
|
||||
|
||||
async def async_step_reauth(
|
||||
self, entry_data: Mapping[str, Any]
|
||||
@@ -115,9 +118,9 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
try:
|
||||
async with asyncio.timeout(10):
|
||||
acquired_token = await pymelcloud.login(
|
||||
email=user_input[CONF_USERNAME],
|
||||
password=user_input[CONF_PASSWORD],
|
||||
session=async_get_clientsession(self.hass),
|
||||
user_input[CONF_USERNAME],
|
||||
user_input[CONF_PASSWORD],
|
||||
async_get_clientsession(self.hass),
|
||||
)
|
||||
except (ClientResponseError, AttributeError) as err:
|
||||
if (
|
||||
@@ -131,7 +134,10 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
errors["base"] = "invalid_auth"
|
||||
else:
|
||||
errors["base"] = "cannot_connect"
|
||||
except (TimeoutError, ClientError):
|
||||
except (
|
||||
TimeoutError,
|
||||
ClientError,
|
||||
):
|
||||
errors["base"] = "cannot_connect"
|
||||
|
||||
return acquired_token, errors
|
||||
@@ -149,9 +155,9 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
try:
|
||||
async with asyncio.timeout(10):
|
||||
acquired_token = await pymelcloud.login(
|
||||
email=user_input[CONF_USERNAME],
|
||||
password=user_input[CONF_PASSWORD],
|
||||
session=async_get_clientsession(self.hass),
|
||||
user_input[CONF_USERNAME],
|
||||
user_input[CONF_PASSWORD],
|
||||
async_get_clientsession(self.hass),
|
||||
)
|
||||
except (ClientResponseError, AttributeError) as err:
|
||||
if (
|
||||
|
||||
@@ -6,7 +6,6 @@ import asyncio
|
||||
from functools import partial
|
||||
from http import HTTPStatus
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
|
||||
@@ -48,7 +47,7 @@ from .util import supports_push
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def push_registrations(hass: HomeAssistant) -> dict[str, str]:
|
||||
def push_registrations(hass):
|
||||
"""Return a dictionary of push enabled registrations."""
|
||||
targets = {}
|
||||
|
||||
@@ -91,32 +90,38 @@ async def async_get_service(
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> MobileAppNotificationService:
|
||||
"""Get the mobile_app notification service."""
|
||||
service = hass.data[DOMAIN][DATA_NOTIFY] = MobileAppNotificationService()
|
||||
service = hass.data[DOMAIN][DATA_NOTIFY] = MobileAppNotificationService(hass)
|
||||
return service
|
||||
|
||||
|
||||
class MobileAppNotificationService(BaseNotificationService):
|
||||
"""Implement the notification service for mobile_app."""
|
||||
|
||||
def __init__(self, hass):
|
||||
"""Initialize the service."""
|
||||
self._hass = hass
|
||||
|
||||
@property
|
||||
def targets(self) -> dict[str, str]:
|
||||
def targets(self):
|
||||
"""Return a dictionary of registered targets."""
|
||||
return push_registrations(self.hass)
|
||||
|
||||
async def async_send_message(self, message: str = "", **kwargs: Any) -> None:
|
||||
async def async_send_message(self, message="", **kwargs):
|
||||
"""Send a message to the Lambda APNS gateway."""
|
||||
data = {ATTR_MESSAGE: message}
|
||||
|
||||
# Remove default title from notifications.
|
||||
if (
|
||||
title_arg := kwargs.get(ATTR_TITLE)
|
||||
) is not None and title_arg != ATTR_TITLE_DEFAULT:
|
||||
data[ATTR_TITLE] = title_arg
|
||||
kwargs.get(ATTR_TITLE) is not None
|
||||
and kwargs.get(ATTR_TITLE) != ATTR_TITLE_DEFAULT
|
||||
):
|
||||
data[ATTR_TITLE] = kwargs.get(ATTR_TITLE)
|
||||
|
||||
if not (targets := kwargs.get(ATTR_TARGET)):
|
||||
targets = push_registrations(self.hass).values()
|
||||
|
||||
if (data_arg := kwargs.get(ATTR_DATA)) is not None:
|
||||
data[ATTR_DATA] = data_arg
|
||||
if kwargs.get(ATTR_DATA) is not None:
|
||||
data[ATTR_DATA] = kwargs.get(ATTR_DATA)
|
||||
|
||||
local_push_channels = self.hass.data[DOMAIN][DATA_PUSH_CHANNEL]
|
||||
|
||||
@@ -161,7 +166,7 @@ class MobileAppNotificationService(BaseNotificationService):
|
||||
|
||||
try:
|
||||
async with asyncio.timeout(10):
|
||||
response = await async_get_clientsession(self.hass).post(
|
||||
response = await async_get_clientsession(self._hass).post(
|
||||
push_url, json=target_data
|
||||
)
|
||||
result = await response.json()
|
||||
|
||||
@@ -1,21 +1,28 @@
|
||||
"""Support for namecheap DNS services."""
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from aiohttp import ClientError, ClientSession
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import SOURCE_IMPORT
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.const import CONF_DOMAIN, CONF_HOST, CONF_PASSWORD
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import NamecheapConfigEntry, NamecheapDnsUpdateCoordinator
|
||||
from .const import DOMAIN, UPDATE_URL
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
INTERVAL = timedelta(minutes=5)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
DOMAIN: vol.Schema(
|
||||
@@ -29,6 +36,8 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
type NamecheapConfigEntry = ConfigEntry[None]
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Initialize the namecheap DNS component."""
|
||||
@@ -45,13 +54,37 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: NamecheapConfigEntry) -> bool:
|
||||
"""Set up Namecheap DynamicDNS from a config entry."""
|
||||
host = entry.data[CONF_HOST]
|
||||
domain = entry.data[CONF_DOMAIN]
|
||||
password = entry.data[CONF_PASSWORD]
|
||||
|
||||
coordinator = NamecheapDnsUpdateCoordinator(hass, entry)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
entry.runtime_data = coordinator
|
||||
session = async_get_clientsession(hass)
|
||||
|
||||
# Add a dummy listener as we do not have regular entities
|
||||
entry.async_on_unload(coordinator.async_add_listener(lambda: None))
|
||||
try:
|
||||
if not await update_namecheapdns(session, host, domain, password):
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="update_failed",
|
||||
translation_placeholders={
|
||||
CONF_DOMAIN: f"{entry.data[CONF_HOST]}.{entry.data[CONF_DOMAIN]}"
|
||||
},
|
||||
)
|
||||
except ClientError as e:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="connection_error",
|
||||
translation_placeholders={
|
||||
CONF_DOMAIN: f"{entry.data[CONF_HOST]}.{entry.data[CONF_DOMAIN]}"
|
||||
},
|
||||
) from e
|
||||
|
||||
async def update_domain_interval(now):
|
||||
"""Update the namecheap DNS entry."""
|
||||
await update_namecheapdns(session, host, domain, password)
|
||||
|
||||
entry.async_on_unload(
|
||||
async_track_time_interval(hass, update_domain_interval, INTERVAL)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
@@ -59,3 +92,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: NamecheapConfigEntry) ->
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: NamecheapConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return True
|
||||
|
||||
|
||||
async def update_namecheapdns(
|
||||
session: ClientSession, host: str, domain: str, password: str
|
||||
):
|
||||
"""Update namecheap DNS entry."""
|
||||
params = {"host": host, "domain": domain, "password": password}
|
||||
|
||||
resp = await session.get(UPDATE_URL, params=params)
|
||||
xml_string = await resp.text()
|
||||
|
||||
if "<ErrCount>0</ErrCount>" not in xml_string:
|
||||
_LOGGER.warning("Updating namecheap domain failed: %s", domain)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@@ -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 (
|
||||
@@ -18,8 +18,8 @@ from homeassistant.helpers.selector import (
|
||||
TextSelectorType,
|
||||
)
|
||||
|
||||
from . import update_namecheapdns
|
||||
from .const import DOMAIN
|
||||
from .helpers import update_namecheapdns
|
||||
from .issue import deprecate_yaml_issue
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -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},
|
||||
)
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
"""Coordinator for the Namecheap DynamicDNS integration."""
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from aiohttp import ClientError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_DOMAIN, CONF_HOST, CONF_PASSWORD
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN
|
||||
from .helpers import update_namecheapdns
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
type NamecheapConfigEntry = ConfigEntry[NamecheapDnsUpdateCoordinator]
|
||||
|
||||
|
||||
INTERVAL = timedelta(minutes=5)
|
||||
|
||||
|
||||
class NamecheapDnsUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
"""Namecheap DynamicDNS update coordinator."""
|
||||
|
||||
config_entry: NamecheapConfigEntry
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config_entry: NamecheapConfigEntry) -> None:
|
||||
"""Initialize the Namecheap DynamicDNS update coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=config_entry,
|
||||
name=DOMAIN,
|
||||
update_interval=INTERVAL,
|
||||
)
|
||||
|
||||
self.session = async_get_clientsession(hass)
|
||||
|
||||
async def _async_update_data(self) -> None:
|
||||
"""Update Namecheap DNS."""
|
||||
host = self.config_entry.data[CONF_HOST]
|
||||
domain = self.config_entry.data[CONF_DOMAIN]
|
||||
password = self.config_entry.data[CONF_PASSWORD]
|
||||
|
||||
try:
|
||||
if not await update_namecheapdns(self.session, host, domain, password):
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="update_failed",
|
||||
translation_placeholders={CONF_DOMAIN: f"{host}.{domain}"},
|
||||
)
|
||||
except ClientError as e:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="connection_error",
|
||||
translation_placeholders={CONF_DOMAIN: f"{host}.{domain}"},
|
||||
) from e
|
||||
@@ -1,24 +0,0 @@
|
||||
"""Helpers for the Namecheap DynamicDNS integration."""
|
||||
|
||||
import logging
|
||||
|
||||
from aiohttp import ClientSession
|
||||
|
||||
from .const import UPDATE_URL
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def update_namecheapdns(
|
||||
session: ClientSession, host: str, domain: str, password: str
|
||||
):
|
||||
"""Update namecheap DNS entry."""
|
||||
params = {"host": host, "domain": domain, "password": password}
|
||||
|
||||
resp = await session.get(UPDATE_URL, params=params)
|
||||
xml_string = await resp.text()
|
||||
|
||||
if "<ErrCount>0</ErrCount>" not in xml_string:
|
||||
return False
|
||||
|
||||
return True
|
||||
@@ -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%]",
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,6 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyrail"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["pyrail==0.4.1"]
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -71,4 +71,4 @@ rules:
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession: done
|
||||
strict-typing: done
|
||||
strict-typing: todo
|
||||
|
||||
@@ -7,5 +7,6 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["prowl"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["prowlpy==1.1.1"]
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -78,7 +77,7 @@ class TelegramNotificationService(BaseNotificationService):
|
||||
self._chat_id = chat_id
|
||||
self.hass = hass
|
||||
|
||||
def send_message(self, message: str = "", **kwargs: Any) -> None:
|
||||
def send_message(self, message="", **kwargs):
|
||||
"""Send a message to a user."""
|
||||
service_data = {ATTR_TARGET: kwargs.get(ATTR_TARGET, self._chat_id)}
|
||||
data = kwargs.get(ATTR_DATA)
|
||||
@@ -127,7 +126,7 @@ class TelegramNotificationService(BaseNotificationService):
|
||||
self.hass.services.call(
|
||||
TELEGRAM_BOT_DOMAIN, "send_photo", service_data=service_data
|
||||
)
|
||||
return
|
||||
return None
|
||||
if data is not None and ATTR_VIDEO in data:
|
||||
videos = data.get(ATTR_VIDEO)
|
||||
videos = videos if isinstance(videos, list) else [videos]
|
||||
@@ -136,7 +135,7 @@ class TelegramNotificationService(BaseNotificationService):
|
||||
self.hass.services.call(
|
||||
TELEGRAM_BOT_DOMAIN, "send_video", service_data=service_data
|
||||
)
|
||||
return
|
||||
return None
|
||||
if data is not None and ATTR_VOICE in data:
|
||||
voices = data.get(ATTR_VOICE)
|
||||
voices = voices if isinstance(voices, list) else [voices]
|
||||
@@ -145,19 +144,17 @@ class TelegramNotificationService(BaseNotificationService):
|
||||
self.hass.services.call(
|
||||
TELEGRAM_BOT_DOMAIN, "send_voice", service_data=service_data
|
||||
)
|
||||
return
|
||||
return None
|
||||
if data is not None and ATTR_LOCATION in data:
|
||||
service_data.update(data.get(ATTR_LOCATION))
|
||||
self.hass.services.call(
|
||||
return self.hass.services.call(
|
||||
TELEGRAM_BOT_DOMAIN, "send_location", service_data=service_data
|
||||
)
|
||||
return
|
||||
if data is not None and ATTR_DOCUMENT in data:
|
||||
service_data.update(data.get(ATTR_DOCUMENT))
|
||||
self.hass.services.call(
|
||||
return self.hass.services.call(
|
||||
TELEGRAM_BOT_DOMAIN, "send_document", service_data=service_data
|
||||
)
|
||||
return
|
||||
|
||||
# Send message
|
||||
|
||||
@@ -171,6 +168,6 @@ class TelegramNotificationService(BaseNotificationService):
|
||||
TELEGRAM_BOT_DOMAIN,
|
||||
service_data,
|
||||
)
|
||||
self.hass.services.call(
|
||||
return self.hass.services.call(
|
||||
TELEGRAM_BOT_DOMAIN, "send_message", service_data=service_data
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@ from typing import Final
|
||||
|
||||
from aiohttp.client_exceptions import ClientResponseError
|
||||
import jwt
|
||||
from tesla_fleet_api import TeslaFleetApi, is_valid_region
|
||||
from tesla_fleet_api import TeslaFleetApi
|
||||
from tesla_fleet_api.const import Scope
|
||||
from tesla_fleet_api.exceptions import (
|
||||
InvalidRegion,
|
||||
@@ -14,7 +14,6 @@ from tesla_fleet_api.exceptions import (
|
||||
OAuthExpired,
|
||||
TeslaFleetError,
|
||||
)
|
||||
from tesla_fleet_api.tesla import VehicleFleet
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN, Platform
|
||||
@@ -80,8 +79,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslaFleetConfigEntry) -
|
||||
|
||||
token = jwt.decode(access_token, options={"verify_signature": False})
|
||||
scopes: list[Scope] = [Scope(s) for s in token["scp"]]
|
||||
region_code = token["ou_code"].lower()
|
||||
region = region_code if is_valid_region(region_code) else None
|
||||
region: str = token["ou_code"].lower()
|
||||
|
||||
oauth_session = OAuth2Session(hass, entry, implementation)
|
||||
|
||||
@@ -133,15 +131,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslaFleetConfigEntry) -
|
||||
product.pop("cached_data", None)
|
||||
vin = product["vin"]
|
||||
signing = product["command_signing"] == "required"
|
||||
api_vehicle: VehicleFleet
|
||||
if signing:
|
||||
if not tesla.private_key:
|
||||
await tesla.get_private_key(hass.config.path("tesla_fleet.key"))
|
||||
api_vehicle = tesla.vehicles.createSigned(vin)
|
||||
api = tesla.vehicles.createSigned(vin)
|
||||
else:
|
||||
api_vehicle = tesla.vehicles.createFleet(vin)
|
||||
api = tesla.vehicles.createFleet(vin)
|
||||
coordinator = TeslaFleetVehicleDataCoordinator(
|
||||
hass, entry, api_vehicle, product, Scope.VEHICLE_LOCATION in scopes
|
||||
hass, entry, api, product, Scope.VEHICLE_LOCATION in scopes
|
||||
)
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
@@ -156,7 +153,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslaFleetConfigEntry) -
|
||||
|
||||
vehicles.append(
|
||||
TeslaFleetVehicleData(
|
||||
api=api_vehicle,
|
||||
api=api,
|
||||
coordinator=coordinator,
|
||||
vin=vin,
|
||||
device=device,
|
||||
@@ -176,16 +173,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslaFleetConfigEntry) -
|
||||
)
|
||||
continue
|
||||
|
||||
api_energy = tesla.energySites.create(site_id)
|
||||
api = tesla.energySites.create(site_id)
|
||||
|
||||
live_coordinator = TeslaFleetEnergySiteLiveCoordinator(
|
||||
hass, entry, api_energy
|
||||
)
|
||||
live_coordinator = TeslaFleetEnergySiteLiveCoordinator(hass, entry, api)
|
||||
history_coordinator = TeslaFleetEnergySiteHistoryCoordinator(
|
||||
hass, entry, api_energy
|
||||
hass, entry, api
|
||||
)
|
||||
info_coordinator = TeslaFleetEnergySiteInfoCoordinator(
|
||||
hass, entry, api_energy, product
|
||||
hass, entry, api, product
|
||||
)
|
||||
|
||||
await live_coordinator.async_config_entry_first_refresh()
|
||||
@@ -219,7 +214,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslaFleetConfigEntry) -
|
||||
|
||||
energysites.append(
|
||||
TeslaFleetEnergyData(
|
||||
api=api_energy,
|
||||
api=api,
|
||||
live_coordinator=live_coordinator,
|
||||
history_coordinator=history_coordinator,
|
||||
info_coordinator=info_coordinator,
|
||||
|
||||
@@ -79,7 +79,7 @@ class TeslaFleetClimateEntity(TeslaFleetVehicleEntity, ClimateEntity):
|
||||
self,
|
||||
data: TeslaFleetVehicleData,
|
||||
side: TeslaFleetClimateSide,
|
||||
scopes: list[Scope],
|
||||
scopes: Scope,
|
||||
) -> None:
|
||||
"""Initialize the climate."""
|
||||
|
||||
@@ -219,7 +219,7 @@ class TeslaFleetCabinOverheatProtectionEntity(TeslaFleetVehicleEntity, ClimateEn
|
||||
def __init__(
|
||||
self,
|
||||
data: TeslaFleetVehicleData,
|
||||
scopes: list[Scope],
|
||||
scopes: Scope,
|
||||
) -> None:
|
||||
"""Initialize the cabin overheat climate entity."""
|
||||
|
||||
|
||||
@@ -178,15 +178,13 @@ class TeslaFleetEnergySiteLiveCoordinator(DataUpdateCoordinator[dict[str, Any]])
|
||||
try:
|
||||
data = (await self.api.live_status())["response"]
|
||||
except RateLimited as e:
|
||||
if isinstance(e.data, dict) and "after" in e.data:
|
||||
LOGGER.warning(
|
||||
"%s rate limited, will retry in %s seconds",
|
||||
self.name,
|
||||
e.data["after"],
|
||||
)
|
||||
LOGGER.warning(
|
||||
"%s rate limited, will retry in %s seconds",
|
||||
self.name,
|
||||
e.data.get("after"),
|
||||
)
|
||||
if "after" in e.data:
|
||||
self.update_interval = timedelta(seconds=int(e.data["after"]))
|
||||
else:
|
||||
LOGGER.warning("%s rate limited, will skip refresh", self.name)
|
||||
return self.data
|
||||
except (InvalidToken, OAuthExpired, LoginRequired) as e:
|
||||
raise ConfigEntryAuthFailed from e
|
||||
@@ -242,15 +240,13 @@ class TeslaFleetEnergySiteHistoryCoordinator(DataUpdateCoordinator[dict[str, Any
|
||||
try:
|
||||
data = (await self.api.energy_history(TeslaEnergyPeriod.DAY))["response"]
|
||||
except RateLimited as e:
|
||||
if isinstance(e.data, dict) and "after" in e.data:
|
||||
LOGGER.warning(
|
||||
"%s rate limited, will retry in %s seconds",
|
||||
self.name,
|
||||
e.data["after"],
|
||||
)
|
||||
LOGGER.warning(
|
||||
"%s rate limited, will retry in %s seconds",
|
||||
self.name,
|
||||
e.data.get("after"),
|
||||
)
|
||||
if "after" in e.data:
|
||||
self.update_interval = timedelta(seconds=int(e.data["after"]))
|
||||
else:
|
||||
LOGGER.warning("%s rate limited, will skip refresh", self.name)
|
||||
return self.data
|
||||
except (InvalidToken, OAuthExpired, LoginRequired) as e:
|
||||
raise ConfigEntryAuthFailed from e
|
||||
@@ -307,15 +303,13 @@ class TeslaFleetEnergySiteInfoCoordinator(DataUpdateCoordinator[dict[str, Any]])
|
||||
try:
|
||||
data = (await self.api.site_info())["response"]
|
||||
except RateLimited as e:
|
||||
if isinstance(e.data, dict) and "after" in e.data:
|
||||
LOGGER.warning(
|
||||
"%s rate limited, will retry in %s seconds",
|
||||
self.name,
|
||||
e.data["after"],
|
||||
)
|
||||
LOGGER.warning(
|
||||
"%s rate limited, will retry in %s seconds",
|
||||
self.name,
|
||||
e.data.get("after"),
|
||||
)
|
||||
if "after" in e.data:
|
||||
self.update_interval = timedelta(seconds=int(e.data["after"]))
|
||||
else:
|
||||
LOGGER.warning("%s rate limited, will skip refresh", self.name)
|
||||
return self.data
|
||||
except (InvalidToken, OAuthExpired, LoginRequired) as e:
|
||||
raise ConfigEntryAuthFailed from e
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Tesla Fleet parent entity class."""
|
||||
|
||||
from abc import abstractmethod
|
||||
from typing import Any, Generic, TypeVar
|
||||
from typing import Any
|
||||
|
||||
from tesla_fleet_api.const import Scope
|
||||
from tesla_fleet_api.tesla.energysite import EnergySite
|
||||
@@ -21,8 +21,6 @@ from .coordinator import (
|
||||
from .helpers import wake_up_vehicle
|
||||
from .models import TeslaFleetEnergyData, TeslaFleetVehicleData
|
||||
|
||||
_ApiT = TypeVar("_ApiT", bound=VehicleFleet | EnergySite)
|
||||
|
||||
|
||||
class TeslaFleetEntity(
|
||||
CoordinatorEntity[
|
||||
@@ -30,15 +28,13 @@ class TeslaFleetEntity(
|
||||
| TeslaFleetEnergySiteLiveCoordinator
|
||||
| TeslaFleetEnergySiteHistoryCoordinator
|
||||
| TeslaFleetEnergySiteInfoCoordinator
|
||||
],
|
||||
Generic[_ApiT],
|
||||
]
|
||||
):
|
||||
"""Parent class for all TeslaFleet entities."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
read_only: bool
|
||||
scoped: bool
|
||||
api: _ApiT
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -46,7 +42,7 @@ class TeslaFleetEntity(
|
||||
| TeslaFleetEnergySiteLiveCoordinator
|
||||
| TeslaFleetEnergySiteHistoryCoordinator
|
||||
| TeslaFleetEnergySiteInfoCoordinator,
|
||||
api: _ApiT,
|
||||
api: VehicleFleet | EnergySite,
|
||||
key: str,
|
||||
) -> None:
|
||||
"""Initialize common aspects of a TeslaFleet entity."""
|
||||
@@ -104,7 +100,7 @@ class TeslaFleetEntity(
|
||||
)
|
||||
|
||||
|
||||
class TeslaFleetVehicleEntity(TeslaFleetEntity[VehicleFleet]):
|
||||
class TeslaFleetVehicleEntity(TeslaFleetEntity):
|
||||
"""Parent class for TeslaFleet Vehicle entities."""
|
||||
|
||||
_last_update: int = 0
|
||||
@@ -132,7 +128,7 @@ class TeslaFleetVehicleEntity(TeslaFleetEntity[VehicleFleet]):
|
||||
await wake_up_vehicle(self.vehicle)
|
||||
|
||||
|
||||
class TeslaFleetEnergyLiveEntity(TeslaFleetEntity[EnergySite]):
|
||||
class TeslaFleetEnergyLiveEntity(TeslaFleetEntity):
|
||||
"""Parent class for TeslaFleet Energy Site Live entities."""
|
||||
|
||||
def __init__(
|
||||
@@ -147,7 +143,7 @@ class TeslaFleetEnergyLiveEntity(TeslaFleetEntity[EnergySite]):
|
||||
super().__init__(data.live_coordinator, data.api, key)
|
||||
|
||||
|
||||
class TeslaFleetEnergyHistoryEntity(TeslaFleetEntity[EnergySite]):
|
||||
class TeslaFleetEnergyHistoryEntity(TeslaFleetEntity):
|
||||
"""Parent class for TeslaFleet Energy Site History entities."""
|
||||
|
||||
def __init__(
|
||||
@@ -162,7 +158,7 @@ class TeslaFleetEnergyHistoryEntity(TeslaFleetEntity[EnergySite]):
|
||||
super().__init__(data.history_coordinator, data.api, key)
|
||||
|
||||
|
||||
class TeslaFleetEnergyInfoEntity(TeslaFleetEntity[EnergySite]):
|
||||
class TeslaFleetEnergyInfoEntity(TeslaFleetEntity):
|
||||
"""Parent class for TeslaFleet Energy Site Info entities."""
|
||||
|
||||
def __init__(
|
||||
@@ -178,7 +174,7 @@ class TeslaFleetEnergyInfoEntity(TeslaFleetEntity[EnergySite]):
|
||||
|
||||
|
||||
class TeslaFleetWallConnectorEntity(
|
||||
TeslaFleetEntity[EnergySite], CoordinatorEntity[TeslaFleetEnergySiteLiveCoordinator]
|
||||
TeslaFleetEntity, CoordinatorEntity[TeslaFleetEnergySiteLiveCoordinator]
|
||||
):
|
||||
"""Parent class for Tesla Fleet Wall Connector entities."""
|
||||
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["tesla-fleet-api"],
|
||||
"requirements": ["tesla-fleet-api==1.4.2"]
|
||||
"requirements": ["tesla-fleet-api==1.3.2"]
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ PARALLEL_UPDATES = 0
|
||||
class TeslaFleetNumberVehicleEntityDescription(NumberEntityDescription):
|
||||
"""Describes TeslaFleet Number entity."""
|
||||
|
||||
func: Callable[[VehicleFleet, int], Awaitable[Any]]
|
||||
func: Callable[[VehicleFleet, float], Awaitable[Any]]
|
||||
native_min_value: float
|
||||
native_max_value: float
|
||||
min_key: str | None = None
|
||||
@@ -74,19 +74,19 @@ VEHICLE_DESCRIPTIONS: tuple[TeslaFleetNumberVehicleEntityDescription, ...] = (
|
||||
class TeslaFleetNumberBatteryEntityDescription(NumberEntityDescription):
|
||||
"""Describes TeslaFleet Number entity."""
|
||||
|
||||
func: Callable[[EnergySite, int], Awaitable[Any]]
|
||||
func: Callable[[EnergySite, float], Awaitable[Any]]
|
||||
requires: str | None = None
|
||||
|
||||
|
||||
ENERGY_INFO_DESCRIPTIONS: tuple[TeslaFleetNumberBatteryEntityDescription, ...] = (
|
||||
TeslaFleetNumberBatteryEntityDescription(
|
||||
key="backup_reserve_percent",
|
||||
func=lambda api, value: api.backup(value),
|
||||
func=lambda api, value: api.backup(int(value)),
|
||||
requires="components_battery",
|
||||
),
|
||||
TeslaFleetNumberBatteryEntityDescription(
|
||||
key="off_grid_vehicle_charging_reserve_percent",
|
||||
func=lambda api, value: api.off_grid_vehicle_charging_reserve(value),
|
||||
func=lambda api, value: api.off_grid_vehicle_charging_reserve(int(value)),
|
||||
requires="components_off_grid_vehicle_charging_reserve_supported",
|
||||
),
|
||||
)
|
||||
|
||||
@@ -136,16 +136,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) -
|
||||
# Remove the protobuff 'cached_data' that we do not use to save memory
|
||||
product.pop("cached_data", None)
|
||||
vin = product["vin"]
|
||||
vehicle = teslemetry.vehicles.create(vin)
|
||||
coordinator = TeslemetryVehicleDataCoordinator(
|
||||
hass, entry, vehicle, product
|
||||
)
|
||||
api = teslemetry.vehicles.create(vin)
|
||||
coordinator = TeslemetryVehicleDataCoordinator(hass, entry, api, product)
|
||||
device = DeviceInfo(
|
||||
identifiers={(DOMAIN, vin)},
|
||||
manufacturer="Tesla",
|
||||
configuration_url="https://teslemetry.com/console",
|
||||
name=product["display_name"],
|
||||
model=vehicle.model,
|
||||
model=api.model,
|
||||
serial_number=vin,
|
||||
)
|
||||
current_devices.add((DOMAIN, vin))
|
||||
@@ -170,7 +168,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) -
|
||||
|
||||
vehicles.append(
|
||||
TeslemetryVehicleData(
|
||||
api=vehicle,
|
||||
api=api,
|
||||
config_entry=entry,
|
||||
coordinator=coordinator,
|
||||
poll=poll,
|
||||
@@ -196,7 +194,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) -
|
||||
)
|
||||
continue
|
||||
|
||||
energy_site = teslemetry.energySites.create(site_id)
|
||||
api = teslemetry.energySites.create(site_id)
|
||||
device = DeviceInfo(
|
||||
identifiers={(DOMAIN, str(site_id))},
|
||||
manufacturer="Tesla",
|
||||
@@ -212,7 +210,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) -
|
||||
|
||||
# Check live status endpoint works before creating its coordinator
|
||||
try:
|
||||
live_status = (await energy_site.live_status())["response"]
|
||||
live_status = (await api.live_status())["response"]
|
||||
except (InvalidToken, Forbidden, SubscriptionRequired) as e:
|
||||
raise ConfigEntryAuthFailed from e
|
||||
except TeslaFleetError as e:
|
||||
@@ -220,19 +218,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: TeslemetryConfigEntry) -
|
||||
|
||||
energysites.append(
|
||||
TeslemetryEnergyData(
|
||||
api=energy_site,
|
||||
api=api,
|
||||
live_coordinator=(
|
||||
TeslemetryEnergySiteLiveCoordinator(
|
||||
hass, entry, energy_site, live_status
|
||||
hass, entry, api, live_status
|
||||
)
|
||||
if isinstance(live_status, dict)
|
||||
else None
|
||||
),
|
||||
info_coordinator=TeslemetryEnergySiteInfoCoordinator(
|
||||
hass, entry, energy_site, product
|
||||
hass, entry, api, product
|
||||
),
|
||||
history_coordinator=(
|
||||
TeslemetryEnergyHistoryCoordinator(hass, entry, energy_site)
|
||||
TeslemetryEnergyHistoryCoordinator(hass, entry, api)
|
||||
if powerwall
|
||||
else None
|
||||
),
|
||||
@@ -316,7 +314,7 @@ async def async_migrate_entry(
|
||||
# Convert legacy access token to OAuth tokens using migrate endpoint
|
||||
try:
|
||||
data = await Teslemetry(session, access_token).migrate_to_oauth(
|
||||
CLIENT_ID, hass.config.location_name
|
||||
CLIENT_ID, access_token, hass.config.location_name
|
||||
)
|
||||
except (ClientError, TypeError) as e:
|
||||
raise ConfigEntryAuthFailed from e
|
||||
|
||||
@@ -7,11 +7,7 @@ from typing import TYPE_CHECKING, Any
|
||||
|
||||
from tesla_fleet_api.const import TeslaEnergyPeriod, VehicleDataEndpoint
|
||||
from tesla_fleet_api.exceptions import (
|
||||
GatewayTimeout,
|
||||
InvalidResponse,
|
||||
InvalidToken,
|
||||
RateLimited,
|
||||
ServiceUnavailable,
|
||||
SubscriptionRequired,
|
||||
TeslaFleetError,
|
||||
)
|
||||
@@ -27,22 +23,6 @@ if TYPE_CHECKING:
|
||||
from .const import ENERGY_HISTORY_FIELDS, LOGGER
|
||||
from .helpers import flatten
|
||||
|
||||
RETRY_EXCEPTIONS = (
|
||||
InvalidResponse,
|
||||
RateLimited,
|
||||
ServiceUnavailable,
|
||||
GatewayTimeout,
|
||||
)
|
||||
|
||||
|
||||
def _get_retry_after(e: TeslaFleetError) -> float:
|
||||
"""Calculate wait time from exception."""
|
||||
if isinstance(e.data, dict):
|
||||
if after := e.data.get("after"):
|
||||
return float(after)
|
||||
return 10.0
|
||||
|
||||
|
||||
VEHICLE_INTERVAL = timedelta(seconds=60)
|
||||
VEHICLE_WAIT = timedelta(minutes=15)
|
||||
ENERGY_LIVE_INTERVAL = timedelta(seconds=30)
|
||||
@@ -89,14 +69,14 @@ class TeslemetryVehicleDataCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
|
||||
async def _async_update_data(self) -> dict[str, Any]:
|
||||
"""Update vehicle data using Teslemetry API."""
|
||||
|
||||
try:
|
||||
data = (await self.api.vehicle_data(endpoints=ENDPOINTS))["response"]
|
||||
except (InvalidToken, SubscriptionRequired) as e:
|
||||
raise ConfigEntryAuthFailed from e
|
||||
except RETRY_EXCEPTIONS as e:
|
||||
raise UpdateFailed(e.message, retry_after=_get_retry_after(e)) from e
|
||||
except TeslaFleetError as e:
|
||||
raise UpdateFailed(e.message) from e
|
||||
|
||||
return flatten(data)
|
||||
|
||||
|
||||
@@ -131,18 +111,19 @@ class TeslemetryEnergySiteLiveCoordinator(DataUpdateCoordinator[dict[str, Any]])
|
||||
|
||||
async def _async_update_data(self) -> dict[str, Any]:
|
||||
"""Update energy site data using Teslemetry API."""
|
||||
|
||||
try:
|
||||
data = (await self.api.live_status())["response"]
|
||||
except (InvalidToken, SubscriptionRequired) as e:
|
||||
raise ConfigEntryAuthFailed from e
|
||||
except RETRY_EXCEPTIONS as e:
|
||||
raise UpdateFailed(e.message, retry_after=_get_retry_after(e)) from e
|
||||
except TeslaFleetError as e:
|
||||
raise UpdateFailed(e.message) from e
|
||||
|
||||
# Convert Wall Connectors from array to dict
|
||||
data["wall_connectors"] = {
|
||||
wc["din"]: wc for wc in (data.get("wall_connectors") or [])
|
||||
}
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@@ -171,14 +152,14 @@ class TeslemetryEnergySiteInfoCoordinator(DataUpdateCoordinator[dict[str, Any]])
|
||||
|
||||
async def _async_update_data(self) -> dict[str, Any]:
|
||||
"""Update energy site data using Teslemetry API."""
|
||||
|
||||
try:
|
||||
data = (await self.api.site_info())["response"]
|
||||
except (InvalidToken, SubscriptionRequired) as e:
|
||||
raise ConfigEntryAuthFailed from e
|
||||
except RETRY_EXCEPTIONS as e:
|
||||
raise UpdateFailed(e.message, retry_after=_get_retry_after(e)) from e
|
||||
except TeslaFleetError as e:
|
||||
raise UpdateFailed(e.message) from e
|
||||
|
||||
return flatten(data)
|
||||
|
||||
|
||||
@@ -206,12 +187,11 @@ class TeslemetryEnergyHistoryCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
|
||||
async def _async_update_data(self) -> dict[str, Any]:
|
||||
"""Update energy site data using Teslemetry API."""
|
||||
|
||||
try:
|
||||
data = (await self.api.energy_history(TeslaEnergyPeriod.DAY))["response"]
|
||||
except (InvalidToken, SubscriptionRequired) as e:
|
||||
raise ConfigEntryAuthFailed from e
|
||||
except RETRY_EXCEPTIONS as e:
|
||||
raise UpdateFailed(e.message, retry_after=_get_retry_after(e)) from e
|
||||
except TeslaFleetError as e:
|
||||
raise UpdateFailed(e.message) from e
|
||||
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["tesla-fleet-api"],
|
||||
"requirements": ["tesla-fleet-api==1.4.2", "teslemetry-stream==0.9.0"]
|
||||
"requirements": ["tesla-fleet-api==1.3.2", "teslemetry-stream==0.9.0"]
|
||||
}
|
||||
|
||||
@@ -149,7 +149,7 @@ def async_setup_services(hass: HomeAssistant) -> None:
|
||||
config = async_get_config_for_device(hass, device)
|
||||
vehicle = async_get_vehicle_for_entry(hass, device, config)
|
||||
|
||||
time: int
|
||||
time: int | None = None
|
||||
# Convert time to minutes since minute
|
||||
if "time" in call.data:
|
||||
(hours, minutes, *_seconds) = call.data["time"].split(":")
|
||||
@@ -158,8 +158,6 @@ def async_setup_services(hass: HomeAssistant) -> None:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN, translation_key="set_scheduled_charging_time"
|
||||
)
|
||||
else:
|
||||
time = 0
|
||||
|
||||
await handle_vehicle_command(
|
||||
vehicle.api.set_scheduled_charging(enable=call.data["enable"], time=time)
|
||||
@@ -200,8 +198,6 @@ def async_setup_services(hass: HomeAssistant) -> None:
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="set_scheduled_departure_preconditioning",
|
||||
)
|
||||
else:
|
||||
departure_time = 0
|
||||
|
||||
# Off peak charging
|
||||
off_peak_charging_enabled = call.data.get(ATTR_OFF_PEAK_CHARGING_ENABLED, False)
|
||||
@@ -218,8 +214,6 @@ def async_setup_services(hass: HomeAssistant) -> None:
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="set_scheduled_departure_off_peak",
|
||||
)
|
||||
else:
|
||||
end_off_peak_time = 0
|
||||
|
||||
await handle_vehicle_command(
|
||||
vehicle.api.set_scheduled_departure(
|
||||
@@ -258,7 +252,9 @@ def async_setup_services(hass: HomeAssistant) -> None:
|
||||
vehicle = async_get_vehicle_for_entry(hass, device, config)
|
||||
|
||||
await handle_vehicle_command(
|
||||
vehicle.api.set_valet_mode(call.data["enable"], call.data["pin"])
|
||||
vehicle.api.set_valet_mode(
|
||||
call.data.get("enable"), call.data.get("pin", "")
|
||||
)
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
@@ -280,14 +276,14 @@ def async_setup_services(hass: HomeAssistant) -> None:
|
||||
config = async_get_config_for_device(hass, device)
|
||||
vehicle = async_get_vehicle_for_entry(hass, device, config)
|
||||
|
||||
enable = call.data["enable"]
|
||||
enable = call.data.get("enable")
|
||||
if enable is True:
|
||||
await handle_vehicle_command(
|
||||
vehicle.api.speed_limit_activate(call.data["pin"])
|
||||
vehicle.api.speed_limit_activate(call.data.get("pin"))
|
||||
)
|
||||
elif enable is False:
|
||||
await handle_vehicle_command(
|
||||
vehicle.api.speed_limit_deactivate(call.data["pin"])
|
||||
vehicle.api.speed_limit_deactivate(call.data.get("pin"))
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
@@ -310,7 +306,7 @@ def async_setup_services(hass: HomeAssistant) -> None:
|
||||
site = async_get_energy_site_for_entry(hass, device, config)
|
||||
|
||||
resp = await handle_command(
|
||||
site.api.time_of_use_settings(call.data[ATTR_TOU_SETTINGS])
|
||||
site.api.time_of_use_settings(call.data.get(ATTR_TOU_SETTINGS))
|
||||
)
|
||||
if "error" in resp:
|
||||
raise HomeAssistantError(
|
||||
|
||||
@@ -1127,15 +1127,6 @@
|
||||
"no_vehicle_data_for_device": {
|
||||
"message": "No vehicle data for device ID: {device_id}"
|
||||
},
|
||||
"set_scheduled_charging_time": {
|
||||
"message": "Scheduled charging time is required when enabling"
|
||||
},
|
||||
"set_scheduled_departure_off_peak": {
|
||||
"message": "Off-peak charging end time is required when enabling"
|
||||
},
|
||||
"set_scheduled_departure_preconditioning": {
|
||||
"message": "Preconditioning departure time is required when enabling"
|
||||
},
|
||||
"wake_up_failed": {
|
||||
"message": "Failed to wake up vehicle: {message}"
|
||||
},
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["tessie", "tesla-fleet-api"],
|
||||
"requirements": ["tessie-api==0.1.1", "tesla-fleet-api==1.4.2"]
|
||||
"requirements": ["tessie-api==0.1.1", "tesla-fleet-api==1.3.2"]
|
||||
}
|
||||
|
||||
@@ -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
10
mypy.ini
generated
@@ -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
|
||||
|
||||
4
requirements_all.txt
generated
4
requirements_all.txt
generated
@@ -2045,7 +2045,7 @@ pyfibaro==0.8.3
|
||||
pyfido==2.1.2
|
||||
|
||||
# homeassistant.components.firefly_iii
|
||||
pyfirefly==0.1.11
|
||||
pyfirefly==0.1.10
|
||||
|
||||
# homeassistant.components.fireservicerota
|
||||
pyfireservicerota==0.0.46
|
||||
@@ -2990,7 +2990,7 @@ temperusb==1.6.1
|
||||
# homeassistant.components.tesla_fleet
|
||||
# homeassistant.components.teslemetry
|
||||
# homeassistant.components.tessie
|
||||
tesla-fleet-api==1.4.2
|
||||
tesla-fleet-api==1.3.2
|
||||
|
||||
# homeassistant.components.powerwall
|
||||
tesla-powerwall==0.5.2
|
||||
|
||||
4
requirements_test_all.txt
generated
4
requirements_test_all.txt
generated
@@ -1734,7 +1734,7 @@ pyfibaro==0.8.3
|
||||
pyfido==2.1.2
|
||||
|
||||
# homeassistant.components.firefly_iii
|
||||
pyfirefly==0.1.11
|
||||
pyfirefly==0.1.10
|
||||
|
||||
# homeassistant.components.fireservicerota
|
||||
pyfireservicerota==0.0.46
|
||||
@@ -2496,7 +2496,7 @@ temperusb==1.6.1
|
||||
# homeassistant.components.tesla_fleet
|
||||
# homeassistant.components.teslemetry
|
||||
# homeassistant.components.tessie
|
||||
tesla-fleet-api==1.4.2
|
||||
tesla-fleet-api==1.3.2
|
||||
|
||||
# homeassistant.components.powerwall
|
||||
tesla-powerwall==0.5.2
|
||||
|
||||
@@ -11,7 +11,6 @@ from homeassistant.components.generic_thermostat.const import (
|
||||
CONF_COLD_TOLERANCE,
|
||||
CONF_HEATER,
|
||||
CONF_HOT_TOLERANCE,
|
||||
CONF_KEEP_ALIVE,
|
||||
CONF_PRESETS,
|
||||
CONF_SENSOR,
|
||||
DOMAIN,
|
||||
@@ -86,7 +85,6 @@ async def test_options(hass: HomeAssistant, snapshot: SnapshotAssertion) -> None
|
||||
CONF_AC_MODE: False,
|
||||
CONF_COLD_TOLERANCE: 0.3,
|
||||
CONF_HOT_TOLERANCE: 0.3,
|
||||
CONF_KEEP_ALIVE: {"seconds": 60},
|
||||
CONF_PRESETS[PRESET_AWAY]: 20,
|
||||
},
|
||||
title="My dehumidifier",
|
||||
@@ -182,46 +180,3 @@ async def test_config_flow_preset_accepts_float(
|
||||
"name": "My thermostat",
|
||||
"target_sensor": "sensor.temperature",
|
||||
}
|
||||
|
||||
|
||||
async def test_config_flow_with_keep_alive(hass: HomeAssistant) -> None:
|
||||
"""Test the config flow when keep_alive is set."""
|
||||
with patch(
|
||||
"homeassistant.components.generic_thermostat.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
# Keep_alive input data for test
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_NAME: "My thermostat",
|
||||
CONF_HEATER: "switch.run",
|
||||
CONF_SENSOR: "sensor.temperature",
|
||||
CONF_AC_MODE: False,
|
||||
CONF_COLD_TOLERANCE: 0.3,
|
||||
CONF_HOT_TOLERANCE: 0.3,
|
||||
CONF_KEEP_ALIVE: {"seconds": 60},
|
||||
},
|
||||
)
|
||||
|
||||
# Complete config flow
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_PRESETS[PRESET_AWAY]: 21,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] == "create_entry"
|
||||
|
||||
val = result["options"].get(CONF_KEEP_ALIVE)
|
||||
assert val is not None
|
||||
assert isinstance(val, dict)
|
||||
assert val == {"seconds": 60}
|
||||
|
||||
await hass.async_block_till_done()
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -68,20 +68,6 @@ def mock_hdfury_client() -> Generator[AsyncMock]:
|
||||
"portseltx0": "0",
|
||||
"portseltx1": "4",
|
||||
"opmode": "0",
|
||||
"RX0": "4K59.937 593MHz 422 BT2020 12b 2.2",
|
||||
"RX1": "no signal",
|
||||
"TX0": "4K59.937 593MHz 422 BT2020 12b 2.2",
|
||||
"TX1": "4K59.937 593MHz 422 BT2020 12b 2.2",
|
||||
"AUD0": "bitstream 48kHz",
|
||||
"AUD1": "bitstream 48kHz",
|
||||
"AUDOUT": "bitstream 48kHz",
|
||||
"EARCRX": "eARC/ARC not active",
|
||||
"SINK0": "LG TV SSCR2: 4K120 444 FRL6 VRR DSC ALLM DV HDR10 HLG",
|
||||
"EDIDA0": "MAT Atmos, DD Atmos, DD, DTS:X+IMAX, DTSHD, DTS, LPCM 2.0 192kHz 24b",
|
||||
"SINK1": "Signify FCD: 4K60 444 DV HDR10+ HLG",
|
||||
"EDIDA1": "DD, DTS, LPCM 2.0 48kHz 24b",
|
||||
"SINK2": "Bose CineMate: 4K60 420 ",
|
||||
"EDIDA2": "DD, DTS, LPCM 7.1 96kHz 24b",
|
||||
}
|
||||
)
|
||||
coord_client.get_config = AsyncMock(
|
||||
|
||||
@@ -22,20 +22,6 @@
|
||||
'relay': '0',
|
||||
}),
|
||||
'info': dict({
|
||||
'AUD0': 'bitstream 48kHz',
|
||||
'AUD1': 'bitstream 48kHz',
|
||||
'AUDOUT': 'bitstream 48kHz',
|
||||
'EARCRX': 'eARC/ARC not active',
|
||||
'EDIDA0': 'MAT Atmos, DD Atmos, DD, DTS:X+IMAX, DTSHD, DTS, LPCM 2.0 192kHz 24b',
|
||||
'EDIDA1': 'DD, DTS, LPCM 2.0 48kHz 24b',
|
||||
'EDIDA2': 'DD, DTS, LPCM 7.1 96kHz 24b',
|
||||
'RX0': '4K59.937 593MHz 422 BT2020 12b 2.2',
|
||||
'RX1': 'no signal',
|
||||
'SINK0': 'LG TV SSCR2: 4K120 444 FRL6 VRR DSC ALLM DV HDR10 HLG',
|
||||
'SINK1': 'Signify FCD: 4K60 444 DV HDR10+ HLG',
|
||||
'SINK2': 'Bose CineMate: 4K60 420 ',
|
||||
'TX0': '4K59.937 593MHz 422 BT2020 12b 2.2',
|
||||
'TX1': '4K59.937 593MHz 422 BT2020 12b 2.2',
|
||||
'opmode': '0',
|
||||
'portseltx0': '0',
|
||||
'portseltx1': '4',
|
||||
|
||||
@@ -1,673 +0,0 @@
|
||||
# serializer version: 1
|
||||
# name: test_sensor_entities[sensor.hdfury_vrroom_02_audio_output-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': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.hdfury_vrroom_02_audio_output',
|
||||
'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': 'Audio output',
|
||||
'platform': 'hdfury',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'audout',
|
||||
'unique_id': '000123456789_AUDOUT',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities[sensor.hdfury_vrroom_02_audio_output-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'HDFury VRROOM-02 Audio output',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.hdfury_vrroom_02_audio_output',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'bitstream 48kHz',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities[sensor.hdfury_vrroom_02_audio_tx0-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': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.hdfury_vrroom_02_audio_tx0',
|
||||
'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': 'Audio TX0',
|
||||
'platform': 'hdfury',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'aud0',
|
||||
'unique_id': '000123456789_AUD0',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities[sensor.hdfury_vrroom_02_audio_tx0-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'HDFury VRROOM-02 Audio TX0',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.hdfury_vrroom_02_audio_tx0',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'bitstream 48kHz',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities[sensor.hdfury_vrroom_02_audio_tx1-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': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.hdfury_vrroom_02_audio_tx1',
|
||||
'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': 'Audio TX1',
|
||||
'platform': 'hdfury',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'aud1',
|
||||
'unique_id': '000123456789_AUD1',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities[sensor.hdfury_vrroom_02_audio_tx1-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'HDFury VRROOM-02 Audio TX1',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.hdfury_vrroom_02_audio_tx1',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'bitstream 48kHz',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities[sensor.hdfury_vrroom_02_earc_arc_status-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': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.hdfury_vrroom_02_earc_arc_status',
|
||||
'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': 'eARC/ARC status',
|
||||
'platform': 'hdfury',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'earcrx',
|
||||
'unique_id': '000123456789_EARCRX',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities[sensor.hdfury_vrroom_02_earc_arc_status-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'HDFury VRROOM-02 eARC/ARC status',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.hdfury_vrroom_02_earc_arc_status',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'eARC/ARC not active',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities[sensor.hdfury_vrroom_02_edid_aud-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': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.hdfury_vrroom_02_edid_aud',
|
||||
'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': 'EDID AUD',
|
||||
'platform': 'hdfury',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'sink2',
|
||||
'unique_id': '000123456789_SINK2',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities[sensor.hdfury_vrroom_02_edid_aud-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'HDFury VRROOM-02 EDID AUD',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.hdfury_vrroom_02_edid_aud',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'Bose CineMate: 4K60 420 ',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities[sensor.hdfury_vrroom_02_edid_auda-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': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.hdfury_vrroom_02_edid_auda',
|
||||
'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': 'EDID AUDA',
|
||||
'platform': 'hdfury',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'edida2',
|
||||
'unique_id': '000123456789_EDIDA2',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities[sensor.hdfury_vrroom_02_edid_auda-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'HDFury VRROOM-02 EDID AUDA',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.hdfury_vrroom_02_edid_auda',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'DD, DTS, LPCM 7.1 96kHz 24b',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities[sensor.hdfury_vrroom_02_edid_tx0-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': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.hdfury_vrroom_02_edid_tx0',
|
||||
'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': 'EDID TX0',
|
||||
'platform': 'hdfury',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'sink0',
|
||||
'unique_id': '000123456789_SINK0',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities[sensor.hdfury_vrroom_02_edid_tx0-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'HDFury VRROOM-02 EDID TX0',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.hdfury_vrroom_02_edid_tx0',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'LG TV SSCR2: 4K120 444 FRL6 VRR DSC ALLM DV HDR10 HLG',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities[sensor.hdfury_vrroom_02_edid_tx1-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': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.hdfury_vrroom_02_edid_tx1',
|
||||
'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': 'EDID TX1',
|
||||
'platform': 'hdfury',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'sink1',
|
||||
'unique_id': '000123456789_SINK1',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities[sensor.hdfury_vrroom_02_edid_tx1-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'HDFury VRROOM-02 EDID TX1',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.hdfury_vrroom_02_edid_tx1',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'Signify FCD: 4K60 444 DV HDR10+ HLG',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities[sensor.hdfury_vrroom_02_edid_txa0-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': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.hdfury_vrroom_02_edid_txa0',
|
||||
'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': 'EDID TXA0',
|
||||
'platform': 'hdfury',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'edida0',
|
||||
'unique_id': '000123456789_EDIDA0',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities[sensor.hdfury_vrroom_02_edid_txa0-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'HDFury VRROOM-02 EDID TXA0',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.hdfury_vrroom_02_edid_txa0',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'MAT Atmos, DD Atmos, DD, DTS:X+IMAX, DTSHD, DTS, LPCM 2.0 192kHz 24b',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities[sensor.hdfury_vrroom_02_edid_txa1-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': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.hdfury_vrroom_02_edid_txa1',
|
||||
'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': 'EDID TXA1',
|
||||
'platform': 'hdfury',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'edida1',
|
||||
'unique_id': '000123456789_EDIDA1',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities[sensor.hdfury_vrroom_02_edid_txa1-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'HDFury VRROOM-02 EDID TXA1',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.hdfury_vrroom_02_edid_txa1',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'DD, DTS, LPCM 2.0 48kHz 24b',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities[sensor.hdfury_vrroom_02_input_rx0-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': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.hdfury_vrroom_02_input_rx0',
|
||||
'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': 'Input RX0',
|
||||
'platform': 'hdfury',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'rx0',
|
||||
'unique_id': '000123456789_RX0',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities[sensor.hdfury_vrroom_02_input_rx0-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'HDFury VRROOM-02 Input RX0',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.hdfury_vrroom_02_input_rx0',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '4K59.937 593MHz 422 BT2020 12b 2.2',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities[sensor.hdfury_vrroom_02_input_rx1-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': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.hdfury_vrroom_02_input_rx1',
|
||||
'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': 'Input RX1',
|
||||
'platform': 'hdfury',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'rx1',
|
||||
'unique_id': '000123456789_RX1',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities[sensor.hdfury_vrroom_02_input_rx1-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'HDFury VRROOM-02 Input RX1',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.hdfury_vrroom_02_input_rx1',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'no signal',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities[sensor.hdfury_vrroom_02_output_tx0-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': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.hdfury_vrroom_02_output_tx0',
|
||||
'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': 'Output TX0',
|
||||
'platform': 'hdfury',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'tx0',
|
||||
'unique_id': '000123456789_TX0',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities[sensor.hdfury_vrroom_02_output_tx0-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'HDFury VRROOM-02 Output TX0',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.hdfury_vrroom_02_output_tx0',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '4K59.937 593MHz 422 BT2020 12b 2.2',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities[sensor.hdfury_vrroom_02_output_tx1-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': 'sensor',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'sensor.hdfury_vrroom_02_output_tx1',
|
||||
'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': 'Output TX1',
|
||||
'platform': 'hdfury',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'tx1',
|
||||
'unique_id': '000123456789_TX1',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_sensor_entities[sensor.hdfury_vrroom_02_output_tx1-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'HDFury VRROOM-02 Output TX1',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.hdfury_vrroom_02_output_tx1',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '4K59.937 593MHz 422 BT2020 12b 2.2',
|
||||
})
|
||||
# ---
|
||||
@@ -1,25 +0,0 @@
|
||||
"""Tests for the HDFury sensor platform."""
|
||||
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
import homeassistant.helpers.entity_registry as er
|
||||
|
||||
from . import setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||
async def test_sensor_entities(
|
||||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test HDFury sensor entities."""
|
||||
|
||||
await setup_integration(hass, mock_config_entry, [Platform.SENSOR])
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
@@ -20,13 +20,6 @@ from homeassistant.components.homekit.const import (
|
||||
TYPE_SWITCH,
|
||||
TYPE_VALVE,
|
||||
)
|
||||
from homeassistant.components.homekit.type_sensors import (
|
||||
AirQualitySensor,
|
||||
CarbonDioxideSensor,
|
||||
PM10Sensor,
|
||||
PM25Sensor,
|
||||
TemperatureSensor,
|
||||
)
|
||||
from homeassistant.components.media_player import (
|
||||
MediaPlayerDeviceClass,
|
||||
MediaPlayerEntityFeature,
|
||||
@@ -49,20 +42,6 @@ from homeassistant.const import (
|
||||
from homeassistant.core import State
|
||||
|
||||
|
||||
def get_identified_type(entity_id, attrs, config=None):
|
||||
"""Helper to return the accessory type name selected by get_accessory."""
|
||||
|
||||
def passthrough(type: type):
|
||||
return lambda *args, **kwargs: type
|
||||
|
||||
# Patch TYPES so that get_accessory returns a type instead of an instance.
|
||||
with patch.dict(
|
||||
TYPES, {type_name: passthrough(v) for type_name, v in TYPES.items()}
|
||||
):
|
||||
entity_state = State(entity_id, "irrelevant", attrs)
|
||||
return get_accessory(None, None, entity_state, 2, config or {})
|
||||
|
||||
|
||||
def test_not_supported(caplog: pytest.LogCaptureFixture) -> None:
|
||||
"""Test if none is returned if entity isn't supported."""
|
||||
# not supported entity
|
||||
@@ -446,58 +425,3 @@ def test_type_camera(type_name, entity_id, state, attrs) -> None:
|
||||
entity_state = State(entity_id, state, attrs)
|
||||
get_accessory(None, None, entity_state, 2, {})
|
||||
assert mock_type.called
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("expected_type", "entity_id", "attrs"),
|
||||
[
|
||||
(
|
||||
PM10Sensor,
|
||||
"sensor.air_quality_pm25",
|
||||
{ATTR_DEVICE_CLASS: SensorDeviceClass.PM10},
|
||||
),
|
||||
(
|
||||
PM25Sensor,
|
||||
"sensor.air_quality_pm10",
|
||||
{ATTR_DEVICE_CLASS: SensorDeviceClass.PM25},
|
||||
),
|
||||
(
|
||||
AirQualitySensor,
|
||||
"sensor.co2_sensor",
|
||||
{ATTR_DEVICE_CLASS: SensorDeviceClass.GAS},
|
||||
),
|
||||
(
|
||||
CarbonDioxideSensor,
|
||||
"sensor.air_quality_gas",
|
||||
{ATTR_DEVICE_CLASS: SensorDeviceClass.CO2},
|
||||
),
|
||||
(
|
||||
TemperatureSensor,
|
||||
"sensor.random_sensor",
|
||||
{ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE},
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_explicit_device_class_takes_precedence(
|
||||
expected_type, entity_id, attrs
|
||||
) -> None:
|
||||
"""Test that explicit device_class takes precedence over entity_id hints."""
|
||||
identified_type = get_identified_type(entity_id, attrs=attrs)
|
||||
assert identified_type == expected_type
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("expected_type", "entity_id", "attrs"),
|
||||
[
|
||||
(PM10Sensor, "sensor.air_quality_pm10", {}),
|
||||
(PM25Sensor, "sensor.air_quality_pm25", {}),
|
||||
(AirQualitySensor, "sensor.air_quality_gas", {}),
|
||||
(CarbonDioxideSensor, "sensor.airmeter_co2", {}),
|
||||
],
|
||||
)
|
||||
def test_entity_id_fallback_when_no_device_class(
|
||||
expected_type, entity_id, attrs
|
||||
) -> None:
|
||||
"""Test that entity_id is used as fallback when device_class is not set."""
|
||||
identified_type = get_identified_type(entity_id, attrs=attrs)
|
||||
assert identified_type == expected_type
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -250,7 +250,7 @@
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'select',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_category': None,
|
||||
'entity_id': 'select.aqara_smart_lock_u200_operating_mode',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
@@ -687,7 +687,7 @@
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'select',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_category': None,
|
||||
'entity_id': 'select.mock_door_lock_operating_mode',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
@@ -866,7 +866,7 @@
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'select',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_category': None,
|
||||
'entity_id': 'select.mock_door_lock_with_unbolt_operating_mode',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
@@ -2571,7 +2571,7 @@
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'select',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_category': None,
|
||||
'entity_id': 'select.mock_lock_operating_mode',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
@@ -3826,7 +3826,7 @@
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'select',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_category': None,
|
||||
'entity_id': 'select.secuyou_smart_lock_operating_mode',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
|
||||
@@ -149,15 +149,7 @@ async def test_notify_works(
|
||||
"""Test notify works."""
|
||||
assert hass.services.has_service("notify", "mobile_app_test") is True
|
||||
await hass.services.async_call(
|
||||
"notify",
|
||||
"mobile_app_test",
|
||||
{
|
||||
"message": "Hello world",
|
||||
"title": "Demo",
|
||||
"target": ["mock-webhook_id"],
|
||||
"data": {"field1": "value1"},
|
||||
},
|
||||
blocking=True,
|
||||
"notify", "mobile_app_test", {"message": "Hello world"}, blocking=True
|
||||
)
|
||||
|
||||
assert len(aioclient_mock.mock_calls) == 1
|
||||
@@ -167,8 +159,6 @@ async def test_notify_works(
|
||||
|
||||
assert call_json["push_token"] == "PUSH_TOKEN"
|
||||
assert call_json["message"] == "Hello world"
|
||||
assert call_json["title"] == "Demo"
|
||||
assert call_json["data"] == {"field1": "value1"}
|
||||
assert call_json["registration_info"]["app_id"] == "io.homeassistant.mobile_app"
|
||||
assert call_json["registration_info"]["app_version"] == "1.0"
|
||||
assert call_json["registration_info"]["webhook_id"] == "mock-webhook_id"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -35,10 +35,7 @@ def mock_expires_at() -> int:
|
||||
|
||||
|
||||
def create_config_entry(
|
||||
expires_at: int,
|
||||
scopes: list[Scope],
|
||||
implementation: str = DOMAIN,
|
||||
region: str = "NA",
|
||||
expires_at: int, scopes: list[Scope], implementation: str = DOMAIN
|
||||
) -> MockConfigEntry:
|
||||
"""Create Tesla Fleet entry in Home Assistant."""
|
||||
access_token = jwt.encode(
|
||||
@@ -46,7 +43,7 @@ def create_config_entry(
|
||||
"sub": UID,
|
||||
"aud": [],
|
||||
"scp": scopes,
|
||||
"ou_code": region,
|
||||
"ou_code": "NA",
|
||||
},
|
||||
key="",
|
||||
algorithm="none",
|
||||
|
||||
@@ -230,52 +230,6 @@ async def test_vehicle_refresh_ratelimited(
|
||||
assert state.state == "unknown"
|
||||
|
||||
|
||||
async def test_vehicle_refresh_ratelimited_no_after(
|
||||
hass: HomeAssistant,
|
||||
normal_config_entry: MockConfigEntry,
|
||||
mock_vehicle_data: AsyncMock,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test coordinator refresh handles 429 without after."""
|
||||
|
||||
await setup_platform(hass, normal_config_entry)
|
||||
# mock_vehicle_data called once during setup
|
||||
assert mock_vehicle_data.call_count == 1
|
||||
|
||||
mock_vehicle_data.side_effect = RateLimited({})
|
||||
freezer.tick(VEHICLE_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Called again during refresh, failed with RateLimited
|
||||
assert mock_vehicle_data.call_count == 2
|
||||
|
||||
freezer.tick(VEHICLE_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Called again because skip refresh doesn't change interval
|
||||
assert mock_vehicle_data.call_count == 3
|
||||
|
||||
|
||||
async def test_init_invalid_region(
|
||||
hass: HomeAssistant,
|
||||
expires_at: int,
|
||||
) -> None:
|
||||
"""Test init with an invalid region in the token."""
|
||||
|
||||
# ou_code 'other' should be caught by the region validation and set to None
|
||||
config_entry = create_config_entry(
|
||||
expires_at, [Scope.VEHICLE_DEVICE_DATA], region="other"
|
||||
)
|
||||
|
||||
with patch("homeassistant.components.tesla_fleet.TeslaFleetApi") as mock_api:
|
||||
await setup_platform(hass, config_entry)
|
||||
# Check if TeslaFleetApi was called with region=None
|
||||
mock_api.assert_called()
|
||||
assert mock_api.call_args.kwargs.get("region") is None
|
||||
|
||||
|
||||
async def test_vehicle_sleep(
|
||||
hass: HomeAssistant,
|
||||
normal_config_entry: MockConfigEntry,
|
||||
|
||||
@@ -85,21 +85,6 @@ async def test_number_services(
|
||||
assert state.state == "60"
|
||||
call.assert_called_once()
|
||||
|
||||
# Test float conversion
|
||||
with patch(
|
||||
"tesla_fleet_api.tesla.VehicleFleet.set_charge_limit",
|
||||
return_value=COMMAND_OK,
|
||||
) as call:
|
||||
await hass.services.async_call(
|
||||
NUMBER_DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_VALUE: 60.5},
|
||||
blocking=True,
|
||||
)
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == "60"
|
||||
call.assert_called_once_with(60)
|
||||
|
||||
entity_id = "number.energy_site_backup_reserve"
|
||||
with patch(
|
||||
"tesla_fleet_api.tesla.EnergySite.backup",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"""Test the Teslemetry init."""
|
||||
|
||||
from copy import deepcopy
|
||||
import time
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
@@ -9,24 +8,17 @@ from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
from tesla_fleet_api.exceptions import (
|
||||
InvalidResponse,
|
||||
InvalidToken,
|
||||
RateLimited,
|
||||
SubscriptionRequired,
|
||||
TeslaFleetError,
|
||||
)
|
||||
|
||||
from homeassistant.components.teslemetry.const import CLIENT_ID, DOMAIN
|
||||
|
||||
# Coordinator constants
|
||||
from homeassistant.components.teslemetry.coordinator import (
|
||||
ENERGY_HISTORY_INTERVAL,
|
||||
ENERGY_LIVE_INTERVAL,
|
||||
VEHICLE_INTERVAL,
|
||||
)
|
||||
from homeassistant.components.teslemetry.coordinator import VEHICLE_INTERVAL
|
||||
from homeassistant.components.teslemetry.models import TeslemetryData
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import (
|
||||
CONF_ACCESS_TOKEN,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
STATE_UNAVAILABLE,
|
||||
@@ -37,16 +29,9 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from . import setup_platform
|
||||
from .const import (
|
||||
CONFIG_V1,
|
||||
ENERGY_HISTORY,
|
||||
LIVE_STATUS,
|
||||
PRODUCTS_MODERN,
|
||||
UNIQUE_ID,
|
||||
VEHICLE_DATA_ALT,
|
||||
)
|
||||
from .const import CONFIG_V1, PRODUCTS_MODERN, UNIQUE_ID, VEHICLE_DATA_ALT
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
ERRORS = [
|
||||
(InvalidToken, ConfigEntryState.SETUP_ERROR),
|
||||
@@ -334,7 +319,9 @@ async def test_migrate_from_version_1_success(hass: HomeAssistant) -> None:
|
||||
await hass.config_entries.async_setup(mock_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
mock_migrate.assert_called_once_with(CLIENT_ID, hass.config.location_name)
|
||||
mock_migrate.assert_called_once_with(
|
||||
CLIENT_ID, CONFIG_V1[CONF_ACCESS_TOKEN], hass.config.location_name
|
||||
)
|
||||
|
||||
assert mock_entry is not None
|
||||
assert mock_entry.version == 2
|
||||
@@ -369,7 +356,9 @@ async def test_migrate_from_version_1_token_endpoint_error(hass: HomeAssistant)
|
||||
await hass.config_entries.async_setup(mock_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
mock_migrate.assert_called_once_with(CLIENT_ID, hass.config.location_name)
|
||||
mock_migrate.assert_called_once_with(
|
||||
CLIENT_ID, CONFIG_V1[CONF_ACCESS_TOKEN], hass.config.location_name
|
||||
)
|
||||
|
||||
entry = hass.config_entries.async_get_entry(mock_entry.entry_id)
|
||||
assert entry is not None
|
||||
@@ -441,175 +430,3 @@ async def test_migrate_from_future_version_fails(hass: HomeAssistant) -> None:
|
||||
assert entry is not None
|
||||
assert entry.state is ConfigEntryState.MIGRATION_ERROR
|
||||
assert entry.version == 3 # Version should remain unchanged
|
||||
|
||||
|
||||
RETRY_EXCEPTIONS = [
|
||||
(RateLimited(data={"after": 5}), 5.0),
|
||||
(InvalidResponse(), 10.0),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("exception", "expected_retry_after"), RETRY_EXCEPTIONS)
|
||||
async def test_site_info_retry_exceptions(
|
||||
hass: HomeAssistant,
|
||||
mock_site_info: AsyncMock,
|
||||
exception: TeslaFleetError,
|
||||
expected_retry_after: float,
|
||||
) -> None:
|
||||
"""Test UpdateFailed with retry_after for site info coordinator."""
|
||||
mock_site_info.side_effect = exception
|
||||
entry = await setup_platform(hass)
|
||||
# Retry exceptions during first refresh cause setup retry
|
||||
assert entry.state is ConfigEntryState.SETUP_RETRY
|
||||
# API should only be called once (no manual retries)
|
||||
assert mock_site_info.call_count == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("exception", "expected_retry_after"), RETRY_EXCEPTIONS)
|
||||
async def test_vehicle_data_retry_exceptions(
|
||||
hass: HomeAssistant,
|
||||
mock_vehicle_data: AsyncMock,
|
||||
mock_legacy: AsyncMock,
|
||||
exception: TeslaFleetError,
|
||||
expected_retry_after: float,
|
||||
) -> None:
|
||||
"""Test UpdateFailed with retry_after for vehicle data coordinator."""
|
||||
mock_vehicle_data.side_effect = exception
|
||||
entry = await setup_platform(hass)
|
||||
# Retry exceptions during first refresh cause setup retry
|
||||
assert entry.state is ConfigEntryState.SETUP_RETRY
|
||||
# API should only be called once (no manual retries)
|
||||
assert mock_vehicle_data.call_count == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("exception", "expected_retry_after"), RETRY_EXCEPTIONS)
|
||||
async def test_live_status_coordinator_retry_exceptions(
|
||||
hass: HomeAssistant,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
mock_live_status: AsyncMock,
|
||||
exception: TeslaFleetError,
|
||||
expected_retry_after: float,
|
||||
) -> None:
|
||||
"""Test live status coordinator raises UpdateFailed with retry_after."""
|
||||
call_count = 0
|
||||
|
||||
def live_status_side_effect():
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
if call_count == 1:
|
||||
return deepcopy(LIVE_STATUS) # Initial call succeeds
|
||||
if call_count == 2:
|
||||
raise exception # Second call raises exception
|
||||
return deepcopy(LIVE_STATUS) # Subsequent calls succeed
|
||||
|
||||
mock_live_status.side_effect = live_status_side_effect
|
||||
|
||||
entry = await setup_platform(hass)
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
assert call_count == 1
|
||||
|
||||
# Trigger coordinator refresh - this will raise the exception
|
||||
freezer.tick(ENERGY_LIVE_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# API was called exactly once for this refresh (no manual retry loop)
|
||||
assert call_count == 2
|
||||
# Entry stays loaded - UpdateFailed with retry_after doesn't break the entry
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("exception", "expected_retry_after"), RETRY_EXCEPTIONS)
|
||||
async def test_energy_history_coordinator_retry_exceptions(
|
||||
hass: HomeAssistant,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
mock_energy_history: AsyncMock,
|
||||
exception: TeslaFleetError,
|
||||
expected_retry_after: float,
|
||||
) -> None:
|
||||
"""Test energy history coordinator raises UpdateFailed with retry_after."""
|
||||
call_count = 0
|
||||
|
||||
def energy_history_side_effect(*args, **kwargs):
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
if call_count == 1:
|
||||
raise exception # First call raises exception
|
||||
return ENERGY_HISTORY # Subsequent calls succeed
|
||||
|
||||
mock_energy_history.side_effect = energy_history_side_effect
|
||||
|
||||
entry = await setup_platform(hass)
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
# Energy history doesn't have first_refresh during setup
|
||||
assert call_count == 0
|
||||
|
||||
# Trigger first coordinator refresh - this will raise the exception
|
||||
freezer.tick(ENERGY_HISTORY_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# API was called exactly once (no manual retry loop)
|
||||
assert call_count == 1
|
||||
# Entry stays loaded - UpdateFailed with retry_after doesn't break the entry
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
|
||||
async def test_live_status_auth_error(
|
||||
hass: HomeAssistant,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test live status coordinator handles auth errors."""
|
||||
call_count = 0
|
||||
|
||||
def live_status_side_effect():
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
if call_count == 1:
|
||||
return deepcopy(LIVE_STATUS)
|
||||
raise InvalidToken
|
||||
|
||||
with patch(
|
||||
"tesla_fleet_api.tesla.energysite.EnergySite.live_status",
|
||||
side_effect=live_status_side_effect,
|
||||
):
|
||||
entry = await setup_platform(hass)
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
# Trigger a coordinator refresh by advancing time
|
||||
freezer.tick(ENERGY_LIVE_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Auth error triggers reauth flow
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
|
||||
async def test_live_status_generic_error(
|
||||
hass: HomeAssistant,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test live status coordinator handles generic TeslaFleetError."""
|
||||
call_count = 0
|
||||
|
||||
def live_status_side_effect():
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
if call_count == 1:
|
||||
return deepcopy(LIVE_STATUS)
|
||||
raise TeslaFleetError
|
||||
|
||||
with patch(
|
||||
"tesla_fleet_api.tesla.energysite.EnergySite.live_status",
|
||||
side_effect=live_status_side_effect,
|
||||
):
|
||||
entry = await setup_platform(hass)
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
# Trigger a coordinator refresh by advancing time
|
||||
freezer.tick(ENERGY_LIVE_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Entry stays loaded but coordinator will have failed
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
@@ -63,40 +63,6 @@ async def test_services(
|
||||
"sensor.energy_site_battery_power"
|
||||
).device_id
|
||||
|
||||
# Test set_scheduled_charging with enable=False (time should default to 0)
|
||||
with patch(
|
||||
"tesla_fleet_api.teslemetry.Vehicle.set_scheduled_charging",
|
||||
return_value=COMMAND_OK,
|
||||
) as set_scheduled_charging_off:
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_SCHEDULED_CHARGING,
|
||||
{
|
||||
CONF_DEVICE_ID: vehicle_device,
|
||||
ATTR_ENABLE: False,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
set_scheduled_charging_off.assert_called_once_with(enable=False, time=0)
|
||||
|
||||
# Test set_scheduled_departure with enable=False (times should default to 0)
|
||||
with patch(
|
||||
"tesla_fleet_api.teslemetry.Vehicle.set_scheduled_departure",
|
||||
return_value=COMMAND_OK,
|
||||
) as set_scheduled_departure_off:
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_SCHEDULED_DEPARTURE,
|
||||
{
|
||||
CONF_DEVICE_ID: vehicle_device,
|
||||
ATTR_ENABLE: False,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
set_scheduled_departure_off.assert_called_once_with(
|
||||
False, False, False, 0, False, False, 0
|
||||
)
|
||||
|
||||
with patch(
|
||||
"tesla_fleet_api.teslemetry.Vehicle.navigation_gps_request",
|
||||
return_value=COMMAND_OK,
|
||||
@@ -342,8 +308,6 @@ async def test_service_validation_errors(
|
||||
"""Tests that the custom services handle bad data."""
|
||||
|
||||
await setup_platform(hass)
|
||||
entity_registry = er.async_get(hass)
|
||||
vehicle_device = entity_registry.async_get("sensor.test_charging").device_id
|
||||
|
||||
# Bad device ID
|
||||
with pytest.raises(ServiceValidationError):
|
||||
@@ -356,39 +320,3 @@ async def test_service_validation_errors(
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
# Test set_scheduled_charging validation error (enable=True but no time)
|
||||
with pytest.raises(ServiceValidationError):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_SCHEDULED_CHARGING,
|
||||
{
|
||||
CONF_DEVICE_ID: vehicle_device,
|
||||
ATTR_ENABLE: True,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
# Test set_scheduled_departure validation error (preconditioning_enabled=True but no departure_time)
|
||||
with pytest.raises(ServiceValidationError):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_SCHEDULED_DEPARTURE,
|
||||
{
|
||||
CONF_DEVICE_ID: vehicle_device,
|
||||
ATTR_PRECONDITIONING_ENABLED: True,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
# Test set_scheduled_departure validation error (off_peak_charging_enabled=True but no end_off_peak_time)
|
||||
with pytest.raises(ServiceValidationError):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_SCHEDULED_DEPARTURE,
|
||||
{
|
||||
CONF_DEVICE_ID: vehicle_device,
|
||||
ATTR_OFF_PEAK_CHARGING_ENABLED: True,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user