Skip to content

Commit

Permalink
Add powerfox integration (home-assistant#131640)
Browse files Browse the repository at this point in the history
  • Loading branch information
klaasnicolaas authored Dec 3, 2024
1 parent 535b477 commit abd3466
Show file tree
Hide file tree
Showing 23 changed files with 1,228 additions and 0 deletions.
1 change: 1 addition & 0 deletions .strict-typing
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,7 @@ homeassistant.components.persistent_notification.*
homeassistant.components.pi_hole.*
homeassistant.components.ping.*
homeassistant.components.plugwise.*
homeassistant.components.powerfox.*
homeassistant.components.powerwall.*
homeassistant.components.private_ble_device.*
homeassistant.components.prometheus.*
Expand Down
2 changes: 2 additions & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -1133,6 +1133,8 @@ build.json @home-assistant/supervisor
/tests/components/point/ @fredrike
/homeassistant/components/poolsense/ @haemishkyd
/tests/components/poolsense/ @haemishkyd
/homeassistant/components/powerfox/ @klaasnicolaas
/tests/components/powerfox/ @klaasnicolaas
/homeassistant/components/powerwall/ @bdraco @jrester @daniel-simpson
/tests/components/powerwall/ @bdraco @jrester @daniel-simpson
/homeassistant/components/private_ble_device/ @Jc2k
Expand Down
55 changes: 55 additions & 0 deletions homeassistant/components/powerfox/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""The Powerfox integration."""

from __future__ import annotations

import asyncio

from powerfox import Powerfox, PowerfoxConnectionError

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession

from .coordinator import PowerfoxDataUpdateCoordinator

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

type PowerfoxConfigEntry = ConfigEntry[list[PowerfoxDataUpdateCoordinator]]


async def async_setup_entry(hass: HomeAssistant, entry: PowerfoxConfigEntry) -> bool:
"""Set up Powerfox from a config entry."""
client = Powerfox(
username=entry.data[CONF_EMAIL],
password=entry.data[CONF_PASSWORD],
session=async_get_clientsession(hass),
)

try:
devices = await client.all_devices()
except PowerfoxConnectionError as err:
await client.close()
raise ConfigEntryNotReady from err

coordinators: list[PowerfoxDataUpdateCoordinator] = [
PowerfoxDataUpdateCoordinator(hass, client, device) for device in devices
]

await asyncio.gather(
*[
coordinator.async_config_entry_first_refresh()
for coordinator in coordinators
]
)

entry.runtime_data = coordinators

await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True


async def async_unload_entry(hass: HomeAssistant, entry: PowerfoxConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
57 changes: 57 additions & 0 deletions homeassistant/components/powerfox/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""Config flow for Powerfox integration."""

from __future__ import annotations

from typing import Any

from powerfox import Powerfox, PowerfoxAuthenticationError, PowerfoxConnectionError
import voluptuous as vol

from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
from homeassistant.helpers.aiohttp_client import async_get_clientsession

from .const import DOMAIN

STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_EMAIL): str,
vol.Required(CONF_PASSWORD): str,
}
)


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

async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
errors: dict[str, str] = {}
if user_input is not None:
self._async_abort_entries_match({CONF_EMAIL: user_input[CONF_EMAIL]})
client = Powerfox(
username=user_input[CONF_EMAIL],
password=user_input[CONF_PASSWORD],
session=async_get_clientsession(self.hass),
)
try:
await client.all_devices()
except PowerfoxAuthenticationError:
errors["base"] = "invalid_auth"
except PowerfoxConnectionError:
errors["base"] = "cannot_connect"
else:
return self.async_create_entry(
title=user_input[CONF_EMAIL],
data={
CONF_EMAIL: user_input[CONF_EMAIL],
CONF_PASSWORD: user_input[CONF_PASSWORD],
},
)
return self.async_show_form(
step_id="user",
errors=errors,
data_schema=STEP_USER_DATA_SCHEMA,
)
11 changes: 11 additions & 0 deletions homeassistant/components/powerfox/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""Constants for the Powerfox integration."""

from __future__ import annotations

from datetime import timedelta
import logging
from typing import Final

DOMAIN: Final = "powerfox"
LOGGER = logging.getLogger(__package__)
SCAN_INTERVAL = timedelta(minutes=5)
40 changes: 40 additions & 0 deletions homeassistant/components/powerfox/coordinator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""Coordinator for Powerfox integration."""

from __future__ import annotations

from powerfox import Device, Powerfox, PowerfoxConnectionError, Poweropti

from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed

from .const import DOMAIN, LOGGER, SCAN_INTERVAL


class PowerfoxDataUpdateCoordinator(DataUpdateCoordinator[Poweropti]):
"""Class to manage fetching Powerfox data from the API."""

config_entry: ConfigEntry

def __init__(
self,
hass: HomeAssistant,
client: Powerfox,
device: Device,
) -> None:
"""Initialize global Powerfox data updater."""
super().__init__(
hass,
LOGGER,
name=DOMAIN,
update_interval=SCAN_INTERVAL,
)
self.client = client
self.device = device

async def _async_update_data(self) -> Poweropti:
"""Fetch data from Powerfox API."""
try:
return await self.client.device(device_id=self.device.id)
except PowerfoxConnectionError as error:
raise UpdateFailed(error) from error
32 changes: 32 additions & 0 deletions homeassistant/components/powerfox/entity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""Generic entity for Powerfox."""

from __future__ import annotations

from powerfox import Device

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

from .const import DOMAIN
from .coordinator import PowerfoxDataUpdateCoordinator


class PowerfoxEntity(CoordinatorEntity[PowerfoxDataUpdateCoordinator]):
"""Base entity for Powerfox."""

_attr_has_entity_name = True

def __init__(
self,
coordinator: PowerfoxDataUpdateCoordinator,
device: Device,
) -> None:
"""Initialize Powerfox entity."""
super().__init__(coordinator)
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, device.id)},
manufacturer="Powerfox",
model=device.type.human_readable,
name=device.name,
serial_number=device.id,
)
16 changes: 16 additions & 0 deletions homeassistant/components/powerfox/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"domain": "powerfox",
"name": "Powerfox",
"codeowners": ["@klaasnicolaas"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/powerfox",
"iot_class": "cloud_polling",
"quality_scale": "bronze",
"requirements": ["powerfox==1.0.0"],
"zeroconf": [
{
"type": "_http._tcp.local.",
"name": "powerfox*"
}
]
}
92 changes: 92 additions & 0 deletions homeassistant/components/powerfox/quality_scale.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
rules:
# Bronze
action-setup:
status: exempt
comment: |
This integration does not provide additional actions.
appropriate-polling: done
brands: done
common-modules: done
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions:
status: exempt
comment: |
This integration does not provide additional actions.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup:
status: exempt
comment: |
Entities of this integration does not explicitly subscribe to events.
entity-unique-id: done
has-entity-name: done
runtime-data: done
test-before-configure: done
test-before-setup: done
unique-config-entry: done

# Silver
action-exceptions:
status: exempt
comment: |
This integration does not provide additional actions.
config-entry-unloading: done
docs-configuration-parameters:
status: exempt
comment: |
This integration does not have an options flow.
docs-installation-parameters: done
entity-unavailable: done
integration-owner: done
log-when-unavailable: done
parallel-updates:
status: exempt
comment: |
This integration uses a coordinator to handle updates.
reauthentication-flow: todo
test-coverage: done

# Gold
devices: done
diagnostics: todo
discovery-update-info:
status: exempt
comment: |
This integration is connecting to a cloud service.
discovery:
status: exempt
comment: |
It can find poweropti devices via zeroconf, but will start a normal user flow.
docs-data-update: done
docs-examples: todo
docs-known-limitations: done
docs-supported-devices: done
docs-supported-functions: done
docs-troubleshooting: todo
docs-use-cases: done
dynamic-devices: todo
entity-category: done
entity-device-class: done
entity-disabled-by-default:
status: exempt
comment: |
This integration does not have any entities that should disabled by default.
entity-translations: done
exception-translations: done
icon-translations:
status: exempt
comment: |
There is no need for icon translations.
reconfiguration-flow: todo
repair-issues:
status: exempt
comment: |
This integration doesn't have any cases where raising an issue is needed.
stale-devices: todo
# Platinum
async-dependency: done
inject-websession: done
strict-typing: done
Loading

0 comments on commit abd3466

Please sign in to comment.