Skip to content

Commit

Permalink
Initial Support for Re-Auth and Reconfiguration (#350)
Browse files Browse the repository at this point in the history
- Updates to config flows to support re-auth when password changes and
reconfiguration to change the configuration normally set during
initialization.
- Add config option to invert solar data, defaults to true. This should
allow the user to choose if they want it inverted, since the energy
dashboard requires positive data but emporia usually provides negative
for generation.
- A bunch of typing and similar style updates
- Update some translation strings
- Add support for using the API simulator without dev code
  • Loading branch information
magico13 authored Feb 2, 2025
1 parent be40e96 commit 92647f6
Show file tree
Hide file tree
Showing 9 changed files with 262 additions and 170 deletions.
61 changes: 49 additions & 12 deletions custom_components/emporia_vue/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,16 @@
from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed

from .const import DOMAIN, ENABLE_1D, ENABLE_1M, ENABLE_1MON, VUE_DATA
from .const import (
CONFIG_TITLE,
CUSTOMER_GID,
DOMAIN,
ENABLE_1D,
ENABLE_1M,
ENABLE_1MON,
SOLAR_INVERT,
VUE_DATA,
)

_LOGGER: logging.Logger = logging.getLogger(__name__)

Expand All @@ -43,6 +52,7 @@
LAST_MINUTE_DATA: dict[str, Any] = {}
LAST_DAY_DATA: dict[str, Any] = {}
LAST_DAY_UPDATE: datetime | None = None
INVERT_SOLAR: bool = True


async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
Expand All @@ -52,6 +62,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
if not conf:
return True

global INVERT_SOLAR
if SOLAR_INVERT in conf:
INVERT_SOLAR = conf[SOLAR_INVERT]

hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
Expand All @@ -62,6 +76,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
ENABLE_1M: conf[ENABLE_1M],
ENABLE_1D: conf[ENABLE_1D],
ENABLE_1MON: conf[ENABLE_1MON],
INVERT_SOLAR: conf[SOLAR_INVERT],
CUSTOMER_GID: conf[CUSTOMER_GID],
CONFIG_TITLE: conf[CONFIG_TITLE],
},
)
)
Expand All @@ -76,12 +93,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
DEVICE_INFORMATION = {}

entry_data = entry.data
_LOGGER.debug("Setting up Emporia Vue with entry data: %s", entry_data)
email: str = entry_data[CONF_EMAIL]
password: str = entry_data[CONF_PASSWORD]
vue = PyEmVue()
loop: asyncio.AbstractEventLoop = asyncio.get_event_loop()
try:
result: bool = await loop.run_in_executor(None, vue.login, email, password)
# support using the simulator by looking at the username
if email.startswith("vue_simulator@"):
host = email.split("@")[1]
result: bool = await loop.run_in_executor(None, vue.login_simulator, host)
else:
result: bool = await loop.run_in_executor(None, vue.login, email, password)
if not result:
_LOGGER.error("Failed to login to Emporia Vue")
raise ConfigEntryAuthFailed("Failed to login to Emporia Vue")
Expand Down Expand Up @@ -176,7 +199,7 @@ async def async_update_day_sensors() -> dict:
update_interval=timedelta(minutes=1),
)
await coordinator_1min.async_config_entry_first_refresh()
_LOGGER.info("1min Update data: %s", coordinator_1min.data)
_LOGGER.debug("1min Update data: %s", coordinator_1min.data)
coordinator_1mon = None
if ENABLE_1MON not in entry_data or entry_data[ENABLE_1MON]:
coordinator_1mon = DataUpdateCoordinator(
Expand All @@ -189,7 +212,7 @@ async def async_update_day_sensors() -> dict:
update_interval=timedelta(hours=1),
)
await coordinator_1mon.async_config_entry_first_refresh()
_LOGGER.info("1mon Update data: %s", coordinator_1mon.data)
_LOGGER.debug("1mon Update data: %s", coordinator_1mon.data)

coordinator_day_sensor = None
if ENABLE_1D not in entry_data or entry_data[ENABLE_1D]:
Expand Down Expand Up @@ -466,11 +489,11 @@ async def parse_flattened_usage_data(
fixed_usage,
)

bidirectional = (
"bidirectional" in info_channel.type.lower()
or "merged" in info_channel.type.lower()
bidirectional = "bidirectional" in info_channel.type.lower()
is_solar = info_channel.channel_type_gid == 13
fixed_usage = fix_usage_sign(
channel_num, fixed_usage, bidirectional, is_solar, INVERT_SOLAR
)
fixed_usage = fix_usage_sign(channel_num, fixed_usage, bidirectional)

data[identifier] = {
"device_gid": gid,
Expand Down Expand Up @@ -550,11 +573,25 @@ def make_channel_id(channel: VueDeviceChannel, scale: str) -> str:
return f"{channel.device_gid}-{channel.channel_num}-{scale}"


def fix_usage_sign(channel_num: str, usage: float, bidirectional: bool) -> 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).
def fix_usage_sign(
channel_num: str,
usage: float,
bidirectional: bool,
is_solar: bool,
invert_solar: str,
) -> float:
"""If the channel is not '1,2,3' or 'Balance' we need it to be positive.
Solar circuits are up to the user to decide. Positive is recommended for the energy dashboard.
(see https://github.com/magico13/ha-emporia-vue/issues/57)
"""
if is_solar:
# Energy dashboard wants solar to be positive, Emporia usually provides negative
if usage and invert_solar:
return -1 * usage
return usage

if usage and not bidirectional and channel_num not in ["1,2,3", "Balance"]:
# With bidirectionality, we need to also check if bidirectional. If yes,
# we either don't abs, or we flip the sign.
Expand Down
30 changes: 17 additions & 13 deletions custom_components/emporia_vue/charger_entity.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
"""Emporia Charger Entity."""

from functools import cached_property
from typing import Any, Optional
from typing import Any

from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from pyemvue import pyemvue
from pyemvue.device import ChargerDevice, VueDevice

from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
)

from .const import DOMAIN


Expand All @@ -16,17 +19,18 @@ class EmporiaChargerEntity(CoordinatorEntity):

def __init__(
self,
coordinator,
coordinator: DataUpdateCoordinator[dict[str, Any]],
vue: pyemvue.PyEmVue,
device: VueDevice,
units: Optional[str],
units: str | None,
device_class: str,
enabled_default=True,
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator)
self._coordinator = coordinator
self._device: VueDevice = device
self._device_gid = str(device.device_gid)
self._vue: pyemvue.PyEmVue = vue
self._enabled_default: bool = enabled_default

Expand All @@ -40,15 +44,15 @@ def available(self) -> bool:
"""Return True if entity is available."""
return self._device is not None

@cached_property
@property
def entity_registry_enabled_default(self) -> bool:
"""Return whether the entity should be enabled when first added to the entity registry."""
return self._enabled_default

@cached_property
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes of the device."""
data: ChargerDevice = self._coordinator.data[self._device.device_gid]
data: ChargerDevice = self._coordinator.data[self._device_gid]
if data:
return {
"charging_rate": data.charging_rate,
Expand All @@ -62,16 +66,16 @@ def extra_state_attributes(self) -> dict[str, Any]:
}
return {}

@cached_property
@property
def unique_id(self) -> str:
"""Unique ID for the charger."""
return f"charger.emporia_vue.{self._device.device_gid}"
return f"charger.emporia_vue.{self._device_gid}"

@cached_property
@property
def device_info(self) -> DeviceInfo:
"""Return the device information."""
return DeviceInfo(
identifiers={(DOMAIN, f"{self._device.device_gid}-1,2,3")},
identifiers={(DOMAIN, f"{self._device_gid}-1,2,3")},
name=self._device.device_name,
model=self._device.model,
sw_version=self._device.firmware,
Expand Down
Loading

0 comments on commit 92647f6

Please sign in to comment.