Move most api calls to init to support auto-retry if failed during setup.

This commit is contained in:
magico13
2021-04-03 23:52:31 -04:00
parent a444abd9fe
commit 0ce55b82ed
3 changed files with 160 additions and 151 deletions

View File

@@ -1,16 +1,20 @@
"""The Emporia Vue integration."""
import asyncio
from datetime import timedelta
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
import logging
from pyemvue.enums import Scale
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
from pyemvue import PyEmVue
from pyemvue.device import VueDevice, VueDeviceChannel
from .const import DOMAIN, VUE_DATA, ENABLE_1S, ENABLE_1M, ENABLE_1D, ENABLE_1MON
@@ -23,17 +27,22 @@ CONFIG_SCHEMA = vol.Schema(
vol.Optional(ENABLE_1S, default=False): cv.boolean,
vol.Optional(ENABLE_1M, default=True): cv.boolean,
vol.Optional(ENABLE_1D, default=True): cv.boolean,
vol.Optional(ENABLE_1MON, default=True): cv.boolean
vol.Optional(ENABLE_1MON, default=True): cv.boolean,
}
)
},
extra=vol.ALLOW_EXTRA
},
extra=vol.ALLOW_EXTRA,
)
_LOGGER = logging.getLogger(__name__)
PLATFORMS = ["sensor", "switch"]
device_gids = []
device_information = []
async def async_setup(hass: HomeAssistant, config: dict):
"""Set up the Emporia Vue component."""
hass.data.setdefault(DOMAIN, {})
@@ -51,7 +60,7 @@ async def async_setup(hass: HomeAssistant, config: dict):
ENABLE_1S: conf[ENABLE_1S],
ENABLE_1M: conf[ENABLE_1M],
ENABLE_1D: conf[ENABLE_1D],
ENABLE_1MON: conf[ENABLE_1MON]
ENABLE_1MON: conf[ENABLE_1MON],
},
)
)
@@ -63,10 +72,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
entry_data = entry.data
email = entry_data[CONF_EMAIL]
password = entry_data[CONF_PASSWORD]
#_LOGGER.info(entry_data)
# _LOGGER.info(entry_data)
vue = PyEmVue()
loop = asyncio.get_event_loop()
try:
loop = asyncio.get_event_loop()
result = await loop.run_in_executor(None, vue.login, email, password)
if not result:
raise Exception("Could not authenticate with Emporia API")
@@ -74,24 +83,87 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
_LOGGER.error("Could not authenticate with Emporia API")
return False
scales_1m = []
scales_1s = []
try:
devices = await loop.run_in_executor(None, vue.get_devices)
_LOGGER.info("Found {0} Emporia devices".format(len(devices)))
for device in devices:
if not device.device_gid in device_gids:
device_gids.append(device.device_gid)
await loop.run_in_executor(None, vue.populate_device_properties, device)
device_information.append(device)
async def async_update_data_1min():
"""Fetch data from API endpoint at a 1 minute interval
This is the place to pre-process the data to lookup tables
so entities can quickly look up their data.
"""
return await update_sensors(vue, scales_1m)
async def async_update_data_1second():
"""Fetch data from API endpoint at a 1 second interval
This is the place to pre-process the data to lookup tables
so entities can quickly look up their data.
"""
return await update_sensors(vue, scales_1s)
if ENABLE_1M not in entry_data or entry_data[ENABLE_1M]:
scales_1m.append(Scale.MINUTE.value)
if ENABLE_1D not in entry_data or entry_data[ENABLE_1D]:
scales_1m.append(Scale.DAY.value)
if ENABLE_1MON not in entry_data or entry_data[ENABLE_1MON]:
scales_1m.append(Scale.MONTH.value)
coordinator_1min = None
if scales_1m:
coordinator_1min = DataUpdateCoordinator(
hass,
_LOGGER,
# Name of the data. For logging purposes.
name="sensor",
update_method=async_update_data_1min,
# Polling interval. Will only be polled if there are subscribers.
update_interval=timedelta(seconds=60),
)
await coordinator_1min.async_config_entry_first_refresh()
coordinator_1s = None
if ENABLE_1S in entry_data and entry_data[ENABLE_1S]:
coordinator_1s = DataUpdateCoordinator(
hass,
_LOGGER,
# Name of the data. For logging purposes.
name="sensor1s",
update_method=async_update_data_1second,
# Polling interval. Will only be polled if there are subscribers.
update_interval=timedelta(seconds=1),
)
await coordinator_1s.async_config_entry_first_refresh()
except Exception as err:
raise ConfigEntryNotReady(
f"Exception while setting up Emporia Vue. Will retry. {err}"
)
hass.data[DOMAIN][entry.entry_id] = {
VUE_DATA: vue,
ENABLE_1S: False if ENABLE_1S not in entry_data else entry_data[ENABLE_1S],
ENABLE_1M: True if ENABLE_1M not in entry_data else entry_data[ENABLE_1M],
ENABLE_1D: True if ENABLE_1D not in entry_data else entry_data[ENABLE_1D],
ENABLE_1MON: True if ENABLE_1MON not in entry_data else entry_data[ENABLE_1MON]
"coordinator_1min": coordinator_1min,
"coordinator_1s": coordinator_1s,
}
for component in PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, component)
)
try:
for component in PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, component)
)
except Exception as err:
raise ConfigEntryNotReady(f"Expected retry error: {err}")
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Unload a config entry."""
unload_ok = all(
@@ -106,3 +178,53 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
async def update_sensors(vue, scales):
try:
# Note: asyncio.TimeoutError and aiohttp.ClientError are already
# handled by the data update coordinator.
data = {}
loop = asyncio.get_event_loop()
for scale in scales:
channels = await loop.run_in_executor(
None, vue.get_devices_usage, device_gids, None, scale
)
if channels:
for channel in channels:
id = "{0}-{1}-{2}".format(
channel.device_gid, channel.channel_num, scale
)
usage = round(channel.usage, 3)
if scale == Scale.MINUTE.value:
usage = round(
60 * 1000 * channel.usage
) # convert from kwh to w rate
elif scale == Scale.SECOND.value:
usage = round(3600 * 1000 * channel.usage) # convert to rate
elif scale == Scale.MINUTES_15.value:
usage = round(
4 * 1000 * channel.usage
) # this might never be used but for safety, convert to rate
info = None
for device in device_information:
if device.device_gid == channel.device_gid:
for channel2 in device.channels:
if channel2.channel_num == channel.channel_num:
info = device
break
data[id] = {
"device_gid": channel.device_gid,
"channel_num": channel.channel_num,
"usage": usage,
"scale": scale,
"info": info,
}
else:
_LOGGER.warn("No channels found during update")
return data
except Exception as err:
raise UpdateFailed(f"Error communicating with Emporia API: {err}")

View File

@@ -13,5 +13,5 @@
"codeowners": [
"@magico13"
],
"version": "0.3.1"
"version": "0.3.2"
}

View File

@@ -1,154 +1,41 @@
"""Platform for sensor integration."""
from datetime import timedelta
import logging
import asyncio
import async_timeout
from homeassistant.const import (
DEVICE_CLASS_POWER,
POWER_WATT,
ENERGY_WATT_HOUR,
ENERGY_KILO_WATT_HOUR,
)
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
UpdateFailed,
)
from .const import DOMAIN, VUE_DATA, ENABLE_1S, ENABLE_1M, ENABLE_1D, ENABLE_1MON
from .const import DOMAIN
from pyemvue import pyemvue
from pyemvue.enums import Scale
from pyemvue.device import VueDevice, VueDeviceChannel, VueDeviceChannelUsage
_LOGGER = logging.getLogger(__name__)
device_information = [] # data is the populated device objects
device_gids = []
scales_1s = [Scale.SECOND.value]
scales_1m = []
async def update_sensors(vue, scales):
try:
# Note: asyncio.TimeoutError and aiohttp.ClientError are already
# handled by the data update coordinator.
data = {}
loop = asyncio.get_event_loop()
for scale in scales:
channels = await loop.run_in_executor(
None, vue.get_devices_usage, device_gids, None, scale
)
if channels:
for channel in channels:
id = "{0}-{1}-{2}".format(
channel.device_gid, channel.channel_num, scale
)
usage = round(channel.usage, 3)
if scale == Scale.MINUTE.value:
usage = round(
60 * 1000 * channel.usage
) # convert from kwh to w rate
elif scale == Scale.SECOND.value:
usage = round(3600 * 1000 * channel.usage) # convert to rate
elif scale == Scale.MINUTES_15.value:
usage = round(
4 * 1000 * channel.usage
) # this might never be used but for safety, convert to rate
data[id] = {
"device_gid": channel.device_gid,
"channel_num": channel.channel_num,
"usage": usage,
"scale": scale,
"channel": channel,
}
else:
_LOGGER.warn("No channels found during update")
return data
except Exception as err:
raise UpdateFailed(f"Error communicating with Emporia API: {err}")
# def setup_platform(hass, config, add_entities, discovery_info=None):
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the sensor platform."""
vue = hass.data[DOMAIN][config_entry.entry_id][VUE_DATA]
# Populate the initial device information, ie get_devices() and populate_device_properties()
loop = asyncio.get_event_loop()
devices = await loop.run_in_executor(None, vue.get_devices)
_LOGGER.info("Found {0} Emporia devices".format(len(devices)))
for device in devices:
if not device.device_gid in device_gids:
device_gids.append(device.device_gid)
await loop.run_in_executor(None, vue.populate_device_properties, device)
device_information.append(device)
async def async_update_data_1min():
"""Fetch data from API endpoint at a 1 minute interval
This is the place to pre-process the data to lookup tables
so entities can quickly look up their data.
"""
return await update_sensors(vue, scales_1m)
async def async_update_data_1second():
"""Fetch data from API endpoint at a 1 second interval
This is the place to pre-process the data to lookup tables
so entities can quickly look up their data.
"""
return await update_sensors(vue, scales_1s)
coordinator_1min = hass.data[DOMAIN][config_entry.entry_id]["coordinator_1min"]
coordinator_1s = hass.data[DOMAIN][config_entry.entry_id]["coordinator_1s"]
_LOGGER.info(hass.data[DOMAIN][config_entry.entry_id])
if hass.data[DOMAIN][config_entry.entry_id][ENABLE_1M]:
scales_1m.append(Scale.MINUTE.value)
if hass.data[DOMAIN][config_entry.entry_id][ENABLE_1D]:
scales_1m.append(Scale.DAY.value)
if hass.data[DOMAIN][config_entry.entry_id][ENABLE_1MON]:
scales_1m.append(Scale.MONTH.value)
if scales_1m:
coordinator_1min = DataUpdateCoordinator(
hass,
_LOGGER,
# Name of the data. For logging purposes.
name="sensor",
update_method=async_update_data_1min,
# Polling interval. Will only be polled if there are subscribers.
update_interval=timedelta(seconds=60),
)
await coordinator_1min.async_refresh()
if coordinator_1min.data:
async_add_entities(
CurrentVuePowerSensor(coordinator_1min, id)
for idx, id in enumerate(coordinator_1min.data)
)
else:
_LOGGER.error("No data found for 1 minute updater")
async_add_entities(
CurrentVuePowerSensor(coordinator_1min, id)
for idx, id in enumerate(coordinator_1min.data)
)
if hass.data[DOMAIN][config_entry.entry_id][ENABLE_1S]:
coordinator_1s = DataUpdateCoordinator(
hass,
_LOGGER,
# Name of the data. For logging purposes.
name="sensor1s",
update_method=async_update_data_1second,
# Polling interval. Will only be polled if there are subscribers.
update_interval=timedelta(seconds=1),
if coordinator_1s:
async_add_entities(
CurrentVuePowerSensor(coordinator_1s, id)
for idx, id in enumerate(coordinator_1s.data)
)
await coordinator_1s.async_refresh()
if coordinator_1s.data:
async_add_entities(
CurrentVuePowerSensor(coordinator_1s, id)
for idx, id in enumerate(coordinator_1s.data)
)
else:
_LOGGER.error("No data found for 1 second updater")
class CurrentVuePowerSensor(CoordinatorEntity, Entity):
@@ -161,15 +48,15 @@ class CurrentVuePowerSensor(CoordinatorEntity, Entity):
self._scale = coordinator.data[id]["scale"]
device_gid = coordinator.data[id]["device_gid"]
channel_num = coordinator.data[id]["channel_num"]
for device in device_information:
if device.device_gid == device_gid:
for channel in device.channels:
if channel.channel_num == channel_num:
self._device = device
self._channel = channel
break
self._device = coordinator.data[id]["info"]
self._channel = None
if self._device is not None:
for channel in self._device.channels:
if channel.channel_num == channel_num:
self._channel = channel
break
if self._channel is None:
_LOGGER.error(
raise RuntimeError(
"No channel found for device_gid {0} and channel_num {1}".format(
device_gid, channel_num
)