-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Creates a new *.accumulated for each power entities. Those are popula…
…ted by reading at regular intervals the energy value from the iotawatt. When declaring outputs relying on a mathematical expression (using other than + and - operator), the default energy sensors can't be used While the `integration` integration could be used, the advantage of those sensors is that they won't lose data if the connection to the iotawatt is lost or if rebooting the HA instance. In these cases, the sensor will fetch the data from when it last stop. This change requires iotawattpy 0.1.0 and HA 2021.8. Fixes #18
- Loading branch information
Showing
15 changed files
with
552 additions
and
299 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.""" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
Oops, something went wrong.