Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add integration for APsystems EZ1 microinverter #114531

Merged
merged 19 commits into from
May 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
745a083
Add APsystems local API integration
mawoka-myblock Mar 31, 2024
097d734
Fix session usage in config_flow in apsystems local api
mawoka-myblock Apr 2, 2024
ba4f692
Remove skip check option for apsystems_loca api
mawoka-myblock Apr 2, 2024
be52942
Update APsystems API dependency and increased test coverage to 100%
mawoka-myblock Apr 9, 2024
c5b4fe8
Utilize EntityDescriptions for APsystems Local integration
mawoka-myblock Apr 9, 2024
14641ad
Ensure coverage entries are sorted (#114424)
epenet Apr 1, 2024
58b8b8b
Use patch instead of Http Mocks for APsystems API tests
mawoka-myblock Apr 9, 2024
68e6e6f
Fix linter waring for apsystemsapi
mawoka-myblock Apr 28, 2024
1f6910d
Fix apsystemsapi test
mawoka-myblock Apr 28, 2024
2807c15
Fix CODEOWNERS for apsystemsapi
mawoka-myblock Apr 29, 2024
17ca04d
Address small PR review changes for apsystems_local
mawoka-myblock May 7, 2024
14a3adc
Remove wrong lines in coveragerc
mawoka-myblock May 7, 2024
8eebf43
Add serial number for apsystems_local
mawoka-myblock May 7, 2024
50058af
Remove option of custom refresh interval fro apsystems_local
mawoka-myblock May 7, 2024
81d2478
Remove function override and fix stale comments
mawoka-myblock May 8, 2024
bc1b144
Use native device id and name storage instead of custom one for apsys…
mawoka-myblock May 13, 2024
b89434a
Use runtime_data for apsystems_local
mawoka-myblock May 13, 2024
de22ae4
Don't store entry data in runtime data
balloob May 14, 2024
b8d550c
Move from apsystems_local to apsystems domain
mawoka-myblock May 14, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ omit =
homeassistant/components/aprilaire/climate.py
homeassistant/components/aprilaire/coordinator.py
homeassistant/components/aprilaire/entity.py
homeassistant/components/apsystems/__init__.py
homeassistant/components/apsystems/const.py
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const doesn't have any coverage checks

homeassistant/components/apsystems/coordinator.py
homeassistant/components/apsystems/sensor.py
homeassistant/components/aqualogic/*
homeassistant/components/aquostv/media_player.py
homeassistant/components/arcam_fmj/__init__.py
Expand Down
1 change: 1 addition & 0 deletions .strict-typing
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ homeassistant.components.api.*
homeassistant.components.apple_tv.*
homeassistant.components.apprise.*
homeassistant.components.aprs.*
homeassistant.components.apsystems.*
homeassistant.components.aqualogic.*
homeassistant.components.aquostv.*
homeassistant.components.aranet.*
Expand Down
2 changes: 2 additions & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ build.json @home-assistant/supervisor
/tests/components/aprilaire/ @chamberlain2007
/homeassistant/components/aprs/ @PhilRW
/tests/components/aprs/ @PhilRW
/homeassistant/components/apsystems/ @mawoka-myblock @SonnenladenGmbH
/tests/components/apsystems/ @mawoka-myblock @SonnenladenGmbH
/homeassistant/components/aranet/ @aschmitz @thecode @anrijs
/tests/components/aranet/ @aschmitz @thecode @anrijs
/homeassistant/components/arcam_fmj/ @elupus
Expand Down
34 changes: 34 additions & 0 deletions homeassistant/components/apsystems/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""The APsystems local API integration."""

from __future__ import annotations

import logging

from APsystemsEZ1 import APsystemsEZ1M

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_IP_ADDRESS, Platform
from homeassistant.core import HomeAssistant

from .coordinator import ApSystemsDataCoordinator

_LOGGER = logging.getLogger(__name__)
mawoka-myblock marked this conversation as resolved.
Show resolved Hide resolved

PLATFORMS: list[Platform] = [Platform.SENSOR]


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up this integration using UI."""
entry.runtime_data = {}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be removed

api = APsystemsEZ1M(ip_address=entry.data[CONF_IP_ADDRESS], timeout=8)
coordinator = ApSystemsDataCoordinator(hass, api)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = {"COORDINATOR": coordinator}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should type the ConfigEntry using the generic type

await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

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, PLATFORMS)
51 changes: 51 additions & 0 deletions homeassistant/components/apsystems/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""The config_flow for APsystems local API integration."""

from aiohttp import client_exceptions
from APsystemsEZ1 import APsystemsEZ1M
import voluptuous as vol

from homeassistant import config_entries
mawoka-myblock marked this conversation as resolved.
Show resolved Hide resolved
from homeassistant.const import CONF_IP_ADDRESS
from homeassistant.helpers.aiohttp_client import async_get_clientsession

from .const import DOMAIN, LOGGER

DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_IP_ADDRESS): str,
}
)


class APsystemsLocalAPIFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Config flow for Apsystems local."""

VERSION = 1

async def async_step_user(
self,
user_input: dict | None = None,
mawoka-myblock marked this conversation as resolved.
Show resolved Hide resolved
) -> config_entries.ConfigFlowResult:
"""Handle a flow initialized by the user."""
_errors = {}
mawoka-myblock marked this conversation as resolved.
Show resolved Hide resolved
session = async_get_clientsession(self.hass, False)

if user_input is not None:
try:
session = async_get_clientsession(self.hass, False)
mawoka-myblock marked this conversation as resolved.
Show resolved Hide resolved
api = APsystemsEZ1M(user_input[CONF_IP_ADDRESS], session=session)
device_info = await api.get_device_info()
await self.async_set_unique_id(device_info.deviceId)
mawoka-myblock marked this conversation as resolved.
Show resolved Hide resolved
mawoka-myblock marked this conversation as resolved.
Show resolved Hide resolved
except (TimeoutError, client_exceptions.ClientConnectionError) as exception:
LOGGER.warning(exception)
mawoka-myblock marked this conversation as resolved.
Show resolved Hide resolved
_errors["base"] = "connection_refused"
mawoka-myblock marked this conversation as resolved.
Show resolved Hide resolved
else:
return self.async_create_entry(
title="Solar",
data=user_input,
)
return self.async_show_form(
step_id="user",
data_schema=DATA_SCHEMA,
errors=_errors,
)
6 changes: 6 additions & 0 deletions homeassistant/components/apsystems/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""Constants for the APsystems Local API integration."""

from logging import Logger, getLogger

LOGGER: Logger = getLogger(__package__)
DOMAIN = "apsystems"
37 changes: 37 additions & 0 deletions homeassistant/components/apsystems/coordinator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""The coordinator for APsystems local API integration."""

from __future__ import annotations

from datetime import timedelta
import logging

from APsystemsEZ1 import APsystemsEZ1M, ReturnOutputData

from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator

_LOGGER = logging.getLogger(__name__)
mawoka-myblock marked this conversation as resolved.
Show resolved Hide resolved


class InverterNotAvailable(Exception):
"""Error used when Device is offline."""
mawoka-myblock marked this conversation as resolved.
Show resolved Hide resolved


class ApSystemsDataCoordinator(DataUpdateCoordinator):
mawoka-myblock marked this conversation as resolved.
Show resolved Hide resolved
"""Coordinator used for all sensors."""

def __init__(self, hass: HomeAssistant, api: APsystemsEZ1M) -> None:
"""Initialize my coordinator."""
super().__init__(
hass,
_LOGGER,
# Name of the data. For logging purposes.
name="APSystems Data",
# Polling interval. Will only be polled if there are subscribers.
mawoka-myblock marked this conversation as resolved.
Show resolved Hide resolved
update_interval=timedelta(seconds=12),
)
self.api = api
self.always_update = True
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this value there?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean the 12? I thought it'd be the perfect middle ground between 10 and 15 seconds.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, the self.always_update = True


async def _async_update_data(self) -> ReturnOutputData:
return await self.api.get_output_data()
13 changes: 13 additions & 0 deletions homeassistant/components/apsystems/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"domain": "apsystems",
"name": "APsystems",
"codeowners": ["@mawoka-myblock", "@SonnenladenGmbH"],
"config_flow": true,
"dependencies": [],
"documentation": "https://www.home-assistant.io/integrations/apsystems",
"homekit": {},
"iot_class": "local_polling",
"requirements": ["apsystems-ez1==1.3.1"],
mawoka-myblock marked this conversation as resolved.
Show resolved Hide resolved
"ssdp": [],
"zeroconf": []
mawoka-myblock marked this conversation as resolved.
Show resolved Hide resolved
}
165 changes: 165 additions & 0 deletions homeassistant/components/apsystems/sensor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
"""The read-only sensors for APsystems local API integration."""

from __future__ import annotations

from collections.abc import Callable
from dataclasses import dataclass

from APsystemsEZ1 import ReturnOutputData

from homeassistant import config_entries
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import UnitOfEnergy, UnitOfPower
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import DiscoveryInfoType
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from .coordinator import ApSystemsDataCoordinator


@dataclass(frozen=True, kw_only=True)
class ApsystemsLocalApiSensorDescription(SensorEntityDescription):
"""Describes Apsystens Inverter sensor entity."""

value_fn: Callable[[ReturnOutputData], float | None]


SENSORS: tuple[ApsystemsLocalApiSensorDescription, ...] = (
ApsystemsLocalApiSensorDescription(
key="total_power",
translation_key="total_power",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda c: c.p1 + c.p2,
),
ApsystemsLocalApiSensorDescription(
key="total_power_p1",
translation_key="total_power_p1",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda c: c.p1,
),
ApsystemsLocalApiSensorDescription(
key="total_power_p2",
translation_key="total_power_p2",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda c: c.p2,
),
ApsystemsLocalApiSensorDescription(
key="lifetime_production",
translation_key="lifetime_production",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
value_fn=lambda c: c.te1 + c.te2,
),
ApsystemsLocalApiSensorDescription(
key="lifetime_production_p1",
translation_key="lifetime_production_p1",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
value_fn=lambda c: c.te1,
),
ApsystemsLocalApiSensorDescription(
key="lifetime_production_p2",
translation_key="lifetime_production_p2",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
value_fn=lambda c: c.te2,
),
ApsystemsLocalApiSensorDescription(
key="today_production",
translation_key="today_production",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
value_fn=lambda c: c.e1 + c.e2,
),
ApsystemsLocalApiSensorDescription(
key="today_production_p1",
translation_key="today_production_p1",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
value_fn=lambda c: c.e1,
),
ApsystemsLocalApiSensorDescription(
key="today_production_p2",
translation_key="today_production_p2",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
value_fn=lambda c: c.e2,
),
)


async def async_setup_entry(
hass: HomeAssistant,
config_entry: config_entries.ConfigEntry,
add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
mawoka-myblock marked this conversation as resolved.
Show resolved Hide resolved
) -> None:
"""Set up the sensor platform."""
config = config_entry.runtime_data
coordinator = config["COORDINATOR"]
device_name = config_entry.title
mawoka-myblock marked this conversation as resolved.
Show resolved Hide resolved
device_id: str = config_entry.unique_id # type: ignore[assignment]
mawoka-myblock marked this conversation as resolved.
Show resolved Hide resolved

add_entities(
ApSystemsSensorWithDescription(coordinator, desc, device_name, device_id)
for desc in SENSORS
)


class ApSystemsSensorWithDescription(CoordinatorEntity, SensorEntity):
mawoka-myblock marked this conversation as resolved.
Show resolved Hide resolved
"""Base sensor to be used with description."""

entity_description: ApsystemsLocalApiSensorDescription

def __init__(
self,
coordinator: ApSystemsDataCoordinator,
entity_description: ApsystemsLocalApiSensorDescription,
device_name: str,
device_id: str,
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator)
self.entity_description = entity_description
self._device_name = device_name
self._device_id = device_id
self._attr_unique_id = f"{device_id}_{entity_description.key}"

@property
def device_info(self) -> DeviceInfo:
mawoka-myblock marked this conversation as resolved.
Show resolved Hide resolved
"""Get the DeviceInfo."""
return DeviceInfo(
identifiers={("apsystems", self._device_id)},
mawoka-myblock marked this conversation as resolved.
Show resolved Hide resolved
name=self._device_name,
serial_number=self._device_id,
manufacturer="APsystems",
model="EZ1-M",
)

@callback
def _handle_coordinator_update(self) -> None:
if self.coordinator.data is None:
return # type: ignore[unreachable]
self._attr_native_value = self.entity_description.value_fn(
self.coordinator.data
)
self.async_write_ha_state()
mawoka-myblock marked this conversation as resolved.
Show resolved Hide resolved
21 changes: 21 additions & 0 deletions homeassistant/components/apsystems/strings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"config": {
"step": {
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]",
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
}
}
Comment on lines +1 to +21
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The strings are not updated. I am missing the config flow string and I am missing all of the entity translations

1 change: 1 addition & 0 deletions homeassistant/generated/config_flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"apcupsd",
"apple_tv",
"aprilaire",
"apsystems",
"aranet",
"arcam_fmj",
"arve",
Expand Down
6 changes: 6 additions & 0 deletions homeassistant/generated/integrations.json
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,12 @@
"config_flow": false,
"iot_class": "cloud_push"
},
"apsystems": {
"name": "APsystems",
"integration_type": "hub",
"config_flow": true,
"iot_class": "local_polling"
},
"aqualogic": {
"name": "AquaLogic",
"integration_type": "hub",
Expand Down
10 changes: 10 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,16 @@ disallow_untyped_defs = true
warn_return_any = true
warn_unreachable = true

[mypy-homeassistant.components.apsystems.*]
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.aqualogic.*]
check_untyped_defs = true
disallow_incomplete_defs = true
Expand Down