Skip to content

Commit

Permalink
Zeversolar integration (#84887)
Browse files Browse the repository at this point in the history
Co-authored-by: Franck Nijhof <frenck@frenck.nl>
  • Loading branch information
kvanzuijlen and frenck authored Jan 3, 2023
1 parent c1a6f83 commit 6349760
Show file tree
Hide file tree
Showing 17 changed files with 459 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -1592,6 +1592,11 @@ omit =
homeassistant/components/zerproc/__init__.py
homeassistant/components/zerproc/const.py
homeassistant/components/zestimate/sensor.py
homeassistant/components/zeversolar/__init__.py
homeassistant/components/zeversolar/const.py
homeassistant/components/zeversolar/coordinator.py
homeassistant/components/zeversolar/entity.py
homeassistant/components/zeversolar/sensor.py
homeassistant/components/zha/api.py
homeassistant/components/zha/core/channels/*
homeassistant/components/zha/core/const.py
Expand Down
2 changes: 2 additions & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -1357,6 +1357,8 @@ build.json @home-assistant/supervisor
/tests/components/zeroconf/ @bdraco
/homeassistant/components/zerproc/ @emlove
/tests/components/zerproc/ @emlove
/homeassistant/components/zeversolar/ @kvanzuijlen
/tests/components/zeversolar/ @kvanzuijlen
/homeassistant/components/zha/ @dmulcahey @adminiuga @puddly
/tests/components/zha/ @dmulcahey @adminiuga @puddly
/homeassistant/components/zodiac/ @JulienTant
Expand Down
25 changes: 25 additions & 0 deletions homeassistant/components/zeversolar/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""The Zeversolar integration."""
from __future__ import annotations

from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant

from .const import DOMAIN, PLATFORMS
from .coordinator import ZeversolarCoordinator


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Zeversolar from a config entry."""
coordinator = ZeversolarCoordinator(hass=hass, entry=entry)
await coordinator.async_config_entry_first_refresh()

hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
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."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
61 changes: 61 additions & 0 deletions homeassistant/components/zeversolar/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""Config flow for zeversolar integration."""
from __future__ import annotations

import logging
from typing import Any

import voluptuous as vol
import zeversolar

from homeassistant import config_entries
from homeassistant.const import CONF_HOST
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import config_validation as cv

from .const import DOMAIN

_LOGGER = logging.getLogger(__name__)

STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOST): cv.string,
},
)


class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for zeversolar."""

VERSION = 1

async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step."""
if user_input is None:
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA
)

errors = {}

client = zeversolar.ZeverSolarClient(host=user_input[CONF_HOST])
try:
data = await self.hass.async_add_executor_job(client.get_data)
except zeversolar.ZeverSolarHTTPNotFound:
errors["base"] = "invalid_host"
except zeversolar.ZeverSolarHTTPError:
errors["base"] = "cannot_connect"
except zeversolar.ZeverSolarTimeout:
errors["base"] = "timeout_connect"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
await self.async_set_unique_id(data.serial_number)
self._abort_if_unique_id_configured()
return self.async_create_entry(title="Zeversolar", data=user_input)

return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)
9 changes: 9 additions & 0 deletions homeassistant/components/zeversolar/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""Constants for the zeversolar integration."""

from homeassistant.const import Platform

DOMAIN = "zeversolar"

PLATFORMS = [
Platform.SENSOR,
]
34 changes: 34 additions & 0 deletions homeassistant/components/zeversolar/coordinator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""Zeversolar coordinator."""
from __future__ import annotations

from datetime import timedelta
import logging

import zeversolar

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator

from .const import DOMAIN

_LOGGER = logging.getLogger(__name__)


class ZeversolarCoordinator(DataUpdateCoordinator[zeversolar.ZeverSolarData]):
"""Data update coordinator."""

def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Initialize the coordinator."""
super().__init__(
hass,
_LOGGER,
name=DOMAIN,
update_interval=timedelta(minutes=1),
)
self._client = zeversolar.ZeverSolarClient(host=entry.data[CONF_HOST])

async def _async_update_data(self) -> zeversolar.ZeverSolarData:
"""Fetch the latest data from the source."""
return await self.hass.async_add_executor_job(self._client.get_data)
29 changes: 29 additions & 0 deletions homeassistant/components/zeversolar/entity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""Base Entity for Zeversolar sensors."""
from __future__ import annotations

from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from .const import DOMAIN
from .coordinator import ZeversolarCoordinator


class ZeversolarEntity(
CoordinatorEntity[ZeversolarCoordinator],
):
"""Defines a base Zeversolar entity."""

_attr_has_entity_name = True

def __init__(
self,
*,
coordinator: ZeversolarCoordinator,
) -> None:
"""Initialize the Zeversolar entity."""
super().__init__(coordinator=coordinator)
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, coordinator.data.serial_number)},
name="Zeversolar Sensor",
manufacturer="Zeversolar",
)
10 changes: 10 additions & 0 deletions homeassistant/components/zeversolar/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"domain": "zeversolar",
"name": "Zeversolar",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/zeversolar",
"requirements": ["zeversolar==0.2.0"],
"codeowners": ["@kvanzuijlen"],
"iot_class": "local_polling",
"integration_type": "device"
}
96 changes: 96 additions & 0 deletions homeassistant/components/zeversolar/sensor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
"""Support for the Zeversolar platform."""
from __future__ import annotations

from collections.abc import Callable
from dataclasses import dataclass

import zeversolar

from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import UnitOfEnergy, UnitOfPower
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from .const import DOMAIN
from .coordinator import ZeversolarCoordinator
from .entity import ZeversolarEntity


@dataclass
class ZeversolarEntityDescriptionMixin:
"""Mixin for required keys."""

value_fn: Callable[[zeversolar.ZeverSolarData], zeversolar.kWh | zeversolar.Watt]


@dataclass
class ZeversolarEntityDescription(
SensorEntityDescription, ZeversolarEntityDescriptionMixin
):
"""Describes Zeversolar sensor entity."""


SENSOR_TYPES = (
ZeversolarEntityDescription(
key="pac",
name="Current power",
icon="mdi:solar-power-variant",
native_unit_of_measurement=UnitOfPower.WATT,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
device_class=SensorDeviceClass.POWER,
value_fn=lambda data: data.pac,
),
ZeversolarEntityDescription(
key="energy_today",
name="Energy today",
icon="mdi:home-battery",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
device_class=SensorDeviceClass.ENERGY,
value_fn=lambda data: data.energy_today,
),
)


async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the Zeversolar sensor."""
coordinator: ZeversolarCoordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities(
ZeversolarSensor(
description=description,
coordinator=coordinator,
)
for description in SENSOR_TYPES
)


class ZeversolarSensor(ZeversolarEntity, SensorEntity):
"""Implementation of the Zeversolar sensor."""

entity_description: ZeversolarEntityDescription

def __init__(
self,
*,
description: ZeversolarEntityDescription,
coordinator: ZeversolarCoordinator,
) -> None:
"""Initialize the sensor."""
self.entity_description = description
super().__init__(coordinator=coordinator)
self._attr_unique_id = f"{coordinator.data.serial_number}_{description.key}"

@property
def native_value(self) -> int | float:
"""Return sensor state."""
return self.entity_description.value_fn(self.coordinator.data)
20 changes: 20 additions & 0 deletions homeassistant/components/zeversolar/strings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"config": {
"step": {
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]"
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"timeout_connect": "[%key:common::config_flow::error::timeout_connect%]",
"invalid_host": "[%key:common::config_flow::error::invalid_host%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
}
}
19 changes: 19 additions & 0 deletions homeassistant/components/zeversolar/translations/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"config": {
"abort": {
"already_configured": "Device is already configured"
},
"error": {
"cannot_connect": "Failed to connect",
"timeout_connect": "Timeout establishing connection",
"unknown": "Unexpected error"
},
"step": {
"user": {
"data": {
"host": "Host"
}
}
}
}
}
1 change: 1 addition & 0 deletions homeassistant/generated/config_flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,7 @@
"youless",
"zamg",
"zerproc",
"zeversolar",
"zha",
"zwave_js",
"zwave_me",
Expand Down
6 changes: 6 additions & 0 deletions homeassistant/generated/integrations.json
Original file line number Diff line number Diff line change
Expand Up @@ -6288,6 +6288,12 @@
"config_flow": false,
"iot_class": "cloud_polling"
},
"zeversolar": {
"name": "Zeversolar",
"integration_type": "device",
"config_flow": true,
"iot_class": "local_polling"
},
"zha": {
"name": "Zigbee Home Automation",
"integration_type": "hub",
Expand Down
3 changes: 3 additions & 0 deletions requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2649,6 +2649,9 @@ zengge==0.2
# homeassistant.components.zeroconf
zeroconf==0.47.1

# homeassistant.components.zeversolar
zeversolar==0.2.0

# homeassistant.components.zha
zha-quirks==0.0.90

Expand Down
3 changes: 3 additions & 0 deletions requirements_test_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1856,6 +1856,9 @@ zamg==0.2.2
# homeassistant.components.zeroconf
zeroconf==0.47.1

# homeassistant.components.zeversolar
zeversolar==0.2.0

# homeassistant.components.zha
zha-quirks==0.0.90

Expand Down
1 change: 1 addition & 0 deletions tests/components/zeversolar/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Tests for the Zeversolar integration."""
Loading

0 comments on commit 6349760

Please sign in to comment.