Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial Support for Re-Auth and Reconfiguration #350

Merged
merged 6 commits into from
Feb 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading