Skip to content

Commit

Permalink
Merge pull request #27 from jyavenard/HighPrecisionEnergy_2021_8
Browse files Browse the repository at this point in the history
Creates a new *.accumulated for each power entities. Those are popula…
  • Loading branch information
gtdiehl authored Sep 2, 2021
2 parents 58caa2f + ce965a1 commit 5c627ff
Show file tree
Hide file tree
Showing 15 changed files with 552 additions and 299 deletions.
149 changes: 13 additions & 136 deletions custom_components/iotawatt/__init__.py
Original file line number Diff line number Diff line change
@@ -1,147 +1,24 @@
"""The iotawatt integration."""
from datetime import timedelta
import logging
from typing import Dict, List

from httpx import AsyncClient
from iotawattpy.iotawatt import Iotawatt
import voluptuous as vol

from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_SCAN_INTERVAL
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
)

from .const import (
COORDINATOR,
DEFAULT_ICON,
DEFAULT_SCAN_INTERVAL,
DOMAIN,
IOTAWATT_API,
SIGNAL_ADD_DEVICE,
)
from .const import DOMAIN
from .coordinator import IotawattUpdater

CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA)
_LOGGER = logging.getLogger(__name__)
PLATFORMS = ("sensor",)

PLATFORMS = ["sensor"]


async def async_setup(hass: HomeAssistant, config: dict):
"""Set up the iotawatt component."""
hass.data.setdefault(DOMAIN, {})
return True


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up iotawatt from a config entry."""
polling_interval = entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)

session = AsyncClient()
if "username" in entry.data.keys():
api = Iotawatt(
entry.data["name"],
entry.data["host"],
session,
entry.data["username"],
entry.data["password"],
)
else:
api = Iotawatt(
entry.data["name"],
entry.data["host"],
session,
)

coordinator = IotawattUpdater(
hass,
api=api,
name="IoTaWatt",
update_interval=polling_interval,
)

await coordinator.async_refresh()

if not coordinator.last_update_success:
raise ConfigEntryNotReady

hass.data[DOMAIN][entry.entry_id] = {
COORDINATOR: coordinator,
IOTAWATT_API: api,
}

for component in PLATFORMS:
_LOGGER.info(f"Setting up platform: {component}")
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, component)
)

coordinator = IotawattUpdater(hass, entry)
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
return True


class IotawattUpdater(DataUpdateCoordinator):
"""Class to manage fetching update data from the IoTaWatt Energy Device."""

def __init__(self, hass: HomeAssistant, api: str, name: str, update_interval: int):
"""Initialize IotaWattUpdater object."""
self.api = api
self.sensorlist: Dict[str, List[str]] = {}

super().__init__(
hass=hass,
logger=_LOGGER,
name=name,
update_interval=timedelta(seconds=update_interval),
)

async def _async_update_data(self):
"""Fetch sensors from IoTaWatt device."""

await self.api.update()
sensors = self.api.getSensors()

for sensor in sensors["sensors"]:
if sensor not in self.sensorlist:
to_add = {
"entity": sensor,
"mac_address": sensors["sensors"][sensor].hub_mac_address,
"name": sensors["sensors"][sensor].getName(),
}
async_dispatcher_send(self.hass, SIGNAL_ADD_DEVICE, to_add)
self.sensorlist[sensor] = sensors["sensors"][sensor]

return sensors


class IotaWattEntity(CoordinatorEntity, SensorEntity):
"""Defines the base IoTaWatt Energy Device entity."""

def __init__(self, coordinator: IotawattUpdater, entity, mac_address, name):
"""Initialize the IoTaWatt Entity."""
super().__init__(coordinator)

self._entity = entity
self._name = name
self._icon = DEFAULT_ICON
self._mac_address = mac_address

@property
def unique_id(self) -> str:
"""Return a unique, Home Assistant friendly identifier for this entity."""
return self._mac_address

@property
def name(self) -> str:
"""Return the name of the entity."""
return self._name

@property
def icon(self):
"""Return the icon for the entity."""
return self._icon
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
111 changes: 58 additions & 53 deletions custom_components/iotawatt/config_flow.py
Original file line number Diff line number Diff line change
@@ -1,102 +1,107 @@
"""Config flow for iotawatt integration."""
import json
from __future__ import annotations

import logging

import httpx
from httpx import AsyncClient
from iotawattpy.iotawatt import Iotawatt
import voluptuous as vol

from homeassistant import config_entries, core, exceptions
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.helpers import httpx_client

from .const import DOMAIN
from .const import CONNECTION_ERRORS, DOMAIN

_LOGGER = logging.getLogger(__name__)

STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_NAME): str,
vol.Required(CONF_HOST): str,
}
)


async def validate_input(hass: core.HomeAssistant, data):
"""Validate the user input allows us to connect.

Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
"""
session = AsyncClient()
iotawatt = Iotawatt(data["name"], data["host"], session)
async def validate_input(
hass: core.HomeAssistant, data: dict[str, str]
) -> dict[str, str]:
"""Validate the user input allows us to connect."""
iotawatt = Iotawatt(
"",
data[CONF_HOST],
httpx_client.get_async_client(hass),
data.get(CONF_USERNAME),
data.get(CONF_PASSWORD),
)
try:
is_connected = await iotawatt.connect()
_LOGGER.debug("isConnected: %s", is_connected)
except (KeyError, json.JSONDecodeError, httpx.HTTPError):
raise CannotConnect
except CONNECTION_ERRORS:
return {"base": "cannot_connect"}
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
return {"base": "unknown"}

if not is_connected:
raise InvalidAuth
return {"base": "invalid_auth"}

# Return info that you want to store in the config entry.
return {"title": data["name"]}
return {}


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

VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL

def __init__(self):
"""Initialize."""
self._data = {}
self._errors = {}

async def async_step_user(self, user_input=None):
"""Handle the initial step."""
if user_input is None:
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA
)
user_input = {}

errors = {}
self._data.update(user_input)
schema = vol.Schema(
{
vol.Required(CONF_HOST, default=user_input.get(CONF_HOST, "")): str,
}
)
if not user_input:
return self.async_show_form(step_id="user", data_schema=schema)

try:
await validate_input(self.hass, user_input)
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
if not (errors := await validate_input(self.hass, user_input)):
return self.async_create_entry(title=user_input[CONF_HOST], data=user_input)

if errors == {"base": "invalid_auth"}:
self._data.update(user_input)
return await self.async_step_auth()
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
return self.async_create_entry(title=self._data["name"], data=user_input)

return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)

return self.async_show_form(step_id="user", data_schema=schema, errors=errors)

async def async_step_auth(self, user_input=None):
"""Authenticate user if authentication is enabled on the IoTaWatt device."""
if user_input is None:
user_input = {}

data_schema = vol.Schema(
{
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
vol.Required(
CONF_USERNAME, default=user_input.get(CONF_USERNAME, "")
): str,
vol.Required(
CONF_PASSWORD, default=user_input.get(CONF_PASSWORD, "")
): str,
}
)
_LOGGER.debug("Data: %s", self._data)
if user_input is None:
if not user_input:
return self.async_show_form(step_id="auth", data_schema=data_schema)
self._data.update(user_input)
return self.async_create_entry(title=self._data["name"], data=self._data)

data = {**self._data, **user_input}

if errors := await validate_input(self.hass, data):
return self.async_show_form(
step_id="auth", data_schema=data_schema, errors=errors
)

return self.async_create_entry(title=data[CONF_HOST], data=data)


class CannotConnect(exceptions.HomeAssistantError):
"""Error to indicate we cannot connect."""


class InvalidAuth(exceptions.HomeAssistantError):
"""Error to indicate there is invalid auth."""
"""Error to indicate there is invalid auth."""
19 changes: 12 additions & 7 deletions custom_components/iotawatt/const.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
"""Constants for the iotawatt integration."""
"""Constants for the IoTaWatt integration."""
from __future__ import annotations

import json

import httpx

DEFAULT_ICON = "mdi:flash"
DEFAULT_SCAN_INTERVAL = 30
DOMAIN = "iotawatt"
COORDINATOR = "coordinator"
IOTAWATT_API = "iotawatt_api"
SIGNAL_ADD_DEVICE = "iotawatt_add_device"
SIGNAL_DELETE_DEVICE = "iotawatt_delete_device"
VOLT_AMPERE_REACTIVE = "VAR"
VOLT_AMPERE_REACTIVE_HOURS = "VARh"

ATTR_LAST_UPDATE = "last_update"

CONNECTION_ERRORS = (KeyError, json.JSONDecodeError, httpx.HTTPError)
Loading

0 comments on commit 5c627ff

Please sign in to comment.