mirror of
https://github.com/magico13/ha-emporia-vue.git
synced 2026-01-09 20:37:59 -05:00
Move most api calls to init to support auto-retry if failed during setup.
This commit is contained in:
@@ -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}")
|
||||
@@ -13,5 +13,5 @@
|
||||
"codeowners": [
|
||||
"@magico13"
|
||||
],
|
||||
"version": "0.3.1"
|
||||
"version": "0.3.2"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user