Add lektrico integration (#102371)

* Add Lektrico Integration

* Make the changes proposed by Lash-L: new coordinator.py, new entity.py; use: translation_key, last_update_sucess, PlatformNotReady; remove: global variables

* Replace FlowResult with ConfigFlowResult and add tests.

* Remove unused lines.

* Remove Options from condif_flow

* Fix ruff and mypy.

* Fix CODEOWNERS.

* Run python3 -m script.hassfest.

* Correct rebase mistake.

* Make modifications suggested by emontnemery.

* Add pytest fixtures.

* Remove meaningless patches.

* Update .coveragerc

* Replace CONF_FRIENDLY_NAME with CONF_NAME.

* Remove underscores.

* Update tests.

* Update test file with is and no config_entries. .

* Set serial_number in DeviceInfo and add return type of the async_update_data to DataUpdateCoordinator.

* Use suggested_unit_of_measurement for KILO_WATT and replace Any in value_fn (sensor file).

* Add device class duration to charging_time sensor.

* Change raising  PlatformNotReady to raising IntegrationError.

* Test the unique id of the entry.

* Rename PF Lx with Power factor Lx and remove PF from strings.json.

* Remove comment.

* Make state and limit reason sensors to be enum sensors.

* Use result variable to check unique_id in test.

* Remove CONF_NAME from entry and __init__ from LektricoFlowHandler.

* Remove session parameter from LektricoDeviceDataUpdateCoordinator.

* Use config_entry: ConfigEntry in coordinator.

* Replace Connected,NeedAuth with Waiting for Authentication.

* Use lektricowifi 0.0.29.

* Use lektricowifi 0.0.39

* Use lektricowifi 0.0.40

* Use lektricowifi 0.0.41

* Replace hass.data with entry.runtime_data

* Delete .coveragerc

* Restructure the user step

* Fix tests

* Add returned value of _async_update_data to class DataUpdateCoordinator

* Use hw_version at DeviceInfo

* Remove a variable

* Use StateType

* Replace friendly_name with device_name

* Use sentence case in translation strings

* Uncomment and fix test_discovered_zeroconf

* Add type LektricoConfigEntry

* Remove commented code

* Remove the type of coordinator in sensor async_setup_entry

* Make zeroconf test end in ABORT, not FORM

* Remove all async_block_till_done from tests

* End test_user_setup_device_offline with CREATE_ENTRY

* Patch the full Device

* Add snapshot tests

* Overwrite the type LektricoSensorEntityDescription outside of the constructor

* Test separate already_configured for zeroconf

---------

Co-authored-by: mihaela.tarjoianu <mihaela.tarjoianu@scada.ro>
Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
Lektri.co 2024-08-30 14:20:15 +03:00 committed by GitHub
parent 397198c6d0
commit 5bd736029f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 1693 additions and 0 deletions

View File

@ -279,6 +279,7 @@ homeassistant.components.lawn_mower.*
homeassistant.components.lcn.*
homeassistant.components.ld2410_ble.*
homeassistant.components.led_ble.*
homeassistant.components.lektrico.*
homeassistant.components.lidarr.*
homeassistant.components.lifx.*
homeassistant.components.light.*

View File

@ -799,6 +799,8 @@ build.json @home-assistant/supervisor
/tests/components/leaone/ @bdraco
/homeassistant/components/led_ble/ @bdraco
/tests/components/led_ble/ @bdraco
/homeassistant/components/lektrico/ @lektrico
/tests/components/lektrico/ @lektrico
/homeassistant/components/lg_netcast/ @Drafteed @splinter98
/tests/components/lg_netcast/ @Drafteed @splinter98
/homeassistant/components/lidarr/ @tkdrob

View File

@ -0,0 +1,51 @@
"""The Lektrico Charging Station integration."""
from __future__ import annotations
from lektricowifi import Device
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_SERIAL_NUMBER, CONF_TYPE, Platform
from homeassistant.core import HomeAssistant
from .coordinator import LektricoDeviceDataUpdateCoordinator
# List the platforms that charger supports.
CHARGERS_PLATFORMS = [Platform.SENSOR]
# List the platforms that load balancer device supports.
LB_DEVICES_PLATFORMS = [Platform.SENSOR]
type LektricoConfigEntry = ConfigEntry[LektricoDeviceDataUpdateCoordinator]
async def async_setup_entry(hass: HomeAssistant, entry: LektricoConfigEntry) -> bool:
"""Set up Lektrico Charging Station from a config entry."""
coordinator = LektricoDeviceDataUpdateCoordinator(
hass,
f"{entry.data[CONF_TYPE]}_{entry.data[ATTR_SERIAL_NUMBER]}",
)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, _get_platforms(entry))
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(
entry, _get_platforms(entry)
)
def _get_platforms(entry: ConfigEntry) -> list[Platform]:
"""Return the platforms for this type of device."""
_device_type: str = entry.data[CONF_TYPE]
if _device_type in (Device.TYPE_1P7K, Device.TYPE_3P22K):
return CHARGERS_PLATFORMS
return LB_DEVICES_PLATFORMS

View File

@ -0,0 +1,138 @@
"""Config flow for Lektrico Charging Station."""
from __future__ import annotations
from typing import Any
from lektricowifi import Device, DeviceConnectionError
import voluptuous as vol
from homeassistant.components.zeroconf import ZeroconfServiceInfo
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import (
ATTR_HW_VERSION,
ATTR_SERIAL_NUMBER,
CONF_HOST,
CONF_TYPE,
)
from homeassistant.core import callback
from homeassistant.helpers.httpx_client import get_async_client
from .const import DOMAIN
STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOST): str,
}
)
class LektricoFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle a Lektrico config flow."""
VERSION = 1
_host: str
_name: str
_serial_number: str
_board_revision: str
_device_type: str
async def async_step_user(
self, user_input: dict[str, str] | None = None
) -> ConfigFlowResult:
"""Handle a flow initiated by the user."""
errors = None
if user_input is not None:
self._host = user_input[CONF_HOST]
# obtain serial number
try:
await self._get_lektrico_device_settings_and_treat_unique_id()
return self._async_create_entry()
except DeviceConnectionError:
errors = {CONF_HOST: "cannot_connect"}
return self._async_show_setup_form(user_input=user_input, errors=errors)
@callback
def _async_show_setup_form(
self,
user_input: dict[str, Any] | None = None,
errors: dict[str, str] | None = None,
) -> ConfigFlowResult:
"""Show the setup form to the user."""
if user_input is None:
user_input = {}
schema = self.add_suggested_values_to_schema(STEP_USER_DATA_SCHEMA, user_input)
return self.async_show_form(
step_id="user",
data_schema=schema,
errors=errors or {},
)
@callback
def _async_create_entry(self) -> ConfigFlowResult:
return self.async_create_entry(
title=self._name,
data={
CONF_HOST: self._host,
ATTR_SERIAL_NUMBER: self._serial_number,
CONF_TYPE: self._device_type,
ATTR_HW_VERSION: self._board_revision,
},
)
async def async_step_zeroconf(
self, discovery_info: ZeroconfServiceInfo
) -> ConfigFlowResult:
"""Handle zeroconf discovery."""
self._host = discovery_info.host # 192.168.100.11
# read settings from the device
try:
await self._get_lektrico_device_settings_and_treat_unique_id()
except DeviceConnectionError:
return self.async_abort(reason="cannot_connect")
self.context["title_placeholders"] = {
"serial_number": self._serial_number,
"name": self._name,
}
return await self.async_step_confirm()
async def _get_lektrico_device_settings_and_treat_unique_id(self) -> None:
"""Get device's serial number from a Lektrico device."""
device = Device(
_host=self._host,
asyncClient=get_async_client(self.hass),
)
settings = await device.device_config()
self._serial_number = str(settings["serial_number"])
self._device_type = settings["type"]
self._board_revision = settings["board_revision"]
self._name = f"{settings["type"]}_{self._serial_number}"
# Check if already configured
# Set unique id
await self.async_set_unique_id(self._serial_number, raise_on_progress=True)
# Abort if already configured, but update the last-known host
self._abort_if_unique_id_configured(
updates={CONF_HOST: self._host}, reload_on_update=True
)
async def async_step_confirm(
self, user_input: dict[str, str] | None = None
) -> ConfigFlowResult:
"""Allow the user to confirm adding the device."""
if user_input is not None:
return self._async_create_entry()
self._set_confirm_only()
return self.async_show_form(step_id="confirm")

View File

@ -0,0 +1,9 @@
"""Constants for the Lektrico Charging Station integration."""
from logging import Logger, getLogger
# Integration domain
DOMAIN = "lektrico"
# Logger
LOGGER: Logger = getLogger(__package__)

View File

@ -0,0 +1,52 @@
"""Coordinator for the Lektrico Charging Station integration."""
from __future__ import annotations
from datetime import timedelta
from typing import Any
from lektricowifi import Device, DeviceConnectionError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_HW_VERSION,
ATTR_SERIAL_NUMBER,
CONF_HOST,
CONF_TYPE,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.httpx_client import get_async_client
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import LOGGER
SCAN_INTERVAL = timedelta(seconds=10)
class LektricoDeviceDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"""Data update coordinator for Lektrico device."""
config_entry: ConfigEntry
def __init__(self, hass: HomeAssistant, device_name: str) -> None:
"""Initialize a Lektrico Device."""
super().__init__(
hass,
LOGGER,
name=device_name,
update_interval=SCAN_INTERVAL,
)
self.device = Device(
self.config_entry.data[CONF_HOST],
asyncClient=get_async_client(hass),
)
self.serial_number: str = self.config_entry.data[ATTR_SERIAL_NUMBER]
self.board_revision: str = self.config_entry.data[ATTR_HW_VERSION]
self.device_type: str = self.config_entry.data[CONF_TYPE]
async def _async_update_data(self) -> dict[str, Any]:
"""Async Update device state."""
try:
return await self.device.device_info(self.device_type)
except DeviceConnectionError as lek_ex:
raise UpdateFailed(lek_ex) from lek_ex

View File

@ -0,0 +1,33 @@
"""Entity classes for the Lektrico integration."""
from __future__ import annotations
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import LektricoDeviceDataUpdateCoordinator
from .const import DOMAIN
class LektricoEntity(CoordinatorEntity[LektricoDeviceDataUpdateCoordinator]):
"""Define an Lektrico entity."""
_attr_has_entity_name = True
def __init__(
self,
coordinator: LektricoDeviceDataUpdateCoordinator,
device_name: str,
) -> None:
"""Initialize."""
super().__init__(coordinator)
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, coordinator.serial_number)},
model=coordinator.device_type.upper(),
name=device_name,
manufacturer="Lektrico",
sw_version=coordinator.data["fw_version"],
hw_version=coordinator.board_revision,
serial_number=coordinator.serial_number,
)

View File

@ -0,0 +1,16 @@
{
"domain": "lektrico",
"name": "Lektrico Charging Station",
"codeowners": ["@lektrico"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/lektrico",
"integration_type": "device",
"iot_class": "local_polling",
"requirements": ["lektricowifi==0.0.41"],
"zeroconf": [
{
"type": "_http._tcp.local.",
"name": "lektrico*"
}
]
}

View File

@ -0,0 +1,324 @@
"""Support for Lektrico charging station sensors."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any
from lektricowifi import Device
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import (
ATTR_SERIAL_NUMBER,
CONF_TYPE,
PERCENTAGE,
UnitOfElectricCurrent,
UnitOfElectricPotential,
UnitOfEnergy,
UnitOfPower,
UnitOfTemperature,
UnitOfTime,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import IntegrationError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
from . import LektricoConfigEntry, LektricoDeviceDataUpdateCoordinator
from .entity import LektricoEntity
@dataclass(frozen=True, kw_only=True)
class LektricoSensorEntityDescription(SensorEntityDescription):
"""A class that describes the Lektrico sensor entities."""
value_fn: Callable[[dict[str, Any]], StateType]
SENSORS_FOR_CHARGERS: tuple[LektricoSensorEntityDescription, ...] = (
LektricoSensorEntityDescription(
key="state",
device_class=SensorDeviceClass.ENUM,
options=[
"available",
"connected",
"need_auth",
"paused",
"charging",
"error",
"updating_firmware",
],
translation_key="state",
value_fn=lambda data: str(data["charger_state"]),
),
LektricoSensorEntityDescription(
key="charging_time",
translation_key="charging_time",
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.SECONDS,
value_fn=lambda data: int(data["charging_time"]),
),
LektricoSensorEntityDescription(
key="power",
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfPower.WATT,
suggested_unit_of_measurement=UnitOfPower.KILO_WATT,
value_fn=lambda data: float(data["instant_power"]),
),
LektricoSensorEntityDescription(
key="energy",
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
value_fn=lambda data: float(data["session_energy"]) / 1000,
),
LektricoSensorEntityDescription(
key="temperature",
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
value_fn=lambda data: float(data["temperature"]),
),
LektricoSensorEntityDescription(
key="lifetime_energy",
translation_key="lifetime_energy",
state_class=SensorStateClass.TOTAL_INCREASING,
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
value_fn=lambda data: int(data["total_charged_energy"]),
),
LektricoSensorEntityDescription(
key="installation_current",
translation_key="installation_current",
device_class=SensorDeviceClass.CURRENT,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
value_fn=lambda data: int(data["install_current"]),
),
LektricoSensorEntityDescription(
key="limit_reason",
translation_key="limit_reason",
device_class=SensorDeviceClass.ENUM,
options=[
"no_limit",
"installation_current",
"user_limit",
"dynamic_limit",
"schedule",
"em_offline",
"em",
"ocpp",
],
value_fn=lambda data: str(data["current_limit_reason"]),
),
)
SENSORS_FOR_LB_DEVICES: tuple[LektricoSensorEntityDescription, ...] = (
LektricoSensorEntityDescription(
key="breaker_current",
translation_key="breaker_current",
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
value_fn=lambda data: int(data["breaker_curent"]),
),
)
SENSORS_FOR_1_PHASE: tuple[LektricoSensorEntityDescription, ...] = (
LektricoSensorEntityDescription(
key="voltage",
device_class=SensorDeviceClass.VOLTAGE,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
value_fn=lambda data: float(data["voltage_l1"]),
),
LektricoSensorEntityDescription(
key="current",
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
value_fn=lambda data: float(data["current_l1"]),
),
)
SENSORS_FOR_3_PHASE: tuple[LektricoSensorEntityDescription, ...] = (
LektricoSensorEntityDescription(
key="voltage_l1",
translation_key="voltage_l1",
device_class=SensorDeviceClass.VOLTAGE,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
value_fn=lambda data: float(data["voltage_l1"]),
),
LektricoSensorEntityDescription(
key="voltage_l2",
translation_key="voltage_l2",
device_class=SensorDeviceClass.VOLTAGE,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
value_fn=lambda data: float(data["voltage_l2"]),
),
LektricoSensorEntityDescription(
key="voltage_l3",
translation_key="voltage_l3",
device_class=SensorDeviceClass.VOLTAGE,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
value_fn=lambda data: float(data["voltage_l3"]),
),
LektricoSensorEntityDescription(
key="current_l1",
translation_key="current_l1",
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
value_fn=lambda data: float(data["current_l1"]),
),
LektricoSensorEntityDescription(
key="current_l2",
translation_key="current_l2",
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
value_fn=lambda data: float(data["current_l2"]),
),
LektricoSensorEntityDescription(
key="current_l3",
translation_key="current_l3",
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
value_fn=lambda data: float(data["current_l3"]),
),
)
SENSORS_FOR_LB_1_PHASE: tuple[LektricoSensorEntityDescription, ...] = (
LektricoSensorEntityDescription(
key="power",
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfPower.WATT,
suggested_unit_of_measurement=UnitOfPower.KILO_WATT,
value_fn=lambda data: float(data["power_l1"]),
),
LektricoSensorEntityDescription(
key="pf",
device_class=SensorDeviceClass.POWER_FACTOR,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
value_fn=lambda data: float(data["power_factor_l1"]) * 100,
),
)
SENSORS_FOR_LB_3_PHASE: tuple[LektricoSensorEntityDescription, ...] = (
LektricoSensorEntityDescription(
key="power_l1",
translation_key="power_l1",
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfPower.WATT,
suggested_unit_of_measurement=UnitOfPower.KILO_WATT,
value_fn=lambda data: float(data["power_l1"]),
),
LektricoSensorEntityDescription(
key="power_l2",
translation_key="power_l2",
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfPower.WATT,
suggested_unit_of_measurement=UnitOfPower.KILO_WATT,
value_fn=lambda data: float(data["power_l2"]),
),
LektricoSensorEntityDescription(
key="power_l3",
translation_key="power_l3",
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfPower.WATT,
suggested_unit_of_measurement=UnitOfPower.KILO_WATT,
value_fn=lambda data: float(data["power_l3"]),
),
LektricoSensorEntityDescription(
key="pf_l1",
translation_key="pf_l1",
device_class=SensorDeviceClass.POWER_FACTOR,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
value_fn=lambda data: float(data["power_factor_l1"]) * 100,
),
LektricoSensorEntityDescription(
key="pf_l2",
translation_key="pf_l2",
device_class=SensorDeviceClass.POWER_FACTOR,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
value_fn=lambda data: float(data["power_factor_l2"]) * 100,
),
LektricoSensorEntityDescription(
key="pf_l3",
translation_key="pf_l3",
device_class=SensorDeviceClass.POWER_FACTOR,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
value_fn=lambda data: float(data["power_factor_l3"]) * 100,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: LektricoConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Lektrico charger based on a config entry."""
coordinator = entry.runtime_data
sensors_to_be_used: tuple[LektricoSensorEntityDescription, ...]
if coordinator.device_type == Device.TYPE_1P7K:
sensors_to_be_used = SENSORS_FOR_CHARGERS + SENSORS_FOR_1_PHASE
elif coordinator.device_type == Device.TYPE_3P22K:
sensors_to_be_used = SENSORS_FOR_CHARGERS + SENSORS_FOR_3_PHASE
elif coordinator.device_type == Device.TYPE_EM:
sensors_to_be_used = (
SENSORS_FOR_LB_DEVICES + SENSORS_FOR_1_PHASE + SENSORS_FOR_LB_1_PHASE
)
elif coordinator.device_type == Device.TYPE_3EM:
sensors_to_be_used = (
SENSORS_FOR_LB_DEVICES + SENSORS_FOR_3_PHASE + SENSORS_FOR_LB_3_PHASE
)
else:
raise IntegrationError
async_add_entities(
LektricoSensor(
description,
coordinator,
f"{entry.data[CONF_TYPE]}_{entry.data[ATTR_SERIAL_NUMBER]}",
)
for description in sensors_to_be_used
)
class LektricoSensor(LektricoEntity, SensorEntity):
"""The entity class for Lektrico charging stations sensors."""
entity_description: LektricoSensorEntityDescription
def __init__(
self,
description: LektricoSensorEntityDescription,
coordinator: LektricoDeviceDataUpdateCoordinator,
device_name: str,
) -> None:
"""Initialize Lektrico charger."""
super().__init__(coordinator, device_name)
self.entity_description = description
self._attr_unique_id = f"{coordinator.serial_number}_{description.key}"
@property
def native_value(self) -> StateType:
"""Return the state of the sensor."""
return self.entity_description.value_fn(self.coordinator.data)

View File

@ -0,0 +1,101 @@
{
"config": {
"step": {
"user": {
"description": "Set required parameters to connect to your device",
"data": {
"host": "[%key:common::config_flow::data::host%]",
"device_name": "[%key:common::config_flow::data::name%]"
}
},
"zeroconf_confirm": {
"description": "Do you want to add the Lektrico Charger with serial number `{serial_number}` to Home Assistant?",
"title": "Discovered Lektrico Charger device"
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
}
},
"entity": {
"sensor": {
"state": {
"name": "State",
"state": {
"available": "Available",
"connected": "Connected",
"need_auth": "Waiting for authentication",
"paused": "Paused",
"charging": "Charging",
"error": "Error",
"updating_firmware": "Updating firmware"
}
},
"charging_time": {
"name": "Charging time"
},
"lifetime_energy": {
"name": "Lifetime energy"
},
"installation_current": {
"name": "Installation current"
},
"limit_reason": {
"name": "Limit reason",
"state": {
"no_limit": "No limit",
"installation_current": "Installation current",
"user_limit": "User limit",
"dynamic_limit": "Dynamic limit",
"schedule": "Schedule",
"em_offline": "EM offline",
"em": "EM",
"ocpp": "OCPP"
}
},
"breaker_current": {
"name": "Breaker current"
},
"voltage_l1": {
"name": "Voltage L1"
},
"voltage_l2": {
"name": "Voltage L2"
},
"voltage_l3": {
"name": "Voltage L3"
},
"current_l1": {
"name": "Current L1"
},
"current_l2": {
"name": "Current L2"
},
"current_l3": {
"name": "Current L3"
},
"power_l1": {
"name": "Power L1"
},
"power_l2": {
"name": "Power L2"
},
"power_l3": {
"name": "Power L3"
},
"pf_l1": {
"name": "Power factor L1"
},
"pf_l2": {
"name": "Power factor L2"
},
"pf_l3": {
"name": "Power factor L3"
}
}
}
}

View File

@ -315,6 +315,7 @@ FLOWS = {
"ld2410_ble",
"leaone",
"led_ble",
"lektrico",
"lg_netcast",
"lg_soundbar",
"lidarr",

View File

@ -3211,6 +3211,12 @@
"integration_type": "virtual",
"supported_by": "netatmo"
},
"lektrico": {
"name": "Lektrico Charging Station",
"integration_type": "device",
"config_flow": true,
"iot_class": "local_polling"
},
"leviton": {
"name": "Leviton",
"iot_standards": [

View File

@ -527,6 +527,10 @@ ZEROCONF = {
"domain": "bosch_shc",
"name": "bosch shc*",
},
{
"domain": "lektrico",
"name": "lektrico*",
},
{
"domain": "loqed",
"name": "loqed*",

View File

@ -2546,6 +2546,16 @@ disallow_untyped_defs = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.lektrico.*]
check_untyped_defs = true
disallow_incomplete_defs = true
disallow_subclassing_any = true
disallow_untyped_calls = true
disallow_untyped_decorators = true
disallow_untyped_defs = true
warn_return_any = true
warn_unreachable = true
[mypy-homeassistant.components.lidarr.*]
check_untyped_defs = true
disallow_incomplete_defs = true

View File

@ -1254,6 +1254,9 @@ leaone-ble==0.1.0
# homeassistant.components.led_ble
led-ble==1.0.2
# homeassistant.components.lektrico
lektricowifi==0.0.41
# homeassistant.components.foscam
libpyfoscam==1.2.2

View File

@ -1047,6 +1047,9 @@ leaone-ble==0.1.0
# homeassistant.components.led_ble
led-ble==1.0.2
# homeassistant.components.lektrico
lektricowifi==0.0.41
# homeassistant.components.foscam
libpyfoscam==1.2.2

View File

@ -0,0 +1,13 @@
"""Tests for Lektrico integration."""
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None:
"""Fixture for setting up the component."""
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()

View File

@ -0,0 +1,92 @@
"""Fixtures for Lektrico Charging Station integration tests."""
from collections.abc import Generator
from ipaddress import ip_address
import json
from unittest.mock import AsyncMock, patch
import pytest
from homeassistant.components.lektrico.const import DOMAIN
from homeassistant.components.zeroconf import ZeroconfServiceInfo
from homeassistant.const import (
ATTR_HW_VERSION,
ATTR_SERIAL_NUMBER,
CONF_HOST,
CONF_TYPE,
)
from tests.common import MockConfigEntry, load_fixture
MOCKED_DEVICE_IP_ADDRESS = "192.168.100.10"
MOCKED_DEVICE_SERIAL_NUMBER = "500006"
MOCKED_DEVICE_TYPE = "1p7k"
MOCKED_DEVICE_BOARD_REV = "B"
MOCKED_DEVICE_ZC_NAME = "Lektrico-1p7k-500006._http._tcp"
MOCKED_DEVICE_ZC_TYPE = "_http._tcp.local."
MOCKED_DEVICE_ZEROCONF_DATA = ZeroconfServiceInfo(
ip_address=ip_address(MOCKED_DEVICE_IP_ADDRESS),
ip_addresses=[ip_address(MOCKED_DEVICE_IP_ADDRESS)],
hostname=f"{MOCKED_DEVICE_ZC_NAME.lower()}.local.",
port=80,
type=MOCKED_DEVICE_ZC_TYPE,
name=MOCKED_DEVICE_ZC_NAME,
properties={
"id": "1p7k_500006",
"fw_id": "20230109-124642/v1.22-36-g56a3edd-develop-dirty",
},
)
@pytest.fixture
def mock_device() -> Generator[AsyncMock]:
"""Mock a Lektrico device."""
with (
patch(
"homeassistant.components.lektrico.Device",
autospec=True,
) as mock_device,
patch(
"homeassistant.components.lektrico.config_flow.Device",
new=mock_device,
),
patch(
"homeassistant.components.lektrico.coordinator.Device",
new=mock_device,
),
):
device = mock_device.return_value
device.device_config.return_value = json.loads(
load_fixture("get_config.json", DOMAIN)
)
device.device_info.return_value = json.loads(
load_fixture("get_info.json", DOMAIN)
)
yield device
@pytest.fixture
def mock_setup_entry() -> Generator[AsyncMock]:
"""Mock setup entry."""
with patch(
"homeassistant.components.lektrico.async_setup_entry", return_value=True
) as mock_setup_entry:
yield mock_setup_entry
@pytest.fixture
def mock_config_entry() -> MockConfigEntry:
"""Mock a config entry."""
return MockConfigEntry(
domain=DOMAIN,
data={
CONF_HOST: MOCKED_DEVICE_IP_ADDRESS,
CONF_TYPE: MOCKED_DEVICE_TYPE,
ATTR_SERIAL_NUMBER: MOCKED_DEVICE_SERIAL_NUMBER,
ATTR_HW_VERSION: "B",
},
unique_id=MOCKED_DEVICE_SERIAL_NUMBER,
)

View File

@ -0,0 +1,16 @@
{
"charger_state": "Available",
"charging_time": 0,
"instant_power": 0,
"session_energy": 0.0,
"temperature": 34.5,
"total_charged_energy": 0,
"install_current": 6,
"current_limit_reason": "Installation current",
"voltage_l1": 220.0,
"current_l1": 0.0,
"type": "1p7k",
"serial_number": "500006",
"board_revision": "B",
"fw_version": "1.44"
}

View File

@ -0,0 +1,5 @@
{
"type": "1p7k",
"serial_number": "500006",
"board_revision": "B"
}

View File

@ -0,0 +1,13 @@
{
"charger_state": "available",
"charging_time": 0,
"instant_power": 0,
"session_energy": 0.0,
"temperature": 34.5,
"total_charged_energy": 0,
"install_current": 6,
"current_limit_reason": "installation_current",
"voltage_l1": 220.0,
"current_l1": 0.0,
"fw_version": "1.44"
}

View File

@ -0,0 +1,33 @@
# serializer version: 1
# name: test_device_info
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'configuration_url': None,
'connections': set({
}),
'disabled_by': None,
'entry_type': None,
'hw_version': 'B',
'id': <ANY>,
'identifiers': set({
tuple(
'lektrico',
'500006',
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Lektrico',
'model': '1P7K',
'model_id': None,
'name': '1p7k_500006',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': '500006',
'suggested_area': None,
'sw_version': '1.44',
'via_device_id': None,
})
# ---

View File

@ -0,0 +1,534 @@
# serializer version: 1
# name: test_all_entities[sensor.1p7k_500006_charging_time-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.1p7k_500006_charging_time',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.DURATION: 'duration'>,
'original_icon': None,
'original_name': 'Charging time',
'platform': 'lektrico',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'charging_time',
'unique_id': '500006_charging_time',
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
})
# ---
# name: test_all_entities[sensor.1p7k_500006_charging_time-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'duration',
'friendly_name': '1p7k_500006 Charging time',
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
}),
'context': <ANY>,
'entity_id': 'sensor.1p7k_500006_charging_time',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0',
})
# ---
# name: test_all_entities[sensor.1p7k_500006_current-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.1p7k_500006_current',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.CURRENT: 'current'>,
'original_icon': None,
'original_name': 'Current',
'platform': 'lektrico',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '500006_current',
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
})
# ---
# name: test_all_entities[sensor.1p7k_500006_current-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'current',
'friendly_name': '1p7k_500006 Current',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
}),
'context': <ANY>,
'entity_id': 'sensor.1p7k_500006_current',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0.0',
})
# ---
# name: test_all_entities[sensor.1p7k_500006_energy-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.1p7k_500006_energy',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
'original_icon': None,
'original_name': 'Energy',
'platform': 'lektrico',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '500006_energy',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
# ---
# name: test_all_entities[sensor.1p7k_500006_energy-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'energy',
'friendly_name': '1p7k_500006 Energy',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
'entity_id': 'sensor.1p7k_500006_energy',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0.0',
})
# ---
# name: test_all_entities[sensor.1p7k_500006_installation_current-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.1p7k_500006_installation_current',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.CURRENT: 'current'>,
'original_icon': None,
'original_name': 'Installation current',
'platform': 'lektrico',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'installation_current',
'unique_id': '500006_installation_current',
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
})
# ---
# name: test_all_entities[sensor.1p7k_500006_installation_current-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'current',
'friendly_name': '1p7k_500006 Installation current',
'unit_of_measurement': <UnitOfElectricCurrent.AMPERE: 'A'>,
}),
'context': <ANY>,
'entity_id': 'sensor.1p7k_500006_installation_current',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '6',
})
# ---
# name: test_all_entities[sensor.1p7k_500006_lifetime_energy-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.1p7k_500006_lifetime_energy',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.ENERGY: 'energy'>,
'original_icon': None,
'original_name': 'Lifetime energy',
'platform': 'lektrico',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'lifetime_energy',
'unique_id': '500006_lifetime_energy',
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
})
# ---
# name: test_all_entities[sensor.1p7k_500006_lifetime_energy-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'energy',
'friendly_name': '1p7k_500006 Lifetime energy',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': <UnitOfEnergy.KILO_WATT_HOUR: 'kWh'>,
}),
'context': <ANY>,
'entity_id': 'sensor.1p7k_500006_lifetime_energy',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0',
})
# ---
# name: test_all_entities[sensor.1p7k_500006_limit_reason-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'no_limit',
'installation_current',
'user_limit',
'dynamic_limit',
'schedule',
'em_offline',
'em',
'ocpp',
]),
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.1p7k_500006_limit_reason',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
'original_icon': None,
'original_name': 'Limit reason',
'platform': 'lektrico',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'limit_reason',
'unique_id': '500006_limit_reason',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[sensor.1p7k_500006_limit_reason-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'enum',
'friendly_name': '1p7k_500006 Limit reason',
'options': list([
'no_limit',
'installation_current',
'user_limit',
'dynamic_limit',
'schedule',
'em_offline',
'em',
'ocpp',
]),
}),
'context': <ANY>,
'entity_id': 'sensor.1p7k_500006_limit_reason',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'installation_current',
})
# ---
# name: test_all_entities[sensor.1p7k_500006_power-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.1p7k_500006_power',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
}),
}),
'original_device_class': <SensorDeviceClass.POWER: 'power'>,
'original_icon': None,
'original_name': 'Power',
'platform': 'lektrico',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '500006_power',
'unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
})
# ---
# name: test_all_entities[sensor.1p7k_500006_power-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'power',
'friendly_name': '1p7k_500006 Power',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfPower.KILO_WATT: 'kW'>,
}),
'context': <ANY>,
'entity_id': 'sensor.1p7k_500006_power',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0.0000',
})
# ---
# name: test_all_entities[sensor.1p7k_500006_state-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'available',
'connected',
'need_auth',
'paused',
'charging',
'error',
'updating_firmware',
]),
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.1p7k_500006_state',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
'original_icon': None,
'original_name': 'State',
'platform': 'lektrico',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'state',
'unique_id': '500006_state',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[sensor.1p7k_500006_state-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'enum',
'friendly_name': '1p7k_500006 State',
'options': list([
'available',
'connected',
'need_auth',
'paused',
'charging',
'error',
'updating_firmware',
]),
}),
'context': <ANY>,
'entity_id': 'sensor.1p7k_500006_state',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'available',
})
# ---
# name: test_all_entities[sensor.1p7k_500006_temperature-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
}),
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.1p7k_500006_temperature',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.TEMPERATURE: 'temperature'>,
'original_icon': None,
'original_name': 'Temperature',
'platform': 'lektrico',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '500006_temperature',
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
})
# ---
# name: test_all_entities[sensor.1p7k_500006_temperature-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'temperature',
'friendly_name': '1p7k_500006 Temperature',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTemperature.CELSIUS: '°C'>,
}),
'context': <ANY>,
'entity_id': 'sensor.1p7k_500006_temperature',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '34.5',
})
# ---
# name: test_all_entities[sensor.1p7k_500006_voltage-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.1p7k_500006_voltage',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.VOLTAGE: 'voltage'>,
'original_icon': None,
'original_name': 'Voltage',
'platform': 'lektrico',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': '500006_voltage',
'unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
})
# ---
# name: test_all_entities[sensor.1p7k_500006_voltage-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'voltage',
'friendly_name': '1p7k_500006 Voltage',
'unit_of_measurement': <UnitOfElectricPotential.VOLT: 'V'>,
}),
'context': <ANY>,
'entity_id': 'sensor.1p7k_500006_voltage',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '220.0',
})
# ---

View File

@ -0,0 +1,173 @@
"""Tests for the Lektrico Charging Station config flow."""
import dataclasses
from ipaddress import ip_address
from lektricowifi import DeviceConnectionError
from homeassistant.components.lektrico.const import DOMAIN
from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF
from homeassistant.const import (
ATTR_HW_VERSION,
ATTR_SERIAL_NUMBER,
CONF_HOST,
CONF_TYPE,
)
from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from .conftest import (
MOCKED_DEVICE_BOARD_REV,
MOCKED_DEVICE_IP_ADDRESS,
MOCKED_DEVICE_SERIAL_NUMBER,
MOCKED_DEVICE_TYPE,
MOCKED_DEVICE_ZEROCONF_DATA,
)
from tests.common import MockConfigEntry
async def test_user_setup(hass: HomeAssistant, mock_device, mock_setup_entry) -> None:
"""Test manually setting up."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_USER},
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == SOURCE_USER
assert "flow_id" in result
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_HOST: MOCKED_DEVICE_IP_ADDRESS,
},
)
assert result.get("type") is FlowResultType.CREATE_ENTRY
assert result.get("title") == f"{MOCKED_DEVICE_TYPE}_{MOCKED_DEVICE_SERIAL_NUMBER}"
assert result.get("data") == {
CONF_HOST: MOCKED_DEVICE_IP_ADDRESS,
ATTR_SERIAL_NUMBER: MOCKED_DEVICE_SERIAL_NUMBER,
CONF_TYPE: MOCKED_DEVICE_TYPE,
ATTR_HW_VERSION: MOCKED_DEVICE_BOARD_REV,
}
assert "result" in result
assert len(mock_setup_entry.mock_calls) == 1
assert result.get("result").unique_id == MOCKED_DEVICE_SERIAL_NUMBER
async def test_user_setup_already_exists(
hass: HomeAssistant, mock_device, mock_config_entry: MockConfigEntry
) -> None:
"""Test manually setting up when the device already exists."""
mock_config_entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
assert not result["errors"]
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_HOST: MOCKED_DEVICE_IP_ADDRESS,
},
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"
async def test_user_setup_device_offline(hass: HomeAssistant, mock_device) -> None:
"""Test manually setting up when device is offline."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "user"
assert not result["errors"]
mock_device.device_config.side_effect = DeviceConnectionError
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_HOST: MOCKED_DEVICE_IP_ADDRESS,
},
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {CONF_HOST: "cannot_connect"}
assert result["step_id"] == "user"
mock_device.device_config.side_effect = None
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
CONF_HOST: MOCKED_DEVICE_IP_ADDRESS,
},
)
assert result["type"] is FlowResultType.CREATE_ENTRY
async def test_discovered_zeroconf(
hass: HomeAssistant, mock_device, mock_setup_entry
) -> None:
"""Test we can setup when discovered from zeroconf."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_ZEROCONF},
data=MOCKED_DEVICE_ZEROCONF_DATA,
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] is None
assert result.get("step_id") == "confirm"
result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result2["type"] is FlowResultType.CREATE_ENTRY
assert result2["data"] == {
CONF_HOST: MOCKED_DEVICE_IP_ADDRESS,
ATTR_SERIAL_NUMBER: MOCKED_DEVICE_SERIAL_NUMBER,
CONF_TYPE: MOCKED_DEVICE_TYPE,
ATTR_HW_VERSION: MOCKED_DEVICE_BOARD_REV,
}
assert result2["title"] == f"{MOCKED_DEVICE_TYPE}_{MOCKED_DEVICE_SERIAL_NUMBER}"
async def test_zeroconf_setup_already_exists(
hass: HomeAssistant, mock_device, mock_config_entry: MockConfigEntry
) -> None:
"""Test we abort zeroconf flow if device already configured."""
mock_config_entry.add_to_hass(hass)
zc_data_new_ip = dataclasses.replace(MOCKED_DEVICE_ZEROCONF_DATA)
zc_data_new_ip.ip_address = ip_address(MOCKED_DEVICE_IP_ADDRESS)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_ZEROCONF},
data=zc_data_new_ip,
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured"
async def test_discovered_zeroconf_device_connection_error(
hass: HomeAssistant, mock_device
) -> None:
"""Test we can setup when discovered from zeroconf but device went offline."""
mock_device.device_config.side_effect = DeviceConnectionError
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_ZEROCONF},
data=MOCKED_DEVICE_ZEROCONF_DATA,
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "cannot_connect"

View File

@ -0,0 +1,29 @@
"""Tests for the Lektrico integration."""
from unittest.mock import AsyncMock
from syrupy import SnapshotAssertion
from homeassistant.components.lektrico.const import DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from . import setup_integration
from tests.common import MockConfigEntry
async def test_device_info(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
mock_device: AsyncMock,
mock_config_entry: MockConfigEntry,
device_registry: dr.DeviceRegistry,
) -> None:
"""Test device registry integration."""
await setup_integration(hass, mock_config_entry)
device_entry = device_registry.async_get_device(
identifiers={(DOMAIN, mock_config_entry.unique_id)}
)
assert device_entry is not None
assert device_entry == snapshot

View File

@ -0,0 +1,31 @@
"""Tests for the Lektrico sensor platform."""
from unittest.mock import AsyncMock, patch
import pytest
from syrupy import SnapshotAssertion
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import setup_integration
from tests.common import MockConfigEntry, snapshot_platform
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_all_entities(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
mock_device: AsyncMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test all entities."""
with patch(
"homeassistant.components.lektrico.CHARGERS_PLATFORMS", [Platform.SENSOR]
):
await setup_integration(hass, mock_config_entry)
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)