Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -1598,6 +1598,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."""
Comment thread
kvanzuijlen marked this conversation as resolved.

_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.89

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.89

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