-
-
Notifications
You must be signed in to change notification settings - Fork 28.4k
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
Changes from all commits
745a083
097d734
ba4f692
be52942
c5b4fe8
14641ad
58b8b8b
68e6e6f
1f6910d
2807c15
17ca04d
14a3adc
8eebf43
50058af
81d2478
bc1b144
b89434a
de22ae4
b8d550c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 = {} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) |
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, | ||
) |
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" |
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this value there? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no, the |
||
|
||
async def _async_update_data(self) -> ReturnOutputData: | ||
return await self.api.get_output_data() |
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
|
||
} |
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
|
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -54,6 +54,7 @@ | |
"apcupsd", | ||
"apple_tv", | ||
"aprilaire", | ||
"apsystems", | ||
"aranet", | ||
"arcam_fmj", | ||
"arve", | ||
|
There was a problem hiding this comment.
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