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.helpers.device_registry import DeviceEntry
from .const import DOMAIN, GATEWAY_SERIAL_PATTERN, PLATFORMS
from .const import DOMAIN, PLATFORMS
type DevoloHomeControlConfigEntry = ConfigEntry[list[HomeControl]]
@@ -33,10 +33,6 @@ async def async_setup_entry(
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:
for gateway in entry.runtime_data:
gateway.websocket_disconnect(

View File

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

View File

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

View File

@@ -45,7 +45,9 @@ class ImgwPibFlowHandler(ConfigFlow, domain=DOMAIN):
try:
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()
except (ClientError, TimeoutError, ApiError):

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/imgw_pib",
"iot_class": "cloud_polling",
"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."""
from homeassistant.components import notify as hass_notify
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv, discovery
from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN, PLATFORMS
from .coordinator import LaMetricDataUpdateCoordinator
from .coordinator import LaMetricConfigEntry, LaMetricDataUpdateCoordinator
from .services import async_setup_services
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:
"""Set up the LaMetric integration."""
async_setup_services(hass)
hass.data[DOMAIN] = {"hass_config": config}
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."""
coordinator = LaMetricDataUpdateCoordinator(hass, entry)
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)
# 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,
DOMAIN,
{CONF_NAME: coordinator.data.name, "entry_id": entry.entry_id},
hass.data[DOMAIN]["hass_config"],
{},
)
)
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."""
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)
return unload_ok

View File

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

View File

@@ -13,13 +13,15 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
from .const import DOMAIN, LOGGER, SCAN_INTERVAL
type LaMetricConfigEntry = ConfigEntry[LaMetricDataUpdateCoordinator]
class LaMetricDataUpdateCoordinator(DataUpdateCoordinator[Device]):
"""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."""
self.lametric = LaMetricDevice(
host=entry.data[CONF_HOST],

View File

@@ -6,11 +6,9 @@ import json
from typing import Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from .const import DOMAIN
from .coordinator import LaMetricDataUpdateCoordinator
from .coordinator import LaMetricConfigEntry
TO_REDACT = {
"device_id",
@@ -21,10 +19,10 @@ TO_REDACT = {
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry
hass: HomeAssistant, entry: LaMetricConfigEntry
) -> dict[str, Any]:
"""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
data = json.loads(coordinator.data.to_json())
return async_redact_data(data, TO_REDACT)

View File

@@ -31,4 +31,5 @@ class LaMetricEntity(CoordinatorEntity[LaMetricDataUpdateCoordinator]):
name=coordinator.data.name,
sw_version=coordinator.data.os_version,
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 .const import DOMAIN
from .coordinator import LaMetricDataUpdateCoordinator
from .coordinator import LaMetricConfigEntry, LaMetricDataUpdateCoordinator
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:
raise ValueError(f"Unknown LaMetric device ID: {device_id}")
for entry_id in device_entry.config_entries:
if (
(entry := hass.config_entries.async_get_entry(entry_id))
and entry.domain == DOMAIN
and entry.entry_id in hass.data[DOMAIN]
):
coordinator: LaMetricDataUpdateCoordinator = hass.data[DOMAIN][
entry.entry_id
]
return coordinator
entry: LaMetricConfigEntry
for entry in hass.config_entries.async_loaded_entries(DOMAIN):
if entry.entry_id in device_entry.config_entries:
return entry.runtime_data
raise ValueError(f"No coordinator for device ID: {device_id}")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,20 +7,19 @@ from typing import Any
import ultraheat_api
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_DEVICE, Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_entries
from .const import DOMAIN
from .coordinator import UltraheatCoordinator
from .coordinator import UltraheatConfigEntry, UltraheatCoordinator
_LOGGER = logging.getLogger(__name__)
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."""
_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)
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)
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."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
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."""
_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__)
type UltraheatConfigEntry = ConfigEntry[UltraheatCoordinator]
class UltraheatCoordinator(DataUpdateCoordinator[HeatMeterResponse]):
"""Coordinator for getting data from the ultraheat api."""
config_entry: ConfigEntry
config_entry: UltraheatConfigEntry
def __init__(
self, hass: HomeAssistant, config_entry: ConfigEntry, api: HeatMeterService
self,
hass: HomeAssistant,
config_entry: UltraheatConfigEntry,
api: HeatMeterService,
) -> None:
"""Initialize my coordinator."""
super().__init__(

View File

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

View File

@@ -7,21 +7,19 @@ import logging
from laundrify_aio import LaundrifyAPI
from laundrify_aio.exceptions import ApiConnectionException, UnauthorizedException
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ACCESS_TOKEN, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN
from .coordinator import LaundrifyUpdateCoordinator
from .coordinator import LaundrifyConfigEntry, LaundrifyUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
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."""
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()
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {
"api": api_client,
"coordinator": coordinator,
}
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
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."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_migrate_entry(hass: HomeAssistant, entry: LaundrifyConfigEntry) -> bool:
"""Migrate entry."""
_LOGGER.debug("Migrating from version %s", entry.version)

View File

@@ -10,28 +10,25 @@ from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN, MANUFACTURER, MODELS
from .coordinator import LaundrifyUpdateCoordinator
from .coordinator import LaundrifyConfigEntry, LaundrifyUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
config: ConfigEntry,
entry: LaundrifyConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up sensors from a config entry created in the integrations UI."""
coordinator: LaundrifyUpdateCoordinator = hass.data[DOMAIN][config.entry_id][
"coordinator"
]
coordinator = entry.runtime_data
async_add_entities(
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__)
type LaundrifyConfigEntry = ConfigEntry[LaundrifyUpdateCoordinator]
class LaundrifyUpdateCoordinator(DataUpdateCoordinator[dict[str, LaundrifyDevice]]):
"""Class to manage fetching laundrify API data."""

View File

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

View File

@@ -16,7 +16,6 @@ from pypck.connection import (
)
from pypck.lcn_defs import LcnEvent
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_DEVICE_ID,
CONF_DOMAIN,
@@ -38,21 +37,20 @@ from homeassistant.helpers import (
from homeassistant.helpers.typing import ConfigType
from .const import (
ADD_ENTITIES_CALLBACKS,
CONF_ACKNOWLEDGE,
CONF_DIM_MODE,
CONF_DOMAIN_DATA,
CONF_SK_NUM_TRIES,
CONF_TARGET_VALUE_LOCKED,
CONF_TRANSITION,
CONNECTION,
DEVICE_CONNECTIONS,
DOMAIN,
PLATFORMS,
)
from .helpers import (
AddressType,
InputType,
LcnConfigEntry,
LcnRuntimeData,
async_update_config_entry,
generate_unique_id,
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:
"""Set up the LCN component."""
hass.data.setdefault(DOMAIN, {})
async_setup_services(hass)
await register_panel_and_ws_api(hass)
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."""
if config_entry.entry_id in hass.data[DOMAIN]:
return False
settings = {
"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
_LOGGER.debug('LCN connected to "%s"', config_entry.title)
hass.data[DOMAIN][config_entry.entry_id] = {
CONNECTION: lcn_connection,
DEVICE_CONNECTIONS: {},
ADD_ENTITIES_CALLBACKS: {},
}
config_entry.runtime_data = LcnRuntimeData(
connection=lcn_connection,
device_connections={},
add_entities_callbacks={},
)
# Update config_entry with LCN device serials
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
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."""
_LOGGER.debug(
"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(
hass: HomeAssistant, config_entry: ConfigEntry
hass: HomeAssistant, config_entry: LcnConfigEntry
) -> None:
"""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)
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."""
# forward unloading to platforms
unload_ok = await hass.config_entries.async_unload_platforms(
config_entry, PLATFORMS
)
if unload_ok and config_entry.entry_id in hass.data[DOMAIN]:
host = hass.data[DOMAIN].pop(config_entry.entry_id)
await host[CONNECTION].async_close()
if unload_ok:
await config_entry.runtime_data.connection.async_close()
return unload_ok
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:
"""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:
"""Close connection and schedule config entry for reload."""
@@ -258,7 +253,7 @@ def async_host_event_received(
def async_host_input_received(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: LcnConfigEntry,
device_registry: dr.DeviceRegistry,
inp: pypck.inputs.Input,
) -> None:
@@ -266,7 +261,7 @@ def async_host_input_received(
if not isinstance(inp, pypck.inputs.ModInput):
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)
address = (
logical_address.seg_id,

View File

@@ -11,7 +11,6 @@ from homeassistant.components.binary_sensor import (
BinarySensorEntity,
)
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.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -22,19 +21,13 @@ from homeassistant.helpers.issue_registry import (
)
from homeassistant.helpers.typing import ConfigType
from .const import (
ADD_ENTITIES_CALLBACKS,
BINSENSOR_PORTS,
CONF_DOMAIN_DATA,
DOMAIN,
SETPOINTS,
)
from .const import BINSENSOR_PORTS, CONF_DOMAIN_DATA, DOMAIN, SETPOINTS
from .entity import LcnEntity
from .helpers import InputType
from .helpers import InputType, LcnConfigEntry
def add_lcn_entities(
config_entry: ConfigEntry,
config_entry: LcnConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
entity_configs: Iterable[ConfigType],
) -> None:
@@ -53,7 +46,7 @@ def add_lcn_entities(
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: LcnConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up LCN switch entities from a config entry."""
@@ -63,7 +56,7 @@ async def async_setup_entry(
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}
)
@@ -79,7 +72,7 @@ async def async_setup_entry(
class LcnRegulatorLockSensor(LcnEntity, BinarySensorEntity):
"""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."""
super().__init__(config, config_entry)
@@ -138,7 +131,7 @@ class LcnRegulatorLockSensor(LcnEntity, BinarySensorEntity):
class LcnBinarySensor(LcnEntity, BinarySensorEntity):
"""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."""
super().__init__(config, config_entry)
@@ -174,7 +167,7 @@ class LcnBinarySensor(LcnEntity, BinarySensorEntity):
class LcnLockKeysSensor(LcnEntity, BinarySensorEntity):
"""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."""
super().__init__(config, config_entry)

View File

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

View File

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

View File

@@ -12,28 +12,25 @@ from homeassistant.components.cover import (
CoverEntity,
CoverEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_DOMAIN, CONF_ENTITIES
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import ConfigType
from .const import (
ADD_ENTITIES_CALLBACKS,
CONF_DOMAIN_DATA,
CONF_MOTOR,
CONF_POSITIONING_MODE,
CONF_REVERSE_TIME,
DOMAIN,
)
from .entity import LcnEntity
from .helpers import InputType
from .helpers import InputType, LcnConfigEntry
PARALLEL_UPDATES = 0
def add_lcn_entities(
config_entry: ConfigEntry,
config_entry: LcnConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
entity_configs: Iterable[ConfigType],
) -> None:
@@ -50,7 +47,7 @@ def add_lcn_entities(
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: LcnConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up LCN cover entities from a config entry."""
@@ -60,7 +57,7 @@ async def async_setup_entry(
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}
)
@@ -81,7 +78,7 @@ class LcnOutputsCover(LcnEntity, CoverEntity):
_attr_is_opening = False
_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."""
super().__init__(config, config_entry)
@@ -188,7 +185,7 @@ class LcnRelayCover(LcnEntity, CoverEntity):
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."""
super().__init__(config, config_entry)

View File

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

View File

@@ -3,11 +3,14 @@
from __future__ import annotations
import asyncio
from collections.abc import Callable, Iterable
from copy import deepcopy
from dataclasses import dataclass
import re
from typing import cast
import pypck
from pypck.connection import PchkConnectionManager
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
@@ -33,12 +36,27 @@ from .const import (
CONF_HARDWARE_TYPE,
CONF_SCENES,
CONF_SOFTWARE_SERIAL,
CONNECTION,
DEVICE_CONNECTIONS,
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
type LcnConfigEntry = ConfigEntry[LcnRuntimeData]
type AddressType = tuple[int, int, bool]
type DeviceConnectionType = pypck.module.ModuleConnection | pypck.module.GroupConnection
@@ -62,10 +80,10 @@ DOMAIN_LOOKUP = {
def get_device_connection(
hass: HomeAssistant, address: AddressType, config_entry: ConfigEntry
hass: HomeAssistant, address: AddressType, config_entry: LcnConfigEntry
) -> DeviceConnectionType:
"""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)
return host_connection.get_address_conn(addr)
@@ -165,7 +183,7 @@ def purge_device_registry(
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."""
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(
hass: HomeAssistant, config_entry: ConfigEntry
hass: HomeAssistant, config_entry: LcnConfigEntry
) -> None:
"""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,
)
hass.data[DOMAIN][config_entry.entry_id][DEVICE_CONNECTIONS][
device_entry.id
] = get_device_connection(hass, address, config_entry)
config_entry.runtime_data.device_connections[device_entry.id] = (
get_device_connection(hass, address, config_entry)
)
async def async_update_device_config(
@@ -254,7 +272,7 @@ async def async_update_device_config(
async def async_update_config_entry(
hass: HomeAssistant, config_entry: ConfigEntry
hass: HomeAssistant, config_entry: LcnConfigEntry
) -> None:
"""Fill missing values in config_entry with infos from LCN bus."""
device_configs = deepcopy(config_entry.data[CONF_DEVICES])

View File

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

View File

@@ -7,28 +7,26 @@ from typing import Any
import pypck
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.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import ConfigType
from .const import (
ADD_ENTITIES_CALLBACKS,
CONF_DOMAIN_DATA,
CONF_OUTPUTS,
CONF_REGISTER,
CONF_TRANSITION,
DOMAIN,
OUTPUT_PORTS,
)
from .entity import LcnEntity
from .helpers import LcnConfigEntry
PARALLEL_UPDATES = 0
def add_lcn_entities(
config_entry: ConfigEntry,
config_entry: LcnConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
entity_configs: Iterable[ConfigType],
) -> None:
@@ -42,7 +40,7 @@ def add_lcn_entities(
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: LcnConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up LCN switch entities from a config entry."""
@@ -52,7 +50,7 @@ async def async_setup_entry(
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}
)
@@ -68,7 +66,7 @@ async def async_setup_entry(
class LcnScene(LcnEntity, 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."""
super().__init__(config, config_entry)

View File

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

View File

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

View File

@@ -7,29 +7,20 @@ from typing import Any
import pypck
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.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import ConfigType
from .const import (
ADD_ENTITIES_CALLBACKS,
CONF_DOMAIN_DATA,
CONF_OUTPUT,
DOMAIN,
OUTPUT_PORTS,
RELAY_PORTS,
SETPOINTS,
)
from .const import CONF_DOMAIN_DATA, CONF_OUTPUT, OUTPUT_PORTS, RELAY_PORTS, SETPOINTS
from .entity import LcnEntity
from .helpers import InputType
from .helpers import InputType, LcnConfigEntry
PARALLEL_UPDATES = 0
def add_lcn_switch_entities(
config_entry: ConfigEntry,
config_entry: LcnConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
entity_configs: Iterable[ConfigType],
) -> None:
@@ -52,7 +43,7 @@ def add_lcn_switch_entities(
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: LcnConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up LCN switch entities from a config entry."""
@@ -62,7 +53,7 @@ async def async_setup_entry(
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}
)
@@ -80,7 +71,7 @@ class LcnOutputSwitch(LcnEntity, SwitchEntity):
_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."""
super().__init__(config, config_entry)
@@ -129,7 +120,7 @@ class LcnRelaySwitch(LcnEntity, SwitchEntity):
_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."""
super().__init__(config, config_entry)
@@ -179,7 +170,7 @@ class LcnRegulatorLockSwitch(LcnEntity, SwitchEntity):
_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."""
super().__init__(config, config_entry)
@@ -235,7 +226,7 @@ class LcnKeyLockSwitch(LcnEntity, SwitchEntity):
_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."""
super().__init__(config, config_entry)

View File

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

View File

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

View File

@@ -90,7 +90,9 @@ def _require_authentication[_WallboxCoordinatorT: WallboxCoordinator, **_P](
except requests.exceptions.HTTPError as wallbox_connection_error:
if wallbox_connection_error.response.status_code == HTTPStatus.FORBIDDEN:
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
@@ -137,56 +139,65 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]):
@_require_authentication
def _get_data(self) -> dict[str, Any]:
"""Get new sensor data for Wallbox component."""
data: dict[str, Any] = self._wallbox.getChargerStatus(self._station)
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
try:
data: dict[str, Any] = self._wallbox.getChargerStatus(self._station)
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_CURRENCY_KEY] = (
f"{data[CHARGER_DATA_KEY][CHARGER_CURRENCY_KEY][CODE_KEY]}/kWh"
)
data[CHARGER_CURRENCY_KEY] = (
f"{data[CHARGER_DATA_KEY][CHARGER_CURRENCY_KEY][CODE_KEY]}/kWh"
)
data[CHARGER_STATUS_DESCRIPTION_KEY] = CHARGER_STATUS.get(
data[CHARGER_STATUS_ID_KEY], ChargerStatus.UNKNOWN
)
data[CHARGER_STATUS_DESCRIPTION_KEY] = CHARGER_STATUS.get(
data[CHARGER_STATUS_ID_KEY], ChargerStatus.UNKNOWN
)
# Set current solar charging mode
eco_smart_enabled = (
data[CHARGER_DATA_KEY]
.get(CHARGER_ECO_SMART_KEY, {})
.get(CHARGER_ECO_SMART_STATUS_KEY)
)
# Set current solar charging mode
eco_smart_enabled = (
data[CHARGER_DATA_KEY]
.get(CHARGER_ECO_SMART_KEY, {})
.get(CHARGER_ECO_SMART_STATUS_KEY)
)
eco_smart_mode = (
data[CHARGER_DATA_KEY]
.get(CHARGER_ECO_SMART_KEY, {})
.get(CHARGER_ECO_SMART_MODE_KEY)
)
if eco_smart_mode is None:
data[CHARGER_ECO_SMART_KEY] = EcoSmartMode.DISABLED
elif eco_smart_enabled is False:
data[CHARGER_ECO_SMART_KEY] = EcoSmartMode.OFF
elif eco_smart_mode == 0:
data[CHARGER_ECO_SMART_KEY] = EcoSmartMode.ECO_MODE
elif eco_smart_mode == 1:
data[CHARGER_ECO_SMART_KEY] = EcoSmartMode.FULL_SOLAR
eco_smart_mode = (
data[CHARGER_DATA_KEY]
.get(CHARGER_ECO_SMART_KEY, {})
.get(CHARGER_ECO_SMART_MODE_KEY)
)
if eco_smart_mode is None:
data[CHARGER_ECO_SMART_KEY] = EcoSmartMode.DISABLED
elif eco_smart_enabled is False:
data[CHARGER_ECO_SMART_KEY] = EcoSmartMode.OFF
elif eco_smart_mode == 0:
data[CHARGER_ECO_SMART_KEY] = EcoSmartMode.ECO_MODE
elif eco_smart_mode == 1:
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]:
"""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:
if wallbox_connection_error.response.status_code == 403:
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:
"""Set maximum charging current for Wallbox."""
@@ -217,7 +234,13 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]):
except requests.exceptions.HTTPError as wallbox_connection_error:
if wallbox_connection_error.response.status_code == 403:
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:
"""Set maximum icp current for Wallbox."""
@@ -227,8 +250,16 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]):
@_require_authentication
def _set_energy_cost(self, energy_cost: float) -> None:
"""Set energy cost for Wallbox."""
self._wallbox.setEnergyCost(self._station, energy_cost)
try:
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:
"""Set energy cost for Wallbox."""
@@ -246,7 +277,13 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]):
except requests.exceptions.HTTPError as wallbox_connection_error:
if wallbox_connection_error.response.status_code == 403:
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:
"""Set wallbox to locked or unlocked."""
@@ -256,11 +293,19 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]):
@_require_authentication
def _pause_charger(self, pause: bool) -> None:
"""Set wallbox to pause or resume."""
if pause:
self._wallbox.pauseChargingSession(self._station)
else:
self._wallbox.resumeChargingSession(self._station)
try:
if pause:
self._wallbox.pauseChargingSession(self._station)
else:
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:
"""Set wallbox to pause or resume."""
@@ -270,13 +315,21 @@ class WallboxCoordinator(DataUpdateCoordinator[dict[str, Any]]):
@_require_authentication
def _set_eco_smart(self, option: str) -> None:
"""Set wallbox solar charging mode."""
if option == EcoSmartMode.ECO_MODE:
self._wallbox.enableEcoSmart(self._station, 0)
elif option == EcoSmartMode.FULL_SOLAR:
self._wallbox.enableEcoSmart(self._station, 1)
else:
self._wallbox.disableEcoSmart(self._station)
try:
if option == EcoSmartMode.ECO_MODE:
self._wallbox.enableEcoSmart(self._station, 0)
elif option == EcoSmartMode.FULL_SOLAR:
self._wallbox.enableEcoSmart(self._station, 1)
else:
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:
"""Set wallbox solar charging mode."""

View File

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

View File

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

View File

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

View File

@@ -112,6 +112,9 @@
"exceptions": {
"api_failed": {
"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"],
"documentation": "https://www.home-assistant.io/integrations/yolink",
"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
def available(self) -> bool:
"""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.
return self.coordinator.dev_net_type != ATTR_DEVICE_MODEL_A
return super().available

View File

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

View File

@@ -46,41 +46,16 @@ dependencies = [
"ciso8601==2.3.2",
"cronsim==2.6",
"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
# integration
"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
# httpcore, anyio, and h11 in gen_requirements_all
"httpx==0.28.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",
"Jinja2==3.1.6",
"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 has loose dependency. We want the latest one.
"cryptography==45.0.3",
@@ -90,22 +65,7 @@ dependencies = [
"orjson==3.10.18",
"packaging>=23.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",
# 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",
"requests==2.32.4",
"securetar==2025.2.1",

8
requirements.txt generated
View File

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

6
requirements_all.txt generated
View File

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

View File

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

View File

@@ -42,7 +42,7 @@ async def test_button_app_next(
assert entry.device_id
device_entry = device_registry.async_get(entry.device_id)
assert device_entry
assert device_entry.configuration_url is None
assert device_entry.configuration_url == "https://127.0.0.1/"
assert device_entry.connections == {
(dr.CONNECTION_NETWORK_MAC, "aa:bb:cc:dd:ee:ff")
}
@@ -89,7 +89,7 @@ async def test_button_app_previous(
assert entry.device_id
device_entry = device_registry.async_get(entry.device_id)
assert device_entry
assert device_entry.configuration_url is None
assert device_entry.configuration_url == "https://127.0.0.1/"
assert device_entry.connections == {
(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
device_entry = device_registry.async_get(entry.device_id)
assert device_entry
assert device_entry.configuration_url is None
assert device_entry.configuration_url == "https://127.0.0.1/"
assert device_entry.connections == {
(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
device_entry = device_registry.async_get(entry.device_id)
assert device_entry
assert device_entry.configuration_url is None
assert device_entry.configuration_url == "https://127.0.0.1/"
assert device_entry.connections == {
(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)
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.entry_type is None
assert device.hw_version is None
@@ -104,7 +104,7 @@ async def test_volume(
device = device_registry.async_get(entry.device_id)
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.entry_type 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)
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.entry_type 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)
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.entry_type 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)
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.entry_type 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
import pytest
from homeassistant.components.laundrify import DOMAIN
from homeassistant.components.laundrify.const import MANUFACTURER
from homeassistant.components.laundrify.const import DOMAIN, MANUFACTURER
from homeassistant.const import CONF_ACCESS_TOKEN
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.response = requests.Response()
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 = {
"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:
"""Test wallbox sensor class setup."""

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,15 +1,16 @@
"""Test Wallbox Lock component."""
from unittest.mock import Mock, patch
import pytest
import requests_mock
from homeassistant.components.switch import SERVICE_TURN_OFF, SERVICE_TURN_ON
from homeassistant.components.wallbox.const import CHARGER_STATUS_ID_KEY
from homeassistant.const import ATTR_ENTITY_ID
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 tests.common import MockConfigEntry
@@ -26,18 +27,20 @@ async def test_wallbox_switch_class(
assert state
assert state.state == "on"
with requests_mock.Mocker() as mock_request:
mock_request.get(
"https://user-api.wall-box.com/users/signin",
json=authorisation_response,
status_code=200,
)
mock_request.post(
"https://api.wall-box.com/v3/chargers/12345/remote-action",
json={CHARGER_STATUS_ID_KEY: 193},
status_code=200,
)
with (
patch(
"homeassistant.components.wallbox.Wallbox.authenticate",
new=Mock(return_value=authorisation_response),
),
patch(
"homeassistant.components.wallbox.Wallbox.pauseChargingSession",
new=Mock(return_value={CHARGER_STATUS_ID_KEY: 193}),
),
patch(
"homeassistant.components.wallbox.Wallbox.resumeChargingSession",
new=Mock(return_value={CHARGER_STATUS_ID_KEY: 193}),
),
):
await hass.services.async_call(
"switch",
SERVICE_TURN_ON,
@@ -64,72 +67,52 @@ async def test_wallbox_switch_class_connection_error(
await setup_integration(hass, entry)
with requests_mock.Mocker() as mock_request:
mock_request.get(
"https://user-api.wall-box.com/users/signin",
json=authorisation_response,
status_code=200,
)
mock_request.post(
"https://api.wall-box.com/v3/chargers/12345/remote-action",
json={CHARGER_STATUS_ID_KEY: 193},
status_code=404,
with (
patch(
"homeassistant.components.wallbox.Wallbox.authenticate",
new=Mock(return_value=authorisation_response),
),
patch(
"homeassistant.components.wallbox.Wallbox.resumeChargingSession",
new=Mock(side_effect=http_404_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,
)
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_authentication_error(
async def test_wallbox_switch_class_too_many_requests(
hass: HomeAssistant, entry: MockConfigEntry
) -> None:
"""Test wallbox switch class connection error."""
await setup_integration(hass, entry)
with requests_mock.Mocker() as mock_request:
mock_request.get(
"https://user-api.wall-box.com/users/signin",
json=authorisation_response,
status_code=200,
with (
patch(
"homeassistant.components.wallbox.Wallbox.authenticate",
new=Mock(return_value=authorisation_response),
),
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_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.config_entries import ConfigEntryState
from homeassistant.const import SIGNAL_BOOTSTRAP_INTEGRATIONS
@@ -667,14 +670,41 @@ async def test_get_services(
hass: HomeAssistant, websocket_client: MockHAClientWebSocket
) -> None:
"""Test get_services command."""
for id_ in (5, 6):
await websocket_client.send_json({"id": id_, "type": "get_services"})
assert ALL_SERVICE_DESCRIPTIONS_JSON_CACHE not in hass.data
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()
assert msg["id"] == id_
assert msg["type"] == const.TYPE_RESULT
assert msg["success"]
assert msg["result"].keys() == hass.services.async_services().keys()
# 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": 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(