Skip to content

Commit

Permalink
A variety of typing and property fixes, with translations and tweakin…
Browse files Browse the repository at this point in the history
…g hub name to show the email.
  • Loading branch information
magico13 committed Jan 31, 2025
1 parent 6955f5b commit 6b05182
Show file tree
Hide file tree
Showing 6 changed files with 97 additions and 82 deletions.
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
11 changes: 6 additions & 5 deletions custom_components/emporia_vue/config_flow.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Config flow for Emporia Vue integration."""

import asyncio
from collections.abc import Mapping
import logging
from typing import Any

Expand Down Expand Up @@ -60,7 +61,7 @@ async def validate_input(data: dict):

# Return info that you want to store in the config entry.
return {
CONFIG_TITLE: f"Customer {hub.vue.customer.customer_gid}",
CONFIG_TITLE: f"{hub.vue.customer.email} ({hub.vue.customer.customer_gid})",
CUSTOMER_GID: f"{hub.vue.customer.customer_gid}",
ENABLE_1M: data[ENABLE_1M],
ENABLE_1D: data[ENABLE_1D],
Expand Down Expand Up @@ -101,7 +102,7 @@ async def async_step_user(self, user_input=None) -> config_entries.ConfigFlowRes
step_id="user", data_schema=USER_CONFIG_SCHEMA, errors=errors
)

async def async_step_reconfigure(self, user_input: dict[str, Any] | None = None):
async def async_step_reconfigure(self, user_input: dict[str, Any] | None = None) -> config_entries.ConfigFlowResult:
"""Handle the reconfiguration step."""
current_config = self._get_reconfigure_entry()
if user_input is not None:
Expand Down Expand Up @@ -147,13 +148,13 @@ async def async_step_reconfigure(self, user_input: dict[str, Any] | None = None)
)

async def async_step_reauth(
self, entry_data: dict[str, Any]
self, entry_data: Mapping[str, Any]
) -> config_entries.ConfigFlowResult:
"""Perform reauthentication upon an API authentication error."""
return await self.async_step_reauth_confirm(entry_data)

async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
self, user_input: Mapping[str, Any] | None = None
) -> config_entries.ConfigFlowResult:
"""Confirm reauthentication dialog."""
errors: dict[str, str] = {}
Expand All @@ -164,7 +165,7 @@ async def async_step_reauth_confirm(
hub = VueHub()
if not await hub.authenticate(
user_input[CONF_EMAIL], user_input[CONF_PASSWORD]
):
) or not hub.vue.customer:
raise InvalidAuth
gid = hub.vue.customer.customer_gid
except InvalidAuth:
Expand Down
10 changes: 2 additions & 8 deletions custom_components/emporia_vue/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,10 @@
"codeowners": ["@magico13"],
"config_flow": true,
"single_config_entry": true,
"dependencies": [],
"documentation": "https://github.com/magico13/ha-emporia-vue",
"homekit": {},
"integration_type": "hub",
"iot_class": "cloud_polling",
"issue_tracker": "https://github.com/magico13/ha-emporia-vue/issues",
"requirements": [
"pyemvue@git+https://github.com/magico13/pyemvue.git@a3f9da4baa471d4aa32ca795756cb65c554ef89d"
],
"ssdp": [],
"version": "0.10.2",
"zeroconf": []
"requirements": ["pyemvue==0.18.7"],
"version": "0.10.3"
}
14 changes: 10 additions & 4 deletions custom_components/emporia_vue/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,19 @@
}
},
"reconfigure": {
"enable_1m": "Power Minute Average Sensor",
"enable_1d": "Energy Today Sensor",
"enable_1mon": "Energy This Month Sensor"
"data": {
"enable_1m": "[%key:component::emporia_vue::config::step::user::data::enable_1m%]",
"enable_1d": "[%key:component::emporia_vue::config::step::user::data::enable_1d%]",
"enable_1mon": "[%key:component::emporia_vue::config::step::user::data::enable_1mon%]"
}
},
"reauth_confirm": {
"title": "[%key:common::config_flow::title::reauth%]",
"description": "The Emporia Vue integration needs to re-authenticate your account"
"description": "The Emporia Vue integration needs to re-authenticate your account",
"data": {
"email": "[%key:common::config_flow::data::email%]",
"password": "[%key:common::config_flow::data::password%]"
}
}
},
"error": {
Expand Down
48 changes: 26 additions & 22 deletions custom_components/emporia_vue/switch.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
"""Platform for switch integration."""

import asyncio
import logging
from datetime import timedelta
from functools import cached_property
import logging
from typing import Any

from pyemvue import PyEmVue
from pyemvue.device import ChargerDevice, OutletDevice, VueDevice
from requests import exceptions

from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
Expand All @@ -16,16 +19,13 @@
DataUpdateCoordinator,
UpdateFailed,
)
from pyemvue import PyEmVue
from pyemvue.device import ChargerDevice, OutletDevice, VueDevice
from requests import exceptions

from .charger_entity import EmporiaChargerEntity
from .const import DOMAIN, VUE_DATA

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

device_information: dict[int, VueDevice] = {} # data is the populated device objects
device_information: dict[str, VueDevice] = {} # data is the populated device objects


async def __async_update_data(vue: PyEmVue):
Expand All @@ -37,17 +37,17 @@ async def __async_update_data(vue: PyEmVue):
try:
# Note: asyncio.TimeoutError and aiohttp.ClientError are already
# handled by the data update coordinator.
data = {}
data : dict[str, Any] = {}
loop: asyncio.AbstractEventLoop = asyncio.get_event_loop()
outlets: list[OutletDevice]
chargers: list[ChargerDevice]
(outlets, chargers) = await loop.run_in_executor(None, vue.get_devices_status)
if outlets:
for outlet in outlets:
data[outlet.device_gid] = outlet
data[str(outlet.device_gid)] = outlet
if chargers:
for charger in chargers:
data[charger.device_gid] = charger
data[str(charger.device_gid)] = charger
return data
except Exception as err:
raise UpdateFailed(f"Error communicating with Emporia API: {err}") from err
Expand All @@ -66,7 +66,7 @@ async def async_setup_entry(
for device in devices:
if device.outlet or device.ev_charger:
await loop.run_in_executor(None, vue.populate_device_properties, device)
device_information[device.device_gid] = device
device_information[str(device.device_gid)] = device

async def async_update_data():
return await __async_update_data(
Expand Down Expand Up @@ -108,7 +108,7 @@ async def async_update_data():
class EmporiaOutletSwitch(CoordinatorEntity, SwitchEntity): # type: ignore
"""Representation of an Emporia Smart Outlet state."""

def __init__(self, coordinator, vue, gid) -> None:
def __init__(self, coordinator: DataUpdateCoordinator[dict[str, Any]], vue: PyEmVue, gid: str) -> None:
"""Pass coordinator to CoordinatorEntity."""
super().__init__(coordinator)
self._vue = vue
Expand Down Expand Up @@ -137,7 +137,7 @@ async def async_turn_off(self, **kwargs: Any) -> None:
)
await self.coordinator.async_request_refresh()

@cached_property
@property
def device_info(self) -> DeviceInfo:
"""Return the device information."""
return DeviceInfo(
Expand All @@ -148,21 +148,23 @@ def device_info(self) -> DeviceInfo:
manufacturer="Emporia",
)

@cached_property
@property
def is_on(self) -> bool:
"""Return the state of the switch."""
return self.coordinator.data[self._device_gid].outlet_on

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

def turn_on(self, **kwargs: Any) -> None:
raise NotImplementedError()
"""Turn the switch on."""
raise NotImplementedError

def turn_off(self, **kwargs: Any) -> None:
raise NotImplementedError()
"""Turn the switch off."""
raise NotImplementedError


class EmporiaChargerSwitch(EmporiaChargerEntity, SwitchEntity): # type: ignore
Expand All @@ -176,10 +178,10 @@ async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the charger off."""
await self._update_switch(False)

@cached_property
@property
def is_on(self) -> bool:
"""Return the state of the switch."""
return self.coordinator.data[self._device.device_gid].charger_on
return self.coordinator.data[self._device_gid].charger_on

async def _update_switch(self, on: bool) -> None:
"""Update the switch."""
Expand All @@ -188,7 +190,7 @@ async def _update_switch(self, on: bool) -> None:
await loop.run_in_executor(
None,
self._vue.update_charger,
self._coordinator.data[self._device.device_gid],
self.coordinator.data[self._device_gid],
on,
)
except exceptions.HTTPError as err:
Expand All @@ -198,10 +200,12 @@ async def _update_switch(self, on: bool) -> None:
err.response.text,
)
raise
await self._coordinator.async_request_refresh()
await self.coordinator.async_request_refresh()

def turn_on(self, **kwargs: Any) -> None:
raise NotImplementedError()
"""Turn the charger on."""
raise NotImplementedError

def turn_off(self, **kwargs: Any) -> None:
raise NotImplementedError()
"""Turn the charger off."""
raise NotImplementedError
66 changes: 36 additions & 30 deletions custom_components/emporia_vue/translations/en.json
Original file line number Diff line number Diff line change
@@ -1,34 +1,40 @@
{
"config": {
"step": {
"user": {
"data": {
"email": "Email",
"password": "Password",
"enable_1m": "Power Minute Average Sensor",
"enable_1d": "Energy Today Sensor",
"enable_1mon": "Energy This Month Sensor"
}
},
"reconfigure": {
"enable_1m": "Power Minute Average Sensor",
"enable_1d": "Energy Today Sensor",
"enable_1mon": "Energy This Month Sensor"
},
"reauth_confirm": {
"title": "[%key:common::config_flow::title::reauth%]",
"description": "The Emporia Vue integration needs to re-authenticate your account"
}
},
"error": {
"cannot_connect": "Failed to connect",
"config": {
"abort": {
"already_configured": "Device is already configured",
"reauth_successful": "Re-authentication was successful",
"reconfigure_successful": "Re-configuration was successful"
},
"error": {
"cannot_connect": "Failed to connect",
"invalid_auth": "Invalid authentication",
"unknown": "Unexpected error"
},
"abort": {
"already_configured": "Device is already configured",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
},
"step": {
"reauth_confirm": {
"description": "The Emporia Vue integration needs to re-authenticate your account",
"title": "Authentication expired for {name}",
"data":{
"email": "Email",
"password": "Password"
}
},
"reconfigure": {
"data": {
"enable_1d": "Energy Today Sensor",
"enable_1m": "Power Minute Average Sensor",
"enable_1mon": "Energy This Month Sensor"
}
},
"user": {
"data": {
"email": "Email",
"enable_1d": "Energy Today Sensor",
"enable_1m": "Power Minute Average Sensor",
"enable_1mon": "Energy This Month Sensor",
"password": "Password"
}
}
}
}
}
}
}

0 comments on commit 6b05182

Please sign in to comment.