Merge branch 'aioesphomeapi_3111' into sub_devices_esphome

This commit is contained in:
J. Nick Koston
2025-06-23 16:09:46 +02:00
64 changed files with 964 additions and 792 deletions

View File

@@ -18,7 +18,7 @@ from homeassistant.core import Event, HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.device_registry import DeviceEntry from homeassistant.helpers.device_registry import DeviceEntry
from .const import DOMAIN, GATEWAY_SERIAL_PATTERN, PLATFORMS from .const import DOMAIN, PLATFORMS
type DevoloHomeControlConfigEntry = ConfigEntry[list[HomeControl]] type DevoloHomeControlConfigEntry = ConfigEntry[list[HomeControl]]
@@ -33,10 +33,6 @@ async def async_setup_entry(
check_mydevolo_and_get_gateway_ids, mydevolo check_mydevolo_and_get_gateway_ids, mydevolo
) )
if entry.unique_id and GATEWAY_SERIAL_PATTERN.match(entry.unique_id):
uuid = await hass.async_add_executor_job(mydevolo.uuid)
hass.config_entries.async_update_entry(entry, unique_id=uuid)
def shutdown(event: Event) -> None: def shutdown(event: Event) -> None:
for gateway in entry.runtime_data: for gateway in entry.runtime_data:
gateway.websocket_disconnect( gateway.websocket_disconnect(

View File

@@ -1,7 +1,5 @@
"""Constants for the devolo_home_control integration.""" """Constants for the devolo_home_control integration."""
import re
from homeassistant.const import Platform from homeassistant.const import Platform
DOMAIN = "devolo_home_control" DOMAIN = "devolo_home_control"
@@ -14,5 +12,4 @@ PLATFORMS = [
Platform.SIREN, Platform.SIREN,
Platform.SWITCH, Platform.SWITCH,
] ]
GATEWAY_SERIAL_PATTERN = re.compile(r"\d{16}")
SUPPORTED_MODEL_TYPES = ["2600", "2601"] SUPPORTED_MODEL_TYPES = ["2600", "2601"]

View File

@@ -17,7 +17,7 @@
"mqtt": ["esphome/discover/#"], "mqtt": ["esphome/discover/#"],
"quality_scale": "platinum", "quality_scale": "platinum",
"requirements": [ "requirements": [
"aioesphomeapi==33.0.0", "aioesphomeapi==33.1.1",
"esphome-dashboard-api==1.3.0", "esphome-dashboard-api==1.3.0",
"bleak-esphome==2.16.0" "bleak-esphome==2.16.0"
], ],

View File

@@ -45,7 +45,9 @@ class ImgwPibFlowHandler(ConfigFlow, domain=DOMAIN):
try: try:
imgwpib = await ImgwPib.create( imgwpib = await ImgwPib.create(
client_session, hydrological_station_id=station_id client_session,
hydrological_station_id=station_id,
hydrological_details=False,
) )
hydrological_data = await imgwpib.get_hydrological_data() hydrological_data = await imgwpib.get_hydrological_data()
except (ClientError, TimeoutError, ApiError): except (ClientError, TimeoutError, ApiError):

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/imgw_pib", "documentation": "https://www.home-assistant.io/integrations/imgw_pib",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"quality_scale": "silver", "quality_scale": "silver",
"requirements": ["imgw_pib==1.0.10"] "requirements": ["imgw_pib==1.1.0"]
} }

View File

@@ -1,14 +1,13 @@
"""Support for LaMetric time.""" """Support for LaMetric time."""
from homeassistant.components import notify as hass_notify from homeassistant.components import notify as hass_notify
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME, Platform from homeassistant.const import CONF_NAME, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv, discovery from homeassistant.helpers import config_validation as cv, discovery
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN, PLATFORMS from .const import DOMAIN, PLATFORMS
from .coordinator import LaMetricDataUpdateCoordinator from .coordinator import LaMetricConfigEntry, LaMetricDataUpdateCoordinator
from .services import async_setup_services from .services import async_setup_services
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
@@ -17,16 +16,16 @@ CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the LaMetric integration.""" """Set up the LaMetric integration."""
async_setup_services(hass) async_setup_services(hass)
hass.data[DOMAIN] = {"hass_config": config}
return True return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: LaMetricConfigEntry) -> bool:
"""Set up LaMetric from a config entry.""" """Set up LaMetric from a config entry."""
coordinator = LaMetricDataUpdateCoordinator(hass, entry) coordinator = LaMetricDataUpdateCoordinator(hass, entry)
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()
hass.data[DOMAIN][entry.entry_id] = coordinator entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
# Set up notify platform, no entry support for notify component yet, # Set up notify platform, no entry support for notify component yet,
@@ -37,15 +36,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
Platform.NOTIFY, Platform.NOTIFY,
DOMAIN, DOMAIN,
{CONF_NAME: coordinator.data.name, "entry_id": entry.entry_id}, {CONF_NAME: coordinator.data.name, "entry_id": entry.entry_id},
hass.data[DOMAIN]["hass_config"], {},
) )
) )
return True return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: LaMetricConfigEntry) -> bool:
"""Unload LaMetric config entry.""" """Unload LaMetric config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
del hass.data[DOMAIN][entry.entry_id]
await hass_notify.async_reload(hass, DOMAIN) await hass_notify.async_reload(hass, DOMAIN)
return unload_ok return unload_ok

View File

@@ -9,13 +9,11 @@ from typing import Any
from demetriek import LaMetricDevice from demetriek import LaMetricDevice
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN from .coordinator import LaMetricConfigEntry, LaMetricDataUpdateCoordinator
from .coordinator import LaMetricDataUpdateCoordinator
from .entity import LaMetricEntity from .entity import LaMetricEntity
from .helpers import lametric_exception_handler from .helpers import lametric_exception_handler
@@ -57,11 +55,11 @@ BUTTONS = [
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
entry: ConfigEntry, entry: LaMetricConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback, async_add_entities: AddConfigEntryEntitiesCallback,
) -> None: ) -> None:
"""Set up LaMetric button based on a config entry.""" """Set up LaMetric button based on a config entry."""
coordinator: LaMetricDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] coordinator = entry.runtime_data
async_add_entities( async_add_entities(
LaMetricButtonEntity( LaMetricButtonEntity(
coordinator=coordinator, coordinator=coordinator,

View File

@@ -13,13 +13,15 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
from .const import DOMAIN, LOGGER, SCAN_INTERVAL from .const import DOMAIN, LOGGER, SCAN_INTERVAL
type LaMetricConfigEntry = ConfigEntry[LaMetricDataUpdateCoordinator]
class LaMetricDataUpdateCoordinator(DataUpdateCoordinator[Device]): class LaMetricDataUpdateCoordinator(DataUpdateCoordinator[Device]):
"""The LaMetric Data Update Coordinator.""" """The LaMetric Data Update Coordinator."""
config_entry: ConfigEntry config_entry: LaMetricConfigEntry
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None: def __init__(self, hass: HomeAssistant, entry: LaMetricConfigEntry) -> None:
"""Initialize the LaMatric coordinator.""" """Initialize the LaMatric coordinator."""
self.lametric = LaMetricDevice( self.lametric = LaMetricDevice(
host=entry.data[CONF_HOST], host=entry.data[CONF_HOST],

View File

@@ -6,11 +6,9 @@ import json
from typing import Any from typing import Any
from homeassistant.components.diagnostics import async_redact_data from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from .const import DOMAIN from .coordinator import LaMetricConfigEntry
from .coordinator import LaMetricDataUpdateCoordinator
TO_REDACT = { TO_REDACT = {
"device_id", "device_id",
@@ -21,10 +19,10 @@ TO_REDACT = {
async def async_get_config_entry_diagnostics( async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry hass: HomeAssistant, entry: LaMetricConfigEntry
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Return diagnostics for a config entry.""" """Return diagnostics for a config entry."""
coordinator: LaMetricDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] coordinator = entry.runtime_data
# Round-trip via JSON to trigger serialization # Round-trip via JSON to trigger serialization
data = json.loads(coordinator.data.to_json()) data = json.loads(coordinator.data.to_json())
return async_redact_data(data, TO_REDACT) return async_redact_data(data, TO_REDACT)

View File

@@ -31,4 +31,5 @@ class LaMetricEntity(CoordinatorEntity[LaMetricDataUpdateCoordinator]):
name=coordinator.data.name, name=coordinator.data.name,
sw_version=coordinator.data.os_version, sw_version=coordinator.data.os_version,
serial_number=coordinator.data.serial_number, serial_number=coordinator.data.serial_number,
configuration_url=f"https://{coordinator.data.wifi.ip}/",
) )

View File

@@ -12,7 +12,7 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr from homeassistant.helpers import device_registry as dr
from .const import DOMAIN from .const import DOMAIN
from .coordinator import LaMetricDataUpdateCoordinator from .coordinator import LaMetricConfigEntry, LaMetricDataUpdateCoordinator
from .entity import LaMetricEntity from .entity import LaMetricEntity
@@ -57,15 +57,9 @@ def async_get_coordinator_by_device_id(
if (device_entry := device_registry.async_get(device_id)) is None: if (device_entry := device_registry.async_get(device_id)) is None:
raise ValueError(f"Unknown LaMetric device ID: {device_id}") raise ValueError(f"Unknown LaMetric device ID: {device_id}")
for entry_id in device_entry.config_entries: entry: LaMetricConfigEntry
if ( for entry in hass.config_entries.async_loaded_entries(DOMAIN):
(entry := hass.config_entries.async_get_entry(entry_id)) if entry.entry_id in device_entry.config_entries:
and entry.domain == DOMAIN return entry.runtime_data
and entry.entry_id in hass.data[DOMAIN]
):
coordinator: LaMetricDataUpdateCoordinator = hass.data[DOMAIN][
entry.entry_id
]
return coordinator
raise ValueError(f"No coordinator for device ID: {device_id}") raise ValueError(f"No coordinator for device ID: {device_id}")

View File

@@ -2,7 +2,7 @@
from __future__ import annotations from __future__ import annotations
from typing import Any from typing import TYPE_CHECKING, Any
from demetriek import ( from demetriek import (
AlarmSound, AlarmSound,
@@ -24,8 +24,8 @@ from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util.enum import try_parse_enum from homeassistant.util.enum import try_parse_enum
from .const import CONF_CYCLES, CONF_ICON_TYPE, CONF_PRIORITY, CONF_SOUND, DOMAIN from .const import CONF_CYCLES, CONF_ICON_TYPE, CONF_PRIORITY, CONF_SOUND
from .coordinator import LaMetricDataUpdateCoordinator from .coordinator import LaMetricConfigEntry
async def async_get_service( async def async_get_service(
@@ -36,10 +36,12 @@ async def async_get_service(
"""Get the LaMetric notification service.""" """Get the LaMetric notification service."""
if discovery_info is None: if discovery_info is None:
return None return None
coordinator: LaMetricDataUpdateCoordinator = hass.data[DOMAIN][ entry: LaMetricConfigEntry | None = hass.config_entries.async_get_entry(
discovery_info["entry_id"] discovery_info["entry_id"]
] )
return LaMetricNotificationService(coordinator.lametric) if TYPE_CHECKING:
assert entry is not None
return LaMetricNotificationService(entry.runtime_data.lametric)
class LaMetricNotificationService(BaseNotificationService): class LaMetricNotificationService(BaseNotificationService):

View File

@@ -9,13 +9,11 @@ from typing import Any
from demetriek import Device, LaMetricDevice, Range from demetriek import Device, LaMetricDevice, Range
from homeassistant.components.number import NumberEntity, NumberEntityDescription from homeassistant.components.number import NumberEntity, NumberEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PERCENTAGE, EntityCategory from homeassistant.const import PERCENTAGE, EntityCategory
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN from .coordinator import LaMetricConfigEntry, LaMetricDataUpdateCoordinator
from .coordinator import LaMetricDataUpdateCoordinator
from .entity import LaMetricEntity from .entity import LaMetricEntity
from .helpers import lametric_exception_handler from .helpers import lametric_exception_handler
@@ -57,11 +55,11 @@ NUMBERS = [
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
entry: ConfigEntry, entry: LaMetricConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback, async_add_entities: AddConfigEntryEntitiesCallback,
) -> None: ) -> None:
"""Set up LaMetric number based on a config entry.""" """Set up LaMetric number based on a config entry."""
coordinator: LaMetricDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] coordinator = entry.runtime_data
async_add_entities( async_add_entities(
LaMetricNumberEntity( LaMetricNumberEntity(
coordinator=coordinator, coordinator=coordinator,

View File

@@ -17,7 +17,7 @@ rules:
Entities of this integration does not explicitly subscribe to events. Entities of this integration does not explicitly subscribe to events.
entity-unique-id: done entity-unique-id: done
has-entity-name: done has-entity-name: done
runtime-data: todo runtime-data: done
test-before-configure: done test-before-configure: done
test-before-setup: done test-before-setup: done
unique-config-entry: done unique-config-entry: done
@@ -33,6 +33,7 @@ rules:
parallel-updates: todo parallel-updates: todo
reauthentication-flow: done reauthentication-flow: done
test-coverage: done test-coverage: done
# Gold # Gold
devices: done devices: done
diagnostics: done diagnostics: done

View File

@@ -9,13 +9,11 @@ from typing import Any
from demetriek import BrightnessMode, Device, LaMetricDevice from demetriek import BrightnessMode, Device, LaMetricDevice
from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN from .coordinator import LaMetricConfigEntry, LaMetricDataUpdateCoordinator
from .coordinator import LaMetricDataUpdateCoordinator
from .entity import LaMetricEntity from .entity import LaMetricEntity
from .helpers import lametric_exception_handler from .helpers import lametric_exception_handler
@@ -42,11 +40,11 @@ SELECTS = [
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
entry: ConfigEntry, entry: LaMetricConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback, async_add_entities: AddConfigEntryEntitiesCallback,
) -> None: ) -> None:
"""Set up LaMetric select based on a config entry.""" """Set up LaMetric select based on a config entry."""
coordinator: LaMetricDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] coordinator = entry.runtime_data
async_add_entities( async_add_entities(
LaMetricSelectEntity( LaMetricSelectEntity(
coordinator=coordinator, coordinator=coordinator,

View File

@@ -12,13 +12,11 @@ from homeassistant.components.sensor import (
SensorEntityDescription, SensorEntityDescription,
SensorStateClass, SensorStateClass,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PERCENTAGE, EntityCategory from homeassistant.const import PERCENTAGE, EntityCategory
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN from .coordinator import LaMetricConfigEntry, LaMetricDataUpdateCoordinator
from .coordinator import LaMetricDataUpdateCoordinator
from .entity import LaMetricEntity from .entity import LaMetricEntity
@@ -44,11 +42,11 @@ SENSORS = [
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
entry: ConfigEntry, entry: LaMetricConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback, async_add_entities: AddConfigEntryEntitiesCallback,
) -> None: ) -> None:
"""Set up LaMetric sensor based on a config entry.""" """Set up LaMetric sensor based on a config entry."""
coordinator: LaMetricDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] coordinator = entry.runtime_data
async_add_entities( async_add_entities(
LaMetricSensorEntity( LaMetricSensorEntity(
coordinator=coordinator, coordinator=coordinator,

View File

@@ -9,13 +9,11 @@ from typing import Any
from demetriek import Device, LaMetricDevice from demetriek import Device, LaMetricDevice
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN from .coordinator import LaMetricConfigEntry, LaMetricDataUpdateCoordinator
from .coordinator import LaMetricDataUpdateCoordinator
from .entity import LaMetricEntity from .entity import LaMetricEntity
from .helpers import lametric_exception_handler from .helpers import lametric_exception_handler
@@ -47,11 +45,11 @@ SWITCHES = [
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
entry: ConfigEntry, entry: LaMetricConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback, async_add_entities: AddConfigEntryEntitiesCallback,
) -> None: ) -> None:
"""Set up LaMetric switch based on a config entry.""" """Set up LaMetric switch based on a config entry."""
coordinator: LaMetricDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] coordinator = entry.runtime_data
async_add_entities( async_add_entities(
LaMetricSwitchEntity( LaMetricSwitchEntity(
coordinator=coordinator, coordinator=coordinator,

View File

@@ -7,20 +7,19 @@ from typing import Any
import ultraheat_api import ultraheat_api
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_DEVICE, Platform from homeassistant.const import CONF_DEVICE, Platform
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_entries from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_entries
from .const import DOMAIN from .const import DOMAIN
from .coordinator import UltraheatCoordinator from .coordinator import UltraheatConfigEntry, UltraheatCoordinator
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
PLATFORMS: list[Platform] = [Platform.SENSOR] PLATFORMS: list[Platform] = [Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: UltraheatConfigEntry) -> bool:
"""Set up heat meter from a config entry.""" """Set up heat meter from a config entry."""
_LOGGER.debug("Initializing %s integration on %s", DOMAIN, entry.data[CONF_DEVICE]) _LOGGER.debug("Initializing %s integration on %s", DOMAIN, entry.data[CONF_DEVICE])
@@ -30,22 +29,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator = UltraheatCoordinator(hass, entry, api) coordinator = UltraheatCoordinator(hass, entry, api)
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: UltraheatConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: async def async_migrate_entry(
hass: HomeAssistant, config_entry: UltraheatConfigEntry
) -> bool:
"""Migrate old entry.""" """Migrate old entry."""
_LOGGER.debug("Migrating from version %s", config_entry.version) _LOGGER.debug("Migrating from version %s", config_entry.version)

View File

@@ -15,14 +15,19 @@ from .const import POLLING_INTERVAL, ULTRAHEAT_TIMEOUT
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
type UltraheatConfigEntry = ConfigEntry[UltraheatCoordinator]
class UltraheatCoordinator(DataUpdateCoordinator[HeatMeterResponse]): class UltraheatCoordinator(DataUpdateCoordinator[HeatMeterResponse]):
"""Coordinator for getting data from the ultraheat api.""" """Coordinator for getting data from the ultraheat api."""
config_entry: ConfigEntry config_entry: UltraheatConfigEntry
def __init__( def __init__(
self, hass: HomeAssistant, config_entry: ConfigEntry, api: HeatMeterService self,
hass: HomeAssistant,
config_entry: UltraheatConfigEntry,
api: HeatMeterService,
) -> None: ) -> None:
"""Initialize my coordinator.""" """Initialize my coordinator."""
super().__init__( super().__init__(

View File

@@ -15,7 +15,6 @@ from homeassistant.components.sensor import (
SensorEntityDescription, SensorEntityDescription,
SensorStateClass, SensorStateClass,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
EntityCategory, EntityCategory,
UnitOfEnergy, UnitOfEnergy,
@@ -29,13 +28,11 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import ( from homeassistant.helpers.update_coordinator import CoordinatorEntity
CoordinatorEntity,
DataUpdateCoordinator,
)
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from . import DOMAIN from .const import DOMAIN
from .coordinator import UltraheatConfigEntry, UltraheatCoordinator
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -270,14 +267,12 @@ HEAT_METER_SENSOR_TYPES = (
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
entry: ConfigEntry, entry: UltraheatConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback, async_add_entities: AddConfigEntryEntitiesCallback,
) -> None: ) -> None:
"""Set up the sensor platform.""" """Set up the sensor platform."""
unique_id = entry.entry_id unique_id = entry.entry_id
coordinator: DataUpdateCoordinator[HeatMeterResponse] = hass.data[DOMAIN][ coordinator = entry.runtime_data
entry.entry_id
]
model = entry.data["model"] model = entry.data["model"]
@@ -295,7 +290,7 @@ async def async_setup_entry(
class HeatMeterSensor( class HeatMeterSensor(
CoordinatorEntity[DataUpdateCoordinator[HeatMeterResponse]], CoordinatorEntity[UltraheatCoordinator],
SensorEntity, SensorEntity,
): ):
"""Representation of a Sensor.""" """Representation of a Sensor."""
@@ -304,7 +299,7 @@ class HeatMeterSensor(
def __init__( def __init__(
self, self,
coordinator: DataUpdateCoordinator[HeatMeterResponse], coordinator: UltraheatCoordinator,
description: HeatMeterSensorEntityDescription, description: HeatMeterSensorEntityDescription,
device: DeviceInfo, device: DeviceInfo,
) -> None: ) -> None:
@@ -312,7 +307,7 @@ class HeatMeterSensor(
super().__init__(coordinator) super().__init__(coordinator)
self.key = description.key self.key = description.key
self._attr_unique_id = ( self._attr_unique_id = (
f"{coordinator.config_entry.data['device_number']}_{description.key}" # type: ignore[union-attr] f"{coordinator.config_entry.data['device_number']}_{description.key}"
) )
self._attr_name = f"Heat Meter {description.name}" self._attr_name = f"Heat Meter {description.name}"
self.entity_description = description self.entity_description = description

View File

@@ -7,21 +7,19 @@ import logging
from laundrify_aio import LaundrifyAPI from laundrify_aio import LaundrifyAPI
from laundrify_aio.exceptions import ApiConnectionException, UnauthorizedException from laundrify_aio.exceptions import ApiConnectionException, UnauthorizedException
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ACCESS_TOKEN, Platform from homeassistant.const import CONF_ACCESS_TOKEN, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN from .coordinator import LaundrifyConfigEntry, LaundrifyUpdateCoordinator
from .coordinator import LaundrifyUpdateCoordinator
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: LaundrifyConfigEntry) -> bool:
"""Set up laundrify from a config entry.""" """Set up laundrify from a config entry."""
session = async_get_clientsession(hass) session = async_get_clientsession(hass)
@@ -38,26 +36,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { entry.runtime_data = coordinator
"api": api_client,
"coordinator": coordinator,
}
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: LaundrifyConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_migrate_entry(hass: HomeAssistant, entry: LaundrifyConfigEntry) -> bool:
"""Migrate entry.""" """Migrate entry."""
_LOGGER.debug("Migrating from version %s", entry.version) _LOGGER.debug("Migrating from version %s", entry.version)

View File

@@ -10,28 +10,25 @@ from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass, BinarySensorDeviceClass,
BinarySensorEntity, BinarySensorEntity,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN, MANUFACTURER, MODELS from .const import DOMAIN, MANUFACTURER, MODELS
from .coordinator import LaundrifyUpdateCoordinator from .coordinator import LaundrifyConfigEntry, LaundrifyUpdateCoordinator
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config: ConfigEntry, entry: LaundrifyConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback, async_add_entities: AddConfigEntryEntitiesCallback,
) -> None: ) -> None:
"""Set up sensors from a config entry created in the integrations UI.""" """Set up sensors from a config entry created in the integrations UI."""
coordinator: LaundrifyUpdateCoordinator = hass.data[DOMAIN][config.entry_id][ coordinator = entry.runtime_data
"coordinator"
]
async_add_entities( async_add_entities(
LaundrifyPowerPlug(coordinator, device) for device in coordinator.data.values() LaundrifyPowerPlug(coordinator, device) for device in coordinator.data.values()

View File

@@ -16,6 +16,8 @@ from .const import DEFAULT_POLL_INTERVAL, DOMAIN, REQUEST_TIMEOUT
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
type LaundrifyConfigEntry = ConfigEntry[LaundrifyUpdateCoordinator]
class LaundrifyUpdateCoordinator(DataUpdateCoordinator[dict[str, LaundrifyDevice]]): class LaundrifyUpdateCoordinator(DataUpdateCoordinator[dict[str, LaundrifyDevice]]):
"""Class to manage fetching laundrify API data.""" """Class to manage fetching laundrify API data."""

View File

@@ -10,7 +10,6 @@ from homeassistant.components.sensor import (
SensorEntity, SensorEntity,
SensorStateClass, SensorStateClass,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import UnitOfEnergy, UnitOfPower from homeassistant.const import UnitOfEnergy, UnitOfPower
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
@@ -18,21 +17,19 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN from .const import DOMAIN
from .coordinator import LaundrifyUpdateCoordinator from .coordinator import LaundrifyConfigEntry, LaundrifyUpdateCoordinator
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config: ConfigEntry, entry: LaundrifyConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback, async_add_entities: AddConfigEntryEntitiesCallback,
) -> None: ) -> None:
"""Add power sensor for passed config_entry in HA.""" """Add power sensor for passed config_entry in HA."""
coordinator: LaundrifyUpdateCoordinator = hass.data[DOMAIN][config.entry_id][ coordinator = entry.runtime_data
"coordinator"
]
sensor_entities: list[LaundrifyPowerSensor | LaundrifyEnergySensor] = [] sensor_entities: list[LaundrifyPowerSensor | LaundrifyEnergySensor] = []
for device in coordinator.data.values(): for device in coordinator.data.values():

View File

@@ -16,7 +16,6 @@ from pypck.connection import (
) )
from pypck.lcn_defs import LcnEvent from pypck.lcn_defs import LcnEvent
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONF_DEVICE_ID, CONF_DEVICE_ID,
CONF_DOMAIN, CONF_DOMAIN,
@@ -38,21 +37,20 @@ from homeassistant.helpers import (
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from .const import ( from .const import (
ADD_ENTITIES_CALLBACKS,
CONF_ACKNOWLEDGE, CONF_ACKNOWLEDGE,
CONF_DIM_MODE, CONF_DIM_MODE,
CONF_DOMAIN_DATA, CONF_DOMAIN_DATA,
CONF_SK_NUM_TRIES, CONF_SK_NUM_TRIES,
CONF_TARGET_VALUE_LOCKED, CONF_TARGET_VALUE_LOCKED,
CONF_TRANSITION, CONF_TRANSITION,
CONNECTION,
DEVICE_CONNECTIONS,
DOMAIN, DOMAIN,
PLATFORMS, PLATFORMS,
) )
from .helpers import ( from .helpers import (
AddressType, AddressType,
InputType, InputType,
LcnConfigEntry,
LcnRuntimeData,
async_update_config_entry, async_update_config_entry,
generate_unique_id, generate_unique_id,
purge_device_registry, purge_device_registry,
@@ -69,18 +67,14 @@ CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the LCN component.""" """Set up the LCN component."""
hass.data.setdefault(DOMAIN, {})
async_setup_services(hass) async_setup_services(hass)
await register_panel_and_ws_api(hass) await register_panel_and_ws_api(hass)
return True return True
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, config_entry: LcnConfigEntry) -> bool:
"""Set up a connection to PCHK host from a config entry.""" """Set up a connection to PCHK host from a config entry."""
if config_entry.entry_id in hass.data[DOMAIN]:
return False
settings = { settings = {
"SK_NUM_TRIES": config_entry.data[CONF_SK_NUM_TRIES], "SK_NUM_TRIES": config_entry.data[CONF_SK_NUM_TRIES],
@@ -114,11 +108,11 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
) from ex ) from ex
_LOGGER.debug('LCN connected to "%s"', config_entry.title) _LOGGER.debug('LCN connected to "%s"', config_entry.title)
hass.data[DOMAIN][config_entry.entry_id] = { config_entry.runtime_data = LcnRuntimeData(
CONNECTION: lcn_connection, connection=lcn_connection,
DEVICE_CONNECTIONS: {}, device_connections={},
ADD_ENTITIES_CALLBACKS: {}, add_entities_callbacks={},
} )
# Update config_entry with LCN device serials # Update config_entry with LCN device serials
await async_update_config_entry(hass, config_entry) await async_update_config_entry(hass, config_entry)
@@ -146,7 +140,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
return True return True
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: async def async_migrate_entry(
hass: HomeAssistant, config_entry: LcnConfigEntry
) -> bool:
"""Migrate old entry.""" """Migrate old entry."""
_LOGGER.debug( _LOGGER.debug(
"Migrating configuration from version %s.%s", "Migrating configuration from version %s.%s",
@@ -195,7 +191,7 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
async def async_migrate_entities( async def async_migrate_entities(
hass: HomeAssistant, config_entry: ConfigEntry hass: HomeAssistant, config_entry: LcnConfigEntry
) -> None: ) -> None:
"""Migrate entity registry.""" """Migrate entity registry."""
@@ -217,25 +213,24 @@ async def async_migrate_entities(
await er.async_migrate_entries(hass, config_entry.entry_id, update_unique_id) await er.async_migrate_entries(hass, config_entry.entry_id, update_unique_id)
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, config_entry: LcnConfigEntry) -> bool:
"""Close connection to PCHK host represented by config_entry.""" """Close connection to PCHK host represented by config_entry."""
# forward unloading to platforms # forward unloading to platforms
unload_ok = await hass.config_entries.async_unload_platforms( unload_ok = await hass.config_entries.async_unload_platforms(
config_entry, PLATFORMS config_entry, PLATFORMS
) )
if unload_ok and config_entry.entry_id in hass.data[DOMAIN]: if unload_ok:
host = hass.data[DOMAIN].pop(config_entry.entry_id) await config_entry.runtime_data.connection.async_close()
await host[CONNECTION].async_close()
return unload_ok return unload_ok
def async_host_event_received( def async_host_event_received(
hass: HomeAssistant, config_entry: ConfigEntry, event: pypck.lcn_defs.LcnEvent hass: HomeAssistant, config_entry: LcnConfigEntry, event: pypck.lcn_defs.LcnEvent
) -> None: ) -> None:
"""Process received event from LCN.""" """Process received event from LCN."""
lcn_connection = hass.data[DOMAIN][config_entry.entry_id][CONNECTION] lcn_connection = config_entry.runtime_data.connection
async def reload_config_entry() -> None: async def reload_config_entry() -> None:
"""Close connection and schedule config entry for reload.""" """Close connection and schedule config entry for reload."""
@@ -258,7 +253,7 @@ def async_host_event_received(
def async_host_input_received( def async_host_input_received(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: LcnConfigEntry,
device_registry: dr.DeviceRegistry, device_registry: dr.DeviceRegistry,
inp: pypck.inputs.Input, inp: pypck.inputs.Input,
) -> None: ) -> None:
@@ -266,7 +261,7 @@ def async_host_input_received(
if not isinstance(inp, pypck.inputs.ModInput): if not isinstance(inp, pypck.inputs.ModInput):
return return
lcn_connection = hass.data[DOMAIN][config_entry.entry_id][CONNECTION] lcn_connection = config_entry.runtime_data.connection
logical_address = lcn_connection.physical_to_logical(inp.physical_source_addr) logical_address = lcn_connection.physical_to_logical(inp.physical_source_addr)
address = ( address = (
logical_address.seg_id, logical_address.seg_id,

View File

@@ -11,7 +11,6 @@ from homeassistant.components.binary_sensor import (
BinarySensorEntity, BinarySensorEntity,
) )
from homeassistant.components.script import scripts_with_entity from homeassistant.components.script import scripts_with_entity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_DOMAIN, CONF_ENTITIES, CONF_SOURCE from homeassistant.const import CONF_DOMAIN, CONF_ENTITIES, CONF_SOURCE
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -22,19 +21,13 @@ from homeassistant.helpers.issue_registry import (
) )
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from .const import ( from .const import BINSENSOR_PORTS, CONF_DOMAIN_DATA, DOMAIN, SETPOINTS
ADD_ENTITIES_CALLBACKS,
BINSENSOR_PORTS,
CONF_DOMAIN_DATA,
DOMAIN,
SETPOINTS,
)
from .entity import LcnEntity from .entity import LcnEntity
from .helpers import InputType from .helpers import InputType, LcnConfigEntry
def add_lcn_entities( def add_lcn_entities(
config_entry: ConfigEntry, config_entry: LcnConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback, async_add_entities: AddConfigEntryEntitiesCallback,
entity_configs: Iterable[ConfigType], entity_configs: Iterable[ConfigType],
) -> None: ) -> None:
@@ -53,7 +46,7 @@ def add_lcn_entities(
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: LcnConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback, async_add_entities: AddConfigEntryEntitiesCallback,
) -> None: ) -> None:
"""Set up LCN switch entities from a config entry.""" """Set up LCN switch entities from a config entry."""
@@ -63,7 +56,7 @@ async def async_setup_entry(
async_add_entities, async_add_entities,
) )
hass.data[DOMAIN][config_entry.entry_id][ADD_ENTITIES_CALLBACKS].update( config_entry.runtime_data.add_entities_callbacks.update(
{DOMAIN_BINARY_SENSOR: add_entities} {DOMAIN_BINARY_SENSOR: add_entities}
) )
@@ -79,7 +72,7 @@ async def async_setup_entry(
class LcnRegulatorLockSensor(LcnEntity, BinarySensorEntity): class LcnRegulatorLockSensor(LcnEntity, BinarySensorEntity):
"""Representation of a LCN binary sensor for regulator locks.""" """Representation of a LCN binary sensor for regulator locks."""
def __init__(self, config: ConfigType, config_entry: ConfigEntry) -> None: def __init__(self, config: ConfigType, config_entry: LcnConfigEntry) -> None:
"""Initialize the LCN binary sensor.""" """Initialize the LCN binary sensor."""
super().__init__(config, config_entry) super().__init__(config, config_entry)
@@ -138,7 +131,7 @@ class LcnRegulatorLockSensor(LcnEntity, BinarySensorEntity):
class LcnBinarySensor(LcnEntity, BinarySensorEntity): class LcnBinarySensor(LcnEntity, BinarySensorEntity):
"""Representation of a LCN binary sensor for binary sensor ports.""" """Representation of a LCN binary sensor for binary sensor ports."""
def __init__(self, config: ConfigType, config_entry: ConfigEntry) -> None: def __init__(self, config: ConfigType, config_entry: LcnConfigEntry) -> None:
"""Initialize the LCN binary sensor.""" """Initialize the LCN binary sensor."""
super().__init__(config, config_entry) super().__init__(config, config_entry)
@@ -174,7 +167,7 @@ class LcnBinarySensor(LcnEntity, BinarySensorEntity):
class LcnLockKeysSensor(LcnEntity, BinarySensorEntity): class LcnLockKeysSensor(LcnEntity, BinarySensorEntity):
"""Representation of a LCN sensor for key locks.""" """Representation of a LCN sensor for key locks."""
def __init__(self, config: ConfigType, config_entry: ConfigEntry) -> None: def __init__(self, config: ConfigType, config_entry: LcnConfigEntry) -> None:
"""Initialize the LCN sensor.""" """Initialize the LCN sensor."""
super().__init__(config, config_entry) super().__init__(config, config_entry)

View File

@@ -12,7 +12,6 @@ from homeassistant.components.climate import (
ClimateEntityFeature, ClimateEntityFeature,
HVACMode, HVACMode,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
ATTR_TEMPERATURE, ATTR_TEMPERATURE,
CONF_DOMAIN, CONF_DOMAIN,
@@ -26,23 +25,21 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from .const import ( from .const import (
ADD_ENTITIES_CALLBACKS,
CONF_DOMAIN_DATA, CONF_DOMAIN_DATA,
CONF_LOCKABLE, CONF_LOCKABLE,
CONF_MAX_TEMP, CONF_MAX_TEMP,
CONF_MIN_TEMP, CONF_MIN_TEMP,
CONF_SETPOINT, CONF_SETPOINT,
CONF_TARGET_VALUE_LOCKED, CONF_TARGET_VALUE_LOCKED,
DOMAIN,
) )
from .entity import LcnEntity from .entity import LcnEntity
from .helpers import InputType from .helpers import InputType, LcnConfigEntry
PARALLEL_UPDATES = 0 PARALLEL_UPDATES = 0
def add_lcn_entities( def add_lcn_entities(
config_entry: ConfigEntry, config_entry: LcnConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback, async_add_entities: AddConfigEntryEntitiesCallback,
entity_configs: Iterable[ConfigType], entity_configs: Iterable[ConfigType],
) -> None: ) -> None:
@@ -56,7 +53,7 @@ def add_lcn_entities(
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: LcnConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback, async_add_entities: AddConfigEntryEntitiesCallback,
) -> None: ) -> None:
"""Set up LCN switch entities from a config entry.""" """Set up LCN switch entities from a config entry."""
@@ -66,7 +63,7 @@ async def async_setup_entry(
async_add_entities, async_add_entities,
) )
hass.data[DOMAIN][config_entry.entry_id][ADD_ENTITIES_CALLBACKS].update( config_entry.runtime_data.add_entities_callbacks.update(
{DOMAIN_CLIMATE: add_entities} {DOMAIN_CLIMATE: add_entities}
) )
@@ -82,7 +79,7 @@ async def async_setup_entry(
class LcnClimate(LcnEntity, ClimateEntity): class LcnClimate(LcnEntity, ClimateEntity):
"""Representation of a LCN climate device.""" """Representation of a LCN climate device."""
def __init__(self, config: ConfigType, config_entry: ConfigEntry) -> None: def __init__(self, config: ConfigType, config_entry: LcnConfigEntry) -> None:
"""Initialize of a LCN climate device.""" """Initialize of a LCN climate device."""
super().__init__(config, config_entry) super().__init__(config, config_entry)

View File

@@ -15,12 +15,8 @@ PLATFORMS = [
] ]
DOMAIN = "lcn" DOMAIN = "lcn"
DATA_LCN = "lcn"
DEFAULT_NAME = "pchk" DEFAULT_NAME = "pchk"
ADD_ENTITIES_CALLBACKS = "add_entities_callbacks"
CONNECTION = "connection"
DEVICE_CONNECTIONS = "device_connections"
CONF_HARDWARE_SERIAL = "hardware_serial" CONF_HARDWARE_SERIAL = "hardware_serial"
CONF_SOFTWARE_SERIAL = "software_serial" CONF_SOFTWARE_SERIAL = "software_serial"
CONF_HARDWARE_TYPE = "hardware_type" CONF_HARDWARE_TYPE = "hardware_type"

View File

@@ -12,28 +12,25 @@ from homeassistant.components.cover import (
CoverEntity, CoverEntity,
CoverEntityFeature, CoverEntityFeature,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_DOMAIN, CONF_ENTITIES from homeassistant.const import CONF_DOMAIN, CONF_ENTITIES
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from .const import ( from .const import (
ADD_ENTITIES_CALLBACKS,
CONF_DOMAIN_DATA, CONF_DOMAIN_DATA,
CONF_MOTOR, CONF_MOTOR,
CONF_POSITIONING_MODE, CONF_POSITIONING_MODE,
CONF_REVERSE_TIME, CONF_REVERSE_TIME,
DOMAIN,
) )
from .entity import LcnEntity from .entity import LcnEntity
from .helpers import InputType from .helpers import InputType, LcnConfigEntry
PARALLEL_UPDATES = 0 PARALLEL_UPDATES = 0
def add_lcn_entities( def add_lcn_entities(
config_entry: ConfigEntry, config_entry: LcnConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback, async_add_entities: AddConfigEntryEntitiesCallback,
entity_configs: Iterable[ConfigType], entity_configs: Iterable[ConfigType],
) -> None: ) -> None:
@@ -50,7 +47,7 @@ def add_lcn_entities(
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: LcnConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback, async_add_entities: AddConfigEntryEntitiesCallback,
) -> None: ) -> None:
"""Set up LCN cover entities from a config entry.""" """Set up LCN cover entities from a config entry."""
@@ -60,7 +57,7 @@ async def async_setup_entry(
async_add_entities, async_add_entities,
) )
hass.data[DOMAIN][config_entry.entry_id][ADD_ENTITIES_CALLBACKS].update( config_entry.runtime_data.add_entities_callbacks.update(
{DOMAIN_COVER: add_entities} {DOMAIN_COVER: add_entities}
) )
@@ -81,7 +78,7 @@ class LcnOutputsCover(LcnEntity, CoverEntity):
_attr_is_opening = False _attr_is_opening = False
_attr_assumed_state = True _attr_assumed_state = True
def __init__(self, config: ConfigType, config_entry: ConfigEntry) -> None: def __init__(self, config: ConfigType, config_entry: LcnConfigEntry) -> None:
"""Initialize the LCN cover.""" """Initialize the LCN cover."""
super().__init__(config, config_entry) super().__init__(config, config_entry)
@@ -188,7 +185,7 @@ class LcnRelayCover(LcnEntity, CoverEntity):
positioning_mode: pypck.lcn_defs.MotorPositioningMode positioning_mode: pypck.lcn_defs.MotorPositioningMode
def __init__(self, config: ConfigType, config_entry: ConfigEntry) -> None: def __init__(self, config: ConfigType, config_entry: LcnConfigEntry) -> None:
"""Initialize the LCN cover.""" """Initialize the LCN cover."""
super().__init__(config, config_entry) super().__init__(config, config_entry)

View File

@@ -2,7 +2,6 @@
from collections.abc import Callable from collections.abc import Callable
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ADDRESS, CONF_DOMAIN, CONF_NAME from homeassistant.const import CONF_ADDRESS, CONF_DOMAIN, CONF_NAME
from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
@@ -13,6 +12,7 @@ from .helpers import (
AddressType, AddressType,
DeviceConnectionType, DeviceConnectionType,
InputType, InputType,
LcnConfigEntry,
generate_unique_id, generate_unique_id,
get_device_connection, get_device_connection,
get_resource, get_resource,
@@ -29,7 +29,7 @@ class LcnEntity(Entity):
def __init__( def __init__(
self, self,
config: ConfigType, config: ConfigType,
config_entry: ConfigEntry, config_entry: LcnConfigEntry,
) -> None: ) -> None:
"""Initialize the LCN device.""" """Initialize the LCN device."""
self.config = config self.config = config

View File

@@ -3,11 +3,14 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from collections.abc import Callable, Iterable
from copy import deepcopy from copy import deepcopy
from dataclasses import dataclass
import re import re
from typing import cast from typing import cast
import pypck import pypck
from pypck.connection import PchkConnectionManager
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
@@ -33,12 +36,27 @@ from .const import (
CONF_HARDWARE_TYPE, CONF_HARDWARE_TYPE,
CONF_SCENES, CONF_SCENES,
CONF_SOFTWARE_SERIAL, CONF_SOFTWARE_SERIAL,
CONNECTION,
DEVICE_CONNECTIONS,
DOMAIN, DOMAIN,
) )
@dataclass
class LcnRuntimeData:
"""Data for LCN config entry."""
connection: PchkConnectionManager
"""Connection to PCHK host."""
device_connections: dict[str, DeviceConnectionType]
"""Logical addresses of devices connected to the host."""
add_entities_callbacks: dict[str, Callable[[Iterable[ConfigType]], None]]
"""Callbacks to add entities for platforms."""
# typing # typing
type LcnConfigEntry = ConfigEntry[LcnRuntimeData]
type AddressType = tuple[int, int, bool] type AddressType = tuple[int, int, bool]
type DeviceConnectionType = pypck.module.ModuleConnection | pypck.module.GroupConnection type DeviceConnectionType = pypck.module.ModuleConnection | pypck.module.GroupConnection
@@ -62,10 +80,10 @@ DOMAIN_LOOKUP = {
def get_device_connection( def get_device_connection(
hass: HomeAssistant, address: AddressType, config_entry: ConfigEntry hass: HomeAssistant, address: AddressType, config_entry: LcnConfigEntry
) -> DeviceConnectionType: ) -> DeviceConnectionType:
"""Return a lcn device_connection.""" """Return a lcn device_connection."""
host_connection = hass.data[DOMAIN][config_entry.entry_id][CONNECTION] host_connection = config_entry.runtime_data.connection
addr = pypck.lcn_addr.LcnAddr(*address) addr = pypck.lcn_addr.LcnAddr(*address)
return host_connection.get_address_conn(addr) return host_connection.get_address_conn(addr)
@@ -165,7 +183,7 @@ def purge_device_registry(
device_registry.async_remove_device(device_id) device_registry.async_remove_device(device_id)
def register_lcn_host_device(hass: HomeAssistant, config_entry: ConfigEntry) -> None: def register_lcn_host_device(hass: HomeAssistant, config_entry: LcnConfigEntry) -> None:
"""Register LCN host for given config_entry in device registry.""" """Register LCN host for given config_entry in device registry."""
device_registry = dr.async_get(hass) device_registry = dr.async_get(hass)
@@ -179,7 +197,7 @@ def register_lcn_host_device(hass: HomeAssistant, config_entry: ConfigEntry) ->
def register_lcn_address_devices( def register_lcn_address_devices(
hass: HomeAssistant, config_entry: ConfigEntry hass: HomeAssistant, config_entry: LcnConfigEntry
) -> None: ) -> None:
"""Register LCN modules and groups defined in config_entry as devices in device registry. """Register LCN modules and groups defined in config_entry as devices in device registry.
@@ -217,9 +235,9 @@ def register_lcn_address_devices(
model=device_model, model=device_model,
) )
hass.data[DOMAIN][config_entry.entry_id][DEVICE_CONNECTIONS][ config_entry.runtime_data.device_connections[device_entry.id] = (
device_entry.id get_device_connection(hass, address, config_entry)
] = get_device_connection(hass, address, config_entry) )
async def async_update_device_config( async def async_update_device_config(
@@ -254,7 +272,7 @@ async def async_update_device_config(
async def async_update_config_entry( async def async_update_config_entry(
hass: HomeAssistant, config_entry: ConfigEntry hass: HomeAssistant, config_entry: LcnConfigEntry
) -> None: ) -> None:
"""Fill missing values in config_entry with infos from LCN bus.""" """Fill missing values in config_entry with infos from LCN bus."""
device_configs = deepcopy(config_entry.data[CONF_DEVICES]) device_configs = deepcopy(config_entry.data[CONF_DEVICES])

View File

@@ -14,29 +14,26 @@ from homeassistant.components.light import (
LightEntity, LightEntity,
LightEntityFeature, LightEntityFeature,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_DOMAIN, CONF_ENTITIES from homeassistant.const import CONF_DOMAIN, CONF_ENTITIES
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from .const import ( from .const import (
ADD_ENTITIES_CALLBACKS,
CONF_DIMMABLE, CONF_DIMMABLE,
CONF_DOMAIN_DATA, CONF_DOMAIN_DATA,
CONF_OUTPUT, CONF_OUTPUT,
CONF_TRANSITION, CONF_TRANSITION,
DOMAIN,
OUTPUT_PORTS, OUTPUT_PORTS,
) )
from .entity import LcnEntity from .entity import LcnEntity
from .helpers import InputType from .helpers import InputType, LcnConfigEntry
PARALLEL_UPDATES = 0 PARALLEL_UPDATES = 0
def add_lcn_entities( def add_lcn_entities(
config_entry: ConfigEntry, config_entry: LcnConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback, async_add_entities: AddConfigEntryEntitiesCallback,
entity_configs: Iterable[ConfigType], entity_configs: Iterable[ConfigType],
) -> None: ) -> None:
@@ -53,7 +50,7 @@ def add_lcn_entities(
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: LcnConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback, async_add_entities: AddConfigEntryEntitiesCallback,
) -> None: ) -> None:
"""Set up LCN light entities from a config entry.""" """Set up LCN light entities from a config entry."""
@@ -63,7 +60,7 @@ async def async_setup_entry(
async_add_entities, async_add_entities,
) )
hass.data[DOMAIN][config_entry.entry_id][ADD_ENTITIES_CALLBACKS].update( config_entry.runtime_data.add_entities_callbacks.update(
{DOMAIN_LIGHT: add_entities} {DOMAIN_LIGHT: add_entities}
) )
@@ -83,7 +80,7 @@ class LcnOutputLight(LcnEntity, LightEntity):
_attr_is_on = False _attr_is_on = False
_attr_brightness = 255 _attr_brightness = 255
def __init__(self, config: ConfigType, config_entry: ConfigEntry) -> None: def __init__(self, config: ConfigType, config_entry: LcnConfigEntry) -> None:
"""Initialize the LCN light.""" """Initialize the LCN light."""
super().__init__(config, config_entry) super().__init__(config, config_entry)
@@ -175,7 +172,7 @@ class LcnRelayLight(LcnEntity, LightEntity):
_attr_supported_color_modes = {ColorMode.ONOFF} _attr_supported_color_modes = {ColorMode.ONOFF}
_attr_is_on = False _attr_is_on = False
def __init__(self, config: ConfigType, config_entry: ConfigEntry) -> None: def __init__(self, config: ConfigType, config_entry: LcnConfigEntry) -> None:
"""Initialize the LCN light.""" """Initialize the LCN light."""
super().__init__(config, config_entry) super().__init__(config, config_entry)

View File

@@ -7,28 +7,26 @@ from typing import Any
import pypck import pypck
from homeassistant.components.scene import DOMAIN as DOMAIN_SCENE, Scene from homeassistant.components.scene import DOMAIN as DOMAIN_SCENE, Scene
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_DOMAIN, CONF_ENTITIES, CONF_SCENE from homeassistant.const import CONF_DOMAIN, CONF_ENTITIES, CONF_SCENE
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from .const import ( from .const import (
ADD_ENTITIES_CALLBACKS,
CONF_DOMAIN_DATA, CONF_DOMAIN_DATA,
CONF_OUTPUTS, CONF_OUTPUTS,
CONF_REGISTER, CONF_REGISTER,
CONF_TRANSITION, CONF_TRANSITION,
DOMAIN,
OUTPUT_PORTS, OUTPUT_PORTS,
) )
from .entity import LcnEntity from .entity import LcnEntity
from .helpers import LcnConfigEntry
PARALLEL_UPDATES = 0 PARALLEL_UPDATES = 0
def add_lcn_entities( def add_lcn_entities(
config_entry: ConfigEntry, config_entry: LcnConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback, async_add_entities: AddConfigEntryEntitiesCallback,
entity_configs: Iterable[ConfigType], entity_configs: Iterable[ConfigType],
) -> None: ) -> None:
@@ -42,7 +40,7 @@ def add_lcn_entities(
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: LcnConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback, async_add_entities: AddConfigEntryEntitiesCallback,
) -> None: ) -> None:
"""Set up LCN switch entities from a config entry.""" """Set up LCN switch entities from a config entry."""
@@ -52,7 +50,7 @@ async def async_setup_entry(
async_add_entities, async_add_entities,
) )
hass.data[DOMAIN][config_entry.entry_id][ADD_ENTITIES_CALLBACKS].update( config_entry.runtime_data.add_entities_callbacks.update(
{DOMAIN_SCENE: add_entities} {DOMAIN_SCENE: add_entities}
) )
@@ -68,7 +66,7 @@ async def async_setup_entry(
class LcnScene(LcnEntity, Scene): class LcnScene(LcnEntity, Scene):
"""Representation of a LCN scene.""" """Representation of a LCN scene."""
def __init__(self, config: ConfigType, config_entry: ConfigEntry) -> None: def __init__(self, config: ConfigType, config_entry: LcnConfigEntry) -> None:
"""Initialize the LCN scene.""" """Initialize the LCN scene."""
super().__init__(config, config_entry) super().__init__(config, config_entry)

View File

@@ -11,7 +11,6 @@ from homeassistant.components.sensor import (
SensorDeviceClass, SensorDeviceClass,
SensorEntity, SensorEntity,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONCENTRATION_PARTS_PER_MILLION, CONCENTRATION_PARTS_PER_MILLION,
CONF_DOMAIN, CONF_DOMAIN,
@@ -29,9 +28,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from .const import ( from .const import (
ADD_ENTITIES_CALLBACKS,
CONF_DOMAIN_DATA, CONF_DOMAIN_DATA,
DOMAIN,
LED_PORTS, LED_PORTS,
S0_INPUTS, S0_INPUTS,
SETPOINTS, SETPOINTS,
@@ -39,7 +36,7 @@ from .const import (
VARIABLES, VARIABLES,
) )
from .entity import LcnEntity from .entity import LcnEntity
from .helpers import InputType from .helpers import InputType, LcnConfigEntry
DEVICE_CLASS_MAPPING = { DEVICE_CLASS_MAPPING = {
pypck.lcn_defs.VarUnit.CELSIUS: SensorDeviceClass.TEMPERATURE, pypck.lcn_defs.VarUnit.CELSIUS: SensorDeviceClass.TEMPERATURE,
@@ -67,7 +64,7 @@ UNIT_OF_MEASUREMENT_MAPPING = {
def add_lcn_entities( def add_lcn_entities(
config_entry: ConfigEntry, config_entry: LcnConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback, async_add_entities: AddConfigEntryEntitiesCallback,
entity_configs: Iterable[ConfigType], entity_configs: Iterable[ConfigType],
) -> None: ) -> None:
@@ -86,7 +83,7 @@ def add_lcn_entities(
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: LcnConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback, async_add_entities: AddConfigEntryEntitiesCallback,
) -> None: ) -> None:
"""Set up LCN switch entities from a config entry.""" """Set up LCN switch entities from a config entry."""
@@ -96,7 +93,7 @@ async def async_setup_entry(
async_add_entities, async_add_entities,
) )
hass.data[DOMAIN][config_entry.entry_id][ADD_ENTITIES_CALLBACKS].update( config_entry.runtime_data.add_entities_callbacks.update(
{DOMAIN_SENSOR: add_entities} {DOMAIN_SENSOR: add_entities}
) )
@@ -112,7 +109,7 @@ async def async_setup_entry(
class LcnVariableSensor(LcnEntity, SensorEntity): class LcnVariableSensor(LcnEntity, SensorEntity):
"""Representation of a LCN sensor for variables.""" """Representation of a LCN sensor for variables."""
def __init__(self, config: ConfigType, config_entry: ConfigEntry) -> None: def __init__(self, config: ConfigType, config_entry: LcnConfigEntry) -> None:
"""Initialize the LCN sensor.""" """Initialize the LCN sensor."""
super().__init__(config, config_entry) super().__init__(config, config_entry)
@@ -157,7 +154,7 @@ class LcnVariableSensor(LcnEntity, SensorEntity):
class LcnLedLogicSensor(LcnEntity, SensorEntity): class LcnLedLogicSensor(LcnEntity, SensorEntity):
"""Representation of a LCN sensor for leds and logicops.""" """Representation of a LCN sensor for leds and logicops."""
def __init__(self, config: ConfigType, config_entry: ConfigEntry) -> None: def __init__(self, config: ConfigType, config_entry: LcnConfigEntry) -> None:
"""Initialize the LCN sensor.""" """Initialize the LCN sensor."""
super().__init__(config, config_entry) super().__init__(config, config_entry)

View File

@@ -36,7 +36,6 @@ from .const import (
CONF_TRANSITION, CONF_TRANSITION,
CONF_VALUE, CONF_VALUE,
CONF_VARIABLE, CONF_VARIABLE,
DEVICE_CONNECTIONS,
DOMAIN, DOMAIN,
LED_PORTS, LED_PORTS,
LED_STATUS, LED_STATUS,
@@ -49,7 +48,7 @@ from .const import (
VAR_UNITS, VAR_UNITS,
VARIABLES, VARIABLES,
) )
from .helpers import DeviceConnectionType, is_states_string from .helpers import DeviceConnectionType, LcnConfigEntry, is_states_string
class LcnServiceCall: class LcnServiceCall:
@@ -68,18 +67,28 @@ class LcnServiceCall:
def get_device_connection(self, service: ServiceCall) -> DeviceConnectionType: def get_device_connection(self, service: ServiceCall) -> DeviceConnectionType:
"""Get address connection object.""" """Get address connection object."""
entries: list[LcnConfigEntry] = self.hass.config_entries.async_loaded_entries(
DOMAIN
)
device_id = service.data[CONF_DEVICE_ID] device_id = service.data[CONF_DEVICE_ID]
device_registry = dr.async_get(self.hass) device_registry = dr.async_get(self.hass)
if not (device := device_registry.async_get(device_id)): if not (device := device_registry.async_get(device_id)) or not (
entry := next(
(
entry
for entry in entries
if entry.entry_id == device.primary_config_entry
),
None,
)
):
raise ServiceValidationError( raise ServiceValidationError(
translation_domain=DOMAIN, translation_domain=DOMAIN,
translation_key="invalid_device_id", translation_key="invalid_device_id",
translation_placeholders={"device_id": device_id}, translation_placeholders={"device_id": device_id},
) )
return self.hass.data[DOMAIN][device.primary_config_entry][DEVICE_CONNECTIONS][ return entry.runtime_data.device_connections[device_id]
device_id
]
async def async_call_service(self, service: ServiceCall) -> ServiceResponse: async def async_call_service(self, service: ServiceCall) -> ServiceResponse:
"""Execute service call.""" """Execute service call."""

View File

@@ -7,29 +7,20 @@ from typing import Any
import pypck import pypck
from homeassistant.components.switch import DOMAIN as DOMAIN_SWITCH, SwitchEntity from homeassistant.components.switch import DOMAIN as DOMAIN_SWITCH, SwitchEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_DOMAIN, CONF_ENTITIES from homeassistant.const import CONF_DOMAIN, CONF_ENTITIES
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from .const import ( from .const import CONF_DOMAIN_DATA, CONF_OUTPUT, OUTPUT_PORTS, RELAY_PORTS, SETPOINTS
ADD_ENTITIES_CALLBACKS,
CONF_DOMAIN_DATA,
CONF_OUTPUT,
DOMAIN,
OUTPUT_PORTS,
RELAY_PORTS,
SETPOINTS,
)
from .entity import LcnEntity from .entity import LcnEntity
from .helpers import InputType from .helpers import InputType, LcnConfigEntry
PARALLEL_UPDATES = 0 PARALLEL_UPDATES = 0
def add_lcn_switch_entities( def add_lcn_switch_entities(
config_entry: ConfigEntry, config_entry: LcnConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback, async_add_entities: AddConfigEntryEntitiesCallback,
entity_configs: Iterable[ConfigType], entity_configs: Iterable[ConfigType],
) -> None: ) -> None:
@@ -52,7 +43,7 @@ def add_lcn_switch_entities(
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: ConfigEntry, config_entry: LcnConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback, async_add_entities: AddConfigEntryEntitiesCallback,
) -> None: ) -> None:
"""Set up LCN switch entities from a config entry.""" """Set up LCN switch entities from a config entry."""
@@ -62,7 +53,7 @@ async def async_setup_entry(
async_add_entities, async_add_entities,
) )
hass.data[DOMAIN][config_entry.entry_id][ADD_ENTITIES_CALLBACKS].update( config_entry.runtime_data.add_entities_callbacks.update(
{DOMAIN_SWITCH: add_entities} {DOMAIN_SWITCH: add_entities}
) )
@@ -80,7 +71,7 @@ class LcnOutputSwitch(LcnEntity, SwitchEntity):
_attr_is_on = False _attr_is_on = False
def __init__(self, config: ConfigType, config_entry: ConfigEntry) -> None: def __init__(self, config: ConfigType, config_entry: LcnConfigEntry) -> None:
"""Initialize the LCN switch.""" """Initialize the LCN switch."""
super().__init__(config, config_entry) super().__init__(config, config_entry)
@@ -129,7 +120,7 @@ class LcnRelaySwitch(LcnEntity, SwitchEntity):
_attr_is_on = False _attr_is_on = False
def __init__(self, config: ConfigType, config_entry: ConfigEntry) -> None: def __init__(self, config: ConfigType, config_entry: LcnConfigEntry) -> None:
"""Initialize the LCN switch.""" """Initialize the LCN switch."""
super().__init__(config, config_entry) super().__init__(config, config_entry)
@@ -179,7 +170,7 @@ class LcnRegulatorLockSwitch(LcnEntity, SwitchEntity):
_attr_is_on = False _attr_is_on = False
def __init__(self, config: ConfigType, config_entry: ConfigEntry) -> None: def __init__(self, config: ConfigType, config_entry: LcnConfigEntry) -> None:
"""Initialize the LCN switch.""" """Initialize the LCN switch."""
super().__init__(config, config_entry) super().__init__(config, config_entry)
@@ -235,7 +226,7 @@ class LcnKeyLockSwitch(LcnEntity, SwitchEntity):
_attr_is_on = False _attr_is_on = False
def __init__(self, config: ConfigType, config_entry: ConfigEntry) -> None: def __init__(self, config: ConfigType, config_entry: LcnConfigEntry) -> None:
"""Initialize the LCN switch.""" """Initialize the LCN switch."""
super().__init__(config, config_entry) super().__init__(config, config_entry)

View File

@@ -4,15 +4,17 @@ from __future__ import annotations
from collections.abc import Awaitable, Callable from collections.abc import Awaitable, Callable
from functools import wraps from functools import wraps
from typing import TYPE_CHECKING, Any, Final from typing import Any, Final
import lcn_frontend as lcn_panel import lcn_frontend as lcn_panel
import voluptuous as vol import voluptuous as vol
from homeassistant.components import panel_custom, websocket_api from homeassistant.components import panel_custom, websocket_api
from homeassistant.components.http import StaticPathConfig from homeassistant.components.http import StaticPathConfig
from homeassistant.components.websocket_api import AsyncWebSocketCommandHandler from homeassistant.components.websocket_api import (
from homeassistant.config_entries import ConfigEntry ActiveConnection,
AsyncWebSocketCommandHandler,
)
from homeassistant.const import ( from homeassistant.const import (
CONF_ADDRESS, CONF_ADDRESS,
CONF_DEVICES, CONF_DEVICES,
@@ -28,16 +30,15 @@ from homeassistant.helpers import (
) )
from .const import ( from .const import (
ADD_ENTITIES_CALLBACKS,
CONF_DOMAIN_DATA, CONF_DOMAIN_DATA,
CONF_HARDWARE_SERIAL, CONF_HARDWARE_SERIAL,
CONF_HARDWARE_TYPE, CONF_HARDWARE_TYPE,
CONF_SOFTWARE_SERIAL, CONF_SOFTWARE_SERIAL,
CONNECTION,
DOMAIN, DOMAIN,
) )
from .helpers import ( from .helpers import (
DeviceConnectionType, DeviceConnectionType,
LcnConfigEntry,
async_update_device_config, async_update_device_config,
generate_unique_id, generate_unique_id,
get_device_config, get_device_config,
@@ -58,11 +59,8 @@ from .schemas import (
DOMAIN_DATA_SWITCH, DOMAIN_DATA_SWITCH,
) )
if TYPE_CHECKING:
from homeassistant.components.websocket_api import ActiveConnection
type AsyncLcnWebSocketCommandHandler = Callable[ type AsyncLcnWebSocketCommandHandler = Callable[
[HomeAssistant, ActiveConnection, dict[str, Any], ConfigEntry], Awaitable[None] [HomeAssistant, ActiveConnection, dict[str, Any], LcnConfigEntry], Awaitable[None]
] ]
URL_BASE: Final = "/lcn_static" URL_BASE: Final = "/lcn_static"
@@ -127,7 +125,7 @@ async def websocket_get_device_configs(
hass: HomeAssistant, hass: HomeAssistant,
connection: websocket_api.ActiveConnection, connection: websocket_api.ActiveConnection,
msg: dict, msg: dict,
config_entry: ConfigEntry, config_entry: LcnConfigEntry,
) -> None: ) -> None:
"""Get device configs.""" """Get device configs."""
connection.send_result(msg["id"], config_entry.data[CONF_DEVICES]) connection.send_result(msg["id"], config_entry.data[CONF_DEVICES])
@@ -147,7 +145,7 @@ async def websocket_get_entity_configs(
hass: HomeAssistant, hass: HomeAssistant,
connection: websocket_api.ActiveConnection, connection: websocket_api.ActiveConnection,
msg: dict, msg: dict,
config_entry: ConfigEntry, config_entry: LcnConfigEntry,
) -> None: ) -> None:
"""Get entities configs.""" """Get entities configs."""
if CONF_ADDRESS in msg: if CONF_ADDRESS in msg:
@@ -178,10 +176,10 @@ async def websocket_scan_devices(
hass: HomeAssistant, hass: HomeAssistant,
connection: websocket_api.ActiveConnection, connection: websocket_api.ActiveConnection,
msg: dict, msg: dict,
config_entry: ConfigEntry, config_entry: LcnConfigEntry,
) -> None: ) -> None:
"""Scan for new devices.""" """Scan for new devices."""
host_connection = hass.data[DOMAIN][config_entry.entry_id][CONNECTION] host_connection = config_entry.runtime_data.connection
await host_connection.scan_modules() await host_connection.scan_modules()
for device_connection in host_connection.address_conns.values(): for device_connection in host_connection.address_conns.values():
@@ -210,7 +208,7 @@ async def websocket_add_device(
hass: HomeAssistant, hass: HomeAssistant,
connection: websocket_api.ActiveConnection, connection: websocket_api.ActiveConnection,
msg: dict, msg: dict,
config_entry: ConfigEntry, config_entry: LcnConfigEntry,
) -> None: ) -> None:
"""Add a device.""" """Add a device."""
if get_device_config(msg[CONF_ADDRESS], config_entry): if get_device_config(msg[CONF_ADDRESS], config_entry):
@@ -256,7 +254,7 @@ async def websocket_delete_device(
hass: HomeAssistant, hass: HomeAssistant,
connection: websocket_api.ActiveConnection, connection: websocket_api.ActiveConnection,
msg: dict, msg: dict,
config_entry: ConfigEntry, config_entry: LcnConfigEntry,
) -> None: ) -> None:
"""Delete a device.""" """Delete a device."""
device_config = get_device_config(msg[CONF_ADDRESS], config_entry) device_config = get_device_config(msg[CONF_ADDRESS], config_entry)
@@ -318,7 +316,7 @@ async def websocket_add_entity(
hass: HomeAssistant, hass: HomeAssistant,
connection: websocket_api.ActiveConnection, connection: websocket_api.ActiveConnection,
msg: dict, msg: dict,
config_entry: ConfigEntry, config_entry: LcnConfigEntry,
) -> None: ) -> None:
"""Add an entity.""" """Add an entity."""
if not (device_config := get_device_config(msg[CONF_ADDRESS], config_entry)): if not (device_config := get_device_config(msg[CONF_ADDRESS], config_entry)):
@@ -347,9 +345,7 @@ async def websocket_add_entity(
} }
# Create new entity and add to corresponding component # Create new entity and add to corresponding component
add_entities = hass.data[DOMAIN][msg["entry_id"]][ADD_ENTITIES_CALLBACKS][ add_entities = config_entry.runtime_data.add_entities_callbacks[msg[CONF_DOMAIN]]
msg[CONF_DOMAIN]
]
add_entities([entity_config]) add_entities([entity_config])
# Add entity config to config_entry # Add entity config to config_entry
@@ -386,7 +382,7 @@ async def websocket_delete_entity(
hass: HomeAssistant, hass: HomeAssistant,
connection: websocket_api.ActiveConnection, connection: websocket_api.ActiveConnection,
msg: dict, msg: dict,
config_entry: ConfigEntry, config_entry: LcnConfigEntry,
) -> None: ) -> None:
"""Delete an entity.""" """Delete an entity."""
entity_config = next( entity_config = next(
@@ -426,7 +422,7 @@ async def websocket_delete_entity(
async def async_create_or_update_device_in_config_entry( async def async_create_or_update_device_in_config_entry(
hass: HomeAssistant, hass: HomeAssistant,
device_connection: DeviceConnectionType, device_connection: DeviceConnectionType,
config_entry: ConfigEntry, config_entry: LcnConfigEntry,
) -> None: ) -> None:
"""Create or update device in config_entry according to given device_connection.""" """Create or update device in config_entry according to given device_connection."""
address = ( address = (
@@ -455,7 +451,7 @@ async def async_create_or_update_device_in_config_entry(
def get_entity_entry( def get_entity_entry(
hass: HomeAssistant, entity_config: dict, config_entry: ConfigEntry hass: HomeAssistant, entity_config: dict, config_entry: LcnConfigEntry
) -> er.RegistryEntry | None: ) -> er.RegistryEntry | None:
"""Get entity RegistryEntry from entity_config.""" """Get entity RegistryEntry from entity_config."""
entity_registry = er.async_get(hass) entity_registry = er.async_get(hass)

View File

@@ -3,7 +3,7 @@
from enum import StrEnum from enum import StrEnum
DOMAIN = "wallbox" DOMAIN = "wallbox"
UPDATE_INTERVAL = 30 UPDATE_INTERVAL = 60
BIDIRECTIONAL_MODEL_PREFIXES = ["QS"] BIDIRECTIONAL_MODEL_PREFIXES = ["QS"]

View File

@@ -90,7 +90,9 @@ def _require_authentication[_WallboxCoordinatorT: WallboxCoordinator, **_P](
except requests.exceptions.HTTPError as wallbox_connection_error: except requests.exceptions.HTTPError as wallbox_connection_error:
if wallbox_connection_error.response.status_code == HTTPStatus.FORBIDDEN: if wallbox_connection_error.response.status_code == HTTPStatus.FORBIDDEN:
raise ConfigEntryAuthFailed from wallbox_connection_error raise ConfigEntryAuthFailed from wallbox_connection_error
raise ConnectionError from wallbox_connection_error raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="api_failed"
) from wallbox_connection_error
return require_authentication return require_authentication
@@ -137,56 +139,65 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]):
@_require_authentication @_require_authentication
def _get_data(self) -> dict[str, Any]: def _get_data(self) -> dict[str, Any]:
"""Get new sensor data for Wallbox component.""" """Get new sensor data for Wallbox component."""
data: dict[str, Any] = self._wallbox.getChargerStatus(self._station) try:
data[CHARGER_MAX_CHARGING_CURRENT_KEY] = data[CHARGER_DATA_KEY][ data: dict[str, Any] = self._wallbox.getChargerStatus(self._station)
CHARGER_MAX_CHARGING_CURRENT_KEY data[CHARGER_MAX_CHARGING_CURRENT_KEY] = data[CHARGER_DATA_KEY][
] CHARGER_MAX_CHARGING_CURRENT_KEY
data[CHARGER_LOCKED_UNLOCKED_KEY] = data[CHARGER_DATA_KEY][
CHARGER_LOCKED_UNLOCKED_KEY
]
data[CHARGER_ENERGY_PRICE_KEY] = data[CHARGER_DATA_KEY][
CHARGER_ENERGY_PRICE_KEY
]
# Only show max_icp_current if power_boost is available in the wallbox unit:
if (
data[CHARGER_DATA_KEY].get(CHARGER_MAX_ICP_CURRENT_KEY, 0) > 0
and CHARGER_POWER_BOOST_KEY
in data[CHARGER_DATA_KEY][CHARGER_PLAN_KEY][CHARGER_FEATURES_KEY]
):
data[CHARGER_MAX_ICP_CURRENT_KEY] = data[CHARGER_DATA_KEY][
CHARGER_MAX_ICP_CURRENT_KEY
] ]
data[CHARGER_LOCKED_UNLOCKED_KEY] = data[CHARGER_DATA_KEY][
CHARGER_LOCKED_UNLOCKED_KEY
]
data[CHARGER_ENERGY_PRICE_KEY] = data[CHARGER_DATA_KEY][
CHARGER_ENERGY_PRICE_KEY
]
# Only show max_icp_current if power_boost is available in the wallbox unit:
if (
data[CHARGER_DATA_KEY].get(CHARGER_MAX_ICP_CURRENT_KEY, 0) > 0
and CHARGER_POWER_BOOST_KEY
in data[CHARGER_DATA_KEY][CHARGER_PLAN_KEY][CHARGER_FEATURES_KEY]
):
data[CHARGER_MAX_ICP_CURRENT_KEY] = data[CHARGER_DATA_KEY][
CHARGER_MAX_ICP_CURRENT_KEY
]
data[CHARGER_CURRENCY_KEY] = ( data[CHARGER_CURRENCY_KEY] = (
f"{data[CHARGER_DATA_KEY][CHARGER_CURRENCY_KEY][CODE_KEY]}/kWh" f"{data[CHARGER_DATA_KEY][CHARGER_CURRENCY_KEY][CODE_KEY]}/kWh"
) )
data[CHARGER_STATUS_DESCRIPTION_KEY] = CHARGER_STATUS.get( data[CHARGER_STATUS_DESCRIPTION_KEY] = CHARGER_STATUS.get(
data[CHARGER_STATUS_ID_KEY], ChargerStatus.UNKNOWN data[CHARGER_STATUS_ID_KEY], ChargerStatus.UNKNOWN
) )
# Set current solar charging mode # Set current solar charging mode
eco_smart_enabled = ( eco_smart_enabled = (
data[CHARGER_DATA_KEY] data[CHARGER_DATA_KEY]
.get(CHARGER_ECO_SMART_KEY, {}) .get(CHARGER_ECO_SMART_KEY, {})
.get(CHARGER_ECO_SMART_STATUS_KEY) .get(CHARGER_ECO_SMART_STATUS_KEY)
) )
eco_smart_mode = ( eco_smart_mode = (
data[CHARGER_DATA_KEY] data[CHARGER_DATA_KEY]
.get(CHARGER_ECO_SMART_KEY, {}) .get(CHARGER_ECO_SMART_KEY, {})
.get(CHARGER_ECO_SMART_MODE_KEY) .get(CHARGER_ECO_SMART_MODE_KEY)
) )
if eco_smart_mode is None: if eco_smart_mode is None:
data[CHARGER_ECO_SMART_KEY] = EcoSmartMode.DISABLED data[CHARGER_ECO_SMART_KEY] = EcoSmartMode.DISABLED
elif eco_smart_enabled is False: elif eco_smart_enabled is False:
data[CHARGER_ECO_SMART_KEY] = EcoSmartMode.OFF data[CHARGER_ECO_SMART_KEY] = EcoSmartMode.OFF
elif eco_smart_mode == 0: elif eco_smart_mode == 0:
data[CHARGER_ECO_SMART_KEY] = EcoSmartMode.ECO_MODE data[CHARGER_ECO_SMART_KEY] = EcoSmartMode.ECO_MODE
elif eco_smart_mode == 1: elif eco_smart_mode == 1:
data[CHARGER_ECO_SMART_KEY] = EcoSmartMode.FULL_SOLAR data[CHARGER_ECO_SMART_KEY] = EcoSmartMode.FULL_SOLAR
return data return data # noqa: TRY300
except requests.exceptions.HTTPError as wallbox_connection_error:
if wallbox_connection_error.response.status_code == 429:
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="too_many_requests"
) from wallbox_connection_error
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="api_failed"
) from wallbox_connection_error
async def _async_update_data(self) -> dict[str, Any]: async def _async_update_data(self) -> dict[str, Any]:
"""Get new sensor data for Wallbox component.""" """Get new sensor data for Wallbox component."""
@@ -200,7 +211,13 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]):
except requests.exceptions.HTTPError as wallbox_connection_error: except requests.exceptions.HTTPError as wallbox_connection_error:
if wallbox_connection_error.response.status_code == 403: if wallbox_connection_error.response.status_code == 403:
raise InvalidAuth from wallbox_connection_error raise InvalidAuth from wallbox_connection_error
raise if wallbox_connection_error.response.status_code == 429:
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="too_many_requests"
) from wallbox_connection_error
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="api_failed"
) from wallbox_connection_error
async def async_set_charging_current(self, charging_current: float) -> None: async def async_set_charging_current(self, charging_current: float) -> None:
"""Set maximum charging current for Wallbox.""" """Set maximum charging current for Wallbox."""
@@ -217,7 +234,13 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]):
except requests.exceptions.HTTPError as wallbox_connection_error: except requests.exceptions.HTTPError as wallbox_connection_error:
if wallbox_connection_error.response.status_code == 403: if wallbox_connection_error.response.status_code == 403:
raise InvalidAuth from wallbox_connection_error raise InvalidAuth from wallbox_connection_error
raise if wallbox_connection_error.response.status_code == 429:
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="too_many_requests"
) from wallbox_connection_error
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="api_failed"
) from wallbox_connection_error
async def async_set_icp_current(self, icp_current: float) -> None: async def async_set_icp_current(self, icp_current: float) -> None:
"""Set maximum icp current for Wallbox.""" """Set maximum icp current for Wallbox."""
@@ -227,8 +250,16 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]):
@_require_authentication @_require_authentication
def _set_energy_cost(self, energy_cost: float) -> None: def _set_energy_cost(self, energy_cost: float) -> None:
"""Set energy cost for Wallbox.""" """Set energy cost for Wallbox."""
try:
self._wallbox.setEnergyCost(self._station, energy_cost) self._wallbox.setEnergyCost(self._station, energy_cost)
except requests.exceptions.HTTPError as wallbox_connection_error:
if wallbox_connection_error.response.status_code == 429:
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="too_many_requests"
) from wallbox_connection_error
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="api_failed"
) from wallbox_connection_error
async def async_set_energy_cost(self, energy_cost: float) -> None: async def async_set_energy_cost(self, energy_cost: float) -> None:
"""Set energy cost for Wallbox.""" """Set energy cost for Wallbox."""
@@ -246,7 +277,13 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]):
except requests.exceptions.HTTPError as wallbox_connection_error: except requests.exceptions.HTTPError as wallbox_connection_error:
if wallbox_connection_error.response.status_code == 403: if wallbox_connection_error.response.status_code == 403:
raise InvalidAuth from wallbox_connection_error raise InvalidAuth from wallbox_connection_error
raise if wallbox_connection_error.response.status_code == 429:
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="too_many_requests"
) from wallbox_connection_error
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="api_failed"
) from wallbox_connection_error
async def async_set_lock_unlock(self, lock: bool) -> None: async def async_set_lock_unlock(self, lock: bool) -> None:
"""Set wallbox to locked or unlocked.""" """Set wallbox to locked or unlocked."""
@@ -256,11 +293,19 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]):
@_require_authentication @_require_authentication
def _pause_charger(self, pause: bool) -> None: def _pause_charger(self, pause: bool) -> None:
"""Set wallbox to pause or resume.""" """Set wallbox to pause or resume."""
try:
if pause: if pause:
self._wallbox.pauseChargingSession(self._station) self._wallbox.pauseChargingSession(self._station)
else: else:
self._wallbox.resumeChargingSession(self._station) self._wallbox.resumeChargingSession(self._station)
except requests.exceptions.HTTPError as wallbox_connection_error:
if wallbox_connection_error.response.status_code == 429:
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="too_many_requests"
) from wallbox_connection_error
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="api_failed"
) from wallbox_connection_error
async def async_pause_charger(self, pause: bool) -> None: async def async_pause_charger(self, pause: bool) -> None:
"""Set wallbox to pause or resume.""" """Set wallbox to pause or resume."""
@@ -270,13 +315,21 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]):
@_require_authentication @_require_authentication
def _set_eco_smart(self, option: str) -> None: def _set_eco_smart(self, option: str) -> None:
"""Set wallbox solar charging mode.""" """Set wallbox solar charging mode."""
try:
if option == EcoSmartMode.ECO_MODE: if option == EcoSmartMode.ECO_MODE:
self._wallbox.enableEcoSmart(self._station, 0) self._wallbox.enableEcoSmart(self._station, 0)
elif option == EcoSmartMode.FULL_SOLAR: elif option == EcoSmartMode.FULL_SOLAR:
self._wallbox.enableEcoSmart(self._station, 1) self._wallbox.enableEcoSmart(self._station, 1)
else: else:
self._wallbox.disableEcoSmart(self._station) self._wallbox.disableEcoSmart(self._station)
except requests.exceptions.HTTPError as wallbox_connection_error:
if wallbox_connection_error.response.status_code == 429:
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="too_many_requests"
) from wallbox_connection_error
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="api_failed"
) from wallbox_connection_error
async def async_set_eco_smart(self, option: str) -> None: async def async_set_eco_smart(self, option: str) -> None:
"""Set wallbox solar charging mode.""" """Set wallbox solar charging mode."""

View File

@@ -7,7 +7,7 @@ from typing import Any
from homeassistant.components.lock import LockEntity, LockEntityDescription from homeassistant.components.lock import LockEntity, LockEntityDescription
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import PlatformNotReady from homeassistant.exceptions import HomeAssistantError, PlatformNotReady
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import ( from .const import (
@@ -41,7 +41,7 @@ async def async_setup_entry(
) )
except InvalidAuth: except InvalidAuth:
return return
except ConnectionError as exc: except HomeAssistantError as exc:
raise PlatformNotReady from exc raise PlatformNotReady from exc
async_add_entities( async_add_entities(

View File

@@ -12,7 +12,7 @@ from typing import cast
from homeassistant.components.number import NumberEntity, NumberEntityDescription from homeassistant.components.number import NumberEntity, NumberEntityDescription
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import PlatformNotReady from homeassistant.exceptions import HomeAssistantError, PlatformNotReady
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import ( from .const import (
@@ -93,7 +93,7 @@ async def async_setup_entry(
) )
except InvalidAuth: except InvalidAuth:
return return
except ConnectionError as exc: except HomeAssistantError as exc:
raise PlatformNotReady from exc raise PlatformNotReady from exc
async_add_entities( async_add_entities(

View File

@@ -3,7 +3,6 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
import logging
from typing import cast from typing import cast
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
@@ -49,11 +48,6 @@ from .const import (
from .coordinator import WallboxCoordinator from .coordinator import WallboxCoordinator
from .entity import WallboxEntity from .entity import WallboxEntity
CHARGER_STATION = "station"
UPDATE_INTERVAL = 30
_LOGGER = logging.getLogger(__name__)
@dataclass(frozen=True) @dataclass(frozen=True)
class WallboxSensorEntityDescription(SensorEntityDescription): class WallboxSensorEntityDescription(SensorEntityDescription):

View File

@@ -112,6 +112,9 @@
"exceptions": { "exceptions": {
"api_failed": { "api_failed": {
"message": "Error communicating with Wallbox API" "message": "Error communicating with Wallbox API"
},
"too_many_requests": {
"message": "Error communicating with Wallbox API, too many requests"
} }
} }
} }

View File

@@ -6,5 +6,5 @@
"dependencies": ["auth", "application_credentials"], "dependencies": ["auth", "application_credentials"],
"documentation": "https://www.home-assistant.io/integrations/yolink", "documentation": "https://www.home-assistant.io/integrations/yolink",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"requirements": ["yolink-api==0.5.2"] "requirements": ["yolink-api==0.5.5"]
} }

View File

@@ -155,7 +155,10 @@ class YoLinkValveEntity(YoLinkEntity, ValveEntity):
@property @property
def available(self) -> bool: def available(self) -> bool:
"""Return true is device is available.""" """Return true is device is available."""
if self.coordinator.dev_net_type is not None: if (
self.coordinator.device.is_support_mode_switching()
and self.coordinator.dev_net_type is not None
):
# When the device operates in Class A mode, it cannot be controlled. # When the device operates in Class A mode, it cannot be controlled.
return self.coordinator.dev_net_type != ATTR_DEVICE_MODEL_A return self.coordinator.dev_net_type != ATTR_DEVICE_MODEL_A
return super().available return super().available

View File

@@ -46,7 +46,6 @@ ifaddr==0.2.0
Jinja2==3.1.6 Jinja2==3.1.6
lru-dict==1.3.0 lru-dict==1.3.0
mutagen==1.47.0 mutagen==1.47.0
numpy==2.3.0
orjson==3.10.18 orjson==3.10.18
packaging>=23.1 packaging>=23.1
paho-mqtt==2.1.0 paho-mqtt==2.1.0

View File

@@ -46,41 +46,16 @@ dependencies = [
"ciso8601==2.3.2", "ciso8601==2.3.2",
"cronsim==2.6", "cronsim==2.6",
"fnv-hash-fast==1.5.0", "fnv-hash-fast==1.5.0",
# ha-ffmpeg is indirectly imported from onboarding via the import chain
# onboarding->cloud->assist_pipeline->tts->ffmpeg. Onboarding needs
# to be setup in stage 0, but we don't want to also promote cloud with all its
# dependencies to stage 0.
"ha-ffmpeg==3.2.2",
# hass-nabucasa is imported by helpers which don't depend on the cloud # hass-nabucasa is imported by helpers which don't depend on the cloud
# integration # integration
"hass-nabucasa==0.103.0", "hass-nabucasa==0.103.0",
# hassil is indirectly imported from onboarding via the import chain
# onboarding->cloud->assist_pipeline->conversation->hassil. Onboarding needs
# to be setup in stage 0, but we don't want to also promote cloud with all its
# dependencies to stage 0.
"hassil==2.2.3",
# When bumping httpx, please check the version pins of # When bumping httpx, please check the version pins of
# httpcore, anyio, and h11 in gen_requirements_all # httpcore, anyio, and h11 in gen_requirements_all
"httpx==0.28.1", "httpx==0.28.1",
"home-assistant-bluetooth==1.13.1", "home-assistant-bluetooth==1.13.1",
# home_assistant_intents is indirectly imported from onboarding via the import chain
# onboarding->cloud->assist_pipeline->conversation->home_assistant_intents. Onboarding needs
# to be setup in stage 0, but we don't want to also promote cloud with all its
# dependencies to stage 0.
"home-assistant-intents==2025.6.10",
"ifaddr==0.2.0", "ifaddr==0.2.0",
"Jinja2==3.1.6", "Jinja2==3.1.6",
"lru-dict==1.3.0", "lru-dict==1.3.0",
# mutagen is indirectly imported from onboarding via the import chain
# onboarding->cloud->assist_pipeline->tts->mutagen. Onboarding needs
# to be setup in stage 0, but we don't want to also promote cloud with all its
# dependencies to stage 0.
"mutagen==1.47.0",
# numpy is indirectly imported from onboarding via the import chain
# onboarding->cloud->alexa->camera->stream->numpy. Onboarding needs
# to be setup in stage 0, but we don't want to also promote cloud with all its
# dependencies to stage 0.
"numpy==2.3.0",
"PyJWT==2.10.1", "PyJWT==2.10.1",
# PyJWT has loose dependency. We want the latest one. # PyJWT has loose dependency. We want the latest one.
"cryptography==45.0.3", "cryptography==45.0.3",
@@ -90,22 +65,7 @@ dependencies = [
"orjson==3.10.18", "orjson==3.10.18",
"packaging>=23.1", "packaging>=23.1",
"psutil-home-assistant==0.0.1", "psutil-home-assistant==0.0.1",
# pymicro_vad is indirectly imported from onboarding via the import chain
# onboarding->cloud->assist_pipeline->pymicro_vad. Onboarding needs
# to be setup in stage 0, but we don't want to also promote cloud with all its
# dependencies to stage 0.
"pymicro-vad==1.0.1",
# pyspeex-noise is indirectly imported from onboarding via the import chain
# onboarding->cloud->assist_pipeline->pyspeex_noise. Onboarding needs
# to be setup in stage 0, but we don't want to also promote cloud with all its
# dependencies to stage 0.
"pyspeex-noise==1.0.2",
"python-slugify==8.0.4", "python-slugify==8.0.4",
# PyTurboJPEG is indirectly imported from onboarding via the import chain
# onboarding->cloud->camera->pyturbojpeg. Onboarding needs
# to be setup in stage 0, but we don't want to also promote cloud with all its
# dependencies to stage 0.
"PyTurboJPEG==1.8.0",
"PyYAML==6.0.2", "PyYAML==6.0.2",
"requests==2.32.4", "requests==2.32.4",
"securetar==2025.2.1", "securetar==2025.2.1",

8
requirements.txt generated
View File

@@ -23,17 +23,12 @@ certifi>=2021.5.30
ciso8601==2.3.2 ciso8601==2.3.2
cronsim==2.6 cronsim==2.6
fnv-hash-fast==1.5.0 fnv-hash-fast==1.5.0
ha-ffmpeg==3.2.2
hass-nabucasa==0.103.0 hass-nabucasa==0.103.0
hassil==2.2.3
httpx==0.28.1 httpx==0.28.1
home-assistant-bluetooth==1.13.1 home-assistant-bluetooth==1.13.1
home-assistant-intents==2025.6.10
ifaddr==0.2.0 ifaddr==0.2.0
Jinja2==3.1.6 Jinja2==3.1.6
lru-dict==1.3.0 lru-dict==1.3.0
mutagen==1.47.0
numpy==2.3.0
PyJWT==2.10.1 PyJWT==2.10.1
cryptography==45.0.3 cryptography==45.0.3
Pillow==11.2.1 Pillow==11.2.1
@@ -42,10 +37,7 @@ pyOpenSSL==25.1.0
orjson==3.10.18 orjson==3.10.18
packaging>=23.1 packaging>=23.1
psutil-home-assistant==0.0.1 psutil-home-assistant==0.0.1
pymicro-vad==1.0.1
pyspeex-noise==1.0.2
python-slugify==8.0.4 python-slugify==8.0.4
PyTurboJPEG==1.8.0
PyYAML==6.0.2 PyYAML==6.0.2
requests==2.32.4 requests==2.32.4
securetar==2025.2.1 securetar==2025.2.1

6
requirements_all.txt generated
View File

@@ -244,7 +244,7 @@ aioelectricitymaps==0.4.0
aioemonitor==1.0.5 aioemonitor==1.0.5
# homeassistant.components.esphome # homeassistant.components.esphome
aioesphomeapi==33.0.0 aioesphomeapi==33.1.1
# homeassistant.components.flo # homeassistant.components.flo
aioflo==2021.11.0 aioflo==2021.11.0
@@ -1231,7 +1231,7 @@ ihcsdk==2.8.5
imeon_inverter_api==0.3.12 imeon_inverter_api==0.3.12
# homeassistant.components.imgw_pib # homeassistant.components.imgw_pib
imgw_pib==1.0.10 imgw_pib==1.1.0
# homeassistant.components.incomfort # homeassistant.components.incomfort
incomfort-client==0.6.9 incomfort-client==0.6.9
@@ -3154,7 +3154,7 @@ yeelight==0.7.16
yeelightsunflower==0.0.10 yeelightsunflower==0.0.10
# homeassistant.components.yolink # homeassistant.components.yolink
yolink-api==0.5.2 yolink-api==0.5.5
# homeassistant.components.youless # homeassistant.components.youless
youless-api==2.2.0 youless-api==2.2.0

View File

@@ -232,7 +232,7 @@ aioelectricitymaps==0.4.0
aioemonitor==1.0.5 aioemonitor==1.0.5
# homeassistant.components.esphome # homeassistant.components.esphome
aioesphomeapi==33.0.0 aioesphomeapi==33.1.1
# homeassistant.components.flo # homeassistant.components.flo
aioflo==2021.11.0 aioflo==2021.11.0
@@ -1062,7 +1062,7 @@ igloohome-api==0.1.1
imeon_inverter_api==0.3.12 imeon_inverter_api==0.3.12
# homeassistant.components.imgw_pib # homeassistant.components.imgw_pib
imgw_pib==1.0.10 imgw_pib==1.1.0
# homeassistant.components.incomfort # homeassistant.components.incomfort
incomfort-client==0.6.9 incomfort-client==0.6.9
@@ -2598,7 +2598,7 @@ yalexs==8.10.0
yeelight==0.7.16 yeelight==0.7.16
# homeassistant.components.yolink # homeassistant.components.yolink
yolink-api==0.5.2 yolink-api==0.5.5
# homeassistant.components.youless # homeassistant.components.youless
youless-api==2.2.0 youless-api==2.2.0

View File

@@ -42,7 +42,7 @@ async def test_button_app_next(
assert entry.device_id assert entry.device_id
device_entry = device_registry.async_get(entry.device_id) device_entry = device_registry.async_get(entry.device_id)
assert device_entry assert device_entry
assert device_entry.configuration_url is None assert device_entry.configuration_url == "https://127.0.0.1/"
assert device_entry.connections == { assert device_entry.connections == {
(dr.CONNECTION_NETWORK_MAC, "aa:bb:cc:dd:ee:ff") (dr.CONNECTION_NETWORK_MAC, "aa:bb:cc:dd:ee:ff")
} }
@@ -89,7 +89,7 @@ async def test_button_app_previous(
assert entry.device_id assert entry.device_id
device_entry = device_registry.async_get(entry.device_id) device_entry = device_registry.async_get(entry.device_id)
assert device_entry assert device_entry
assert device_entry.configuration_url is None assert device_entry.configuration_url == "https://127.0.0.1/"
assert device_entry.connections == { assert device_entry.connections == {
(dr.CONNECTION_NETWORK_MAC, "aa:bb:cc:dd:ee:ff") (dr.CONNECTION_NETWORK_MAC, "aa:bb:cc:dd:ee:ff")
} }
@@ -137,7 +137,7 @@ async def test_button_dismiss_current_notification(
assert entry.device_id assert entry.device_id
device_entry = device_registry.async_get(entry.device_id) device_entry = device_registry.async_get(entry.device_id)
assert device_entry assert device_entry
assert device_entry.configuration_url is None assert device_entry.configuration_url == "https://127.0.0.1/"
assert device_entry.connections == { assert device_entry.connections == {
(dr.CONNECTION_NETWORK_MAC, "aa:bb:cc:dd:ee:ff") (dr.CONNECTION_NETWORK_MAC, "aa:bb:cc:dd:ee:ff")
} }
@@ -185,7 +185,7 @@ async def test_button_dismiss_all_notifications(
assert entry.device_id assert entry.device_id
device_entry = device_registry.async_get(entry.device_id) device_entry = device_registry.async_get(entry.device_id)
assert device_entry assert device_entry
assert device_entry.configuration_url is None assert device_entry.configuration_url == "https://127.0.0.1/"
assert device_entry.connections == { assert device_entry.connections == {
(dr.CONNECTION_NETWORK_MAC, "aa:bb:cc:dd:ee:ff") (dr.CONNECTION_NETWORK_MAC, "aa:bb:cc:dd:ee:ff")
} }

View File

@@ -55,7 +55,7 @@ async def test_brightness(
device = device_registry.async_get(entry.device_id) device = device_registry.async_get(entry.device_id)
assert device assert device
assert device.configuration_url is None assert device.configuration_url == "https://127.0.0.1/"
assert device.connections == {(dr.CONNECTION_NETWORK_MAC, "aa:bb:cc:dd:ee:ff")} assert device.connections == {(dr.CONNECTION_NETWORK_MAC, "aa:bb:cc:dd:ee:ff")}
assert device.entry_type is None assert device.entry_type is None
assert device.hw_version is None assert device.hw_version is None
@@ -104,7 +104,7 @@ async def test_volume(
device = device_registry.async_get(entry.device_id) device = device_registry.async_get(entry.device_id)
assert device assert device
assert device.configuration_url is None assert device.configuration_url == "https://127.0.0.1/"
assert device.connections == {(dr.CONNECTION_NETWORK_MAC, "aa:bb:cc:dd:ee:ff")} assert device.connections == {(dr.CONNECTION_NETWORK_MAC, "aa:bb:cc:dd:ee:ff")}
assert device.entry_type is None assert device.entry_type is None
assert device.hw_version is None assert device.hw_version is None

View File

@@ -48,7 +48,7 @@ async def test_brightness_mode(
device = device_registry.async_get(entry.device_id) device = device_registry.async_get(entry.device_id)
assert device assert device
assert device.configuration_url is None assert device.configuration_url == "https://127.0.0.1/"
assert device.connections == {(dr.CONNECTION_NETWORK_MAC, "aa:bb:cc:dd:ee:ff")} assert device.connections == {(dr.CONNECTION_NETWORK_MAC, "aa:bb:cc:dd:ee:ff")}
assert device.entry_type is None assert device.entry_type is None
assert device.hw_version is None assert device.hw_version is None

View File

@@ -41,7 +41,7 @@ async def test_wifi_signal(
device = device_registry.async_get(entry.device_id) device = device_registry.async_get(entry.device_id)
assert device assert device
assert device.configuration_url is None assert device.configuration_url == "https://127.0.0.1/"
assert device.connections == {(dr.CONNECTION_NETWORK_MAC, "aa:bb:cc:dd:ee:ff")} assert device.connections == {(dr.CONNECTION_NETWORK_MAC, "aa:bb:cc:dd:ee:ff")}
assert device.entry_type is None assert device.entry_type is None
assert device.hw_version is None assert device.hw_version is None

View File

@@ -50,7 +50,7 @@ async def test_bluetooth(
device = device_registry.async_get(entry.device_id) device = device_registry.async_get(entry.device_id)
assert device assert device
assert device.configuration_url is None assert device.configuration_url == "https://127.0.0.1/"
assert device.connections == {(dr.CONNECTION_NETWORK_MAC, "aa:bb:cc:dd:ee:ff")} assert device.connections == {(dr.CONNECTION_NETWORK_MAC, "aa:bb:cc:dd:ee:ff")}
assert device.entry_type is None assert device.entry_type is None
assert device.hw_version is None assert device.hw_version is None

View File

@@ -6,8 +6,7 @@ from unittest.mock import AsyncMock, patch
from laundrify_aio import LaundrifyAPI, LaundrifyDevice from laundrify_aio import LaundrifyAPI, LaundrifyDevice
import pytest import pytest
from homeassistant.components.laundrify import DOMAIN from homeassistant.components.laundrify.const import DOMAIN, MANUFACTURER
from homeassistant.components.laundrify.const import MANUFACTURER
from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant

View File

@@ -162,6 +162,9 @@ test_response_no_power_boost = {
http_404_error = requests.exceptions.HTTPError() http_404_error = requests.exceptions.HTTPError()
http_404_error.response = requests.Response() http_404_error.response = requests.Response()
http_404_error.response.status_code = HTTPStatus.NOT_FOUND http_404_error.response.status_code = HTTPStatus.NOT_FOUND
http_429_error = requests.exceptions.HTTPError()
http_429_error.response = requests.Response()
http_429_error.response.status_code = HTTPStatus.TOO_MANY_REQUESTS
authorisation_response = { authorisation_response = {
"data": { "data": {
@@ -192,6 +195,24 @@ authorisation_response_unauthorised = {
} }
} }
invalid_reauth_response = {
"jwt": "fakekeyhere",
"refresh_token": "refresh_fakekeyhere",
"user_id": 12345,
"ttl": 145656758,
"refresh_token_ttl": 145756758,
"error": False,
"status": 200,
}
http_403_error = requests.exceptions.HTTPError()
http_403_error.response = requests.Response()
http_403_error.response.status_code = HTTPStatus.FORBIDDEN
http_404_error = requests.exceptions.HTTPError()
http_404_error.response = requests.Response()
http_404_error.response.status_code = HTTPStatus.NOT_FOUND
async def setup_integration(hass: HomeAssistant, entry: MockConfigEntry) -> None: async def setup_integration(hass: HomeAssistant, entry: MockConfigEntry) -> None:
"""Test wallbox sensor class setup.""" """Test wallbox sensor class setup."""

View File

@@ -1,9 +1,6 @@
"""Test the Wallbox config flow.""" """Test the Wallbox config flow."""
from http import HTTPStatus from unittest.mock import Mock, patch
import json
import requests_mock
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components.wallbox import config_flow from homeassistant.components.wallbox import config_flow
@@ -24,23 +21,21 @@ from homeassistant.data_entry_flow import FlowResultType
from . import ( from . import (
authorisation_response, authorisation_response,
authorisation_response_unauthorised, authorisation_response_unauthorised,
http_403_error,
http_404_error,
setup_integration, setup_integration,
) )
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
test_response = json.loads( test_response = {
json.dumps( CHARGER_CHARGING_POWER_KEY: 0,
{ CHARGER_MAX_AVAILABLE_POWER_KEY: "xx",
CHARGER_CHARGING_POWER_KEY: 0, CHARGER_CHARGING_SPEED_KEY: 0,
CHARGER_MAX_AVAILABLE_POWER_KEY: "xx", CHARGER_ADDED_RANGE_KEY: "xx",
CHARGER_CHARGING_SPEED_KEY: 0, CHARGER_ADDED_ENERGY_KEY: "44.697",
CHARGER_ADDED_RANGE_KEY: "xx", CHARGER_DATA_KEY: {CHARGER_MAX_CHARGING_CURRENT_KEY: 24},
CHARGER_ADDED_ENERGY_KEY: "44.697", }
CHARGER_DATA_KEY: {CHARGER_MAX_CHARGING_CURRENT_KEY: 24},
}
)
)
async def test_show_set_form(hass: HomeAssistant) -> None: async def test_show_set_form(hass: HomeAssistant) -> None:
@@ -59,17 +54,16 @@ async def test_form_cannot_authenticate(hass: HomeAssistant) -> None:
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
) )
with requests_mock.Mocker() as mock_request: with (
mock_request.get( patch(
"https://user-api.wall-box.com/users/signin", "homeassistant.components.wallbox.Wallbox.authenticate",
json=authorisation_response, new=Mock(side_effect=http_403_error),
status_code=HTTPStatus.FORBIDDEN, ),
) patch(
mock_request.get( "homeassistant.components.wallbox.Wallbox.pauseChargingSession",
"https://api.wall-box.com/chargers/status/12345", new=Mock(side_effect=http_403_error),
json=test_response, ),
status_code=HTTPStatus.FORBIDDEN, ):
)
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
{ {
@@ -89,17 +83,16 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None:
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
) )
with requests_mock.Mocker() as mock_request: with (
mock_request.get( patch(
"https://user-api.wall-box.com/users/signin", "homeassistant.components.wallbox.Wallbox.authenticate",
json=authorisation_response_unauthorised, new=Mock(side_effect=http_404_error),
status_code=HTTPStatus.NOT_FOUND, ),
) patch(
mock_request.get( "homeassistant.components.wallbox.Wallbox.pauseChargingSession",
"https://api.wall-box.com/chargers/status/12345", new=Mock(side_effect=http_404_error),
json=test_response, ),
status_code=HTTPStatus.NOT_FOUND, ):
)
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
{ {
@@ -119,17 +112,16 @@ async def test_form_validate_input(hass: HomeAssistant) -> None:
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
) )
with requests_mock.Mocker() as mock_request: with (
mock_request.get( patch(
"https://user-api.wall-box.com/users/signin", "homeassistant.components.wallbox.Wallbox.authenticate",
json=authorisation_response, new=Mock(return_value=authorisation_response),
status_code=HTTPStatus.OK, ),
) patch(
mock_request.get( "homeassistant.components.wallbox.Wallbox.getChargerStatus",
"https://api.wall-box.com/chargers/status/12345", new=Mock(return_value=test_response),
json=test_response, ),
status_code=HTTPStatus.OK, ):
)
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
{ {
@@ -148,18 +140,16 @@ async def test_form_reauth(hass: HomeAssistant, entry: MockConfigEntry) -> None:
await setup_integration(hass, entry) await setup_integration(hass, entry)
assert entry.state is ConfigEntryState.LOADED assert entry.state is ConfigEntryState.LOADED
with requests_mock.Mocker() as mock_request: with (
mock_request.get( patch(
"https://user-api.wall-box.com/users/signin", "homeassistant.components.wallbox.Wallbox.authenticate",
json=authorisation_response, new=Mock(return_value=authorisation_response_unauthorised),
status_code=200, ),
) patch(
mock_request.get( "homeassistant.components.wallbox.Wallbox.getChargerStatus",
"https://api.wall-box.com/chargers/status/12345", new=Mock(return_value=test_response),
json=test_response, ),
status_code=200, ):
)
result = await entry.start_reauth_flow(hass) result = await entry.start_reauth_flow(hass)
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(
@@ -183,26 +173,16 @@ async def test_form_reauth_invalid(hass: HomeAssistant, entry: MockConfigEntry)
await setup_integration(hass, entry) await setup_integration(hass, entry)
assert entry.state is ConfigEntryState.LOADED assert entry.state is ConfigEntryState.LOADED
with requests_mock.Mocker() as mock_request: with (
mock_request.get( patch(
"https://user-api.wall-box.com/users/signin", "homeassistant.components.wallbox.Wallbox.authenticate",
json={ new=Mock(return_value=authorisation_response_unauthorised),
"jwt": "fakekeyhere", ),
"refresh_token": "refresh_fakekeyhere", patch(
"user_id": 12345, "homeassistant.components.wallbox.Wallbox.getChargerStatus",
"ttl": 145656758, new=Mock(return_value=test_response),
"refresh_token_ttl": 145756758, ),
"error": False, ):
"status": 200,
},
status_code=200,
)
mock_request.get(
"https://api.wall-box.com/chargers/status/12345",
json=test_response,
status_code=200,
)
result = await entry.start_reauth_flow(hass) result = await entry.start_reauth_flow(hass)
result2 = await hass.config_entries.flow.async_configure( result2 = await hass.config_entries.flow.async_configure(

View File

@@ -1,16 +1,15 @@
"""Test Wallbox Init Component.""" """Test Wallbox Init Component."""
import requests_mock from unittest.mock import Mock, patch
from homeassistant.components.wallbox.const import ( from homeassistant.components.wallbox.const import DOMAIN
CHARGER_MAX_CHARGING_CURRENT_KEY,
DOMAIN,
)
from homeassistant.config_entries import ConfigEntryState from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from . import ( from . import (
authorisation_response, authorisation_response,
http_403_error,
http_429_error,
setup_integration, setup_integration,
setup_integration_connection_error, setup_integration_connection_error,
setup_integration_no_eco_mode, setup_integration_no_eco_mode,
@@ -53,18 +52,16 @@ async def test_wallbox_refresh_failed_connection_error_auth(
await setup_integration(hass, entry) await setup_integration(hass, entry)
assert entry.state is ConfigEntryState.LOADED assert entry.state is ConfigEntryState.LOADED
with requests_mock.Mocker() as mock_request: with (
mock_request.get( patch(
"https://user-api.wall-box.com/users/signin", "homeassistant.components.wallbox.Wallbox.authenticate",
json=authorisation_response, new=Mock(side_effect=http_429_error),
status_code=404, ),
) patch(
mock_request.get( "homeassistant.components.wallbox.Wallbox.pauseChargingSession",
"https://api.wall-box.com/chargers/status/12345", new=Mock(return_value=test_response),
json=test_response, ),
status_code=200, ):
)
wallbox = hass.data[DOMAIN][entry.entry_id] wallbox = hass.data[DOMAIN][entry.entry_id]
await wallbox.async_refresh() await wallbox.async_refresh()
@@ -81,18 +78,68 @@ async def test_wallbox_refresh_failed_invalid_auth(
await setup_integration(hass, entry) await setup_integration(hass, entry)
assert entry.state is ConfigEntryState.LOADED assert entry.state is ConfigEntryState.LOADED
with requests_mock.Mocker() as mock_request: with (
mock_request.get( patch(
"https://user-api.wall-box.com/users/signin", "homeassistant.components.wallbox.Wallbox.authenticate",
json=authorisation_response, new=Mock(side_effect=http_403_error),
status_code=403, ),
) patch(
mock_request.put( "homeassistant.components.wallbox.Wallbox.pauseChargingSession",
"https://api.wall-box.com/v2/charger/12345", new=Mock(side_effect=http_403_error),
json={CHARGER_MAX_CHARGING_CURRENT_KEY: 20}, ),
status_code=403, ):
) wallbox = hass.data[DOMAIN][entry.entry_id]
await wallbox.async_refresh()
assert await hass.config_entries.async_unload(entry.entry_id)
assert entry.state is ConfigEntryState.NOT_LOADED
async def test_wallbox_refresh_failed_http_error(
hass: HomeAssistant, entry: MockConfigEntry
) -> None:
"""Test Wallbox setup with authentication error."""
await setup_integration(hass, entry)
assert entry.state is ConfigEntryState.LOADED
with (
patch(
"homeassistant.components.wallbox.Wallbox.authenticate",
new=Mock(return_value=authorisation_response),
),
patch(
"homeassistant.components.wallbox.Wallbox.getChargerStatus",
new=Mock(side_effect=http_403_error),
),
):
wallbox = hass.data[DOMAIN][entry.entry_id]
await wallbox.async_refresh()
assert await hass.config_entries.async_unload(entry.entry_id)
assert entry.state is ConfigEntryState.NOT_LOADED
async def test_wallbox_refresh_failed_too_many_requests(
hass: HomeAssistant, entry: MockConfigEntry
) -> None:
"""Test Wallbox setup with authentication error."""
await setup_integration(hass, entry)
assert entry.state is ConfigEntryState.LOADED
with (
patch(
"homeassistant.components.wallbox.Wallbox.authenticate",
new=Mock(return_value=authorisation_response),
),
patch(
"homeassistant.components.wallbox.Wallbox.getChargerStatus",
new=Mock(side_effect=http_429_error),
),
):
wallbox = hass.data[DOMAIN][entry.entry_id] wallbox = hass.data[DOMAIN][entry.entry_id]
await wallbox.async_refresh() await wallbox.async_refresh()
@@ -109,18 +156,16 @@ async def test_wallbox_refresh_failed_connection_error(
await setup_integration(hass, entry) await setup_integration(hass, entry)
assert entry.state is ConfigEntryState.LOADED assert entry.state is ConfigEntryState.LOADED
with requests_mock.Mocker() as mock_request: with (
mock_request.get( patch(
"https://user-api.wall-box.com/users/signin", "homeassistant.components.wallbox.Wallbox.authenticate",
json=authorisation_response, new=Mock(return_value=authorisation_response),
status_code=200, ),
) patch(
mock_request.get( "homeassistant.components.wallbox.Wallbox.pauseChargingSession",
"https://api.wall-box.com/chargers/status/12345", new=Mock(side_effect=http_403_error),
json=test_response, ),
status_code=403, ):
)
wallbox = hass.data[DOMAIN][entry.entry_id] wallbox = hass.data[DOMAIN][entry.entry_id]
await wallbox.async_refresh() await wallbox.async_refresh()

View File

@@ -1,15 +1,18 @@
"""Test Wallbox Lock component.""" """Test Wallbox Lock component."""
from unittest.mock import Mock, patch
import pytest import pytest
import requests_mock
from homeassistant.components.lock import SERVICE_LOCK, SERVICE_UNLOCK from homeassistant.components.lock import SERVICE_LOCK, SERVICE_UNLOCK
from homeassistant.components.wallbox.const import CHARGER_LOCKED_UNLOCKED_KEY from homeassistant.components.wallbox.const import CHARGER_LOCKED_UNLOCKED_KEY
from homeassistant.const import ATTR_ENTITY_ID from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from . import ( from . import (
authorisation_response, authorisation_response,
http_429_error,
setup_integration, setup_integration,
setup_integration_platform_not_ready, setup_integration_platform_not_ready,
setup_integration_read_only, setup_integration_read_only,
@@ -28,18 +31,20 @@ async def test_wallbox_lock_class(hass: HomeAssistant, entry: MockConfigEntry) -
assert state assert state
assert state.state == "unlocked" assert state.state == "unlocked"
with requests_mock.Mocker() as mock_request: with (
mock_request.get( patch(
"https://user-api.wall-box.com/users/signin", "homeassistant.components.wallbox.Wallbox.authenticate",
json=authorisation_response, new=Mock(return_value=authorisation_response),
status_code=200, ),
) patch(
mock_request.put( "homeassistant.components.wallbox.Wallbox.lockCharger",
"https://api.wall-box.com/v2/charger/12345", new=Mock(return_value={CHARGER_LOCKED_UNLOCKED_KEY: False}),
json={CHARGER_LOCKED_UNLOCKED_KEY: False}, ),
status_code=200, patch(
) "homeassistant.components.wallbox.Wallbox.unlockCharger",
new=Mock(return_value={CHARGER_LOCKED_UNLOCKED_KEY: False}),
),
):
await hass.services.async_call( await hass.services.async_call(
"lock", "lock",
SERVICE_LOCK, SERVICE_LOCK,
@@ -66,36 +71,73 @@ async def test_wallbox_lock_class_connection_error(
await setup_integration(hass, entry) await setup_integration(hass, entry)
with requests_mock.Mocker() as mock_request: with (
mock_request.get( patch(
"https://user-api.wall-box.com/users/signin", "homeassistant.components.wallbox.Wallbox.authenticate",
json=authorisation_response, new=Mock(return_value=authorisation_response),
status_code=200, ),
) patch(
mock_request.put( "homeassistant.components.wallbox.Wallbox.lockCharger",
"https://api.wall-box.com/v2/charger/12345", new=Mock(side_effect=ConnectionError),
json={CHARGER_LOCKED_UNLOCKED_KEY: False}, ),
status_code=404, pytest.raises(ConnectionError),
):
await hass.services.async_call(
"lock",
SERVICE_LOCK,
{
ATTR_ENTITY_ID: MOCK_LOCK_ENTITY_ID,
},
blocking=True,
) )
with pytest.raises(ConnectionError): with (
await hass.services.async_call( patch(
"lock", "homeassistant.components.wallbox.Wallbox.authenticate",
SERVICE_LOCK, new=Mock(return_value=authorisation_response),
{ ),
ATTR_ENTITY_ID: MOCK_LOCK_ENTITY_ID, patch(
}, "homeassistant.components.wallbox.Wallbox.lockCharger",
blocking=True, new=Mock(side_effect=ConnectionError),
) ),
with pytest.raises(ConnectionError): patch(
await hass.services.async_call( "homeassistant.components.wallbox.Wallbox.unlockCharger",
"lock", new=Mock(side_effect=ConnectionError),
SERVICE_UNLOCK, ),
{ pytest.raises(ConnectionError),
ATTR_ENTITY_ID: MOCK_LOCK_ENTITY_ID, ):
}, await hass.services.async_call(
blocking=True, "lock",
) SERVICE_UNLOCK,
{
ATTR_ENTITY_ID: MOCK_LOCK_ENTITY_ID,
},
blocking=True,
)
with (
patch(
"homeassistant.components.wallbox.Wallbox.authenticate",
new=Mock(return_value=authorisation_response),
),
patch(
"homeassistant.components.wallbox.Wallbox.lockCharger",
new=Mock(side_effect=http_429_error),
),
patch(
"homeassistant.components.wallbox.Wallbox.unlockCharger",
new=Mock(side_effect=http_429_error),
),
pytest.raises(HomeAssistantError),
):
await hass.services.async_call(
"lock",
SERVICE_UNLOCK,
{
ATTR_ENTITY_ID: MOCK_LOCK_ENTITY_ID,
},
blocking=True,
)
async def test_wallbox_lock_class_authentication_error( async def test_wallbox_lock_class_authentication_error(

View File

@@ -1,22 +1,26 @@
"""Test Wallbox Switch component.""" """Test Wallbox Switch component."""
from unittest.mock import Mock, patch
import pytest import pytest
import requests_mock
from homeassistant.components.input_number import ATTR_VALUE, SERVICE_SET_VALUE from homeassistant.components.input_number import ATTR_VALUE, SERVICE_SET_VALUE
from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN
from homeassistant.components.wallbox import InvalidAuth
from homeassistant.components.wallbox.const import ( from homeassistant.components.wallbox.const import (
CHARGER_ENERGY_PRICE_KEY, CHARGER_ENERGY_PRICE_KEY,
CHARGER_MAX_CHARGING_CURRENT_KEY, CHARGER_MAX_CHARGING_CURRENT_KEY,
CHARGER_MAX_ICP_CURRENT_KEY, CHARGER_MAX_ICP_CURRENT_KEY,
) )
from homeassistant.components.wallbox.coordinator import InvalidAuth
from homeassistant.const import ATTR_ENTITY_ID from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.exceptions import HomeAssistantError
from . import ( from . import (
authorisation_response, authorisation_response,
http_403_error,
http_404_error,
http_429_error,
setup_integration, setup_integration,
setup_integration_bidir, setup_integration_bidir,
setup_integration_platform_not_ready, setup_integration_platform_not_ready,
@@ -29,6 +33,14 @@ from .const import (
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
mock_wallbox = Mock()
mock_wallbox.authenticate = Mock(return_value=authorisation_response)
mock_wallbox.setEnergyCost = Mock(return_value={CHARGER_ENERGY_PRICE_KEY: 1.1})
mock_wallbox.setMaxChargingCurrent = Mock(
return_value={CHARGER_MAX_CHARGING_CURRENT_KEY: 20}
)
mock_wallbox.setIcpMaxCurrent = Mock(return_value={CHARGER_MAX_ICP_CURRENT_KEY: 10})
async def test_wallbox_number_class( async def test_wallbox_number_class(
hass: HomeAssistant, entry: MockConfigEntry hass: HomeAssistant, entry: MockConfigEntry
@@ -37,17 +49,16 @@ async def test_wallbox_number_class(
await setup_integration(hass, entry) await setup_integration(hass, entry)
with requests_mock.Mocker() as mock_request: with (
mock_request.get( patch(
"https://user-api.wall-box.com/users/signin", "homeassistant.components.wallbox.Wallbox.authenticate",
json=authorisation_response, new=Mock(return_value=authorisation_response),
status_code=200, ),
) patch(
mock_request.put( "homeassistant.components.wallbox.Wallbox.setMaxChargingCurrent",
"https://api.wall-box.com/v2/charger/12345", new=Mock(return_value={CHARGER_MAX_CHARGING_CURRENT_KEY: 20}),
json={CHARGER_MAX_CHARGING_CURRENT_KEY: 20}, ),
status_code=200, ):
)
state = hass.states.get(MOCK_NUMBER_ENTITY_ID) state = hass.states.get(MOCK_NUMBER_ENTITY_ID)
assert state.attributes["min"] == 6 assert state.attributes["min"] == 6
assert state.attributes["max"] == 25 assert state.attributes["max"] == 25
@@ -82,19 +93,16 @@ async def test_wallbox_number_energy_class(
await setup_integration(hass, entry) await setup_integration(hass, entry)
with requests_mock.Mocker() as mock_request: with (
mock_request.get( patch(
"https://user-api.wall-box.com/users/signin", "homeassistant.components.wallbox.Wallbox.authenticate",
json=authorisation_response, new=Mock(return_value=authorisation_response),
status_code=200, ),
) patch(
"homeassistant.components.wallbox.Wallbox.setEnergyCost",
mock_request.post( new=Mock(return_value={CHARGER_ENERGY_PRICE_KEY: 1.1}),
"https://api.wall-box.com/chargers/config/12345", ),
json={CHARGER_ENERGY_PRICE_KEY: 1.1}, ):
status_code=200,
)
await hass.services.async_call( await hass.services.async_call(
"number", "number",
SERVICE_SET_VALUE, SERVICE_SET_VALUE,
@@ -113,59 +121,113 @@ async def test_wallbox_number_class_connection_error(
await setup_integration(hass, entry) await setup_integration(hass, entry)
with requests_mock.Mocker() as mock_request: with (
mock_request.get( patch(
"https://user-api.wall-box.com/users/signin", "homeassistant.components.wallbox.Wallbox.authenticate",
json=authorisation_response, new=Mock(return_value=authorisation_response),
status_code=200, ),
) patch(
mock_request.put( "homeassistant.components.wallbox.Wallbox.setMaxChargingCurrent",
"https://api.wall-box.com/v2/charger/12345", new=Mock(side_effect=http_404_error),
json={CHARGER_MAX_CHARGING_CURRENT_KEY: 20}, ),
status_code=404, pytest.raises(HomeAssistantError),
):
await hass.services.async_call(
"number",
SERVICE_SET_VALUE,
{
ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ID,
ATTR_VALUE: 20,
},
blocking=True,
) )
with pytest.raises(ConnectionError):
await hass.services.async_call(
"number",
SERVICE_SET_VALUE,
{
ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ID,
ATTR_VALUE: 20,
},
blocking=True,
)
async def test_wallbox_number_class_too_many_requests(
async def test_wallbox_number_class_energy_price_connection_error(
hass: HomeAssistant, entry: MockConfigEntry hass: HomeAssistant, entry: MockConfigEntry
) -> None: ) -> None:
"""Test wallbox sensor class.""" """Test wallbox sensor class."""
await setup_integration(hass, entry) await setup_integration(hass, entry)
with requests_mock.Mocker() as mock_request: with (
mock_request.get( patch(
"https://user-api.wall-box.com/users/signin", "homeassistant.components.wallbox.Wallbox.authenticate",
json=authorisation_response, new=Mock(return_value=authorisation_response),
status_code=200, ),
) patch(
mock_request.post( "homeassistant.components.wallbox.Wallbox.setMaxChargingCurrent",
"https://api.wall-box.com/chargers/config/12345", new=Mock(side_effect=http_429_error),
json={CHARGER_ENERGY_PRICE_KEY: 1.1}, ),
status_code=404, pytest.raises(HomeAssistantError),
):
await hass.services.async_call(
"number",
SERVICE_SET_VALUE,
{
ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ID,
ATTR_VALUE: 20,
},
blocking=True,
) )
with pytest.raises(ConnectionError):
await hass.services.async_call( async def test_wallbox_number_class_energy_price_update_failed(
"number", hass: HomeAssistant, entry: MockConfigEntry
SERVICE_SET_VALUE, ) -> None:
{ """Test wallbox sensor class."""
ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ENERGY_PRICE_ID,
ATTR_VALUE: 1.1, await setup_integration(hass, entry)
},
blocking=True, with (
) patch(
"homeassistant.components.wallbox.Wallbox.authenticate",
new=Mock(return_value=authorisation_response),
),
patch(
"homeassistant.components.wallbox.Wallbox.setEnergyCost",
new=Mock(side_effect=http_429_error),
),
pytest.raises(HomeAssistantError),
):
await hass.services.async_call(
"number",
SERVICE_SET_VALUE,
{
ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ENERGY_PRICE_ID,
ATTR_VALUE: 1.1,
},
blocking=True,
)
async def test_wallbox_number_class_energy_price_update_connection_error(
hass: HomeAssistant, entry: MockConfigEntry
) -> None:
"""Test wallbox sensor class."""
await setup_integration(hass, entry)
with (
patch(
"homeassistant.components.wallbox.Wallbox.authenticate",
new=Mock(return_value=authorisation_response),
),
patch(
"homeassistant.components.wallbox.Wallbox.setEnergyCost",
new=Mock(side_effect=http_404_error),
),
pytest.raises(HomeAssistantError),
):
await hass.services.async_call(
"number",
SERVICE_SET_VALUE,
{
ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ENERGY_PRICE_ID,
ATTR_VALUE: 1.1,
},
blocking=True,
)
async def test_wallbox_number_class_energy_price_auth_error( async def test_wallbox_number_class_energy_price_auth_error(
@@ -175,28 +237,26 @@ async def test_wallbox_number_class_energy_price_auth_error(
await setup_integration(hass, entry) await setup_integration(hass, entry)
with requests_mock.Mocker() as mock_request: with (
mock_request.get( patch(
"https://user-api.wall-box.com/users/signin", "homeassistant.components.wallbox.Wallbox.authenticate",
json=authorisation_response, new=Mock(return_value=authorisation_response),
status_code=200, ),
patch(
"homeassistant.components.wallbox.Wallbox.setEnergyCost",
new=Mock(side_effect=http_429_error),
),
pytest.raises(HomeAssistantError),
):
await hass.services.async_call(
"number",
SERVICE_SET_VALUE,
{
ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ENERGY_PRICE_ID,
ATTR_VALUE: 1.1,
},
blocking=True,
) )
mock_request.post(
"https://api.wall-box.com/chargers/config/12345",
json={CHARGER_ENERGY_PRICE_KEY: 1.1},
status_code=403,
)
with pytest.raises(ConfigEntryAuthFailed):
await hass.services.async_call(
"number",
SERVICE_SET_VALUE,
{
ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ENERGY_PRICE_ID,
ATTR_VALUE: 1.1,
},
blocking=True,
)
async def test_wallbox_number_class_platform_not_ready( async def test_wallbox_number_class_platform_not_ready(
@@ -218,19 +278,16 @@ async def test_wallbox_number_class_icp_energy(
await setup_integration(hass, entry) await setup_integration(hass, entry)
with requests_mock.Mocker() as mock_request: with (
mock_request.get( patch(
"https://user-api.wall-box.com/users/signin", "homeassistant.components.wallbox.Wallbox.authenticate",
json=authorisation_response, new=Mock(return_value=authorisation_response),
status_code=200, ),
) patch(
"homeassistant.components.wallbox.Wallbox.setIcpMaxCurrent",
mock_request.post( new=Mock(return_value={CHARGER_MAX_ICP_CURRENT_KEY: 10}),
"https://api.wall-box.com/chargers/config/12345", ),
json={CHARGER_MAX_ICP_CURRENT_KEY: 10}, ):
status_code=200,
)
await hass.services.async_call( await hass.services.async_call(
NUMBER_DOMAIN, NUMBER_DOMAIN,
SERVICE_SET_VALUE, SERVICE_SET_VALUE,
@@ -249,28 +306,26 @@ async def test_wallbox_number_class_icp_energy_auth_error(
await setup_integration(hass, entry) await setup_integration(hass, entry)
with requests_mock.Mocker() as mock_request: with (
mock_request.get( patch(
"https://user-api.wall-box.com/users/signin", "homeassistant.components.wallbox.Wallbox.authenticate",
json=authorisation_response, new=Mock(return_value=authorisation_response),
status_code=200, ),
patch(
"homeassistant.components.wallbox.Wallbox.setIcpMaxCurrent",
new=Mock(side_effect=http_403_error),
),
pytest.raises(InvalidAuth),
):
await hass.services.async_call(
NUMBER_DOMAIN,
SERVICE_SET_VALUE,
{
ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ICP_CURRENT_ID,
ATTR_VALUE: 10,
},
blocking=True,
) )
mock_request.post(
"https://api.wall-box.com/chargers/config/12345",
json={CHARGER_MAX_ICP_CURRENT_KEY: 10},
status_code=403,
)
with pytest.raises(InvalidAuth):
await hass.services.async_call(
NUMBER_DOMAIN,
SERVICE_SET_VALUE,
{
ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ICP_CURRENT_ID,
ATTR_VALUE: 10,
},
blocking=True,
)
async def test_wallbox_number_class_icp_energy_connection_error( async def test_wallbox_number_class_icp_energy_connection_error(
@@ -280,25 +335,52 @@ async def test_wallbox_number_class_icp_energy_connection_error(
await setup_integration(hass, entry) await setup_integration(hass, entry)
with requests_mock.Mocker() as mock_request: with (
mock_request.get( patch(
"https://user-api.wall-box.com/users/signin", "homeassistant.components.wallbox.Wallbox.authenticate",
json=authorisation_response, new=Mock(return_value=authorisation_response),
status_code=200, ),
) patch(
mock_request.post( "homeassistant.components.wallbox.Wallbox.setIcpMaxCurrent",
"https://api.wall-box.com/chargers/config/12345", new=Mock(side_effect=http_404_error),
json={CHARGER_MAX_ICP_CURRENT_KEY: 10}, ),
status_code=404, pytest.raises(HomeAssistantError),
):
await hass.services.async_call(
NUMBER_DOMAIN,
SERVICE_SET_VALUE,
{
ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ICP_CURRENT_ID,
ATTR_VALUE: 10,
},
blocking=True,
) )
with pytest.raises(ConnectionError):
await hass.services.async_call( async def test_wallbox_number_class_icp_energy_too_many_request(
NUMBER_DOMAIN, hass: HomeAssistant, entry: MockConfigEntry
SERVICE_SET_VALUE, ) -> None:
{ """Test wallbox sensor class."""
ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ICP_CURRENT_ID,
ATTR_VALUE: 10, await setup_integration(hass, entry)
},
blocking=True, with (
) patch(
"homeassistant.components.wallbox.Wallbox.authenticate",
new=Mock(return_value=authorisation_response),
),
patch(
"homeassistant.components.wallbox.Wallbox.setIcpMaxCurrent",
new=Mock(side_effect=http_429_error),
),
pytest.raises(HomeAssistantError),
):
await hass.services.async_call(
NUMBER_DOMAIN,
SERVICE_SET_VALUE,
{
ATTR_ENTITY_ID: MOCK_NUMBER_ENTITY_ICP_CURRENT_ID,
ATTR_VALUE: 10,
},
blocking=True,
)

View File

@@ -16,6 +16,7 @@ from homeassistant.core import HomeAssistant, HomeAssistantError
from . import ( from . import (
authorisation_response, authorisation_response,
http_404_error, http_404_error,
http_429_error,
setup_integration_select, setup_integration_select,
test_response, test_response,
test_response_eco_mode, test_response_eco_mode,
@@ -109,7 +110,41 @@ async def test_wallbox_select_class_error(
"homeassistant.components.wallbox.Wallbox.enableEcoSmart", "homeassistant.components.wallbox.Wallbox.enableEcoSmart",
new=Mock(side_effect=error), new=Mock(side_effect=error),
), ),
pytest.raises(HomeAssistantError, match="Error communicating with Wallbox API"), pytest.raises(HomeAssistantError),
):
await hass.services.async_call(
SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
{
ATTR_ENTITY_ID: MOCK_SELECT_ENTITY_ID,
ATTR_OPTION: mode,
},
blocking=True,
)
@pytest.mark.parametrize(("mode", "response"), TEST_OPTIONS)
async def test_wallbox_select_too_many_requests_error(
hass: HomeAssistant,
entry: MockConfigEntry,
mode,
response,
mock_authenticate,
) -> None:
"""Test wallbox select class connection error."""
await setup_integration_select(hass, entry, response)
with (
patch(
"homeassistant.components.wallbox.Wallbox.disableEcoSmart",
new=Mock(side_effect=http_429_error),
),
patch(
"homeassistant.components.wallbox.Wallbox.enableEcoSmart",
new=Mock(side_effect=http_429_error),
),
pytest.raises(HomeAssistantError),
): ):
await hass.services.async_call( await hass.services.async_call(
SELECT_DOMAIN, SELECT_DOMAIN,

View File

@@ -1,15 +1,16 @@
"""Test Wallbox Lock component.""" """Test Wallbox Lock component."""
from unittest.mock import Mock, patch
import pytest import pytest
import requests_mock
from homeassistant.components.switch import SERVICE_TURN_OFF, SERVICE_TURN_ON from homeassistant.components.switch import SERVICE_TURN_OFF, SERVICE_TURN_ON
from homeassistant.components.wallbox.const import CHARGER_STATUS_ID_KEY from homeassistant.components.wallbox.const import CHARGER_STATUS_ID_KEY
from homeassistant.const import ATTR_ENTITY_ID from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.exceptions import HomeAssistantError
from . import authorisation_response, setup_integration from . import authorisation_response, http_404_error, http_429_error, setup_integration
from .const import MOCK_SWITCH_ENTITY_ID from .const import MOCK_SWITCH_ENTITY_ID
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
@@ -26,18 +27,20 @@ async def test_wallbox_switch_class(
assert state assert state
assert state.state == "on" assert state.state == "on"
with requests_mock.Mocker() as mock_request: with (
mock_request.get( patch(
"https://user-api.wall-box.com/users/signin", "homeassistant.components.wallbox.Wallbox.authenticate",
json=authorisation_response, new=Mock(return_value=authorisation_response),
status_code=200, ),
) patch(
mock_request.post( "homeassistant.components.wallbox.Wallbox.pauseChargingSession",
"https://api.wall-box.com/v3/chargers/12345/remote-action", new=Mock(return_value={CHARGER_STATUS_ID_KEY: 193}),
json={CHARGER_STATUS_ID_KEY: 193}, ),
status_code=200, patch(
) "homeassistant.components.wallbox.Wallbox.resumeChargingSession",
new=Mock(return_value={CHARGER_STATUS_ID_KEY: 193}),
),
):
await hass.services.async_call( await hass.services.async_call(
"switch", "switch",
SERVICE_TURN_ON, SERVICE_TURN_ON,
@@ -64,72 +67,52 @@ async def test_wallbox_switch_class_connection_error(
await setup_integration(hass, entry) await setup_integration(hass, entry)
with requests_mock.Mocker() as mock_request: with (
mock_request.get( patch(
"https://user-api.wall-box.com/users/signin", "homeassistant.components.wallbox.Wallbox.authenticate",
json=authorisation_response, new=Mock(return_value=authorisation_response),
status_code=200, ),
) patch(
mock_request.post( "homeassistant.components.wallbox.Wallbox.resumeChargingSession",
"https://api.wall-box.com/v3/chargers/12345/remote-action", new=Mock(side_effect=http_404_error),
json={CHARGER_STATUS_ID_KEY: 193}, ),
status_code=404, pytest.raises(HomeAssistantError),
):
# Test behavior when a connection error occurs
await hass.services.async_call(
"switch",
SERVICE_TURN_ON,
{
ATTR_ENTITY_ID: MOCK_SWITCH_ENTITY_ID,
},
blocking=True,
) )
with pytest.raises(ConnectionError):
await hass.services.async_call(
"switch",
SERVICE_TURN_ON,
{
ATTR_ENTITY_ID: MOCK_SWITCH_ENTITY_ID,
},
blocking=True,
)
with pytest.raises(ConnectionError):
await hass.services.async_call(
"switch",
SERVICE_TURN_OFF,
{
ATTR_ENTITY_ID: MOCK_SWITCH_ENTITY_ID,
},
blocking=True,
)
async def test_wallbox_switch_class_too_many_requests(
async def test_wallbox_switch_class_authentication_error(
hass: HomeAssistant, entry: MockConfigEntry hass: HomeAssistant, entry: MockConfigEntry
) -> None: ) -> None:
"""Test wallbox switch class connection error.""" """Test wallbox switch class connection error."""
await setup_integration(hass, entry) await setup_integration(hass, entry)
with requests_mock.Mocker() as mock_request: with (
mock_request.get( patch(
"https://user-api.wall-box.com/users/signin", "homeassistant.components.wallbox.Wallbox.authenticate",
json=authorisation_response, new=Mock(return_value=authorisation_response),
status_code=200, ),
patch(
"homeassistant.components.wallbox.Wallbox.resumeChargingSession",
new=Mock(side_effect=http_429_error),
),
pytest.raises(HomeAssistantError),
):
# Test behavior when a connection error occurs
await hass.services.async_call(
"switch",
SERVICE_TURN_ON,
{
ATTR_ENTITY_ID: MOCK_SWITCH_ENTITY_ID,
},
blocking=True,
) )
mock_request.post(
"https://api.wall-box.com/v3/chargers/12345/remote-action",
json={CHARGER_STATUS_ID_KEY: 193},
status_code=403,
)
with pytest.raises(ConfigEntryAuthFailed):
await hass.services.async_call(
"switch",
SERVICE_TURN_ON,
{
ATTR_ENTITY_ID: MOCK_SWITCH_ENTITY_ID,
},
blocking=True,
)
with pytest.raises(ConfigEntryAuthFailed):
await hass.services.async_call(
"switch",
SERVICE_TURN_OFF,
{
ATTR_ENTITY_ID: MOCK_SWITCH_ENTITY_ID,
},
blocking=True,
)

View File

@@ -17,6 +17,9 @@ from homeassistant.components.websocket_api.auth import (
TYPE_AUTH_OK, TYPE_AUTH_OK,
TYPE_AUTH_REQUIRED, TYPE_AUTH_REQUIRED,
) )
from homeassistant.components.websocket_api.commands import (
ALL_SERVICE_DESCRIPTIONS_JSON_CACHE,
)
from homeassistant.components.websocket_api.const import FEATURE_COALESCE_MESSAGES, URL from homeassistant.components.websocket_api.const import FEATURE_COALESCE_MESSAGES, URL
from homeassistant.config_entries import ConfigEntryState from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import SIGNAL_BOOTSTRAP_INTEGRATIONS from homeassistant.const import SIGNAL_BOOTSTRAP_INTEGRATIONS
@@ -667,14 +670,41 @@ async def test_get_services(
hass: HomeAssistant, websocket_client: MockHAClientWebSocket hass: HomeAssistant, websocket_client: MockHAClientWebSocket
) -> None: ) -> None:
"""Test get_services command.""" """Test get_services command."""
for id_ in (5, 6): assert ALL_SERVICE_DESCRIPTIONS_JSON_CACHE not in hass.data
await websocket_client.send_json({"id": id_, "type": "get_services"}) await websocket_client.send_json_auto_id({"type": "get_services"})
msg = await websocket_client.receive_json()
assert msg == {"id": 1, "result": {}, "success": True, "type": "result"}
msg = await websocket_client.receive_json() # Check cache is reused
assert msg["id"] == id_ old_cache = hass.data[ALL_SERVICE_DESCRIPTIONS_JSON_CACHE]
assert msg["type"] == const.TYPE_RESULT await websocket_client.send_json_auto_id({"type": "get_services"})
assert msg["success"] msg = await websocket_client.receive_json()
assert msg["result"].keys() == hass.services.async_services().keys() assert msg == {"id": 2, "result": {}, "success": True, "type": "result"}
assert hass.data[ALL_SERVICE_DESCRIPTIONS_JSON_CACHE] is old_cache
# Load a service and check cache is updated
assert await async_setup_component(hass, "logger", {})
await websocket_client.send_json_auto_id({"type": "get_services"})
msg = await websocket_client.receive_json()
assert msg == {
"id": 3,
"result": {"logger": {"set_default_level": ANY, "set_level": ANY}},
"success": True,
"type": "result",
}
assert hass.data[ALL_SERVICE_DESCRIPTIONS_JSON_CACHE] is not old_cache
# Check cache is reused
old_cache = hass.data[ALL_SERVICE_DESCRIPTIONS_JSON_CACHE]
await websocket_client.send_json_auto_id({"type": "get_services"})
msg = await websocket_client.receive_json()
assert msg == {
"id": 4,
"result": {"logger": {"set_default_level": ANY, "set_level": ANY}},
"success": True,
"type": "result",
}
assert hass.data[ALL_SERVICE_DESCRIPTIONS_JSON_CACHE] is old_cache
async def test_get_config( async def test_get_config(