mirror of
https://github.com/magico13/ha-emporia-vue.git
synced 2026-01-08 20:07:56 -05:00
Improvements to None handling and additional logging
This commit is contained in:
@@ -1,11 +1,17 @@
|
||||
"""The Emporia Vue integration."""
|
||||
import asyncio
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Any
|
||||
import dateutil
|
||||
import logging
|
||||
|
||||
from pyemvue import PyEmVue
|
||||
from pyemvue.device import VueDeviceChannel
|
||||
from pyemvue.device import (
|
||||
VueDevice,
|
||||
VueDeviceChannel,
|
||||
VueUsageDevice,
|
||||
VueDeviceChannelUsage,
|
||||
)
|
||||
from pyemvue.enums import Scale
|
||||
import re
|
||||
|
||||
@@ -40,11 +46,11 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS = ["sensor", "switch"]
|
||||
|
||||
device_gids = []
|
||||
device_information = {}
|
||||
last_minute_data = {}
|
||||
last_day_data = {}
|
||||
last_day_update = None
|
||||
DEVICE_GIDS: list[int] = []
|
||||
DEVICE_INFORMATION: dict[int, VueDevice] = {}
|
||||
LAST_MINUTE_DATA: dict[str, Any] = {}
|
||||
LAST_DAY_DATA: dict[str, Any] = {}
|
||||
LAST_DAY_UPDATE: datetime = None
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: dict):
|
||||
@@ -72,10 +78,10 @@ async def async_setup(hass: HomeAssistant, config: dict):
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||
"""Set up Emporia Vue from a config entry."""
|
||||
global device_gids
|
||||
global device_information
|
||||
device_gids = []
|
||||
device_information = {}
|
||||
global DEVICE_GIDS
|
||||
global DEVICE_INFORMATION
|
||||
DEVICE_GIDS = []
|
||||
DEVICE_INFORMATION = {}
|
||||
|
||||
entry_data = entry.data
|
||||
email = entry_data[CONF_EMAIL]
|
||||
@@ -93,21 +99,22 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||
|
||||
try:
|
||||
devices = await loop.run_in_executor(None, vue.get_devices)
|
||||
total_channels = 0
|
||||
for d in devices:
|
||||
total_channels += len(d.channels)
|
||||
_LOGGER.info(
|
||||
"Found {0} Emporia devices with {1} total channels".format(
|
||||
len(devices), total_channels
|
||||
)
|
||||
)
|
||||
for device in devices:
|
||||
if not device.device_gid in device_gids:
|
||||
device_gids.append(device.device_gid)
|
||||
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[device.device_gid] = device
|
||||
DEVICE_INFORMATION[device.device_gid] = device
|
||||
else:
|
||||
device_information[device.device_gid].channels += device.channels
|
||||
DEVICE_INFORMATION[device.device_gid].channels += device.channels
|
||||
|
||||
total_channels = 0
|
||||
for _, device in DEVICE_INFORMATION.items():
|
||||
total_channels += len(device.channels)
|
||||
_LOGGER.info(
|
||||
"Found %s Emporia devices with %s total channels",
|
||||
len(DEVICE_INFORMATION.keys()),
|
||||
total_channels,
|
||||
)
|
||||
|
||||
async def async_update_data_1min():
|
||||
"""Fetch data from API endpoint at a 1 minute interval
|
||||
@@ -115,12 +122,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||
This is the place to pre-process the data to lookup tables
|
||||
so entities can quickly look up their data.
|
||||
"""
|
||||
global last_minute_data
|
||||
data = await update_sensors(vue, [Scale.MINUTE.value])
|
||||
# store this, then have the daily sensors pull from it and integrate
|
||||
# then the daily can "true up" hourly (or more frequent) in case it's incorrect
|
||||
if data:
|
||||
last_minute_data = data
|
||||
global LAST_MINUTE_DATA
|
||||
LAST_MINUTE_DATA = data
|
||||
return data
|
||||
|
||||
async def async_update_data_1hr():
|
||||
@@ -132,35 +139,36 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||
return await update_sensors(vue, [Scale.MONTH.value])
|
||||
|
||||
async def async_update_day_sensors():
|
||||
global last_day_update
|
||||
global last_day_data
|
||||
global LAST_DAY_UPDATE
|
||||
global LAST_DAY_DATA
|
||||
now = datetime.now(timezone.utc)
|
||||
if not last_day_update or (now - last_day_update) > timedelta(minutes=15):
|
||||
if not LAST_DAY_UPDATE or (now - LAST_DAY_UPDATE) > timedelta(minutes=15):
|
||||
_LOGGER.info("Updating day sensors")
|
||||
last_day_update = now
|
||||
last_day_data = await update_sensors(vue, [Scale.DAY.value])
|
||||
LAST_DAY_UPDATE = now
|
||||
LAST_DAY_DATA = await update_sensors(vue, [Scale.DAY.value])
|
||||
else:
|
||||
# integrate the minute data
|
||||
_LOGGER.info("Integrating minute data into day sensors")
|
||||
if last_minute_data:
|
||||
for identifier, data in last_minute_data.items():
|
||||
if LAST_MINUTE_DATA:
|
||||
for identifier, data in LAST_MINUTE_DATA.items():
|
||||
device_gid, channel_gid, _ = identifier.split("-")
|
||||
day_id = f"{device_gid}-{channel_gid}-{Scale.DAY.value}"
|
||||
if (
|
||||
data
|
||||
and last_day_data
|
||||
and day_id in last_day_data
|
||||
and last_day_data[day_id]
|
||||
and "usage" in last_day_data[day_id]
|
||||
and last_day_data[day_id]["usage"] is not None
|
||||
and LAST_DAY_DATA
|
||||
and day_id in LAST_DAY_DATA
|
||||
and LAST_DAY_DATA[day_id]
|
||||
and "usage" in LAST_DAY_DATA[day_id]
|
||||
and LAST_DAY_DATA[day_id]["usage"] is not None
|
||||
):
|
||||
# if we just passed midnight, then reset back to zero
|
||||
handle_midnight(now, int(device_gid), day_id)
|
||||
timestamp: datetime = LAST_DAY_DATA[day_id]["timestamp"]
|
||||
check_for_midnight(timestamp, int(device_gid), day_id)
|
||||
|
||||
last_day_data[day_id]["usage"] += data[
|
||||
LAST_DAY_DATA[day_id]["usage"] += data[
|
||||
"usage"
|
||||
] # already in kwh
|
||||
return last_day_data
|
||||
return LAST_DAY_DATA
|
||||
|
||||
coordinator_1min = None
|
||||
if ENABLE_1M not in entry_data or entry_data[ENABLE_1M]:
|
||||
@@ -240,14 +248,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||
|
||||
charger_gid = int(gid_match.group(0))
|
||||
if (
|
||||
charger_gid not in device_information
|
||||
or not device_information[charger_gid].ev_charger
|
||||
charger_gid not in DEVICE_INFORMATION
|
||||
or not DEVICE_INFORMATION[charger_gid].ev_charger
|
||||
):
|
||||
raise HomeAssistantError(
|
||||
f"Set Charging Current called on invalid device with entity id {charger_entity.entity_id} (unique id {unique_entity_id})"
|
||||
)
|
||||
|
||||
charger_info = device_information[charger_gid]
|
||||
charger_info = DEVICE_INFORMATION[charger_gid]
|
||||
# Scale the current to a minimum of 6 amps and max of the circuit max
|
||||
current = max(6, current)
|
||||
current = min(current, charger_info.ev_charger.max_charging_rate)
|
||||
@@ -258,7 +266,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||
updated_charger = await loop.run_in_executor(
|
||||
None, vue.update_charger, charger_info.ev_charger, None, current
|
||||
)
|
||||
device_information[charger_gid].ev_charger = updated_charger
|
||||
DEVICE_INFORMATION[charger_gid].ev_charger = updated_charger
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN, "set_charger_current", handle_set_charger_current
|
||||
@@ -305,7 +313,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||
return unload_ok
|
||||
|
||||
|
||||
async def update_sensors(vue, scales):
|
||||
async def update_sensors(vue: PyEmVue, scales: list[str]):
|
||||
try:
|
||||
# Note: asyncio.TimeoutError and aiohttp.ClientError are already
|
||||
# handled by the data update coordinator.
|
||||
@@ -314,17 +322,17 @@ async def update_sensors(vue, scales):
|
||||
for scale in scales:
|
||||
utcnow = datetime.now(timezone.utc)
|
||||
usage_dict = await loop.run_in_executor(
|
||||
None, vue.get_device_list_usage, device_gids, utcnow, scale
|
||||
None, vue.get_device_list_usage, DEVICE_GIDS, utcnow, scale
|
||||
)
|
||||
if not usage_dict:
|
||||
_LOGGER.warning(
|
||||
"No channels found during update for scale %s. Retrying", scale
|
||||
)
|
||||
usage_dict = await loop.run_in_executor(
|
||||
None, vue.get_device_list_usage, device_gids, utcnow, scale
|
||||
None, vue.get_device_list_usage, DEVICE_GIDS, utcnow, scale
|
||||
)
|
||||
if usage_dict:
|
||||
recurse_usage_data(usage_dict, scale, data)
|
||||
recurse_usage_data(usage_dict, scale, data, utcnow)
|
||||
else:
|
||||
raise UpdateFailed(f"No channels found during update for scale {scale}")
|
||||
|
||||
@@ -334,8 +342,20 @@ async def update_sensors(vue, scales):
|
||||
raise UpdateFailed(f"Error communicating with Emporia API: {err}")
|
||||
|
||||
|
||||
def recurse_usage_data(usage_devices, scale, data):
|
||||
def recurse_usage_data(
|
||||
usage_devices: dict[int, VueUsageDevice],
|
||||
scale: str,
|
||||
data: dict[str, Any],
|
||||
requested_time: datetime,
|
||||
):
|
||||
"""Loop through the result from get_device_list_usage and pull out the data we want to use."""
|
||||
for gid, device in usage_devices.items():
|
||||
if (device.timestamp - requested_time).total_seconds() > 30:
|
||||
_LOGGER.warning(
|
||||
"More than 30 seconds have passed between the requested datetime and the returned datetime. Requested: %s Returned: %s",
|
||||
requested_time,
|
||||
device.timestamp,
|
||||
)
|
||||
for channel_num, channel in device.channels.items():
|
||||
if not channel:
|
||||
continue
|
||||
@@ -365,27 +385,50 @@ def recurse_usage_data(usage_devices, scale, data):
|
||||
reset_datetime = reset_datetime.replace(
|
||||
day=reset_day
|
||||
) - dateutil.relativedelta.relativedelta(months=1)
|
||||
fixed_usage = channel.usage
|
||||
if fixed_usage is None:
|
||||
prev_value = 0
|
||||
if (
|
||||
scale is Scale.MINUTE.value
|
||||
and identifier in LAST_MINUTE_DATA
|
||||
and "usage" in LAST_MINUTE_DATA[identifier]
|
||||
):
|
||||
prev_value = LAST_MINUTE_DATA[identifier]["usage"]
|
||||
elif (
|
||||
scale is Scale.DAY.value
|
||||
and identifier in LAST_DAY_DATA
|
||||
and "usage" in LAST_DAY_DATA[identifier]
|
||||
):
|
||||
prev_value = LAST_DAY_DATA[identifier]["usage"]
|
||||
_LOGGER.warning(
|
||||
"Got None usage for device %s channel %s scale %s and timestamp %s. Using previous known value of %s",
|
||||
gid,
|
||||
channel_num,
|
||||
scale,
|
||||
device.timestamp.isoformat(),
|
||||
prev_value,
|
||||
)
|
||||
fixed_usage = prev_value
|
||||
|
||||
# _LOGGER.info(
|
||||
# "Reset time for %s is %s", identifier, reset_datetime.isoformat()
|
||||
# )
|
||||
fixed_usage = fix_usage_sign(channel_num, fixed_usage)
|
||||
|
||||
data[identifier] = {
|
||||
"device_gid": gid,
|
||||
"channel_num": channel_num,
|
||||
"usage": fix_usage_sign(channel_num, channel.usage),
|
||||
"usage": fixed_usage,
|
||||
"scale": scale,
|
||||
"info": info,
|
||||
"reset": reset_datetime,
|
||||
"timestamp": device.timestamp,
|
||||
}
|
||||
if channel.nested_devices:
|
||||
recurse_usage_data(channel.nested_devices, scale, data)
|
||||
recurse_usage_data(channel.nested_devices, scale, data, requested_time)
|
||||
|
||||
|
||||
def find_device_info_for_channel(channel):
|
||||
def find_device_info_for_channel(channel: VueDeviceChannelUsage):
|
||||
device_info = None
|
||||
if channel.device_gid in device_information:
|
||||
device_info = device_information[channel.device_gid]
|
||||
if channel.device_gid in DEVICE_INFORMATION:
|
||||
device_info = DEVICE_INFORMATION[channel.device_gid]
|
||||
if channel.channel_num in [
|
||||
"MainsFromGrid",
|
||||
"MainsToGrid",
|
||||
@@ -406,29 +449,33 @@ def find_device_info_for_channel(channel):
|
||||
channel.device_gid,
|
||||
channel.channel_num,
|
||||
)
|
||||
multiplier = 1.0
|
||||
type_gid = 1
|
||||
if channel_123:
|
||||
multiplier = channel_123.channel_multiplier
|
||||
type_gid = channel_123.channel_type_gid
|
||||
|
||||
device_info.channels.append(
|
||||
VueDeviceChannel(
|
||||
gid=channel.device_gid,
|
||||
name=channel.name,
|
||||
channelNum=channel.channel_num,
|
||||
channelMultiplier=channel_123.channel_multiplier,
|
||||
channelTypeGid=channel_123.channel_type_gid,
|
||||
channelMultiplier=multiplier,
|
||||
channelTypeGid=type_gid,
|
||||
)
|
||||
)
|
||||
return device_info
|
||||
|
||||
|
||||
def make_channel_id(channel, scale):
|
||||
def make_channel_id(channel: VueDeviceChannelUsage, scale: str):
|
||||
"""Format the channel id for a channel and scale"""
|
||||
return "{0}-{1}-{2}".format(channel.device_gid, channel.channel_num, scale)
|
||||
|
||||
|
||||
def fix_usage_sign(channel_num, usage):
|
||||
def fix_usage_sign(channel_num: str, usage: float):
|
||||
"""If the channel is not '1,2,3' or 'Balance' we need it to be positive (see https://github.com/magico13/ha-emporia-vue/issues/57)"""
|
||||
if usage and channel_num not in ["1,2,3", "Balance"]:
|
||||
return abs(usage)
|
||||
if not usage:
|
||||
usage = 0
|
||||
return usage
|
||||
|
||||
|
||||
@@ -441,21 +488,21 @@ def change_time_to_local(time: datetime, tz_string: str):
|
||||
return time.astimezone(tz_info)
|
||||
|
||||
|
||||
def handle_midnight(now: datetime, device_gid: int, day_id: str):
|
||||
"""If midnight has recently passed, reset the last_day_data for Day sensors to zero"""
|
||||
global device_information
|
||||
global last_day_data
|
||||
if device_gid in device_information:
|
||||
device_info = device_information[device_gid]
|
||||
local_time = change_time_to_local(now, device_info.time_zone)
|
||||
def check_for_midnight(timestamp: datetime, device_gid: int, day_id: str):
|
||||
"""If midnight has recently passed, reset the LAST_DAY_DATA for Day sensors to zero"""
|
||||
if device_gid in DEVICE_INFORMATION:
|
||||
device_info = DEVICE_INFORMATION[device_gid]
|
||||
local_time = change_time_to_local(timestamp, device_info.time_zone)
|
||||
local_midnight = local_time.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
if (local_time - local_midnight) < timedelta(minutes=1, seconds=45):
|
||||
# Midnight happened since the last update, reset to zero
|
||||
last_reset = LAST_DAY_DATA[day_id]["reset"]
|
||||
if local_midnight > last_reset:
|
||||
# New reset time found
|
||||
_LOGGER.warning(
|
||||
"Midnight happened recently for id %s! Current time is %s, midnight is %s",
|
||||
"Midnight happened recently for id %s! Timestamp is %s, midnight is %s, previous reset was %s",
|
||||
day_id,
|
||||
local_time,
|
||||
local_midnight,
|
||||
last_reset,
|
||||
)
|
||||
last_day_data[day_id]["usage"] = 0
|
||||
last_day_data[day_id]["reset"] = local_midnight
|
||||
LAST_DAY_DATA[day_id]["usage"] = 0
|
||||
LAST_DAY_DATA[day_id]["reset"] = local_midnight
|
||||
|
||||
Reference in New Issue
Block a user