diff --git a/LICENSE.txt b/LICENSE.txt index eb68f8c..a788c34 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 Michael Marvin +Copyright (c) 2024 Michael Marvin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/custom_components/emporia_vue/__init__.py b/custom_components/emporia_vue/__init__.py index b94e71d..f35acf8 100644 --- a/custom_components/emporia_vue/__init__.py +++ b/custom_components/emporia_vue/__init__.py @@ -1,33 +1,32 @@ """The Emporia Vue integration.""" import asyncio from datetime import datetime, timedelta, timezone -from typing import Any, Optional -import dateutil.tz -import dateutil.relativedelta import logging +import re +from typing import Any, Optional +import dateutil.relativedelta +import dateutil.tz from pyemvue import PyEmVue from pyemvue.device import ( VueDevice, VueDeviceChannel, - VueUsageDevice, VueDeviceChannelUsage, + VueUsageDevice, ) from pyemvue.enums import Scale -import re import requests - import voluptuous as vol -from homeassistant.config_entries import ConfigEntry, SOURCE_IMPORT +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_EMAIL, CONF_PASSWORD from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError -import homeassistant.helpers.config_validation as cv from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.update_coordinator import CoordinatorEntity, DataUpdateCoordinator, UpdateFailed +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import DOMAIN, VUE_DATA, ENABLE_1M, ENABLE_1D, ENABLE_1MON +from .const import DOMAIN, ENABLE_1D, ENABLE_1M, ENABLE_1MON, VUE_DATA CONFIG_SCHEMA = vol.Schema( { @@ -91,6 +90,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): loop = asyncio.get_event_loop() try: result = await loop.run_in_executor(None, vue.login, email, password) + #result = await loop.run_in_executor(None, vue.login_simulator, "http://localhost:8000", email, password) if not result: raise Exception("Could not authenticate with Emporia API") except Exception: @@ -100,7 +100,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): try: devices = await loop.run_in_executor(None, vue.get_devices) for device in devices: - if not device.device_gid in DEVICE_GIDS: + if device.device_gid not in DEVICE_GIDS: DEVICE_GIDS.append(device.device_gid) _LOGGER.info("Adding gid %s to DEVICE_GIDS list", device.device_gid) # await loop.run_in_executor(None, vue.populate_device_properties, device) @@ -118,7 +118,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): ) async def async_update_data_1min(): - """Fetch data from API endpoint at a 1 minute interval + """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. @@ -132,7 +132,7 @@ async def async_update_data_1min(): return data async def async_update_data_1mon(): - """Fetch data from API endpoint at a 1 hour interval + """Fetch data from API endpoint at a 1 hour interval. This is the place to pre-process the data to lookup tables so entities can quickly look up their data. @@ -213,7 +213,7 @@ async def async_update_day_sensors(): # Setup custom services async def handle_set_charger_current(call): - """Handle setting the EV Charger current""" + """Handle setting the EV Charger current.""" _LOGGER.debug( "executing set_charger_current: %s %s", str(call.service), @@ -350,6 +350,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): async def update_sensors(vue: PyEmVue, scales: list[str]): + """Fetch data from API endpoint.""" try: # Note: asyncio.TimeoutError and aiohttp.ClientError are already # handled by the data update coordinator. @@ -482,7 +483,7 @@ async def parse_flattened_usage_data( str(unused_data), ) channels_were_added = False - for identifier, channel in unused_data.items(): + for _, channel in unused_data.items(): channels_were_added |= await handle_special_channels_for_device(channel) # we'll also need to register these entities I think. They might show up automatically on the first run # When we're done handling the unused data we need to rerun the update @@ -492,6 +493,7 @@ async def parse_flattened_usage_data( async def handle_special_channels_for_device(channel: VueDeviceChannel) -> bool: + """Handle the special channels for a device, if they exist.""" device_info = None if channel.device_gid in DEVICE_INFORMATION: device_info = DEVICE_INFORMATION[channel.device_gid] @@ -546,12 +548,12 @@ async def handle_special_channels_for_device(channel: VueDeviceChannel) -> bool: def make_channel_id(channel: VueDeviceChannel, scale: str): - """Format the channel id for a channel and scale""" - return "{0}-{1}-{2}".format(channel.device_gid, channel.channel_num, scale) + """Format the channel id for a channel and scale.""" + return f"{channel.device_gid}-{channel.channel_num}-{scale}" 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 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) return usage @@ -567,7 +569,7 @@ def change_time_to_local(time: datetime, tz_string: str): 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 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) @@ -589,7 +591,7 @@ def check_for_midnight(timestamp: datetime, device_gid: int, day_id: str): def determine_reset_datetime( local_time: datetime, monthly_cycle_start: int, is_month: bool ): - """Determine the last reset datetime (aware) based on the passed time and cycle start date""" + """Determine the last reset datetime (aware) based on the passed time and cycle start date.""" reset_datetime = local_time.replace(hour=0, minute=0, second=0, microsecond=0) if is_month: # Month should use the last billing_cycle_start_day of either this or last month diff --git a/custom_components/emporia_vue/charger_entity.py b/custom_components/emporia_vue/charger_entity.py index 0776274..a75a7c3 100644 --- a/custom_components/emporia_vue/charger_entity.py +++ b/custom_components/emporia_vue/charger_entity.py @@ -1,20 +1,16 @@ -import logging +"""Emporia Charger Entity.""" from typing import Any, Optional -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, -) - -from .const import DOMAIN - from pyemvue import pyemvue -from pyemvue.device import VueDevice, ChargerDevice +from pyemvue.device import ChargerDevice, VueDevice -_LOGGER = logging.getLogger(__name__) +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN class EmporiaChargerEntity(CoordinatorEntity): - """Emporia Charger Entity""" + """Emporia Charger Entity.""" def __init__( self, @@ -25,6 +21,7 @@ def __init__( device_class: str, enabled_default=True, ) -> None: + """Initialize the sensor.""" super().__init__(coordinator) self._coordinator = coordinator self._device = device @@ -37,10 +34,12 @@ def __init__( @property def entity_registry_enabled_default(self): + """Return whether the entity should be enabled when first added to the entity registry.""" return self._enabled_default @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] if data: return { @@ -57,14 +56,14 @@ def extra_state_attributes(self) -> dict[str, Any]: @property def unique_id(self) -> str: - """Unique ID for the charger""" + """Unique ID for the charger.""" return f"charger.emporia_vue.{self._device.device_gid}" @property def device_info(self): """Return the device information.""" return { - "identifiers": {(DOMAIN, "{0}-1,2,3".format(self._device.device_gid))}, + "identifiers": {(DOMAIN, f"{self._device.device_gid}-1,2,3")}, "name": self._device.device_name + "-1,2,3", "model": self._device.model, "sw_version": self._device.firmware, diff --git a/custom_components/emporia_vue/config_flow.py b/custom_components/emporia_vue/config_flow.py index 651d9ae..b9251e0 100644 --- a/custom_components/emporia_vue/config_flow.py +++ b/custom_components/emporia_vue/config_flow.py @@ -1,15 +1,14 @@ """Config flow for Emporia Vue integration.""" -import logging import asyncio +import logging + +from pyemvue import PyEmVue import voluptuous as vol from homeassistant import config_entries, core, exceptions from homeassistant.const import CONF_EMAIL, CONF_PASSWORD -from .const import DOMAIN # pylint:disable=unused-import -from .const import ENABLE_1M, ENABLE_1D, ENABLE_1MON - -from pyemvue import PyEmVue +from .const import DOMAIN, ENABLE_1D, ENABLE_1M, ENABLE_1MON _LOGGER = logging.getLogger(__name__) diff --git a/custom_components/emporia_vue/const.py b/custom_components/emporia_vue/const.py index 5b37031..7db0b5a 100644 --- a/custom_components/emporia_vue/const.py +++ b/custom_components/emporia_vue/const.py @@ -5,4 +5,4 @@ ENABLE_1S = "enable_1s" ENABLE_1M = "enable_1m" ENABLE_1D = "enable_1d" -ENABLE_1MON = "enable_1mon" \ No newline at end of file +ENABLE_1MON = "enable_1mon" diff --git a/custom_components/emporia_vue/manifest.json b/custom_components/emporia_vue/manifest.json index 0686ef8..dbafe31 100644 --- a/custom_components/emporia_vue/manifest.json +++ b/custom_components/emporia_vue/manifest.json @@ -9,8 +9,8 @@ "integration_type": "hub", "iot_class": "cloud_polling", "issue_tracker": "https://github.com/magico13/ha-emporia-vue/issues", - "requirements": ["pyemvue==0.17.2"], + "requirements": ["pyemvue==0.18.2"], "ssdp": [], - "version": "0.9.0", + "version": "0.9.1", "zeroconf": [] } diff --git a/custom_components/emporia_vue/sensor.py b/custom_components/emporia_vue/sensor.py index 1f2b46e..28569f8 100644 --- a/custom_components/emporia_vue/sensor.py +++ b/custom_components/emporia_vue/sensor.py @@ -1,27 +1,21 @@ """Platform for sensor integration.""" +import logging from typing import Optional + +from pyemvue.device import VueDevice, VueDeviceChannel +from pyemvue.enums import Scale + from homeassistant.components.sensor import ( - SensorStateClass, SensorDeviceClass, SensorEntity, + SensorStateClass, ) -import logging - -from homeassistant.const import ( - UnitOfEnergy, - UnitOfPower, -) +from homeassistant.const import UnitOfEnergy, UnitOfPower from homeassistant.core import HomeAssistant - -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, -) +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN -from pyemvue.enums import Scale -from pyemvue.device import VueDevice, VueDeviceChannel - _LOGGER = logging.getLogger(__name__) @@ -120,22 +114,21 @@ def last_reset(self): @property def unique_id(self): - """Unique ID for the sensor""" + """Unique ID for the sensor.""" if self._scale == Scale.MINUTE.value: return f"sensor.emporia_vue.instant.{self._channel.device_gid}-{self._channel.channel_num}" return f"sensor.emporia_vue.{self._scale}.{self._channel.device_gid}-{self._channel.channel_num}" @property def device_info(self): + """Return the device info.""" device_name = self._channel.name or self._device.device_name return { "identifiers": { # Serial numbers are unique identifiers within a specific domain ( DOMAIN, - "{0}-{1}".format( - self._device.device_gid, self._channel.channel_num - ), + f"{self._device.device_gid}-{self._channel.channel_num}", ) }, "name": device_name, @@ -160,7 +153,7 @@ def scale_usage(self, usage): return usage def scale_is_energy(self): - """Returns True if the scale is an energy unit instead of power (hour and bigger)""" + """Return True if the scale is an energy unit instead of power.""" return self._scale not in ( Scale.MINUTE.value, Scale.SECOND.value, diff --git a/custom_components/emporia_vue/switch.py b/custom_components/emporia_vue/switch.py index e2c92c5..cffeb65 100644 --- a/custom_components/emporia_vue/switch.py +++ b/custom_components/emporia_vue/switch.py @@ -2,12 +2,11 @@ import asyncio from datetime import timedelta import logging + +from pyemvue.device import ChargerDevice, OutletDevice, VueDevice import requests -from homeassistant.components.switch import ( - SwitchDeviceClass, - SwitchEntity, -) +from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -17,8 +16,6 @@ UpdateFailed, ) -from pyemvue.device import ChargerDevice, OutletDevice, VueDevice - from .charger_entity import EmporiaChargerEntity from .const import DOMAIN, VUE_DATA @@ -82,9 +79,11 @@ async def async_update_data(): switches = [] for _, gid in enumerate(coordinator.data): + if gid not in device_information: + continue if device_information[gid].outlet: switches.append(EmporiaOutletSwitch(coordinator, vue, gid)) - elif device_information[gid].ev_charger: + elif device_information[gid].ev_charger: switches.append( EmporiaChargerSwitch( coordinator, @@ -99,7 +98,7 @@ async def async_update_data(): class EmporiaOutletSwitch(CoordinatorEntity, SwitchEntity): - """Representation of an Emporia Smart Outlet state""" + """Representation of an Emporia Smart Outlet state.""" def __init__(self, coordinator, vue, gid) -> None: """Pass coordinator to CoordinatorEntity.""" @@ -142,15 +141,16 @@ async def async_turn_off(self, **kwargs): @property def unique_id(self): - """Unique ID for the switch""" + """Unique ID for the switch.""" return f"switch.emporia_vue.{self._device_gid}" @property def device_info(self): + """Return the device information.""" return { "identifiers": { # Serial numbers are unique identifiers within a specific domain - (DOMAIN, "{0}-1,2,3".format(self._device_gid)) + (DOMAIN, f"{self._device_gid}-1,2,3") }, "name": self._device.device_name + "-1,2,3", "model": self._device.model, @@ -161,7 +161,7 @@ def device_info(self): class EmporiaChargerSwitch(EmporiaChargerEntity, SwitchEntity): - """Representation of an Emporia Charger switch state""" + """Representation of an Emporia Charger switch state.""" @property def is_on(self): @@ -177,7 +177,7 @@ async def async_turn_off(self, **kwargs): await self._update_switch(False) async def _update_switch(self, on: bool): - """Update the switch""" + """Update the switch.""" loop = asyncio.get_event_loop() try: await loop.run_in_executor(